来看看我目前的一个项目。这个是一个多租户的财务跟踪系统。有一个组织继承的关系。首先得新建一个组织。
表单如下:
这个表单能让用户输入关于组织的一些信息,包括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;
}
}