FunctionalConcepts 6.0.0.4
dotnet add package FunctionalConcepts --version 6.0.0.4
NuGet\Install-Package FunctionalConcepts -Version 6.0.0.4
<PackageReference Include="FunctionalConcepts" Version="6.0.0.4" />
paket add FunctionalConcepts --version 6.0.0.4
#r "nuget: FunctionalConcepts, 6.0.0.4"
// Install FunctionalConcepts as a Cake Addin #addin nuget:?package=FunctionalConcepts&version=6.0.0.4 // Install FunctionalConcepts as a Cake Tool #tool nuget:?package=FunctionalConcepts&version=6.0.0.4
Fluente Discriminated Union para padrão Result.
dotnet add package FunctionalConcepts
Dê uma estrela ⭐!
Gostou? Mostre seu apoio dando uma estrela 😄
Começando 🏃
Temos 3 tipos de união discriminada: Result<T>
, Choice<TLeft, TRight>
e Option<T>
. Escolha qual utilizar. A documentação ainda está em desenvolvimento para maior clareza.
Substitua Throw
por retorno de Result<T>
Isso aqui 👇
public float Operation(int num1, int num2)
{
if (num2 == 0)
{
throw new Exception("Impossível dividir por zero");
}
return num1 / num2;
}
try
{
var result = Operation(4, 2);
Console.WriteLine(result * 3);
}
catch (Exception e)
{
Console.WriteLine(e.Message);
return;
}
Se torna isso 👇
public Result<float> Operation(int a, int b)
{
if (b == 0)
{
return (Code: 500, Message: "Impossível dividir por zero");
}
return a / b;
}
var result = Operation(4, 2);
var msg = result.Match(
val => $"{(val * 3)}",
fail => $"código: {fail.Code} msg: {fail.Message}");
Console.WriteLine(msg);
Métodos
O objeto Result possui alguns métodos que permitem um trabalho funcional, por serem métodos puros, não modificam o estado do resultado atual.
Exemplo real
Um exemplo real de manipulação, onde a busca na camada de repositório é feita e retorna uma opção. Caso essa opção tenha um valor válido, o livro é excluído; caso contrário, retorna uma mensagem de erro NotFound para a camada superior, que neste caso é uma API.
public async Task<Result<Success>> Handle(
BookRemoveCommand cmd,
CancellationToken cancellationToken)
{
var maybeBook = await _repositoryBook_.Get(cmd.Id);
return await maybeBook.MatchAsync(
async book => Result.Of(await _repositoryBook_.Delete(book, cancellationToken)),
() => (NotFoundError)$"Livro com id: {cmd.Id} não encontrado");
}
Criando um Result
Conversão implícita
Existem conversores implícitos de TSuccess ou de BaseError para Result<TSuccess>. Por exemplo:
Result<Success> resultado = Result.Success; // pode ser feito com resultado = default(Success)
Success é uma estrutura vazia para representação de retorno, substituindo o "void". Será abordado no futuro.
Aqui está um exemplo com classe complexa:
string msg = "mensagem de teste";
ExemploTeste teste = new ExemploTeste(msg);
Result<ExemploTeste> resultado = teste;
Pode ser feito como retorno em um método também, caso necessário:
public Result<int> ResultAsInt()
{
return 5;
}
Aqui está um exemplo de resultado que será lido como erro. É possível fazer uma conversão de tupla para o objeto result e passar seus valores para Código e Mensagem:
public Result<int> ResultAsErro()
{
return (404, "objeto não encontrado");
}
Caso precise propagar uma exceção com o result, também é possível adicioná-lo à tupla, evitando assim a propagação de exceção e dando mais controle sobre as falhas. Como no exemplo abaixo:
public Result<float> Operation(int a, int b)
{
try {
return a / b;
}
catch(Exception ex)
{
return (Code: 500, Message: "Impossível dividir por zero", Exception: ex);
// também é possível retornar apenas: (500, "Impossível dividir por zero", ex)
}
}
Utilizando o Factory
Result
Em algumas situações, como interfaces por exemplo, a conversão implicita não é possivel, dessa forma é possivel a criação por um metodo especifico, o Of, abaixo o exemplo.
IQueryable<int> query = new List<int> { 1, 2, 3}.AsQueryable();
//cria um result de sucesso
Result<IQueryable<int>> result = Result.Of(query);
//cria um result de falha
Result<int> result = Result.Of<IQueryable<int>>(404, "object not found");
Também é possivel utilizar com tipos comuns.
Result<int> result = Result.Of(5);//cria um result de sucesso
Result<int> result = Result.Of<int>(404, "object not found");//cria um result de falha
E em retorno de metodos.
public Result<int> GetValue()
{
return Result.Of(5);
}
Quando um cenario de falha, deve ser expecificado o tipo entre <> pois do contrario o tipo ficaria Resul<ConflictError>, redundante, pois Result por si ja assume o papel de Erro e deve ser especificado a o sucesso entre <>
public Result<int> ErrorAsResult()
{
return Result.Of<int>((ConflictError)"Mensagem de conflito");
}
Erros
Caso queira, é possivel a inicialização de novas instancias de erros com o BaseError.New()
BaseError error = BaseError.New(404, "404 em base error");
Options
Em options segue a mesma ideia do result.
var opt = Option.Of<int>(16); // opt é do tipo Option<int>
Propriedades
Caso o resultado seja uma falha, existe uma propriedade boolean que indica essa condição, caso queira utilizar.
Result
IsFail
int userId = 19;
Result<int> result = userId;
if (result.IsFail)
{
// se result for erro, esse trecho será executado
}
Error
´
Não é possivel acessar o erro ou o valor diretamente, para isso é preciso utilizar de alguns metodos existentes dentro da biblioteca para acessalos de maneira segura. Dessa forma, evitamos ifs de comparação de nulavel dentro do codigo e garatimos o fluxo correto de acordo com seus valores, segue exemplos abaixo.
Metodos.
Then
Método que permite seguir um fluxo mais fluente com base no resultado em caso de situação de sucesso.
Result<int> foo = result.Then(v => v + 5);
Também é possível aumentar a fluência, ou seja, adicionar mais operadores para seguir o fluxo. Claro, se algum dos cenários retornar um erro ou se o resultado já for um erro, o fluxo não executará a função passada.
Result<string> foo = result
.Then(val => val + 5)
.Then(val => val + 2)
Else
metodo para o fluxo em caso de falha
Result<Company> result = Company.GetFirst();
result.Else(fail => {
Console.WriteLine(fail.Message)
});
Match
e MatchAsync
O Match recebe duas funções como parametros, onSome and onError, onSome é executado quando Result for um Sucesso, do contrario é executada a função passada em onError.
Match
string foo = result.Match(
some => some,
error => $"Msg: {error.Message}");
//Em caso de sucess, Foo assume o valor da mensagem dentro de result, em caso de falha Foo fica com valor "Msg: mensagem"
Async no Match
Mesma coisa que Match normal, contudo, aceita funções que retornam Task para executar.
string foo = await result.MatchAsync(
async some => await Task.FromResult(some),
async error => await Task.FromResult($"Msg: {error.Message}"));
Error Types
Built in error types
Temos um enum de erros. No entanto, esse enum é apenas utilizado para indicar quais erros cada classe de erro retorna. A classe base BaseError recebe um inteiro para possibilitar erros personalizados por cada programa.
public enum EErrorCode
{
Unauthorized = 401,
Forbidden = 0403,
NotFound = 0404,
Conflict = 0409,
NotAllowed = 0405,
InvalidObject = 0422,
Unhandled = 0500,
ServiceUnavailable = 0503,
}
Tipos de erros e como inicializalos
Primeiro, a classe base permite a criação de erros além dos já existentes.
private Result<int> FuncReturnError(){
return (501, "erro com codigo 501")
}
Result<int> result = 1;
Result<string> foo = result
.Then(val => val + 5)
.Then(_ => FuncReturnError())
.Then(v => Console.WriteLine($"value is: {v}"))
Nota: Na última situação, onde o valor seria mostrado no console, não será executada porque FuncReturnError retorna um tipo de erro.
Erros são criados implicitamente com base em uma tupla informada entre parênteses. Também é possível propagar uma exceção em casos que sejam necessários.
private Result<int> FuncReturnError()
{
try
{
//algum erro acontece aqui
}
catch(Exception exn)
{
return (501, "erro com codigo 501", exn)
}
}
Os seguintes erros já estão presentes, com seus respectivos códigos de erro. É importante lembrar que tudo é feito implicitamente; ou seja, apenas crie a mensagem que estará presente no erro.
ConflictError conflict = "mensagem de conflict";
ForbiddenError forbidden = "mensagem de forbidden";
InvalidObjectError invalidObj = "mensagem de invalidObj";
NotAllowedError notAllowed = "mensagem de notAllowed";
NotFoundError norFound = "mensagem de norFound";
ServiceUnavailableError unavailable = "mensagem de unavailable";
UnauthorizedError unauth = "mensagem de unauth";
UnhandledError unhandled = "mensagem de unhandled";
Também é possível passar Exception para um erro padrão, contudo, será necessário inicializá-lo como uma tupla.
ConflictError conflict = ("mensagem de conflict", exn);
E claro, para acessar a mensagem ou o código de erro, basta acessar as propriedades correspondentes.
conflict.Code;
conflict.Message;
conflict.Exception;
Mediator + FluentValidation + FunctionalConcepts
🤝
Quando se utiliza MediatR, é bastante comum usar FluentValidation para validar o request. As validações ocorrem com Behavior que lançam exceções se o request estiver inválido.
Usando conceitos funcionais com Result, criamos um Behavior que retorna um erro em vez de lançar uma exceção.
Um exemplo de Behavior: 👇
public class ValidatorBehavior<TRequest, TResponse>
: IPipelineBehavior<TRequest, Result<TResponse>>
where TRequest : notnull
{
private readonly IValidator<TRequest>[] _validators;
public ValidatorBehavior(IValidator<TRequest>[] validators) => _validators = validators;
public async Task<Result<TResponse>> Handle(TRequest request, RequestHandlerDelegate<Result<TResponse>> next, CancellationToken cancellationToken)
{
List<FluentValidation.Results.ValidationFailure> failures = _validators
.Select(v => v.Validate(request))
.SelectMany(result => result.Errors)
.Where(error => error != null)
.ToList();
return failures.Any()
? (InvalidObjectError)("Erro ao executar validations", new ValidationException(failures))
: await next();
}
}
Contribution 🤲
Se tiver alguma pergunta, comentário ou sugestão, por favor, abra uma issue ou crie um pull request.🙂
Creditos🙏
- LanguageExt - Library with complexy approch arround results and functional programming in C#
- ErrorOr - Simple way to functional with errors, amazing library.
- OneOf - Provides F# style discriminated unions behavior for C#
License 🪪
Licensed under the terms of GNU General Public License v3.0 license.
Product | Versions Compatible and additional computed target framework versions. |
---|---|
.NET | net6.0 is compatible. net6.0-android was computed. net6.0-ios was computed. net6.0-maccatalyst was computed. net6.0-macos was computed. net6.0-tvos was computed. net6.0-windows was computed. net7.0 was computed. net7.0-android was computed. net7.0-ios was computed. net7.0-maccatalyst was computed. net7.0-macos was computed. net7.0-tvos was computed. net7.0-windows was computed. net8.0 was computed. net8.0-android was computed. net8.0-browser was computed. net8.0-ios was computed. net8.0-maccatalyst was computed. net8.0-macos was computed. net8.0-tvos was computed. net8.0-windows was computed. |
-
net6.0
- No dependencies.
NuGet packages
This package is not used by any NuGet packages.
GitHub repositories
This package is not used by any popular GitHub repositories.
Versão 6.0.0.4 fix bind e map