Giter Club home page Giter Club logo

cranks.result's Introduction

Cranks.Result

A simple, strongly typed and boilerplate poor implementation of the Result pattern.

Nuget .NET

Description

Results.Crank aims to be a modern implementation of the results pattern, making use of new language features the got introduced in the recent years for C#, like Records and Pattern Matching. It's supposed to allow writing typed error handling code without having to write lots of boilerplate and focus on what's important: You're code.

Concepts

Designing methods for failure

If a method can fail by design, it should indicate so. The extent of how far you go with this is your decision. Personally I try to differentiate between errors that can happen by design, and errors that are unexpected.

  • A GetById method can fail by design, as the id can simply not exist. In this case, indicate so and use the IResult/IResult<TValue> types this library provides
  • A GetAll method can not fail by design (in most cases at least). It could fail if there is network error, but this would be an unexpected error which should still be thrown. In this case your method should return the result directly, without encapsulating it in a IResult<TValue>.

A few good and more indepth reads on this topic:

IReason and IResult

An IReason can either be an Error or a Success and is nothing more than a typed 1object encapsulating some error/success information.

An IResult on the other hand indicates if a method has Passed or Failed during its execution. In that sense (and also implementation wise), each IResult is also a IReason. The same way, each Passed is also a Success and Failed is also an Error.

An IResult can either be valueless, or it can contain a value, in which case IResult<TValue> should be used.

IResult should never be derived, whereas IReason can (and often should) be derived to provide typed error or success objects. Do not derive IReason directly though, but derive from either Error or Success.

All implementations of IReason and IResult are records and fully immutable. Modify them using the extension methods provided in ResultExtensions. To create new IResult use the factory methods in the static class Result. Extension methods and factory methods are fully alligned, allowing you to fluently type together result objects. For the technically interested, this is done using a source generator, converting the extension methods to normal static methods which forward to the extension methods.

Usage & Examples

Indicate a failable method

When there are designed for scenarios the make your fail, it should indicate so. You can do this by return IResult for methods without a return value, or IResult<TValue> for methods with a return value.

public IResult Method()
{
    if (successful)
    {
        return Result.Pass();
    }
    else
    {
        return Result.Fail();
    }
}

