【问题标题】:How can I pass service layer validation messages back to the caller?如何将服务层验证消息传递回调用方?
【发布时间】:2012-04-23 23:42:11
【问题描述】:

我做了很多研究,包括这里的 SO,我似乎找不到明确的方向。我目前有一个 ASP.NET MVC3 应用程序,其服务层位于存储库之上。

在我的服务层,我有如下功能:

public class MyService{

    public void CreateDebitRequest(int userId, int cardId, decimal Amount, .... )
    {
    //perform some sort of validation on parameters, save to database
    }

    public void CreateCreditRequest(.....)
    }
        //perform some sort of validation on parameters, save to database
    }

    public void CreateBatchFile()
    {
        //construct a file using a semi-complex process which could fail
        //write the file to the server, which could fail
    }


    public PaymentTransaction ChargePaymentCard(int paymentCardId, decimal amount)
    {
        //validate customer is eligible for amount, call 3rd party payments api call,
        //...save to database, other potential failures, etc.
    }

}

我看到有人说参数验证不是很例外,所以抛出异常不是很合适。我也不喜欢传入输出参数(例如字符串)并检查空值的想法。我已经考虑实现一个 ValidationDictionary 类,并使其成为任何给定服务类的属性(它将包含一个 IsValid 布尔值和一个错误消息列表,并且可以在服务层中的任何给定函数调用之后检查以查看如何事情过去了)。运行任何给定函数后,我可以检查 ValidationDictionary 状态:

var svc = new MyService();
svc.CreateBatchFile();
if (svc.ValidationDictionary.IsValid)
    //proceed
else
   //display values from svc.ValidationDictionary.Messages...

对此我不喜欢的一点是,我必须为每个服务层函数调用更新它,以避免它保留旧值(如果我选择不将它用于许多或大多数函数,人们会仍然期望它在运行任何给定函数后具有有意义的值或空值)。我考虑的另一件事是为每个可能具有详细验证信息的函数调用传递 ValidationDictionary,但随后我又回到使用 out 参数...

你们有什么建议吗?我似乎想不出任何干净的方法来做到这一点。有时为函数返回 null 就足够了,但有时我希望将更多的验证信息传递回调用者。任何建议将不胜感激!

编辑澄清: 我的服务层不知道它是一个 MVC 应用程序正在使用它。服务层只有某些公共函数,例如 CreateBatchFile() 或 AddDebitRequest()。有时返回 null 足以让消费者(在这种情况下是控制器,但可能是其他东西)知道发生了什么,有时消费者希望从服务层获得更多信息(如果消费者是,则可能传递给 ModelState控制器)。如何从服务层本身冒泡?

