如何将模型验证从控制器中正确地分离到服务层?

3
我正在重构我工作的项目。我的现有控制器使用了仓储模式,但我仍然进行了一些比我感到舒适的脚手架工作。而且我的一些控制器可能需要传入10个以上的仓储库(通过Ninject)。因此,我决定引入一个服务层,在这里我的意图是每个控制器有一个服务,并且每个服务将注入多个仓储库并完成我需要的工作。迄今为止,这个方法非常有效,但我遇到了一些困惑:如何将模型验证从控制器移到服务层中?例如,看看我OfficesController上的Edit方法:
[HttpPost]
public async Task<RedirectToRouteResult> Edit(
    short id,
    FormCollection form,
    [Bind(Prefix = "Office.Coordinates", Include = "Latitude,Longitude")] Coordinate[] coordinates) {
    if (id > 0) {
        Office office = await this.OfficesService.GetOfficeAsync(id);

        if ((office != null)
            && base.TryUpdateModel(office, "Office", new string[2] {
                "Name",
                "RegionId"
            }, form)
            && base.ModelState.IsValid) {
            this.OfficesService.UpdateOfficeAsync(office, coordinates);
        }

        return base.RedirectToAction("Edit", new {
            id = id
        });
    }

    return base.RedirectToAction("Default");
}

与控制器的方法相比,它的问题在于我仍然从数据库中获取一个Office对象,进行更新、验证,然后再保存。在这种情况下,复杂性增加而不是减少。之前,我在方法中调用了存储库,现在我调用了服务,该服务调用存储库执行相同的功能。到目前为止,这种复杂性的增加只在我的Edit方法中表现出来,在其他地方,复杂性大大降低,这正是我想要的。
那么,将验证和模型更新逻辑从控制器移到服务的正确方式是什么?欢迎提出建议!
供参考,这是我的项目结构:
- Data:包含所有模型类 - Data.Google.Maps:包含我需要反序列化特定Kml的所有类 - Data.Models:包含我的DbContext、配置、视图模型和部分视图模型 - Data.Repositories:包含所有与DbContext通信的存储库。由于EF本身就是一个伪存储库,我利用我的“存储库”作为更具体的查询数据的方式。例如:FindTechnicians()FindActive()等。 - Data.Services:包含我将使用的所有服务。服务将注入一个或多个存储库,并执行我需要完成的所有逻辑,然后将一个完整的视图模型返回给控制器。 - Identity:包含我的ASP.NET Identity实现。 - Web.Private:包含实际的MVC项目。

你的服务有多少存储库依赖项?你只是移动了依赖项过度注入问题,还是解决了它? - danludwig
我将它移到服务层,因为我知道我将不得不对一些存储库的结果进行更复杂的处理,然后将完成的模型传递到控制器中。 - Gup3rSuR4c
1个回答

2
以下是两篇您可能尚未阅读的文章: 您的问题的答案是FluentValidation.NET和依赖注入。
使用它,您可以像这样做:
private readonly IExecuteCommands _commands;

[HttpPost]
public async Task<RedirectToRouteResult> Edit(short id, UpdateOffice command) {

    // with FV.NET plugged in, if your command validator fails,
    // ModelState will already be invalid
    if (!ModelState.IsValid) return View(command);

    await _commands.Execute(command);
    return RedirectToAction(orWhateverYouDoAfterSuccess);
}

该命令只是一个普通的DTO,类似于视图模型。可能看起来像这样:
public class UpdateOffice
{
    public int OfficeId { get; set; }
    public int RegionId { get; set; }
    public string Name { get; set; }
}

...以及神奇的验证器:

public class ValidateUpdateOfficeCommand : AbstractValidator<UpdateOffice>
{
    public ValidateUpdateOfficeCommand(DbContext dbContext)
    {
        RuleFor(x => x.OfficeId)
            .MustFindOfficeById(dbContext);

        RuleFor(x => x.RegionId)
            .MustFindRegionById(dbContext);

        RuleFor(x => x.Name)
            .NotEmpty()
            .Length(1, 200)
            .MustBeUniqueOfficeName(dbContext, x => x.OfficeId);
    }
}

如果您已设置了验证器的依赖项注入并使用FV MVC验证提供程序,则在执行操作方法之前,将运行每个验证规则。如果存在验证错误,则ModelState.IsValid将为false。

您还解决了控制器和(可能)服务层中的过度注入问题。只需使用3个接口依赖项即可运行任何查询、执行任何命令或验证任何对象。


我今天早些时候读了第一篇文章,还没有读第二篇文章,主要是因为Steve的文章通常比较冗长。关于你提到的FluentValidation.NET,它与EF Fluent API有何不同,如果我正在使用EF Fluent API,它仍然有好处吗? - Gup3rSuR4c
EF流畅API用于将关系模式映射到概念对象模型,其验证最小化。您可以使字段必填并指定最大长度,但仅限于此。有了上述内容,您的验证器会检查以确保未违反所有其他业务规则。在SQL中,您必须使用触发器来完成此操作,而EF没有流畅的API。 - danludwig

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接