gpt4 book ai didi

c# - 验证引发异常的 CQS 系统

转载 作者:太空狗 更新时间:2023-10-29 19:46:26 24 4
gpt4 key购买 nike

我一直在读到异常应该只用于“异常”的东西,而不是用来控制程序的流程。然而,对于 CQS 实现,这似乎是不可能的,除非我开始修改实现来处理它。我想展示我是如何实现这个的,看看这是否真的很糟糕。我正在使用装饰器,因此命令无法返回任何内容(异步任务除外),因此 ValidationResult 是不可能的。让我知道!

此示例将使用 ASP.NET MVC

Controller :(api)

[Route(ApiConstants.ROOT_API_URL_VERSION_1 + "DigimonWorld2Admin/Digimon/Create")]
public class CreateCommandController : MetalKidApiControllerBase
{
private readonly IMediator _mediator;

public CreateCommandController(IMediator mediator) => _mediator = mediator;

[HttpPost]
public async Task Post([FromBody]CreateCommand command) =>
await _mediator.ExecuteAsync(command);
}

CommandExceptionDecorator 是链中的第一个:
public class CommandHandlerExceptionDecorator<TCommand> : ICommandHandler<TCommand> where TCommand : ICommand
{
private readonly ICommandHandler<TCommand> _commandHandler;
private readonly ILogger _logger;
private readonly IUserContext _userContext;

public CommandHandlerExceptionDecorator(ICommandHandler<TCommand> commandHandler, ILogger logger,
IUserContext userContext)
{
Guard.IsNotNull(commandHandler, nameof(commandHandler));
Guard.IsNotNull(logger, nameof(logger));

_commandHandler = commandHandler;
_logger = logger;
_userContext = userContext;
}

public async Task ExecuteAsync(TCommand command, CancellationToken token = default(CancellationToken))
{
try
{
await _commandHandler.ExecuteAsync(command, token).ConfigureAwait(false);
}
catch (BrokenRuleException)
{
throw; // Let caller catch this directly
}
catch (UserFriendlyException ex)
{
await _logger.LogAsync(new LogEntry(LogTypeEnum.Error, _userContext,
"Friendly exception with command: " + typeof(TCommand).FullName, ex, command)).ConfigureAwait(false);
throw; // Let caller catch this directly
}
catch (NoPermissionException ex)
{
await _logger.LogAsync(new LogEntry(LogTypeEnum.Error, _userContext,
"No Permission exception with command: " + typeof(TCommand).FullName, ex, command)).ConfigureAwait(false);
throw new UserFriendlyException(CommonResource.Error_NoPermission); // Rethrow with a specific message
}
catch (ConcurrencyException ex)
{
await _logger.LogAsync(new LogEntry(LogTypeEnum.Error, _userContext,
"Concurrency error with command: " + typeof(TCommand).FullName, ex, command)).ConfigureAwait(false);
throw new UserFriendlyException(CommonResource.Error_Concurrency); // Rethrow with a specific message
}
catch (Exception ex)
{
await _logger.LogAsync(new LogEntry(LogTypeEnum.Error, _userContext,
"Error with command: " + typeof(TCommand).FullName, ex, command)).ConfigureAwait(false);
throw new UserFriendlyException(CommonResource.Error_Generic); // Rethrow with a specific message
}
}
}

验证装饰器:
public class CommandHandlerValidatorDecorator<TCommand> : ICommandHandler<TCommand> where TCommand : ICommand
{
private readonly ICommandHandler<TCommand> _commandHandler;
private readonly IEnumerable<ICommandValidator<TCommand>> _validators;

public CommandHandlerValidatorDecorator(
ICommandHandler<TCommand> commandHandler,
ICollection<ICommandValidator<TCommand>> validators)
{
Guard.IsNotNull(commandHandler, nameof(commandHandler));
Guard.IsNotNull(validators, nameof(validators));

_commandHandler = commandHandler;
_validators = validators;
}

public async Task ExecuteAsync(TCommand command, CancellationToken token = default(CancellationToken))
{
var brokenRules = (await Task.WhenAll(_validators.AsParallel()
.Select(a => a.ValidateCommandAsync(command, token)))
.ConfigureAwait(false)).SelectMany(a => a).ToList();

if (brokenRules.Any())
{
throw new BrokenRuleException(brokenRules);
}

await _commandHandler.ExecuteAsync(command, token).ConfigureAwait(false);
}
}

存在其他装饰器,但对这个问题并不重要。

