【问题标题】:Where should be the validation in a ASP.Net MVC scenario having Repository, Service Layer and using Model Binder?在具有存储库、服务层和使用模型绑定器的 ASP.Net MVC 场景中,验证应该在哪里?
【发布时间】:2009-04-24 02:55:10
【问题描述】:

相关: What’s the best way to implement field validation using ASP.NET MVC?

让我们假设一个包含以下项目的解决方案:

Foo; // the MVC web project
Foo.Models;
Foo.Repositories;
Foo.Services;

Foo.Models 是包含所有实体的应用程序域,无论使用 EF、NH、POCO 还是其他任何东西都无关紧要。这是一个例子:

public class User
{
    public string Username { get; set; }

    public string Email { get; set; }

    public string Password { get; set; }
}

Foo.Repositories 中有一个UserRepository,在Foo.Services 中有一个UserService

在 Web 应用程序中,让我们考虑一个模型绑定器,如下所示:

public class UserBinder : DefaultModelBinder
{
    //...
}

我看到三个不同的选项,用于放置验证:

  • Foo.Models 中如下:

    public class User
    {
        public string Username { get; set; }
    
        public string Email { get; set; }
    
        public string Password { get; set; }
    
        public ICollection<KeyValuePair<string, string>> ValidateErrors()
        {
            //Validate if Username, Email and Password has been passed
        }
    }
    
  • Foo.Services 喜欢:

    public class UserService
    {
        public ICollection<KeyValuePair<string, string>> ValidateErrors()
        {
            //Validate if Username, Email and Password has been passed
        }
    }
    
  • 在模型绑定器内的Foo

    public class UserBinder : DefaultModelBinder
    {
        protected override void OnModelUpdated(ControllerContext controllerContext, ModelBindingContext bindingContext)
        {
            var user = (User)bindingContext.Model;
    
            // validate everything here
    
            base.OnModelUpdated(controllerContext, bindingContext);
        }
    }
    

要注意的另一件事是,考虑到前两个选项 [Model 和 Service],还需要做出另一个决定:ValidateErrors 方法可以直接在控制器上或 Binder 内部调用。

我有两个关于场景的问题:

  1. 验证应该是:

    • 在从控制器调用的模型中?
    • 在从活页夹调用的模型中?
    • 在从控制器调用的服务中?
    • 在从活页夹调用的服务中?
    • 直接在活页夹中?
    • 还有其他想法吗?
  2. 以上所有场景都讨论了用户创建。但是用户登录呢? 假设用户使用用户名和密码登录应用程序,因此不需要验证电子邮件。 这个验证应该在哪里?

    • 在从控制器调用的模型中?
    • 在从控制器调用的服务中?
    • 还有其他想法吗?

