GithubHelp home page GithubHelp logo

jscarle / lightresults Goto Github PK

View Code? Open in Web Editor NEW
68.0 3.0 1.0 236 KB

An extremely light and modern Operation Result Pattern library for .NET.

Home Page: https://jscarle.github.io/LightResults/

License: MIT License

C# 99.94% HTML 0.06%
result results csharp dotnet pattern error handling fluentresults

lightresults's Introduction

Banner

LightResults - Operation Result Patterns for .NET

LightResults is an extremely light and modern .NET library that provides a simple and flexible implementation of the Result Pattern. The Result Pattern is a way of representing the outcome of an operation, whether it's successful or has encountered an error, in a more explicit and structured manner. This project is heavily inspired by Michael Altmann's excellent work with FluentResults.

main nuget downloads

References

This library targets .NET Standard 2.0, .NET 6.0, .NET 7.0, and .NET 8.0.

Dependencies

This library has no dependencies.

Advantages of this library

  • ๐Ÿชถ Lightweight โ€” Only contains what's necessary to implement the Result Pattern.
  • โš™๏ธ Extensible โ€” Simple interfaces and base classes make it easy to adapt.
  • ๐Ÿงฑ Immutable โ€” Results and errors are immutable and cannot be changed after being created.
  • ๐Ÿงต Thread-safe โ€” The Error list and Metadata dictionary use Immutable classes for thread-safety.
  • โœจ Modern โ€” Built against the latest version of .NET using the most recent best practices.
  • ๐Ÿงช Native โ€” Written, compiled, and tested against the latest versions of .NET.
  • โค๏ธ Compatible โ€” Available for dozens of versions of .NET as a .NET Standard 2.0 library.
  • ๐Ÿชš Trimmable โ€” Compatible with ahead-of-time compilation (AOT) as of .NET 7.0.
  • ๐Ÿš€ Performant โ€” Heavily optimized and benchmarked to aim for the highest possible performance.

Extensions

Several extensions are available to simplify implementation that use LightResults.

Documentation

Make sure to read the docs for the full API.

Getting Started

LightResults consists of only three classes Result, Result<TValue>, and Error.

  • The Result class represents a generic result indicating success or failure.
  • The Result<TValue> class represents a result with a value.
  • The Error class represents an error with a message and associated metadata.

Creating a successful result

Successful results can be created using the Ok method.

var successResult = Result.Ok();

var successResultWithValue = Result.Ok(349.4);

Creating a failed result

Failed results can be created using the Fail method.

var failedResult = Result.Fail();

var failedResultWithMessage = Result.Fail("Operation failed!");

var failedResultWithMessageAndMetadata = Result.Fail("Operation failed!", ("Exception", ex));

Checking the state of a result

There are two properties for results, IsSuccess and IsFailed. Both are mutually exclusive.

if (result.IsSuccess)
{
    // The result is successful therefore IsFailed will be false.
}

if (result.IsFailed)
{
    // The result is failed therefore IsSuccess will be false.
    if (result.Error.Message.Length > 0)
        Console.WriteLine(result.Error.Message);
    else
        Console.WriteLine("An unknown error occured!");
}

Getting the value

The value from a successful result can be retrieved through the Value property.

if (result.IsSuccess)
{
    var value = result.Value;
}

Creating errors

Errors can be created with or without a message.

var errorWithoutMessage = new Error();

var errorWithMessage = new Error("Something went wrong!");

With metadata.

var errorWithMetadataTuple = new Error(("Key", "Value"));

var metadata = new Dictionary<string, object> { { "Key", "Value" } };
var errorWithMetadataDictionary = new Error(metadata);

Or with a message and metadata.

var errorWithMetadataTuple = new Error("Something went wrong!", ("Key", "Value"));

var metadata = new Dictionary<string, object> { { "Key", "Value" } };
var errorWithMetadataDictionary = new Error("Something went wrong!", metadata);

Custom errors

The best way to represent specific errors is to make custom error classes that inherit from Error and define the error message as a base constructor parameter.

public sealed class NotFoundError : Error
{
    public NotFoundError()
        : base("The resource cannot be found.")
    {
    }
}

var notFoundError = new NotFoundError();
var notFoundResult = Result.Fail(notFoundError);

Then the result can be checked against that error type.

if (result.IsFailed && result.HasError<NotFound>())
{
    // Looks like the resource was not found, we better do something about that!
}

This can be especially useful when combined with metadata to handle exceptions.

