- Notifications
You must be signed in to change notification settings - Fork124
A generalised Result object implementation for .NET/C#
License
altmann/FluentResults
Folders and files
Name | Name | Last commit message | Last commit date | |
---|---|---|---|---|
Repository files navigation
FluentResults is a lightweight .NET library developed to solve a common problem. It returns an object indicating success or failure of an operation instead of throwing/using exceptions.
You can installFluentResults with NuGet:
Install-Package FluentResults
❤️ The most needed community feature is pushed to nuget:FluentResults.Extensions.AspNetCore Readdocumentation. Try it, test it,give feedback.
- Generalised container which works in all contexts (ASP.NET MVC/WebApi, WPF, DDD Domain Model, etc)
- Storemultiple errors in one Result object
- Storepowerful and elaborative Error and Success objects instead of only error messages in string format
- Designing Errors/Success in an object-oriented way
- Store theroot cause with chain of errors in a hierarchical way
- Provide
- .NET Standard, .NET Core, .NET 5+ and .NET Full Framework support (details see.NET Targeting)
- SourceLink support
- powerfulcode samples which show the integration with famous or common frameworks/libraries
- NEW EnhancedFluentAssertions Extension to assert FluentResult objects in an elegant way
- IN PREVIEWReturning Result Objects from ASP.NET Controller
To be honest, the pattern - returning a Result object indicating success or failure - is not at all a new idea. This pattern comes from functional programming languages. With FluentResults this pattern can also be applied in .NET/C#.
The articleExceptions for Flow Control by Vladimir Khorikov describes very good in which scenarios the Result pattern makes sense and in which not. See thelist of Best Practices and thelist of resources to learn more about the Result Pattern.
A Result can store multiple Error and Success messages.
// create a result which indicates successResultsuccessResult1=Result.Ok();// create a result which indicates failureResulterrorResult1=Result.Fail("My error message");ResulterrorResult2=Result.Fail(newError("My error message"));ResulterrorResult3=Result.Fail(newStartDateIsAfterEndDateError(startDate,endDate));ResulterrorResult4=Result.Fail(newList<string>{"Error 1","Error 2"});ResulterrorResult5=Result.Fail(newList<IError>{newError("Error 1"),newError("Error 2")});
The classResult
is typically used by void methods which have no return value.
publicResultDoTask(){if(this.State==TaskState.Done)returnResult.Fail("Task is in the wrong state.");// rest of the logicreturnResult.Ok();}
Additionally a value from a specific type can also be stored if necessary.
// create a result which indicates successResult<int>successResult1=Result.Ok(42);Result<MyCustomObject>successResult2=Result.Ok(newMyCustomObject());// create a result which indicates failureResult<int>errorResult=Result.Fail<int>("My error message");
The classResult<T>
is typically used by methods with a return type.
publicResult<Task>GetTask(){if(this.State==TaskState.Deleted)returnResult.Fail<Task>("Deleted Tasks can not be displayed.");// rest of the logicreturnResult.Ok(task);}
After you get a Result object from a method you have to process it. This means, you have to check if the operation was completed successfully or not. The propertiesIsSuccess
andIsFailed
in the Result object indicate success or failure. The value of aResult<T>
can be accessed via the propertiesValue
andValueOrDefault
.
Result<int>result=DoSomething();// get all reasons why result object indicates success or failure.// contains Error and Success messagesIEnumerable<IReason>reasons=result.Reasons;// get all Error messagesIEnumerable<IError>errors=result.Errors;// get all Success messagesIEnumerable<ISuccess>successes=result.Successes;if(result.IsFailed){// handle error casevarvalue1=result.Value;// throws exception because result is in failed statevarvalue2=result.ValueOrDefault;// return default value (=0) because result is in failed statereturn;}// handle success casevarvalue3=result.Value;// return value and doesn't throw exception because result is in success statevarvalue4=result.ValueOrDefault;// return value because result is in success state
There are many Result Libraries which store only simple string messages. FluentResults instead stores powerful object-oriented Error and Success objects. The advantage is all the relevant information of an error or success is encapsulated within one class.
The entire public api of this library uses the interfacesIReason
,IError
andISuccess
for representing a reason, error or success.IError
andISuccess
inherit fromIReason
. If at least oneIError
object exists in theReasons
property then the result indicates a failure and the propertyIsSuccess
is false.
You can create your ownSuccess
orError
classes when you inherit fromISuccess
orIError
or if you inherit fromSuccess
orError
.
publicclassStartDateIsAfterEndDateError:Error{publicStartDateIsAfterEndDateError(DateTimestartDate,DateTimeendDate):base($"The start date{startDate} is after the end date{endDate}"){Metadata.Add("ErrorCode","12");}}
With this mechanism you can also create a classWarning
. You can choose if a Warning in your system indicates a success or a failure by inheriting fromSuccess
orError
classes.
In some cases it is necessary to chain multiple error and success messages in one result object.
varresult=Result.Fail("error message 1").WithError("error message 2").WithError("error message 3").WithSuccess("success message 1");
Very often you have to create a fail or success result depending on a condition. Usually you can write it in this way:
varresult=string.IsNullOrEmpty(firstName)?Result.Fail("First Name is empty"):Result.Ok();
With the methodsFailIf()
andOkIf()
you can also write in a more readable way:
varresult=Result.FailIf(string.IsNullOrEmpty(firstName),"First Name is empty");
If an error instance should be lazily initialized, overloads acceptingFunc<string>
orFunc<IError>
can be used to that effect:
varlist=Enumerable.Range(1,9).ToList();varresult=Result.FailIf(list.Any(IsDivisibleByTen),()=>newError($"Item{list.First(IsDivisibleByTen)} should not be on the list"));boolIsDivisibleByTen(inti)=>i%10==0;// rest of the code
In some scenarios you want to execute an action. If this action throws an exception then the exception should be caught and transformed to a result object.
varresult=Result.Try(()=>DoSomethingCritical());
You can also return your ownResult
object
varresult=Result.Try(()=>{if(IsInvalid()){returnResult.Fail("Some error");}intid=DoSomethingCritical();returnResult.Ok(id);});
In the above example the default catchHandler is used. The behavior of the default catchHandler can be overwritten via the global Result settings (see next example). You can control how the Error object looks.
Result.Setup(cfg=>{cfg.DefaultTryCatchHandler= exception=>{if(exceptionisSqlTypeExceptionsqlException)returnnewExceptionalError("Sql Fehler",sqlException);if(exceptionisDomainExceptiondomainException)returnnewError("Domain Fehler").CausedBy(newExceptionError(domainException.Message,domainException));returnnewError(exception.Message);};});varresult=Result.Try(()=>DoSomethingCritical());
It is also possible to pass a custom catchHandler via theTry(..)
method.
varresult=Result.Try(()=>DoSomethingCritical(), ex=>newMyCustomExceptionError(ex));
You can also store the root cause of the error in the error object. With the methodCausedBy(...)
the root cause can be passed as Error, list of Errors, string, list of strings or as exception. The root cause is stored in theReasons
property of the error object.
Example 1 - root cause is an exception
try{//export csv file}catch(CsvExportExceptionex){returnResult.Fail(newError("CSV Export not executed successfully").CausedBy(ex));}
Example 2 - root cause is an error
ErrorrootCauseError=newError("This is the root cause of the error");Resultresult=Result.Fail(newError("Do something failed",rootCauseError));
Example 3 - reading root cause from errors
Resultresult= ....;if(result.IsSuccess)return;foreach(IErrorerrorinresult.Errors){foreach(ExceptionalErrorcausedByExceptionalErrorinerror.Reasons.OfType<ExceptionalError>()){Console.WriteLine(causedByExceptionalError.Exception);}}
It is possible to add metadata to Error or Success objects.
One way of doing that is to call the methodWithMetadata(...)
directly where result object is being created.
varresult1=Result.Fail(newError("Error 1").WithMetadata("metadata name","metadata value"));varresult2=Result.Ok().WithSuccess(newSuccess("Success 1").WithMetadata("metadata name","metadata value"));
Another way is to callWithMetadata(...)
in constructor of theError
orSuccess
class.
publicclassDomainError:Error{publicDomainError(stringmessage):base(message){WithMetadata("ErrorCode","12");}}
Multiple results can be merged with the static methodMerge()
.
varresult1=Result.Ok();varresult2=Result.Fail("first error");varresult3=Result.Ok<int>();varmergedResult=Result.Merge(result1,result2,result3);
A list of results can be merged to one result with the extension methodMerge()
.
varresult1=Result.Ok();varresult2=Result.Fail("first error");varresult3=Result.Ok<int>();varresults=newList<Result>{result1,result2,result3};varmergedResult=results.Merge();
A result object can be converted to another result object with methodsToResult()
andToResult<TValue>()
.
// converting a result to a result from type Result<int> with default value of intResult.Ok().ToResult<int>();// converting a result to a result from type Result<int> with a custom valueResult.Ok().ToResult<int>(5);// converting a failed result to a result from type Result<int> without passing a custom value// because result is in failed state and therefore no value is neededResult.Fail("Failed").ToResult<int>();// converting a result to a result from type Result<float>Result.Ok<int>(5).ToResult<float>(v=>v);// converting a result from type Result<int> to result from type Result<float> without passing the converting// logic because result is in failed state and therefore no converting logic neededResult.Fail<int>("Failed").ToResult<float>();// converting a result to a result from type ResultResult.Ok<int>().ToResult();
A value of a result object to another value can be transformed via method ``Map(..)`
// converting a result to a result from type Result<float>Result.Ok<int>(5).Map(v=>newDto(5));
stringmyString="hello world";Result<T>result=myString;
from a single error
errormyError=newError("error msg");Resultresult=myError;
or from a list of errors
List<Error>myErrors=newList<Error>(){newError("error 1"),newError("error 2")};Resultresult=myErrors;
Binding is a transformation that returns aResult
|Result<T>
.It only evaluates the transformation if the original result is successful.The reasons of bothResult
will be merged into a new flattenedResult
.
// converting a result to a result which may failResult<string>r=Result.Ok(8).Bind(v=>v==5?"five":Result.Fail<string>("It is not five"));// converting a failed result to a result, which can also fail,// returns a result with the errors of the first result only,// the transformation is not evaluated because the value of the first result is not availableResult<string>r=Result.Fail<int>("Not available").Bind(v=>v==5?"five":Result.Fail<string>("It is not five"));// converting a result with value to a Result via a transformation which may failResult.Ok(5).Bind(x=>Result.OkIf(x==6,"Number is not 6"));// converting a result without value into a ResultResult.Ok().Bind(()=>Result.Ok(5));// just running an action if the original result is sucessful.Resultr=Result.Ok().Bind(()=>Result.Ok());
TheBind
has asynchronous overloads.
varresult=awaitResult.Ok(5).Bind(int n=>Task.FromResult(Result.Ok(n+1).WithSuccess("Added one"))).Bind(int n=>/* next continuation */);
Within the FluentResults library in some scenarios an ISuccess, IError or IExceptionalError object is created. For example if the methodResult.Fail("My Error")
is called then internally an IError object is created. If you need to overwrite this behavior and create in this scenario a custom error class then you can set the error factory via the settings. The same extension points are also available for ISuccess and IExceptionalError.
Result.Setup(cfg=>{cfg.SuccessFactory= successMessage=>newSuccess(successMessage).WithMetadata("Timestamp",DateTime.Now);cfg.ErrorFactory= errorMessage=>newError(errorMessage).WithMetadata("Timestamp",DateTime.Now);cfg.ExceptionalErrorFactory=(errorMessage,exception)=>newExceptionalError(errorMessage??exception.Message,exception).WithMetadata("Timestamp",DateTime.Now);});
If you want to add some information to all successes in a result you can useMapSuccesses(...)
on a result object.
varresult=Result.Ok().WithSuccess("Success 1");varresult2=result.MapSuccesses(e=>newSuccess("Prefix: "+e.Message));
If you want to add some information to all errors in a result you can useMapErrors(...)
on a result object. This method only iterate through the first level of errors, the root cause errors (in error.Reasons) are not changed.
varresult=Result.Fail("Error 1");varresult2=result.MapErrors(e=>newError("Prefix: "+e.Message));
Similar to the catch block for exceptions, the checking and handling of errors within Result object is also supported using some methods:
// check if the Result object contains an error from a specific typeresult.HasError<MyCustomError>();// check if the Result object contains an error from a specific type and with a specific conditionresult.HasError<MyCustomError>(myCustomError=>myCustomError.MyField==2);// check if the Result object contains an error with a specific metadata keyresult.HasError(error=>error.HasMetadataKey("MyKey"));// check if the Result object contains an error with a specific metadataresult.HasError(error=>error.HasMetadata("MyKey", metadataValue=>(string)metadataValue=="MyValue"));
AllHasError()
methods have an optional out parameter result to access the found errorors.
Checking if a result object contains a specific success object can be done with the methodHasSuccess()
// check if the Result object contains a success from a specific typeresult.HasSuccess<MyCustomSuccess>();// check if the Result object contains a success from a specific type and with a specific conditionresult.HasSuccess<MyCustomSuccess>(success=>success.MyField==3);
AllHasSuccess()
methods have an optional out parameter result to access the found successes.
Checking if a result object contains an error with an specific exception type can be done with the methodHasException()
// check if the Result object contains an exception from a specific typeresult.HasException<MyCustomException>();// check if the Result object contains an exception from a specific type and with a specific conditionresult.HasException<MyCustomException>(MyCustomException=>MyCustomException.MyField==1);
AllHasException()
methods have an optional out parameter result to access the found error.
varresult=Result.Fail<int>("Error 1");varoutcome=resultswitch{{IsFailed:true}=>$"Errored because{result.Errors}",{IsSuccess:true}=>$"Value is{result.Value}", _=>null};
var(isSuccess,isFailed,value,errors)=Result.Fail<bool>("Failure 1");var(isSuccess,isFailed,errors)=Result.Fail("Failure 1");
Sometimes it is necessary to log results. First create a logger:
publicclassMyConsoleLogger:IResultLogger{publicvoidLog(stringcontext,stringcontent,ResultBaseresult,LogLevellogLevel){Console.WriteLine("Result: {0} {1} <{2}>",result.Reasons.Select(reason=>reason.Message),content,context);}publicvoidLog<TContext>(stringcontent,ResultBaseresult,LogLevellogLevel){Console.WriteLine("Result: {0} {1} <{2}>",result.Reasons.Select(reason=>reason.Message),content,typeof(TContext).FullName);}}
Then you must register your logger in the Result settings:
varmyLogger=newMyConsoleLogger();Result.Setup(cfg=>{cfg.Logger=myLogger;});
Finally the logger can be used on any result:
varresult=Result.Fail("Operation failed").Log();
Additionally, a context can be passed in form of a string or of a generic type parameter. A custom message that provide more information can also be passed as content.
varresult=Result.Fail("Operation failed").Log("logger context","More info about the result");varresult2=Result.Fail("Operation failed").Log<MyLoggerContext>("More info about the result");
It's also possible to specify the desired log level:
varresult=Result.Ok().Log(LogLevel.Debug);varresult=Result.Fail().Log<MyContext>("Additional context",LogLevel.Error);
You can also log results only on successes or failures:
Result<int>result=DoSomething();// log with default log level 'Information'result.LogIfSuccess();// log with default log level 'Error'result.LogIfFailed();
Try it with the power of FluentAssertions andFluentResults.Extensions.FluentAssertions. Since v2.0 the assertion package is out of the experimental phase and its really a great enhancement to assert result objects in a fluent way.
FluentResults 3.x and above supports .NET Standard 2.0 and .NET Standard 2.1.If you need support for .NET Standard 1.1, .NET 4.6.1 or .NET 4.5 useFluentResults 2.x.
Here are some samples and best practices to be followed while using FluentResult or the Result pattern in general with some famous or commonly used frameworks and libraries.
- Domain model with a command handler
- Protecting domain invariants by using for example factory methods returning a Result object
- Make each error unique by making your own custom Error classes inheriting from IError interface or Error class
- If the method doesn't have a failure scenario then don't use the Result class as return type
- Be aware that you can merge multiple failed results or return the first failed result asap
Serializing Result objects (ASP.NET WebApi,Hangfire)
- Asp.net WebController
- Hangfire Job
- Don't serialize FluentResult result objects.
- Make your own custom ResultDto class for your public api in your system boundaries
- So you can control which data is submitted and which data is serialized
- Your public api is independent of third party libraries like FluentResults
- You can keep your public api stable
MediatR request handlers returning Result objects
- Full functional .NET Core sample code with commands/queries and a ValidationPipelineBehavior
- Returns business validation errors via a Result object from a MediatR request handler back to the consumer
- Don't throw exceptions based on business validation errors
- Inject command and query validation via MediatR PipelineBehavior and return a Result object instead of throwing an exception
- Error Handling — Returning Results by Michael Altmann
- Operation Result Pattern by Carl-Hugo Marcotte
- Exceptions for flow control in C# by Vladimir Khorikov
- Error handling: Exception or Result? by Vladimir Khorikov
- What is an exceptional situation in code? by Vladimir Khorikov
- Advanced error handling techniques by Vladimir Khorikov
- A Simple Guide by Isaac Cummings
- Flexible Error Handling w/ the Result Class by Khalil Stemmler
- Combining ASP.NET Core validation attributes with Value Objects by Vladimir Khorikov
I love this project but implementing features, answering issues or maintaining ci/release pipelines takes time - this is my freetime. If you like FluentResult and you find it useful, consider making a donation. Click on the sponsor button on the top right side.
Thanks to all the contributers and to all the people who gave feedback!
Copyright (c) Michael Altmann. SeeLICENSE for details.
About
A generalised Result object implementation for .NET/C#