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() 之前调用验证方法,但在将对象添加到上下文之后。在此简单方案中验证数据的最佳做法是什么?最好验证参数?如果验证方法在将对象添加到上下文后引发异常,对象发布会发生什么情况?
更新:
我必须根据数据验证错误抛出一组自定义异常。
我强烈建议您(如果可能的话)修改您的实体,以便资源库是私有的(不用担心,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;
}
后,在调用 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;
}