public sealed class UnhandledExceptionError : Error
{
    public UnhandledExceptionError(Exception ex)
        : base("An unhandled exception occured.", ("Exception", ex))
    {
    }
}

Which allows us to continue using try-catch blocks in our code but return from them with a result instead of throwing an exception.

public Result DoSomeWork()
{
    try
    {
        // We must not throw an exception in this method!
    }
    catch(Exception ex)
    {
        var unhandledExceptionError = new UnhandledExceptionError(ex);
        return Result.Fail(unhandledExceptionError);
    }
    
    return Result.Ok();
}

We can further simplify creating errors by creating an error factory.

public static AppError
{
    public Result NotFound()
    {
        var notFoundError = new NotFoundError();
        return Result.Fail(notFoundError);
    }

    public Result UnhandledException(Exception ex)
    {
        var unhandledExceptionError = new UnhandledExceptionError(ex)
        return Result.Fail(unhandledExceptionError);
    }
}

Which clearly and explicitly describes the results.

public Result GetPerson(int id)
{
    var person = _database.GetPerson(id);
    
    if (person is null)
        return AppError.NotFound();
    
    return Result.Ok();
}

Static abstract members in interfaces

Note: Applies to .NET 7.0 (C# 11.0) or higher only!

Thanks to the static abstract members in interfaces introduced in .NET 7.0 (C# 11.0), it is possible to use generics to obtain access to the methods of the generic variant of the result. As such the error factory can be enhanced to take advantage of that.

public static AppError
{
    public Result NotFound()
    {
        var notFoundError = new NotFoundError();
        return Result.Failt(notFoundError);
    }
    
    public TResult NotFound<TResult>()
    {
        var notFoundError = new NotFoundError(); 
        return TResult.Fail(notFoundError);
    }
}

lightresults's People

Contributors

dependabot[bot] avatar jscarle avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar

Forkers

pressxtochris

lightresults's Issues

Blazor serilog ingest

Hi, I'm having an issue when trying to serialize the error property in Result. Do you have a solution for that by any chance?

at LightResults.Result1.get_Error() at System.Text.Json.Serialization.Metadata.JsonPropertyInfo1.GetMemberAndWriteJson(Object obj, WriteStack& state, Utf8JsonWriter writer)
at System.Text.Json.Serialization.Converters.ObjectDefaultConverter1.OnTryWrite(Utf8JsonWriter writer, T value, JsonSerializerOptions options, WriteStack& state) at System.Text.Json.Serialization.JsonConverter1.TryWrite(Utf8JsonWriter writer, T& value, JsonSerializerOptions options, WriteStack& state)
at System.Text.Json.Serialization.JsonConverter1.WriteCore(Utf8JsonWriter writer, T& value, JsonSerializerOptions options, WriteStack& state) at System.Text.Json.Serialization.Metadata.JsonTypeInfo1.d__9.MoveNext()
at System.Text.Json.Serialization.Metadata.JsonTypeInfo1.<SerializeAsync>d__9.MoveNext() at System.Text.Json.Serialization.Metadata.JsonTypeInfo1.d__9.MoveNext()
at Microsoft.AspNetCore.Mvc.Formatters.SystemTextJsonOutputFormatter.d__5.MoveNext()
at Microsoft.AspNetCore.Mvc.Infrastructure.ResourceInvoker.<g__Logged|22_0>d.MoveNext()
at Microsoft.AspNetCore.Mvc.Infrastructure.ResourceInvoker.<g__Awaited|30_0>d`2.MoveNext()
at Microsoft.AspNetCore.Mvc.Infrastructure.ResourceInvoker.Rethrow(ResultExecutedContextSealed context)
at Microsoft.AspNetCore.Mvc.Infrastructure.ResourceInvoker.ResultNext[TFilter,TFilterAsync](State& next, Scope& scope, Object& state, Boolean& isCompleted)
at Microsoft.AspNetCore.Mvc.Infrastructure.ResourceInvoker.InvokeResultFilters()
--- End of stack trace from previous location ---
at Microsoft.AspNetCore.Mvc.Infrastructure.ResourceInvoker.<g__Awaited|25_0>d.MoveNext()
at Microsoft.AspNetCore.Mvc.Infrastructure.ResourceInvoker.Rethrow(ResourceExecutedContextSealed context)
at Microsoft.AspNetCore.Mvc.Infrastructure.ResourceInvoker.Next(State& next, Scope& scope, Object& state, Boolean& isCompleted)
at Microsoft.AspNetCore.Mvc.Infrastructure.ResourceInvoker.<g__Awaited|20_0>d.MoveNext()
at Microsoft.AspNetCore.Mvc.Infrastructure.ResourceInvoker.<g__Logged|17_1>d.MoveNext()
at Microsoft.AspNetCore.Mvc.Infrastructure.ResourceInvoker.<g__Logged|17_1>d.MoveNext()
at Microsoft.AspNetCore.Routing.EndpointMiddleware.<g__AwaitRequestTask|7_0>d.MoveNext()
at Microsoft.AspNetCore.Authorization.AuthorizationMiddleware.d__11.MoveNext()
at Serilog.AspNetCore.Ingestion.SerilogIngestionMiddleware.d__6.MoveNext()
at Serilog.AspNetCore.RequestLoggingMiddleware.d__10.MoveNext()
at Microsoft.AspNetCore.Diagnostics.ExceptionHandlerMiddlewareImpl.<g__Awaited|10_0>d.MoveNext()

Default Result<MyClass> shouldn't be Success

Consider this (valid) code:

MyClass myClass = new();
myClass.CheckSuccess();//checks the result without assigning anything to the result variable

public class MyClass
{
    public void CheckSuccess()
    {
        if(result.IsSuccess(out var value))
            Console.WriteLine($"{value.SomeProp}");
    }
    
    public void Asign()//this is only here to make the warning disappear
    {
        result = Result.Ok(this);
    }
    Result<MyClass> result;
    public string SomeProp { get; set; } = "Hej";
}

Which throws System.NullReferenceException
Because the Result class check for absence of the errors, it present itself as successful even if the value is null.

I agree the sample isn't well designed code, but LightResults shouldn't allow throwing such exception.

Also the workaround here is pretty simple - just assign failed result.

There are some solution to this problem - probably the best one would be to start the Result with an error and remove it on Result creation.

If you give me green light I can work on it.

How to make Result to force me to not use result.Value if result.IsFailed==true

Consider this:

Resutl<MyClass> result = _service.GetMyClass();//is set to failed path
Console.WriteLine($"{result.Value.SomeProperty}");

Which will fail with Result is failed. Value is not set., which is logical, right.

My concern is that I shouldn't be allowed to shoot myself in my leg or IDE should be able to tell.

Similarly like nullable is handled in .net.

Extension method to collect `IEnumerable<Result<T>>` into `Result<IEnumerable<T>>`

I have some cases where I'm concurrently calling a method that returns a Result<T> many times, so I end up with an IEnumerable<Result<T>>. I'd like to collect these into a Result<IEnumerable<T>>. Currently I can work with ternaries and LINQ to get all the values (if successful) or errors (if not), but it's not super pretty.

Here are a couple of example use cases:

public abstract Result DoOneThing(Guid id);

public Result DoManyThings(IEnumerable<Guid> ids)
{
    var results = ids.Select(DoOneThing).ToList();

    return results.TrueForAll(r => r.IsSuccess())
        ? Result.Ok()
        : Result.Fail(results.SelectMany(x => x.Errors));
}


public abstract Result<int> GetOneThing(Guid id);

public Result<IEnumerable<int>> GetManyThings(IEnumerable<Guid> ids)
{
    var results = ids.Select(GetOneThing).ToList();

    return results.TrueForAll(r => r.IsSuccess())
        ? Result.Ok(results.Select(r => r.IsSuccess(out var value) ? value : default))
        : Result.Fail<IEnumerable<int>>(results.SelectMany(x => x.Errors));
}


public abstract Task<Result> DoOneThingAsync(Guid id);

public async Task<Result> DoManyThingsAsync(IEnumerable<Guid> ids)
{
    var tasks = ids.Select(async i => await DoOneThingAsync(i));
    var results = await Task.WhenAll(tasks);

    return Array.TrueForAll(results, r => r.IsSuccess())
        ? Result.Ok()
        : Result.Fail(results.SelectMany(x => x.Errors));
}


public abstract Task<Result<int>> GetOneThingAsync(Guid id);

public async Task<Result<IEnumerable<int>>> GetManyThingsAsync(IEnumerable<Guid> ids)
{
    var tasks = ids.Select(async i => await GetOneThingAsync(i));
    var results = await Task.WhenAll(tasks);

    return Array.TrueForAll(results, r => r.IsSuccess())
        ? Result.Ok(results.Select(r => r.IsSuccess(out var value) ? value : default))
        : Result.Fail<IEnumerable<int>>(results.SelectMany(x => x.Errors));
}

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    ๐Ÿ–– Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. ๐Ÿ“Š๐Ÿ“ˆ๐ŸŽ‰

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google โค๏ธ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.