• [译]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;
    
        }
    }
    
  • 相关阅读:
    用例的粒度问题
    REST和RPC最大区别
    成功的结对编程要点
    我认为技术经理应该做的事儿
    敏捷测试实践
    DDD-围绕业务逻辑编程
    依赖反转原则
    Kafka和Rabbitmq的最大区别
    Cassandra快速两次写入导致顺序不对的问题
    DotNetBar之SupergridControl显示图片,行距自动调整
  • 原文地址:https://www.cnblogs.com/irocker/p/a-non-trivial-example-of-mediatr-usage.html
Copyright © 2020-2023  润新知