public IResult<string> Method()
{
    if (successful)
    {
        return Result.Pass("success value);
    }
    else
    {
        return Result.Fail<string>();
    }
}

Provide a message with your result

It can be useful to provide a quick message with your result, e.g. for logging purposes, to indicate what happend

public IResult Method()
{
    if (successful)
    {
        return Result.Pass()
                     .WithMessage("all good");
    }
    else
    {
        return Result.Fail()
                     .WithMessage("oh no!");
    }
}

Add errors or successes to indicate what caused your result

You can add Error and Success objects as causes for your result, to better understand what happend.

public IResult Method()
{
    if (successful)
    {
        return Result.Pass()
                     .WithSuccess(new Success("I did everything right"));
    }
    else
    {
        return Result.Fail()
                     .WithError(new Error("I mad a boo"));
    }
}

Since all IResults are IReasons you can also add another methods result as the reasons for your result.

public IResult Method()
{
    if (Validate is Failed failed)
    {
        return Result.WithError(failed)
                     .WithMessage("Validation failed");
    }

    return Result.Passed();
}

private IResult Validate()
{
    // validate and return result indicating validation success
}

Use factory methods and the fluent api to build up your results

In most cases, passing or failing in a method depends on different operations, and is not clear from the beginning. You can use a handful of useful methods to build up your results.

public IResult<int> Method(int a, int b)
{
    return Result.WithErrorIf(a < b, new Error("a is smaller than b"))
                 .WithErrorIf(a > b, "b is smaller than a") // strings get casted to Error/Success records if appropriate
                 .WithSuccessIf(a == b, new Success("a and b are equal"))
                 .WithValue(a * b); // value only gets added if Passed. In Failed scenarios it gets dropped.
}

Checking a methods IResult

When you have a method that returns an IResult, you can take different actions depending on the result. It is recommended to use pattern matching for this case, of course you can also use a more classical approach.

public IResult Method(int a, int b)
{
    var result = MethodThatReturnsResultOfInt();

    switch (result)
    {
        case Passed<int> { Value: var value}:
            _logger.LogInfo($"The method returned {value}");
            break;

        case Failed { Message: var message }:
            _logger.LogWarning($"Something went wrong: {message}");
            break;
    }
}

Custom Errors and Successes

It is possible (and recommended) to use your own typed Error and Success objects. Although it is possible to use the Cranks.Result with just the default Error and Success objects, you get much more out of it when using them.

Errors are simple records and can be defined with a single line of code! This makes them easy to define without cluttering your application in lots of boilerplate code.

public record MySimpleError : Error;

public record MyErrorWithDataFields(int value) : Error;

public record MyErrorWithCustomMessage(int value) : Error($"This value is invalid: {value}");

You can use your customized errors everywhere you would otherwise use an error:

public IResult Method()
{
    return Result.WithError<MySimpleError>()
                 .WithError(new MyErrorWithDataFields(42))
                 .WithErrorIf(condition, new MyErrorWithCustomMessage(1337));
}

Todos and possible enhancements

  • Analyzer which throws an error when trying to derive from IResult outside of the library
  • Improved Usage: Try, TryOrError Others?
  • Idea to improve source generation to provide Result.FactoryMethods. But it has downsides... Analyze more.
  • ASP.NET Core wrapper. Provide base error types and mapping rules for IActionResult so that results can be simply returned
  • Stringifier to modify how errors get converted to strings?

cranks.result's People

Contributors

crazycrank avatar

Stargazers

Hamed Naeemaei avatar Carlos Acitores Deval avatar Víctor Fernández Portero avatar Luca Milan avatar Ahmed Şeref avatar Eduardo avatar Saleem Basit avatar XtremeOwnage avatar James Gutierrez avatar Brian Scott avatar  avatar  avatar Alan Keller avatar

Watchers

 avatar

cranks.result's Issues

Does this really need .net 5.0 ?

Does this really need .net 5.0 ?
I had seen your post on reddit and it looked nice. Month later, I think about it and went to try to implement it. I am using the latest long term support .net version which is net core 3.1. So basically I can't use this library. Perhaps this library could be released under .net standard 2.1 ? If there is a good reason to have it as 5.0 that's fine, but otherwise, it's just losing compatibility with older projects.

Result.TryAction / Try Action

I am sorry, I don't have any time available to sit down and merge/pull this currently, but, I can offer a few pieces of code on how I leverage TryAction.

        /// <summary>
        /// Invoke the specified action, and return a tuple which provides the value, and indicates if action was successful.
        /// </summary>
        /// <typeparam name="T"></typeparam>
        /// <param name="Action"></param>
        /// <param name="OnError">Optional- This code is invoked if an exception is caught. </param>
        /// <returns></returns>
        protected static T TryAction<T>(Func<T> Action, Func<Exception, T> OnError = null)
        {
            if (Action is null)
                throw new ArgumentNullException(nameof(Action));

            try
            {
                return Action.Invoke();
            }
            catch (Exception ex)
            {
                if (!(OnError is null))
                    return OnError.Invoke(ex);

                return default(T);
            }
        }
        /// <summary>
        /// Invoke the provided action.
        /// </summary>
        /// <param name="Action">The action to be invoked.</param>
        /// <param name="OnError">OPTIONAL: This code will be invoked upon caught exception. The returned boolean value will be passed to the return of this method.</param>
        /// <returns>bool indicating if the action was successful or not. Can be overwritten by the OnError action.</returns>
        protected static bool TryAction(Action Action, Func<Exception, bool> OnError = null)
        {
            if (Action is null)
                throw new ArgumentNullException(nameof(Action));

            try
            {
                Action.Invoke();
                return true;
            }
            catch (Exception ex)
            {
                if (!(OnError is null))
                    return OnError.Invoke(ex);

                return false;
            }
        }

Also- this isn't noted on the repo anywhere- however, I also have this code.

It may be useful for a "TimedResult", or Result.WithTime() etc.

       /// <summary>
        /// Will execute the provided Func, time the total duration, and log the results as a debug message to the ILogger instance.
        /// </summary>
        /// <typeparam name="T"></typeparam>
        /// <param name="ActionDescription">Brief description which will be logged as "Action"</param>
        /// <param name="Action">The action to execute.</param>
        /// <remarks>Use this to time something which returns a countable collection.</remarks>
        /// <returns>Results from the action.</returns>
        protected T Log_And_Time<T>(string ActionDescription, Func<T> Action)
        {
            if (Action == null)
                throw new NullReferenceException($"{nameof(Action)} is null");

            Stopwatch sw = Stopwatch.StartNew();
            T result = Action.Invoke();
            sw.Stop();

            Dictionary<string, object> PropsToLog = new Dictionary<string, object>()
            {
                {"Action", ActionDescription },
                { "TimeMS", sw.ElapsedMilliseconds },
                { "Time", sw.Elapsed.Humanize() }
            };

            if (result is ICollection res_col)
                PropsToLog.Add("Record_Count", res_col.Count);

            this.log.Debug(PropsToLog);

            return result;
        }

        /// <summary>
        /// Will execute the provided Func, time the total duration, and log the results as a debug message to the ILogger instance.
        /// </summary>
        /// <typeparam name="T"></typeparam>
        /// <param name="ActionDescription">Brief description which will be logged as "Action"</param>
        /// <param name="Action">The action to execute.</param>
        /// <remarks>Use this to time something which returns a countable collection.</remarks>
        /// <returns>Results from the action.</returns>
        protected Task<T> Log_And_TimeAsync<T>(string ActionDescription, Func<T> Action)
        {
            if (Action == null)
                throw new NullReferenceException($"{nameof(Action)} is null");

            var token = new System.Threading.CancellationToken(false);
            Stopwatch sw = Stopwatch.StartNew();

            var the_task = Task.Run(Action);

            return the_task.ContinueWith((Task<T> finishedTask) =>
            {
                sw.Stop();

                Dictionary<string, object> PropsToLog = new Dictionary<string, object>()
                {
                    {"Action", ActionDescription },
                    { "TimeMS", sw.ElapsedMilliseconds },
                    { "Time", sw.Elapsed.Humanize() }
                };

                if (finishedTask.Result is ICollection res_col)
                    PropsToLog.Add("Record_Count", res_col.Count);

                this.log.Debug(PropsToLog);

                return finishedTask.Result;
            }, token, TaskContinuationOptions.OnlyOnRanToCompletion, TaskScheduler.Default);
        }

        /// <summary>
        /// Will execute the provided action, time the total duration, and log the results as a debug message to the ILogger instance.
        /// </summary>
        /// <typeparam name="T"></typeparam>
        /// <param name="ActionDescription">Brief description which will be logged as "Action"</param>
        /// <param name="Action">The action to execute.</param>
        /// <remarks>Use this to time and log a simple action.</remarks>
        /// <returns>Results from the action.</returns>
        protected Task Log_And_TimeAsync(string ActionDescription, Action Action, CancellationToken? cancellationToken = null)
        {
            if (Action == null)
                throw new ArgumentNullException(nameof(Action), $"{nameof(Action)} is null");

            if (!cancellationToken.HasValue)
                cancellationToken = new CancellationToken(false);

            Stopwatch sw = Stopwatch.StartNew();

            var task = Task.Run(Action);
            task.ContinueWith((task) =>
            {
                sw.Stop();

                this.log.Debug(new
                {
                    Action = ActionDescription,
                    TimeMS = sw.ElapsedMilliseconds,
                    Time = sw.Elapsed.Humanize(),
                });
            }, cancellationToken.Value, TaskContinuationOptions.OnlyOnRanToCompletion, TaskScheduler.Default);

            return task;
        }

        /// <summary>
        /// Will execute the provided action, time the total duration, and log the results as a debug message to the ILogger instance.
        /// </summary>
        /// <typeparam name="T"></typeparam>
        /// <param name="ActionDescription">Brief description which will be logged as "Action"</param>
        /// <param name="Action">The action to execute.</param>
        /// <remarks>Use this to time and log a simple action.</remarks>
        /// <returns>Results from the action.</returns>
        protected void Log_And_Time(string ActionDescription, Action Action)
        {
            if (Action == null)
                throw new ArgumentNullException(nameof(Action), $"{nameof(Action)} is null");

            Stopwatch sw = Stopwatch.StartNew();
            Action.Invoke();
            sw.Stop();

            this.log.Debug(new
            {
                Action = ActionDescription,
                TimeMS = sw.ElapsedMilliseconds,
                Time = sw.Elapsed.Humanize(),
            });
        }
        

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.