[译]A NON-TRIVIAL EXAMPLE OF MEDIATR USAGE

原文

来看看我目前的一个项目。这个是一个多租户的财务跟踪系统。有一个组织继承的关系。首先得新建一个组织。

表单如下:

这个表单能让用户输入关于组织的一些信息,包括active directory组,一个唯一的简写名。在客户端使用ajax确保active directory组存在。

POST Action如下:

// POST: Organizations/Create
[HttpPost]
[ValidateAntiForgeryToken]
public async Task<ActionResult> Create(OrganizationEditorForm form)  
{
    Logger.Trace("Create::Post::{0}", form.ParentOrganizationId);

    if (ModelState.IsValid)
    {
        var command = new AddOrEditOrganizationCommand(form, ModelState);
        var result = await mediator.SendAsync(command);

        if(result.IsSuccess)
            return RedirectToAction("Index", new { Id = result.Result });

        ModelState.AddModelError("", result.Result.ToString());
     }

     return View(form);
}

基于mvc的模型绑定,绑定view model和做一些基础的基于datannotation的数据验证。如果验证失败,定向到表单页面并显示ModelState的错误。

接下来我构造了一个AddOrEditOrganzationCommand,它包含了view model和当前的ModelState。这能让我们将在服务端的验证结果附加到ModelState上。这个command对象只是简单的包含了我们需要的数据。

public class AddOrEditOrganizationCommand : IAsyncRequest<ICommandResult>  
{
    public OrganizationEditorForm Editor { get; set; }
    public ModelStateDictionary ModelState { get; set; }

    public AddOrEditOrganizationCommand(OrganizationEditorForm editor,
        ModelStateDictionary modelState)
    {
        Editor = editor;
        ModelState = modelState;
    }
}

这个command通过mediator来发送,返回一个结果。我的结果类型是 (SuccessResult 和 FailureResult) ,基于下面的接口:

public interface ICommandResult  
{
    bool IsSuccess { get; }
    bool IsFailure { get; }
    object Result { get; set; }
 }

如果是成功的结果,重定向到用户最近创建的组织的详细页。如果失败,将失败的消息添加到ModelState中,并在form页面显示。

现在我们需要handler来处理命令。

public class OrganizationEditorFormValidatorHandler : CommandValidator<AddOrEditOrganizationCommand>  
    {
        private readonly ApplicationDbContext context;

        public OrganizationEditorFormValidatorHandler(ApplicationDbContext context)
        {
            this.context = context;
            Validators = new Action<AddOrEditOrganizationCommand>[]
            {
                EnsureNameIsUnique, EnsureGroupIsUnique, EnsureAbbreviationIsUnique
            };
        }

        public void EnsureNameIsUnique(AddOrEditOrganizationCommand message)
        {
            Logger.Trace("EnsureNameIsUnique::{0}", message.Editor.Name);

            var isUnique = !context.Organizations
                .Where(o => o.Id != message.Editor.OrganizationId)
                .Any(o => o.Name.Equals(message.Editor.Name,
                        StringComparison.InvariantCultureIgnoreCase));

            if(isUnique)
                return;

            message.ModelState.AddModelError("Name", 
                    "The organization name ({0}) is in use by another organization."
                    .FormatWith(message.Editor.Name));
        }

        public void EnsureGroupIsUnique(AddOrEditOrganizationCommand message)
        {
            Logger.Trace("EnsureGroupIsUnique::{0}", message.Editor.GroupName);

            var isUnique = !context.Organizations
                .Where(o => o.Id != message.Editor.OrganizationId)
                .Any(o => o.GroupName.Equals(message.Editor.GroupName,
                        StringComparison.InvariantCultureIgnoreCase));

            if (isUnique)
                return;

            message.ModelState.AddModelError("Group", 
                "The Active Directory Group ({0}) is in use by another organization."
                    .FormatWith(message.Editor.GroupName));
        }

        public void EnsureAbbreviationIsUnique(AddOrEditOrganizationCommand message)
        {
            Logger.Trace("EnsureAbbreviationIsUnique::{0}",
                    message.Editor.Abbreviation);

            var isUnique = !context.Organizations
                .Where(o => o.Id != message.Editor.OrganizationId)
                .Any(o => o.Abbreviation.Equals(message.Editor.Abbreviation,
                        StringComparison.InvariantCultureIgnoreCase));

            if (isUnique)
                return;

            message.ModelState.AddModelError("Abbreviation", 
                    "The Abbreviation ({0}) is in use by another organization."
                        .FormatWith(message.Editor.Name));
        }
    }

