int
, came in string
- return 400your@mail.com
, and 123Petya
- return 422Unfortunately, the standard ASP.NET MVC binding mechanism does not distinguish between type mismatch errors (gotstring
instead ofint
) and validation, so if you want to distinguish between 400 and 422 response codes, you will have to do it yourself. But it's not about that.
BusinessLogicException
and log them separately (or not log). Then make a similar HttpException
for HttpException
, DbValidationException
and others. Hmm, you need to remember which exceptions you need to catch and which not. Similarly, in Java, there are checked exceptions, let's bring in .NET! It is only necessary to take into account that not all exceptions can be caught and processed and not to forget about the peculiarities of working with exceptions in the TPL . And how is it, well, this performance .RequestDto -> IActionResult
, but RequestDto -> IActionResult | Exception
RequestDto -> IActionResult | Exception
: the method may succeed or something may go wrong.try / catch
block, or override the processing method at the application level. The first option is bad because you have to duplicate try / catch
everywhere and keep track of the types of errors being caught. The second is that we lose the execution context. public class Result { public bool Success { get; private set; } public string Error { get; private set; } public bool Failure { get { return !Success; } } protected Result(bool success, string error) { Contracts.Require(success || !string.IsNullOrEmpty(error)); Contracts.Require(!success || string.IsNullOrEmpty(error)); Success = success; Error = error; } //... }
string
type is not quite convenient for working with errors. Replace the string with the type Failure
. Unlike the Scott variant, Failure
is not a union-type, but an ordinary class. Pattern matching to work with errors is replaceable with polymorphism. In order to save additional error information we will use the Data
property. Often this data needs to be simply serialized, so a specific type is not so important. public class Failure { public Failure(params Failure[] failures) { if (!failures.Any()) { throw new ArgumentException(nameof(failures)); } Message = failures.Select(x => x.Message).Join(Environment.NewLine); var dict = new Dictionary<string, object>(); for(var i = 0; i < failures.Length; i++) { dict[(i + 1).ToString()] = failures[i]; } Data = new ReadOnlyDictionary<string, object>(dict); } public Failure(string message) { Message = message; } public Failure(string message, IDictionary<string, object> data) { Message = message; Data = new ReadOnlyDictionary<string, object>(data); } public string Message { get; } public ReadOnlyDictionary<string, object> Data { get; protected set; } }
public class ValidationFailure: Failure { public ValidationResult[] ValidationResults { get; } public ValidationFailure(IEnumerable<ValidationResult> validationResults) : base(ValidationResultsToStrings(validationResults)) { ValidationResults = validationResults?.ToArray(); if (ValidationResults == null || !ValidationResults.Any()) { throw new ArgumentException(nameof(validationResults)); } Data = new ReadOnlyDictionary<string, object>( ValidationResults.ToDictionary( x => x.MemberNames.Join(","), x => (object)x.ErrorMessage)); } private static string ValidationResultsToStrings( IEnumerable<ValidationResult> validationResults) => validationResults .Select(x => x.ErrorMessage) .Join(Environment.NewLine); }
Value
Result
operator overload &, | true false
&, | true false
so that &&
and ||
. Close the value and instead provide the function Return
. Now it is impossible to make a mistake and not to check the IsFaulted
property: the method requires TDestination
both parameter T
and Failure
lead to type TDestination
. This fixes a problem with return codes that you can forget to check. The result simply cannot be obtained without processing the variant with an error. public class Result { public static implicit operator Result (Failure failure) => new Result(failure); // https://stackoverflow.com/questions/5203093/how-does-operator-overloading-of-true-and-false-work public static bool operator false(Result result) => result.IsFaulted; public static bool operator true(Result result) => !result.IsFaulted; public static Result operator &(Result result1, Result result2) => Result.Combine(result1, result2); public static Result operator |(Result result1, Result result2) => result1.IsFaulted ? result2 : result1; public Failure Failure { get; private set; } public bool IsFaulted => Failure != null; }
result.Return<IActionResult>(Ok, x => BadRequest(x.Message));
public IActionResult Post(ChangeUserNameCommand command) { var res = command.Validate(); if (res.IsFaulted) return res; return ChangeUserName(command) .OnSuccess(SendEmail) .Return<IActionResult>(Ok, x => BadRequest(x.Message)); }
if(res.IsFaulted) return res;
will have to be repeated after each step. I would like to avoid this. This is SelectMany
cycle of Eric Lippert ’s articles about the nature of SelectMany
and the letter with the letter M can be by the way. In general, LINQ syntax supports not only IEnumerable
, but also any other types. The main SelectMany
implement SelectMany
aka Bind
. Add some scary code with templates. Here I will not go into details on how bind
works. If interested, read through Lippert or Vlashin . public static class ResultExtensions { public static Result<TDestination> Select<TSource, TDestination>( this Result<TSource> source, Func<TSource, TDestination> selector) => source.IsFaulted ? new Result<TDestination>(source.Failure) : selector(source.Value); public static Result<TDestination> SelectMany<TSource, TDestination>( this Result<TSource> source, Func<TSource, Result<TDestination>> selector) => source.IsFaulted ? new Result<TDestination>(source.Failure) : selector(source.Value); public static Result<TDestination> SelectMany<TSource, TIntermediate, TDestination>( this Result<TSource> result, Func<TSource, Result<TIntermediate>> inermidiateSelector, Func<TSource, TIntermediate, TDestination> resultSelector) => result.SelectMany<TSource, TDestination>(s => inermidiateSelector(s) .SelectMany<TIntermediate, TDestination>(m => resultSelector(s, m))); }
if(result.IsFaulted)
are performed “under the hood” using the LINQ syntax. public Result<UserNameChangedEvent> Declarative(ChangeUserNameCommand command) => from validatedCommand in command.Validate() from domainEvent in ChangeUserName(validatedCommand).OnSuccess(SendEmail) select domainEvent;
Source: https://habr.com/ru/post/347284/
All Articles