命令处理程序验证器的示例:(每个规则在幕后都在自己的线程上运行)
public class CreateCommandValidator : CommandValidatorBase<CreateCommand>
{
private readonly IDigimonWorld2ContextFactory _contextFactory;

public CreateCommandValidator(IDigimonWorld2ContextFactory contextFactory)
{
_contextFactory = contextFactory;
}

protected override void CreateRules(CancellationToken token = default(CancellationToken))
{
AddRule(() => Validate.If(string.IsNullOrEmpty(Command.Name))
?.CreateRequiredBrokenRule(DigimonResources.Digipedia_CreateCommnad_Name, nameof(Command.Name)));
AddRule(() => Validate.If(Command.DigimonTypeId == 0)
?.CreateRequiredBrokenRule(DigimonResources.Digipedia_CreateCommnad_DigimonTypeId,
nameof(Command.DigimonTypeId)));
AddRule(() => Validate.If(Command.RankId == 0)
?.CreateRequiredBrokenRule(DigimonResources.Digipedia_CreateCommnad_RankId, nameof(Command.RankId)));

AddRule(async () =>
{
using (var context = _contextFactory.Create(false))
{
return Validate.If(
!string.IsNullOrEmpty(Command.Name) &&
await context.Digimons
.AnyAsync(a => a.Name == Command.Name, token)
.ConfigureAwait(false))
?.CreateAlreadyInUseBrokenRule(DigimonResources.Digipedia_CreateCommnad_Name, Command.Name,
nameof(Command.Name));
}
});
}
}

实际命令处理程序:
public class CreateCommandValidatorHandler : ICommandHandler<CreateCommand>
{
private const int ExpectedChangesCount = 1;

private readonly IDigimonWorld2ContextFactory _contextFactory;
private readonly IMapper<CreateCommand, DigimonEntity> _mapper;

public CreateCommandValidatorHandler(
IDigimonWorld2ContextFactory contextFactory,
IMapper<CreateCommand, DigimonEntity> mapper)
{
_contextFactory = contextFactory;
_mapper = mapper;
}

public async Task ExecuteAsync(CreateCommand command, CancellationToken token = default(CancellationToken))
{
using (var context = _contextFactory.Create())
{
var entity = _mapper.Map(command);
context.Digimons.Add(entity);
await context.SaveChangesAsync(ExpectedChangesCount, token).ConfigureAwait(false);
}
}
}

当因验证规则损坏而引发异常时,正常流程就会中断。每一步都假设上一步成功。这使得代码非常干净,因为我们不关心实际实现过程中的失败。所有命令最终都经过相同的逻辑,因此我们只需编写一次。在 MVC 的最顶层,我处理 BrokenRuleException 是这样的:(我执行 AJAX 调用,而不是整页帖子)
internal static class ErrorConfiguration
{
public static void Configure(
IApplicationBuilder app, IHostingEnvironment env, ILoggerFactory loggerFactory, IConfigurationRoot configuration)
{
loggerFactory.AddConsole(configuration.GetSection("Logging"));
loggerFactory.AddDebug();

if (env.IsDevelopment())
{
app.UseDeveloperExceptionPage();
app.UseBrowserLink();
}
else
{
app.UseExceptionHandler("/Home/Error");
}

app.UseExceptionHandler(errorApp =>
{
errorApp.Run(async context =>
{
var error = context.Features.Get<IExceptionHandlerFeature>()?.Error;

context.Response.StatusCode = GetErrorStatus(error);
context.Response.ContentType = "application/json";

var message = GetErrorData(error);
await context.Response.WriteAsync(message, Encoding.UTF8);
});
});
}

private static string GetErrorData(Exception ex)
{
if (ex is BrokenRuleException brokenRules)
{
return JsonConvert.SerializeObject(new
{
BrokenRules = brokenRules.BrokenRules
});
}

if (ex is UserFriendlyException userFriendly)
{
return JsonConvert.SerializeObject(new
{
Message = userFriendly.Message
});
}

return JsonConvert.SerializeObject(new
{
Message = MetalKid.Common.CommonResource.Error_Generic
});
}

private static int GetErrorStatus(Exception ex)
{
if (ex is BrokenRuleException || ex is UserFriendlyException)
{
return (int)HttpStatusCode.BadRequest;
}
return (int)HttpStatusCode.InternalServerError;
}
}

BrokenRule 类具有消息和关系字段。这种关系允许 UI 将消息与页面上的某些内容(即 div 或表单标签等)相关联,以在正确的位置显示消息
public class BrokenRule
{
public string RuleMessage { get; set; }
public string Relation { get; set; }

public BrokenRule() { }

public BrokenRule(string ruleMessage, string relation = "")
{
Guard.IsNotNullOrWhiteSpace(ruleMessage, nameof(ruleMessage));

RuleMessage = ruleMessage;
Relation = relation;
}
}

如果我不这样做, Controller 将必须首先调用验证类,查看结果,然后将其作为 400 返回,并带有正确的响应。最有可能的是,您必须调用辅助类才能正确转换它。但是, Controller 最终会看起来像这样或类似的东西:
[Route(ApiConstants.ROOT_API_URL_VERSION_1 + "DigimonWorld2Admin/Digimon/Create")]
public class CreateCommandController : MetalKidApiControllerBase
{
private readonly IMediator _mediator;
private readonly ICreateCommandValidator _validator;

public CreateCommandController(IMediator mediator, ICreateCommandValidator validator)
{
_mediator = mediator;
_validator = validator
}

[HttpPost]
public async Task<IHttpResult> Post([FromBody]CreateCommand command)
{
var validationResult = _validator.Validate(command);
if (validationResult.Errors.Count > 0)
{
return ValidationHelper.Response(validationResult);
}
await _mediator.ExecuteAsync(command);
return Ok();
}
}