CommandValidator包含一些简单的帮助方法,用来迭代定义的验证方法并执行他们。每个验证方法执行一些特别的逻辑,并在出错的时候将错误消息添加到ModelState。

下面的command handler是将表单的信息存储到数据库中。

public class AddOrEditOrganizationCommandHandler : IAsyncRequestHandler<AddOrEditOrganizationCommand, ICommandResult>  
    {
        public ILogger Logger { get; set; }

        private readonly ApplicationDbContext context;

        public AddOrEditOrganizationCommandHandler(ApplicationDbContext context)
        {
            this.context = context;
        }

        public async Task<ICommandResult> Handle(AddOrEditOrganizationCommand message)
        {
            Logger.Trace("Handle");

            if (message.ModelState.NotValid())
                return new FailureResult("Validation Failed");

            if (message.Editor.OrganizationId.HasValue)
                return await Edit(message);

            return await Add(message);
        }


        private async Task<ICommandResult> Add(AddOrEditOrganizationCommand message)
        {
            Logger.Trace("Add");

            var organization = message.Editor.BuildOrganiation(context);

            context.Organizations.Add(organization);

            await context.SaveChangesAsync();

            Logger.Information("Add::Success Id:{0}", organization.Id);

            return new SuccessResult(organization.Id);
        }

        private async Task<ICommandResult> Edit(AddOrEditOrganizationCommand message)
        {
            Logger.Trace("Edit::{0}", message.Editor.OrganizationId);

            var organization = context.Organizations
                    .Find(message.Editor.OrganizationId);

            message.Editor.UpdateOrganization(organization);

            await context.SaveChangesAsync();

            Logger.Information("Edit::Success Id:{0}", organization.Id);

            return new SuccessResult(organization.Id);
        }
    }

这个handle非常简单。首先检查上一次的验证结果,如果失败直接返回失败结果。然后根据ID判断是执行新增还是编辑方法。

现在我们还没有为组织启用或禁用feature。我想将保存组织信息和处理feature的代码逻辑分隔开。

因此我新增一个UpdateOrganizationFeaturesPostHandler来处理feature。

public class UpdateOrganizationFeaturesPostHandler : IAsyncPostRequestHandler<AddOrEditOrganizationCommand, ICommandResult>  
    {
        public ILogger Logger { get; set; }

        private readonly ApplicationDbContext context;

        public UpdateOrganizationFeaturesPostHandler(ApplicationDbContext context)
        {
            this.context = context;
        }

        public async Task Handle(AddOrEditOrganizationCommand command, 
            ICommandResult result)
        {
            Logger.Trace("Handle");

            if (result.IsFailure)
                return;

            var organization = await context.Organizations
                                        .Include(o => o.Features)
                                        .FirstAsync(o => o.Id == (int) result.Result);



            var enabledFeatures = command.Editor.EnabledFeatures
                                    .Select(int.Parse).ToArray();

            //disable features
            organization.Features
                .Where(f => !enabledFeatures.Contains(f.Id))
                .ToArray()
                .ForEach(f => organization.Features.Remove(f));

            //enable features    
            context.Features
                .Where(f => enabledFeatures.Contains(f.Id))
                .ToArray()
                .ForEach(organization.Features.Add);

            await context.SaveChangesAsync();
        }
    }

Create的Get Action如下:

// GET: Organizations/Create/{1}
public async Task<ActionResult> Create(int? id)  
{
    Logger.Trace("Create::Get::{0}", id);

    var query = new OrganizationEditorFormQuery(parentOrganizationId: id);
    var form = await mediator.SendAsync(query);
    return View(form);
 }

模型绑定如下:

[ModelBinderType(typeof(OrganizationEditorForm))]
public class OrganizationEditorFormModelBinder : DefaultModelBinder  
{
    public ILogger Logger { get; set; }

    private readonly ApplicationDbContext dbContext;

    public OrganizationEditorFormModelBinder(ApplicationDbContext dbContext)
    {
        this.dbContext = dbContext;
    }

    public override object BindModel(ControllerContext controllerContext,
        ModelBindingContext bindingContext)
    {
        Logger.Trace("BindModel");

        var form = base.BindModel(controllerContext, bindingContext)
                .CastOrDefault<OrganizationEditorForm>();

        if (form.ParentOrganizationId.HasValue)
            form.ParentOrganization = dbContext.Organizations
                .FirstOrDefault(o => o.Id == form.ParentOrganizationId);

        return form;

    }
}
posted @ 2018-03-27 18:22  irocker  阅读(355)  评论(0编辑  收藏  举报