【发布时间】:2017-09-11 13:41:26
【问题描述】:
我一直在阅读,异常应该只针对“异常”的东西,而不是用于控制程序的流程。但是,对于 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。当顶部的代码想要使用该 CreateCommand 调用 CreateCommandHandler 时,SimpleInjector 将首先调用最后定义的装饰器(在本例中为 ExceptionDecorator)。这个装饰器处理所有异常并为所有命令记录它们,因为它是通用定义的。我只需要编写一次该代码。然后它将调用转发给下一个装饰器。在这种情况下,它可能是 ValidationDecorator。这将验证 CreateCommand 以确保它有效。如果是,它会将其转发到创建实体的实际命令。如果没有,它会引发异常,因为我无法返回任何东西。 CQS 声明命令必须是无效的。不过,Task 没问题,因为它只是为了实现 async/await 样式。它实际上没有返回任何东西。由于我无法在那里返回违反的规则,因此我抛出了一个异常。我只是想知道这种方法是否可行,因为它使所有不同级别的所有代码都特定于任务(SRP),我现在和将来只需在所有命令中编写一次。任何 UI 都可以简单地捕获任何出现的 BrokenRuleException 并知道如何处理该数据以显示它。这可以通用地编写,因此我们也可以显示任何命令的任何错误(由于规则上的 Relation 属性)。这样,我们就写一次就完成了。然而,问题是我一直看到用户验证不是“异常”的,所以我们不应该抛出异常。这样做的问题是,如果我真正遵循这条路径,它将使我的代码更加复杂且难以维护,因为每个命令调用者都必须编写相同的代码才能做到这一点。如果我只为任何验证错误抛出一个 BrokenRuleException,那还可以吗?
【问题讨论】:
标签: c# asp.net-mvc validation exception