我一直在阅读关于异常只应该用于“异常”情况而不应用于控制程序流程的文章。然而,在使用CQS实现时,除非我开始篡改实现来处理它,否则这似乎是不可能的。我想展示一下我的实现方式,看看这是否真的很糟糕。我正在使用装饰器,因此命令不能返回任何内容(除了异步任务),因此无法使用ValidationResult。请告诉我您的想法!
此示例将使用ASP.NET MVC。
控制器:(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;
}
}
如果我不这样做,控制器就必须先调用一个验证类,查看结果,然后将其作为400返回并带有正确的响应。最可能的是,您需要调用一个辅助类来正确转换它。但是,那么控制器最终会变成这样或类似的东西:
[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})并处理好它。每个UI都需要创建和处理这个吗?然而,如果在此调用期间发生异常,我们必须直接在此中介者中处理它。你有什么想法吗?
编辑2: 也许我应该快速解释一下装饰器。例如,我有这个CreateCommand(在这种情况下具有特定的命名空间)。有一个CommandHandler定义了处理此命令的方法,该方法被定义为ICommandHandler接口中的一个方法:
Task ExecuteAsync(TCommand, CancellationToken token);
每个装饰器也实现了同样的接口。Simple Injector允许您定义这些新类,如CommandHandlerExceptionDecorator和CommandHandlerValidationDecorator,使用相同的接口。当顶部的代码想要调用CreateCommandHandler并使用CreateCommand时,SimpleInjector将首先调用最后定义的装饰器(在本例中是ExceptionDecorator)。该装饰器处理所有异常并记录所有命令的日志,因为它是通用定义的。我只需编写一次该代码。然后它将调用下一个装饰器。在这种情况下,它可以是ValidationDecorator。这将验证CreateCommand以确保它是有效的。如果是有效的,它将转发到实际的命令,其中执行实体的创建。如果不是,它会抛出异常,因为我无法返回任何内容。CQS规定命令必须为空。但Task类型可以,因为它只是实现异步/等待风格。它实际上没有返回任何东西。由于我没有办法返回损坏的规则,因此我抛出异常。我只是想知道这种方法是否可行,因为它使得所有不同级别的代码都特定于任务(SRP),而且我现在只需要在所有命令上编写一次。任何UI都可以简单地捕获任何BrokenRuleException并知道如何处理该数据以显示它。这可以通用编写,因此我们也可以显示任何命令的任何错误(由规则上的Relation属性确定)。这样,我们只需编写一次就完成了。然而,问题在于我不断地看到用户验证并不是“异常”,因此我们不应该引发异常。如果我只为任何验证错误抛出一个BrokenRuleException,那是否仍然可以?