FunctionalConcepts 6.0.0

There is a newer version of this package available.
See the version list below for details.
dotnet add package FunctionalConcepts --version 6.0.0                
NuGet\Install-Package FunctionalConcepts -Version 6.0.0                
This command is intended to be used within the Package Manager Console in Visual Studio, as it uses the NuGet module's version of Install-Package.
<PackageReference Include="FunctionalConcepts" Version="6.0.0" />                
For projects that support PackageReference, copy this XML node into the project file to reference the package.
paket add FunctionalConcepts --version 6.0.0                
#r "nuget: FunctionalConcepts, 6.0.0"                
#r directive can be used in F# Interactive and Polyglot Notebooks. Copy this into the interactive tool or source code of the script to reference the package.
// Install FunctionalConcepts as a Cake Addin
#addin nuget:?package=FunctionalConcepts&version=6.0.0

// Install FunctionalConcepts as a Cake Tool
#tool nuget:?package=FunctionalConcepts&version=6.0.0                

NuGet

Build publish FunctionalConcepts to nuget

GitHub contributors GitHub Stars GitHub license codecov


Union discriminada fluente de um padrão de resultado.

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 Operação(int num1, int num2)
{
    if (num2 == 0)
    {
        throw new Exception("Impossível dividir por zero");
    }

    return num1 / num2;
}

try
{
    var resultado = Operação(4, 2);
    Console.WriteLine(resultado * 3); // 6
}
catch (Exception e)
{
    Console.WriteLine(e.Message);
    return;
}

Se torna isso 👇

public Result<float> Operação(int a, int b)
{
    if (b == 0)
    {
        return (Código: 500, Mensagem: "Impossível dividir por zero");
    }

    return a / b;
}

var resultado = Operação(4, 2);

var msg = resultado.Match(
    val => $"{(resultado.Valor * 3)}",
    falha => $"código: {falha.Código} msg: {falha.Mensagem}");

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<Sucesso>> Handle(
    ComandoRemoverLivro solicitação,
    CancellationToken cancellationToken)
{

    var talvezLivro = await _repositorioLivro.ObterPorId(solicitação.Id);

    return await talvezLivro.MatchAsync(
        async livro => Result.Of(await _repositorioLivro.Excluir(livro, cancellationToken)),
        () => (ErroNãoEncontrado)$"Livro com id: {solicitação.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<Sucesso> resultado = Result.Sucesso; // pode ser feito com resultado = default(Sucesso)

Sucesso é uma estrutura vazia para representação de retorno, substituindo o "void". Será abordado mais adiante.

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> IntComoResultado()
{
    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> ErroComoResultado()
{
    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> Operação(int a, int b)
{
    try {
       return a / b;
    }
    catch(Exception ex)
    {
        return (Código: 500, Mensagem: "Impossível dividir por zero", Exceção: 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 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. 
Compatible target framework(s)
Included target framework(s) (in package)
Learn more about Target Frameworks and .NET Standard.
  • 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.

Version Downloads Last updated
6.0.0.4 134 8/16/2024
6.0.0.3 119 8/14/2024
6.0.0.2 109 7/20/2024
6.0.0.1 102 6/20/2024
6.0.0 100 6/20/2024
1.0.0 101 6/17/2024

Primeira versão lançada, versão 6.0 acompanha a versão 6 do ,NET