C# 实体框架:添加到上下文和 saveChanges() 之间的数据验证

本文关键字:之间 验证 数据 saveChanges 框架 实体 添加 上下文 | 更新日期: 2023-09-27 18:31:27

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

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

在我的邮政管理器中,我有以下方法:

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() 之前调用验证方法,但在将对象添加到上下文之后。在此简单方案中验证数据的最佳做法是什么?最好验证参数?如果验证方法在将对象添加到上下文后引发异常,对象发布会发生什么情况?

更新:

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

C# 实体框架:添加到上下文和 saveChanges() 之间的数据验证

我强烈建议您(如果可能的话)修改您的实体,以便资源库是私有的(不用担心,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 的实体中会导致类更容易单独测试几个数量级,如果你关心这种事情的话。

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

数据注释 (DA) 允许您指定属性的属性,以描述可接受的值。 重要的是要知道 DA 完全独立于数据库和 ORM API(如实体框架),因此使用 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; }
}

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

是的,你可以。 若要在应用程序的任何时间验证此对象(无论它是否已达到 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,就会检测到验证属性,在那里也会在那里进行验证,以防您忘记或对象此后发生更改。

我认为您应该按照上面所说的@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 class: 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

我总是使用两个验证:

  • 客户端 - 将 jQuery 非侵入式验证与数据注释结合使用
  • 服务器端验证 - 这里取决于应用程序 - 验证在控制器操作或更深的业务逻辑中执行。这样做的好地方是在您的上下文中重写 OnSave 方法并在那里执行

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

您可以通过以下方式修改代码:

    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;
    }
将数据添加到 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;
}