【问题讨论】:

    标签: asp.net-mvc validation


    【解决方案1】:

    查看ASP.NET MVC Contact Manager Sample Application,我认为它的架构非常好

    http://www.asp.net/learn/mvc/tutorial-26-cs.aspx'>http://www.asp.net/learn/mvc/tutorial-26-cs.aspx

    【讨论】:

    • 你得到了我的投票,但是,大多数演示的缺点是他们建议在编辑视图和控制器操作之间存在一对一的联系。当您需要多个编辑屏幕(例如购物车)时,就会缺少一些东西。
    • 抱歉,这个示例将所有验证都放在控制器上,这是一个坏主意。我认为最好的做法是“瘦控制器,胖模型”。控制器应该仅作为视图和其他层 [模型、服务等...] 之间的“工作流管理器”工作。
    • Homemdelata,我在示例中也看到了控制器验证模型,这太糟糕了。验证肯定属于模型本身,故事结束。
    • @Iconic 验证不应该属于模型本身,因为在某些情况下模型的验证逻辑依赖于控制器或数据库表上的其他模型。例如客户订单号应仅在客户订单中是唯一的。甚至“必须将至少 1 项标记为 IsDefault” - 您不能删除唯一标记为“IsDefault”的项
    【解决方案2】:

    我非常喜欢从控制器调用验证并让验证例程返回一个 ActionResult,以便控制器知道如何处理结果。

    【讨论】:

    • 但它违背了“瘦控制器,胖模型”范式[我最喜欢的]
    【解决方案3】:

    对于它的价值,这是我在当前项目中找到的:

    我有ModelsRepositories(如果你愿意,可以叫他们Services)和ViewModels。我尽量避免编写自定义模型绑定器,因为 (a) 它很无聊,并且 (b) 放置验证的地方很奇怪,恕我直言。对我来说,模型绑定器只是从请求中获取项目并将它们推入对象中。例如,PHP 在将项目从标头提取到 $_POST 数组时不做任何验证;这是我们插入数组的东西,它关心它的内容。

    我的Model 对象通常不允许自己进入无效状态。这意味着在构造函数期间传入必需的参数,如果尝试将其设置为无效值,属性将引发异常。而且,一般来说,我尝试将我的Model 对象设计为不可变的。例如,我有一个用于邮寄地址的Address 对象,该对象由AddressBuilder 对象构成,通过检查可以从AddressSchemeRepository 检索的AddressScheme 来查看给定国家/地区的字段要求。呸。但我认为这是一个很好的例子,因为它在概念上很简单(“验证邮寄地址”)并使其在现实世界的使用中变得复杂(“我们接受来自 30 多个国家/地区的地址,并且这些格式规则位于数据库中,而不是在我的代码中”)。

    由于构造这个 Model 对象有点痛苦——它应该也是这样,因为它对加载到其中的数据非常特别——我有一个,比如说,InputAddressViewModel 对象,我的视图绑定到。 InputAddressViewModel 实现 IDataErrorInfo 以便我获得 ASP.NET MVC 的 DefaultModelBinder 以自动将错误添加到 ModelState。对于我提前知道的简单验证例程(电话号码格式、需要名字、电子邮件地址格式),我可以在 InputAddressViewModel 中实现这些。

    拥有视图模型的另一个优点是,因为它无耻地为特定视图量身定制,所以您的真实模型更具可重用性,因为它不必做出任何奇怪的让步以使其适合 UI 显示(例如,需要实现INotifyPropertyChangedSerializable 或任何混乱)。

    在我与实际的Model 中的AddressScheme 交互之前,我不会知道有关地址的其他验证错误。这些错误将是控制器编排到ModelState 的工作。比如:

    public ActionResult InputAddress(InputAddressViewModel model) { if (ModelState.IsValid) { // "Front-line" validation passed; let's execute the save operation // in the our view model var result = model.Execute(); // The view model returns a status code to help the // controller decide where to redirect the user next switch (result.Status) { case InputAddressViewModelExecuteResult.Saved: return RedirectToAction("my-work-is-done-here"); case InputAddressViewModelExecuteResult.UserCorrectableError: // Something went wrong after we interacted with the // datastore, like a bogus Canadian postal code or // something. Our view model will have updated the // Error property, but we need to call TryUpdateModel() // to get these new errors to get added to // the ModelState, since they were just added and the // model binder ran before this method even got called. TryUpdateModel(model); break; } // Redisplay the input form to the user, using that nifty // Html.ValidationMessage to convey model state errors return View(model); } }

    switch 可能看起来令人反感,但我认为这是有道理的:视图模型只是一个普通的旧类,对RequestHttpContext 没有任何了解。这使得视图模型的逻辑易于单独测试,而无需借助模拟,并且通过以在网站上有意义的方式解释模型的结果,将控制器代码留给 control --它可以重定向,它可以设置cookie等。

    InputAddressViewModelExecute() 方法看起来像(有些人会坚持将此代码放入控制器将调用的服务对象中,但对我来说,视图模型会对数据进行如此多的处理为了使它适合真实的模型,放在这里是有意义的):

    public InputAddressViewModelExecuteResult Execute() { InputAddressViewModelExecuteResult result; if (this.errors.Count > 0) { throw new InvalidOperationException( "Don't call me when I have errors"); } // This is just my abstraction for clearly demarcating when // I have an open connection to a highly contentious resource, // like a database connection or a network share using (ConnectionScope cs = new ConnectionScope()) { var scheme = new AddressSchemeRepository().Load(this.Country); var builder = new AddressBuilder(scheme) .WithCityAs(this.City) .WithStateOrProvinceAs(this.StateOrProvince); if (!builder.CanBuild()) { this.errors.Add("Blah", builder.Error); result = new InputAddressViewModelExecuteResult() { Status = InputAddressViewModelExecuteStatus .UserCorrectableError }; } else { var address = builder.Build(); // save the address or something... result = new InputAddressViewModelExecuteResult() { Status = InputAddressViewModelExecuteStatus.Success, Address = address }; } } return result; }

    这有意义吗?这是最佳实践吗?我不知道;这当然很冗长;这是我在过去两周思考这个问题后才想到的。我认为您将有一些重复验证-您的 UI 不能完全愚蠢,并且在将它们提交到您的模型/存储库/服务/之前不知道哪些字段是必需的不管怎样——否则表单可以简单地自己生成。

    我应该补充一点,这样做的动力是我一直有点讨厌微软的“设置一个属性 -> 验证一个属性”的心态,因为现实中从来没有这样的工作。你总是最终得到一个无效的对象,因为有人在去数据存储的路上忘记了调用IsValid 或类似的东西。所以拥有视图模型的另一个原因是它会根据这种让步进行自我调整,因此我们可以很容易地从请求中提取项目、模型状态中的验证错误等大量 CRUD 工作损害我们模型本身的完整性。如果我手头有一个Address 对象,我知道这很好。如果我手头有一个InputAddressViewModel 对象,我知道我需要调用它的Execute() 方法来获得那个金色的Address 对象。

    我期待阅读其他一些答案。

    【讨论】:

    • 您的视图模型对象调用数据库?将 UI 和数据库逻辑层从彼此中抽象出来……
    • 模型不与数据库对话;它是由它水合的物体的集合。 某事 需要从数据库中“获取”该模型,让模型自己玩,然后将模型对自己所做的任何事情都持久化回数据库。控制器可以做到这一点,但由于它有自己的视图模型,你选择哪个并不重要。对我来说,控制器在大多数情况下都遵循视图模型,并且只处理重定向和 cookie。这使得视图模型易于单元测试。 db仍然是抽象的,但是网络边界和它的访问没有被忽略。
    【解决方案4】:

    经过大量研究,我想我得到了问题的答案,所以我决定分享。

    验证码应该在模型上。 根据“瘦控制器,胖模型”的想法,并考虑到模型会知道它需要验证什么。

    例如,假设我决定在其他解决方案中使用Foo.Models,但我决定不使用任何其他项目并且验证在其他项目中。 在这种情况下,我必须重新编码整个验证,这完全是浪费时间,对吧?

    好的。验证码必须在模型中,但是应该在哪里调用呢?

    必须在将其保存到数据库或文件的位置调用此验证。 在提议的场景中,我将存储库视为一个域,那么我们应该考虑将验证放在更改保存之前[在此示例中,我使用的是实体框架,但这不是必需的,它只是为了显示]:

    
    public class UserRepository : IRepository<User>
    {
        public void Create(User user)
        {
            user.Validate();
    
            var db = dbFooEntities();
    
            db.AddToUsers(user);
            db.SaveChanges();
        }
    }
    

    根据 MS 的建议,模型验证应该引发异常,并且控制器必须使用发现的错误填充 ModelState [一旦我完成我的应用程序,我将尝试使用示例代码更新此答案]。

    这样我们就有了问题 #1 的答案。

    关于登录验证的问题 #2 怎么样?

    由于登录不是您保留数据的情况,因此验证应保留在服务上,因为在这种情况下登录是一项服务。

    所以,问题的答案是:

    1. 在从 REPOSITORY [由控制器调用]

    2. 调用的模型中
    3. 在从控制器调用的服务中

    【讨论】:

    • 这样你就失去了上下文。有时验证规则会根据业务层中发生的情况而变化。在这种情况下,我建议将其上移一层(服务层?)。
    • 但是 Todd,模型是 O-RM,所以对象 [O] 应该代表与您的关系映射 [RM] 相同的规则。这就是为什么我认为验证不会改变。如果你对模型有不同的业务规则,你应该改变整个 O-RM,我认为
    【解决方案5】:

    这很有趣,它对我决定在哪里进行验证很有帮助。 目前,我对实现“验证”方法的每个模型感觉最深刻,该方法从存储库或服务中调用。

    但是,如何验证所选用户名是否唯一? 该代码应该在 User 模型中,还是在 UserService 类中,还是在 UserRepository 类中?

    如果唯一性验证应该在 User 模型中,那么 User 模型应该可以访问 UserService 或 UserRepository 类。这可以吗,还是违反任何“最佳实践”模式?

    例如:

    class User
    {
      string Username { get; set; }
      string Email { get; set; }
      string Password { get; set; } // hashed and salted of course :)
    
      IEnumerable<RuleViolation> Validate()
      {
        List<RuleViolation> violations = new List<RuleViolation>();
        IUserService service = MyApplicationService.UserService; // MyApplicationService is a singleton class, especialy designed so that the User model can access application services
    
        // Username is required
        if ( string.IsNullOrEmpty(Username) )
           violations.Add(new RuleViolation("Username", "Username is required"));
    
        // Username must be unique: Should uniqueness be validated here?
        else if( !service.IsUsernameAvailable(Username)
           violations.Add(new RuleViolation("Username", "Username is already taken!"));
    
        // Validate email etc...
    
        return violations;
      }
    }
    
    interface IUserRepository
    {
      void Save(User item);
    }
    
    interface IUserService
    {
      IUserRepository UserRepository { get; }
      void Save(User item);
    }
    
    class UserService : IUserService
    {
      public UserService(IUserRepository userRepository)
      {
         this.UserRepository = userRepository;
      }
    
      IUserRepository UserRepository { get; private set}
    
      public void Save(User user)
      {
         IEnumerable<RuleViolation> violations = user.Validate();
    
         if(violations.Count() > 0)
             throw new RuleViolationException(violations); // this will be catched by the Controller, which will copy the violations to the ModelState errors collection. But the question is, should we validat the user here, or in the UserRepository class?
    
          UserRepository.Save(user);
      }
    }
    
    class UserRepository : IUserRepository
    {
       void Save(User item)
       {
          IEnumerable<RuleViolation> violations = user.Validate();
    
         if(violations.Count() > 0)
             throw new RuleViolationException(violations); // this will be catched by the Controller, which will copy the violations to the ModelState errors collection. But the question is, should we validate the user here, or in the UserService class?
    
          UserRepository.Save(user);
    
       }
    }
    

    我的猜测是验证应该尽可能接近模型。所以我会说 UserRepository 应该是负责验证它正在添加的模型的人。

    对我来说最重要的问题是:用户模型是否应该知道 IUserService / IUserRepository 接口,以便它可以验证用户名的唯一性? 还是应该 IUserService 服务验证唯一性?

    我很好奇你对此的看法!

    【讨论】:

    • 我的观点是,如果我必须在余生中为每个具有 Name 属性的对象编写相同的 if(string.IsNullOrEmpty()) 逻辑,我会非常不高兴。
    • 那么,什么会让你开心呢?您如何验证必填字段?也许流利的验证?
    【解决方案6】:

    我将 DataAnnotations 属性与 MVC 模型绑定器结合使用来进行验证,这非常棒。由于我将用户输入视为命令视图模型,因此它是保持域清洁不受外界关注的最干净的方法。

    http://bradwilson.typepad.com/blog/2009/04/dataannotations-and-aspnet-mvc.html

    这也让我可以利用 LosTechies.com 的 AutoForm:

    http://www.lostechies.com/blogs/hex/archive/2009/06/17/opinionated-input-builders-part-8-the-auto-form.aspx

    我希望 MVC 2 VS 2010 中的客户端验证工具也能利用这些属性。

    因此,我现在正以极快的速度制作用户输入视图模型、命令,并将它们绑定到 AutoForm 功能和我自己的自定义 UI 模板中,以从这些属性中获取 AutoGrid 和 AutoOutput。

    没有什么比说更好的了:

    Html.AutoForm(Model);
    

    或者

    Html.AutoGrid(Model.Products);
    

    并以非常干燥和正交的方式获得验证和 html 生成。我的控制器很轻,我的域是原始的,而且我的时间是通过在每个具有 FirstName 属性的对象上编写相同的 if(string.IsNullOrEmpty()) 方法来占用的。

    对我来说,这种方法并不像其他人所写的那样“哲学”。我正在尝试对 MVC 开发非常务实,并且从这些位中获得了巨大的收益。

    【讨论】:

      猜你喜欢
      • 2013-11-18
      • 1970-01-01
      • 1970-01-01
      • 2011-05-03
      • 1970-01-01
      • 2011-07-12
      • 2021-09-15
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多