需要对每个命令重复此验证检查。如果忘记了,后果会很严重。使用异常样式,代码保持紧凑,开发人员不必担心每次都添加冗余代码。

我真的很想得到大家的反馈。谢谢!

* 编辑 *
另一种可能的选择是为响应本身设置另一个“中介”,它可以首先直接运行验证,然后继续:
[Route(ApiConstants.ROOT_API_URL_VERSION_1 + "DigimonWorld2Admin/Digimon/Create")]
public class CreateCommandController : MetalKidApiControllerBase
{
private readonly IResultMediator _mediator;

public CreateCommandController(IResultMediator mediator) => _mediator = mediator;

[HttpPost]
public async Task<IHttpAction> Post([FromBody]CreateCommand command) =>
await _mediator.ExecuteAsync(command);
}

在这个新的 ResultMediator 类中,它会查找 CommandValidator,如果有任何验证错误,它会简单地返回 BadRequest(new { BrokenRules =brokenRules}) 并称其为 good。这是每个 UI 只需要创建和处理的东西吗?但是,如果在此调用期间出现异常,则我们必须直接在此中介器中进行处理。想法?

编辑 2:
也许我应该快速解释一下装饰器。例如,我有这个 CreateCommand(在这种情况下有一个特定的命名空间)。有一个处理此命令的 CommandHandler 定义为 ICommandHandler。这个接口(interface)有一个方法定义为:
Task ExecuteAsync(TCommand, CancellationToken token);

每个装饰器也实现相同的接口(interface)。 Simple Injector 允许您使用相同的接口(interface)定义这些新类,例如 CommandHandlerExceptionDecorator 和 CommandHandlerValidationDecorator。当顶部的代码想要使用该 CreateCommand 调用 CreateCommandHandler 时,SimpleInjector 将首先调用最后定义的装饰器(在本例中为 ExceptionDecorator)。这个装饰器处理所有异常并为所有命令记录它们,因为它是通用定义的。我只需要编写一次该代码。然后它将调用转发到下一个装饰器。在这种情况下,它可能是 ValidationDecorator。这将验证 CreateCommand 以确保它有效。如果是,它会将其转发到实际的命令,在该命令中创建实体。如果没有,它会抛出异常,因为我无法返回任何东西。 CQS 声明命令必须是无效的。不过,Task 没问题,因为它只是为了实现 async/await 风格。它实际上没有返回任何内容。由于我无法在那里返回破坏的规则,所以我抛出了一个异常。我只是想知道这种方法是否可行,因为它使所有不同级别的所有代码都特定于任务 (SRP),而且我现在和将来只需在所有命令中编写一次。任何 UI 都可以简单地捕获任何出现的 BrokenRuleException,并且知道如何处理该数据以显示它。这可以通用地编写,因此我们也可以显示任何命令的任何错误(由于规则上的 Relation 属性)。这样,我们一次写完就完成了。然而,问题是我一直看到用户验证不是“异常”,所以我们不应该抛出异常。这样做的问题是,如果我真正遵循该路径,它将使我的代码变得更加复杂且不易维护,因为每个命令调用者都必须编写相同的代码才能做到这一点。如果我只为任何验证错误抛出一个 BrokenRuleException,那还可以吗?

最佳答案

我的命令使用了非常相似的模式,基于 MediatR来自 Jimmy Bogard(使用管道功能在我的处理程序周围添加多个装饰器),并使用 Fluent Validation对于验证器。

我和你经历了类似的思考过程——我的验证器抛出异常(它们以与你类似的方式被捕获,在 MVC 的顶部),但有很多人会告诉你这不应该做——尤其是我最喜欢的科技预言机 Martin Fowler

一些想法:

  • 我对我们作为开发人员遇到的一些“你不应该……”公理有点警惕,并且相信继续朝着命令和验证器的干净、DRY 模式前进比遵循该规则更重要。
  • 以类似的方式,“您不得从命令中返回任何内容”在我看来可能被颠覆,并且似乎是某些 debate 的主题。无论如何,允许您使用上面链接的通知模式。
  • 最后,您面向用户的应用程序是否进行了任何客户端验证?也许有人会争辩说,如果客户端应用程序应该防止命令处于无效状态,那么无论如何服务器端的异常都将是异常的,并且问题就会消失。

  • 希望能在一些小方面有所帮助。我将对有关此问题的任何其他观点感兴趣。

    关于c# - 验证引发异常的 CQS 系统,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/46157174/

    24 4 0
    Copyright 2021 - 2024 cfsdn All Rights Reserved 蜀ICP备2022000587号
    广告合作:1813099741@qq.com 6ren.com