【问题讨论】:

    标签: c# asp.net-mvc-3 validation repository service-layer


    【解决方案1】:

    这就是我所做的。有一个用于验证的类,而不是传递参数传递一个视图模型。因此,在您的情况下,ValidationResult 只是一个带有 MemberName 和 ErrorMessage 属性的简单类:

    public class DebitRequestValidator{
    
      public IEnumerable<ValidationResult> Validate(DebitRequestModel model){
    
        //do some validation
        yield return new ValidationResult {
          MemberName = "cardId",
          ErrorMessage = "Invalid CardId."
        }
      }  
    

    }

    然后创建一个控制器扩展方法将这些验证结果复制到模型状态。

    public static class ControllerExtensions
    {
        public static void AddModelErrors(this ModelStateDictionary modelState, IEnumerable<ValidationResult> validationResults)
        {
            if (validationResults == null) return;
    
            foreach (var validationResult in validationResults)
            {
                modelState.AddModelError(validationResult.MemberName, validationResult.ErrorMessage);
            }
        }
    }
    

    然后在您的控制器中执行类似的操作

    [HttpPost]
    public ActionResult DebitRequest(DebitRequestModel model) {
      var validator = new DebitRequestValidator();
      var results = validator.Validate(model);
      ModelState.AddModelErrors(results);
      if (!ModelState.IsValid)
        return View(model)
    
      //else do other stuff here
    }
    

    然后在您的视图中您可以像正常一样显示错误。

    @Html.ValidationMessageFor(m => m.CardId)
    

    【讨论】:

    • 我的服务层不知道它是一个 MVC 应用程序正在使用它。它只是具有某些公共函数,例如 CreateBatchFile 或 AddDebitRequest。有时返回 null 足以让控制器知道发生了什么,有时控制器希望从服务层获得更多信息(可能传递给 ModelState 等)。如何从服务层本身冒泡?
    • @Josh - 这个例子中的服务层对 MVC 没有依赖,只有 ValidationResult 类,它只是我创建的一个普通类。您可以通过传入 ModelState 来做同样的事情,但我认为返回 IEnumerable 是一种更好的方法,因为它更容易测试。
    • 我会在哪里返回这个 IEnumberable?我知道在调用服务函数之前应该在某种程度上进行验证,但是如果在服务层的实际函数中发生错误(例如在 svc.CreateBatchFile() 中)会发生什么?我是否必须将一个空的 IEnumerable 作为输出参数传递给服务函数,然后在调用我的服务函数后检查它?例如:svc.CreateBatchFile(出验证结果)?谢谢
    • @Josh - 您可以从服务函数返回验证结果,无需输出参数。如果有任何错误,控制器扩展(ModelState.AddModelErrors 东西)将负责将其添加到模型状态。
    • 在我的例子中,我有诸如 ChargePaymentCard() 之类的项目,它返回一个 PaymentTransaction,或者 CreateUser,它返回一个用户。如果我返回 ValidationResults,那么我似乎失去了将相关项目返回给函数的能力......?
    【解决方案2】:

    我使用了一个传递消息数组(或类集合)的系统,每个元素都有代码、描述和友好消息。我们过去只是简单地检查是否有任何东西。它在 UI 和另一个“服务”层之间运行良好,所有异常都被很好地捕获,它们被转换为这些验证规则......只是一个想法

    【讨论】:

      【解决方案3】:

      使用在视图和控制器操作方法之间传递的 ViewModel 对象。 ViewModel 对象可以通过Validate(ValidationDictionary validationDictionary) 方法处理验证。

      控制器必须在调用服务层中的任何方法之前调用 ViewModel 对象的 Validate 方法。这应该只对 http POST 操作是必需的。

      然后您的视图将必须显示验证消息。

      此解决方案要求在控制器操作和视图之间传递视图模型对象,但现在这主要由 MVC 中的 ModelBinder 处理。

      您的控制器(http post)操作将如下所示:

      [HttpPost]
      public ActionResult Foo(BarViewModel viewModel)
      {
          viewModel.Validate(ValidationDictionary);
      
          if (!ModelState.IsValid)
          {
              return View(viewModel);
          }
      
          // Calls to servicelayer
      }
      

      ViewModel 中的 Validate 方法如下所示:

      public void Validate(ValidationDictionary validationDictionary)
      {
          if (SomeProperty.Length > 30)
          {
              validationDictionary.AddError("SomeProperty", "Max length is 30 chars");
          }
      }
      

      【讨论】:

      • 嗯...我想我说的是较低级别。我们假设用户按下了一个按钮,并且视图模型验证顺利进行。然后我的控制器将调用服务层中的一个函数,该函数可能由于各种原因而失败(或者视图模型验证比服务实际需要的更慷慨,所以当服务为自己验证参数时,它会失败) .我如何将这些信息从服务层传递回控制器,甚至可以使用 viewmodel 或 modelstate 进行显示?
      • 我确保所有验证逻辑都存在于视图模型中。为什么您希望验证逻辑遍布各处?输入验证属于用户界面模块。如果服务层出现问题,您应该抛出异常,以便为日志记录提供更好的错误消息。然后,您的 MVC 应用程序可以根据异常/http 代码重定向到友好的错误页面。
      • 假设我在服务层,并验证传递给 CreateDebitRequest(...) 的日期不超过未来三天。似乎我应该在服务层中验证这一点,除了我在链中更高的地方验证它。你同意吗?如果是这样,那么您是否认为我实际上应该在我的服务层中执行验证,但只是在验证失败时抛出异常?或者我不应该在我的服务层中验证这个日期(我会对依赖服务消费者感到紧张)?谢谢...
      • 我认为您应该在服务层之上的级别进行用户界面验证(如 CreateDebitRequest 的日期)。如果您自己不编写上述层并且不信任该层,我认为您的服务层应该抛出异常。服务层是 MVC 应用程序正在使用的 Web 服务层吗?在这种情况下,服务层之上的 web 服务层应该进行输入验证,并使用适当的 http 代码和消息相应地抛出异常。
      • 有趣。不,此时没有 Web 服务。所以你本质上是说任何进入服务层的无效都是值得例外的(听起来很合理,但只是想清楚)?消费者有责任在数据进入之前通过验证数据尽量不抛出异常,但如果消费者不负责任,那么服务层将抛出异常作为最后的手段。这听起来准确吗?它的清洁度很吸引人。
      【解决方案4】:

      如果您只是进行 ViewModel 验证,FluentValidation 是一个出色的库。

      如果您想将业务验证作为对用户的反馈,您可以使用适配器模式,它会为您提供您想要的。

      创建一个接口(IValidationDictionary 或类似的东西)。此接口将定义一个 AddError 方法,并将传递给您的服务以添加错误消息。

      public interface IValidationDictionary
      {
          void AddError(string key, string errorMessage);
      }
      

      为您的 mvc 应用程序创建一个 ModelStateAdapter。

      public class ModelStateAdapter : IValidationDictionary
      {
          private ModelStateDictionary _modelState;
      
          public ModelStateAdapter(ModelStateDictionary modelState)
          {
              _modelState = modelState;
          }
      
          public void AddError(string key, string errorMessage)
          {
              _modelState.AddModelError(key, errorMessage);
          }
      }
      

      需要验证的服务调用需要 IValidationDictionary

          public class MyService
          {        
              public void CreateDebitRequest(int userId, int cardId, decimal Amount, .... , IValidationDictionary validationDictionary)
                {
                      if(userId == 0)
                          validationDictionary.AddError("UserId", "UserId cannot be 0");
                }
           }
      

      然后,您将依赖于 IValidationDictionary,但不依赖于 MVC,这也会使您的解决方案可测试。

      如果您需要在没有ModelStateDictionary 的应用中实现服务,您只需在用于保存错误的类上实现IValidationDictionary 接口。

      控制器示例:

      public ActionResult Test(ViewModel viewModel)
      {
          var modelStateAdapter = new ModelStateAdapter(ModelState);
          _serviceName.CreateDebitRequest(viewModel.UserId, viewModel.CardId, ... , modelStateAdapter);
      
          if(ModelState.IsValid)
              return View("Success")
      
          return View(viewModel);
      }
      

      这种方法的优点:

      • 不依赖调用库
      • 可以模拟 IValidationDictionary 进行测试。

      这种方法的缺点:

      • 您需要将 IValidationDictionary 传递给您想要对其进行验证的每个方法,然后将其返回给用户。

        或者

        您需要在要验证的每个控制器操作中初始化服务的验证字典(如果您决定将 IValidationDictionary 作为私有字段)。

      【讨论】:

        猜你喜欢
        • 1970-01-01
        • 2018-05-21
        • 1970-01-01
        • 2020-09-18
        • 2016-11-21
        • 2014-05-23
        • 2019-12-08
        • 2010-09-06
        相关资源
        最近更新 更多