【问题标题】:C# Entity Framework: Data validation between add to context and saveChanges()C# Entity Framework:添加到上下文和 saveChanges() 之间的数据验证
【发布时间】:2016-01-21 13:06:21
【问题描述】:

我有一个使用 C# 中的实体框架的简单场景。我有一个实体帖子:

public class Post
{
    public int Id { get; set; }
    public string Name { get; set; }
    public string Description { get; set; }
}

在我的 PostManager 中,我有这些方法:

public int AddPost(string name, string description)
    {
        var post = new Post() { Name = name, Description = description };

        using (var db = new DbContext())
        {
          var res = db.Posts.Add(post);
          res.Validate();
          db.SaveChanges();
          return res.Id;
        }
    }

    public void UpdatePost(int postId, string newName, string newDescription)
    {
        using (var db = new DbContext())
        {
            var data = (from post in db.Posts.AsEnumerable()
                where post.Id == postId
                select post).FirstOrDefault();

            data.Name = newName;
            data.Description = newDescription;
            data.Validate();
            db.SaveChanges();
        }
    }

方法validate()引用类:

public static class Validator
{
    public static void Validate(this Post post)
    {
        if ( // some control)
            throw new someException();
    }

我在 savechanges() 之前调用 validate 方法,但在将对象添加到上下文之后。在这个简单的场景中验证数据的最佳实践是什么?最好验证论点吗?如果 validate 方法在将对象添加到上下文后抛出异常,对象 post 会发生什么?

更新:

我必须根据数据验证错误抛出一组自定义异常。

【问题讨论】:

  • 我通常只使用数据注释msdn.microsoft.com/en-us/library/dd901590(VS.95).aspx EF 将“寻找它”;否则还有其他方法可以在运行时从您自己的代码中调用它
  • @Micky 你好!在我的项目中,当我验证数据时,我必须抛出 customException。可以使用数据注释来做到这一点吗?
  • 没问题,看看我下面的答案

标签: c# entity-framework validation


【解决方案1】:

我强烈建议您(如果可能的话)修改您的实体,以便设置器是私有的(不用担心,EF 仍然可以在创建代理时设置它们),将默认构造函数标记为受保护(EF 仍然可以这样做延迟加载/代理创建),并让唯一可用的公共构造函数检查参数。

这有几个好处:

  • 您可以限制可以更改实体状态的位置数量,从而减少重复
  • 你保护你的类的不变量。通过强制通过构造函数创建实体,您可以确保您的实体对象不可能以无效或未知状态存在。
  • 您会获得更高的凝聚力。通过将数据约束置于更接近数据本身的位置,可以更轻松地理解和推理您的类。
  • 您的代码会在更高程度上实现自我记录。人们永远不必怀疑“如果我在这个int 属性上设置一个负值可以吗?”如果一开始就不可能做到的话。
  • 关注点分离。您的经理不应该知道如何验证实体,这只会导致高度耦合。我见过许多经理成长为无法维护的怪物,因为他们只是无所事事。持久化、加载、验证、错误处理、转换、映射等。这基本上与 SOLID OOP 截然相反。

我知道现在真的很流行将所有“模型”制作成带有 getter 和 setter 的愚蠢属性包,并且只有一个默认构造函数,因为(糟糕的)ORM 迫使我们这样做,但现在情况已不再如此,这个 imo 有很多问题。

代码示例:

public class Post
{
    protected Post() // this constructor is only for EF proxy creation
    {
    }

    public Post(string name, string description)
    {
        if (/* validation check, inline or delegate */)
            throw new ArgumentException();

        Name = name;
        Description = description;
    }

    public int Id { get; private set; }
    public string Name { get; private set; }
    public string Description { get; private set; }
}

那么你的PostManager 代码就变得微不足道了:

using (var db = new DbContext())
{
    var post = new Post(name, description); // possibly try-catch here
    db.Posts.Add(post);
    db.SaveChanges();
    return post.Id;
}

如果创建/验证逻辑极其复杂,则此模式非常适合重构为负责创建的工厂。

我还要注意,将数据封装在暴露最小状态更改 API 的实体中会导致几个数量级的类更容易单独测试,如果您完全关心这类事情的话。

【讨论】:

  • 感谢您的回复!我还必须在更新操作中进行验证。如果我更新一个帖子实体,我也必须在“set”方法中编写所有验证条件..
  • 如果您有一组已知的需要实体更改状态的用例,您应该创建封装这些操作的实例方法。这些也很容易测试,具有很高的内聚性(将所有验证保存在一个巨大的验证静态类中也是维护噩梦的燃料)并有助于保护您的不变量。如果验证很简单,您可以考虑直接公开 setter,但我发现在大多数情况下,访问私有 setter 的方法更清洁且更易于维护。
  • 我看不出“刚好足以让对象处于有效状态”与我所写的相反。需要的值应该注入,不需要的值当然应该省略。虽然我认为在可能的情况下支持不变性通常是明智的,但我从未提出所有类都应该是不可变的情况。 private setter 不是不可变的类。正如我在上面的评论中所说,在适当的情况下应该使类的突变成为可能,但它应该是一小部分众所周知的操作,而不是一扇敞开的被滥用的大门。
  • 私有设置器阻止更改跟踪也是不真实的。我什至在答案中明确解决了这个问题,并且我有自己的生产代码和 EF 文档作为它的活生生的证明。
  • 什么?您是在谈论可变性还是更改跟踪?它们是不同的东西。如果你想允许对象的突变,你可以公开一个公共的 setter 或一个方法。关键是您在分配数据时验证数据,您可以防止对象处于无效状态。不要让任何人随心所欲地改变对象。这是可靠的编程 101。另一方面,更改跟踪是由 EF 完成的,这一点也不关心您设置的属性的访问修饰符。如果您不相信我,请阅读文档。
【解决方案2】:

正如我在上面的 cmets 中提到的,您可能需要查看 .NET System.ComponentModel.DataAnnotations 命名空间。

数据注释 (DA) 允许您指定属性的属性以描述可接受的值。重要的是要知道 DA完全独立于数据库和 ORM API(例如 Entity Framework),因此用 DA 属性修饰的类可以在系统的任何层中使用数据层;周转基金; ASP.NET MVC 或 WPF。

在下面的示例中,我定义了一个具有一系列属性的Muppet 类。

  • Name 是必需的,最大长度为 50。

  • Scaryness 采用 int,但它必须在 {0...100} 的范围内。

  • Email 装饰有一个虚构的自定义验证器,用于验证应包含电子邮件的字符串。

例子:

public class Muppet
{
    [Required]
    [StringLength(50)]
    public string Name {get; set;}  

    public Color Color {get; set; }

    [Range(0,100)]
    public int Scaryness {get; set; }

    [MyCustomEmailValidator]
    public string Email {get;set; }
}

在我的项目中,我必须在验证数据时抛出 customException。可以使用数据注释来做到这一点吗?

是的,你可以。要在您的应用程序的任何时间验证此对象(无论它是否已达到 EF),只需执行以下操作:

using System;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using System.Linq;

.
.
.
Post post = ... // fill it in
Validator.Validate(post);

public static class Validator
{
    public static void Validate(this Post post)
    {
        // uses the extension method GetValidationErrors defined below
        if (post.GetValidationErrors().Any())
        {
            throw new MyCustomException();
        }
     }
}


public static class ValidationHelpers
{

    public static IEnumerable<ValidationResult> GetValidationErrors(this object obj)
    {
        var validationResults = new List<ValidationResult>();
        var context = new ValidationContext(obj, null, null);
        Validator.TryValidateObject(obj, context, validationResults, true);
        return validationResults;
    }
.
.
.

如果你想得到验证错误信息,你可以使用这个方法:

    /// <summary>
    /// Gets the validation error messages for column.
    /// </summary>
    /// <param name="obj">The object.</param>
    /// <returns></returns>
    public static string GetValidationErrorMessages(this object obj)
    {
        var error = "";

        var errors = obj.GetValidationErrors();
        var validationResults = errors as ValidationResult[] ?? errors.ToArray();
        if (!validationResults.Any())
        {
            return error;
        }

        foreach (var ee in validationResults)
        {
            foreach (var n in ee.MemberNames)
            {
                error += ee + "; ";
            }
        }

        return error;
    }

牛排刀的免费设置是,一旦对象到达 EF 就会检测到验证属性,在那里它也会被验证,以防你忘记或对象被更改。

【讨论】:

    【解决方案3】:

    我认为你应该像上面@Micky 所说的那样使用数据注释。您当前的方法是在添加后手动验证。

    using System.ComponentModel.DataAnnotations;
    // Your class
    public class Post
    {
        [Required]
        public int Id { get; set; }
        [Required,MaxLength(50)]
        public string Name { get; set; }
        [Required,MinLength(15),MyCustomCheck] // << Here is your custom validator
        public string Description { get; set; }
    }
    
    // Your factory methods
    public class MyFactory() {
         public bool AddPost() {
         var post = new Post() { Id = 1, Name = null, Description = "This is my test post"};
            try {
                using (var db = new DbContext()) {
                    db.Posts.Add(post);
                    db.SaveChanges();
                    return true;
                }
            } catch(System.Data.Entity.Validation.DbEntityValidationException e) {
                Console.WriteLine("Something went wrong....");
            } catch(MyCustomException e) {
                Console.WriteLine(" a Custom Exception was triggered from a custom data annotation...");
            }
            return false;
    
         }
    }
    
    // The custom attribute
    [AttributeUsage(AttributeTargets.Property | AttributeTargets.Field, AllowMultiple = false)]
    sealed public class MyCustomCheckAttribute : ValidationAttribute
    {
        public override bool IsValid(object value)
            {
              if (value instanceof string) {
                    throw new MyCustomException("The custom exception was just triggered....")
              } else {
                return true;
              }
            }
    }
    
    // Your custom exception
    public class MyCustomException : Exception() {}
    

    另请参阅: DbEntityValidationException 类:https://msdn.microsoft.com/en-us/library/system.data.entity.validation.dbentityvalidationexception(v=vs.113).aspx

    默认数据注释 http://www.entityframeworktutorial.net/code-first/dataannotation-in-code-first.aspx

    构建您的自定义数据注释(验证器): https://msdn.microsoft.com/en-us/library/cc668224.aspx

    【讨论】:

    • 感谢您的回复。我必须抛出自定义异常。可以通过数据标注来实现吗?
    • 我建议使用 ValidationException 类,因为它内置在许多其他子系统中(ASP.NET 有 ModelState: if(ModelState.IsValid) { // submit } else { //显示错误 } 您应该使用 validationexception 类的原因是它已经具有(自动)保存所有验证问题的属性。为什么要使用不同的异常?
    • 我必须为考试完成这个程序集。自定义异常集是项目要求,以简化评估
    • 您可以尝试在自定义数据注释中抛出 MyCustomBusinessLogicException()。只需在我的代码中添加一个 catch(MyCustomBusinessLogicException e) {}。 - 代码已更新以反映最后一点。您所要做的就是创建一个自定义数据注释。
    • 谢谢!我尝试使用自定义数据注释
    【解决方案4】:

    我总是使用两个验证:

    • 客户端 - 结合使用 jQuery Unobtrusive Validation 和 Data Annotations
    • 服务器端验证 - 此处取决于应用程序 - 验证在控制器操作中或更深层次的业务逻辑中执行。这样做的好地方是在您的上下文中覆盖 OnSave 方法并在那里进行

    请记住,您可以编写自定义数据注释属性来验证您需要的任何内容。

    【讨论】:

      【解决方案5】:

      可以这样修改代码:

          public int AddPost(string name, string description)
          {
              var post = new Post() { Name = name, Description = description };
              if(res.Validate())
              {
                  using (var db = new DbContext())
                  {
                    var res = db.Posts.Add(post);
                    db.SaveChanges();
                    return res.Id;
                  }
              }
              else
                  return -1; //if not success
         }
      
      
          public static bool Validate(this Post post)
          {
              bool isValid=false;
              //validate post and change isValid to true if success
              if(isvalid)
                  return true;
              }
              else
                  return false;
          }
      

      【讨论】:

      • 嗨!谢谢你的答复。我也必须在 UpdatePost 中这样做。当我从 DbContext 获取对象时,我必须对更新数据后检索到的对象调用验证方法
      【解决方案6】:

      在将数据添加到 DbContext 之后,在调用 SaveChanges() 之前,您可以调用 DbContext 的 GetValidationErrors() 方法并检查其计数以快速检查是否有任何错误。您可以进一步枚举所有错误并获取每个错误的详细信息。我在 GetValidationErrorsString() 扩展方法中捆绑了从 ICollection 到字符串的错误转换。

       if (db.GetValidationErrors().Count() > 0)
       {
          var errorString = db.GetValidationErrorsString();
       }
      
      
       public static string GetValidationErrorsString(this DbContext dbContext)
      {
          var validationErrors = dbContext.GetValidationErrors();
          string errorString = string.Empty;
          foreach (var error in validationErrors)
          {
      
              foreach (var innerError in error.ValidationErrors)
              {
                  errorString += string.Format("Property: {0}, Error: {1}<br/>", innerError.PropertyName, innerError.ErrorMessage);
              }
          }
          return errorString;
      }
      

      【讨论】:

        猜你喜欢
        • 2014-08-28
        • 1970-01-01
        • 1970-01-01
        • 2020-08-11
        • 1970-01-01
        • 2017-08-04
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        相关资源
        最近更新 更多