[Entity Framework 6] 使用 ValidateEntity 實作自訂驗證

上篇提到 Model Validation,在 EF 裡面,也是可以吃的到 ValidationAttribute,當調用 SaveChanegs 就會進行 Model 的檢查,當需要把檢查機制寫在 EF 的時候,就可以利用此招

情境一:

DataTime的最小值為西元1年1月1日,這對 SQL Server 的 DateTime型別是非法數值,但對 .NET 來講合法,在開發的過程常常會碰到這種錯誤訊息,這是因為沒有處理到日期欄位,EF 也沒幫我們檢查出來,丟給 SQL Server 後就爆掉了

System.Data.SqlClient.SqlException: The conversion of a datetime2 data type to a datetime data type resulted in an out-of-range value.

為此我想要檢查 Entity Model 的每一個日期欄位都有塞值,沒有塞到的就回報我是哪一個欄位有問題

情境二:

新增資料時確保 Log資料有新增 

 

實作

Entity Model 如下:

[Table("Member")]
internal class Member
{
    public Member()
    {
        this.Logs = new HashSet<MemberLog>();
    }
 
    [Key]
    public Guid Id { getset}
 
    [StringLength(100)]
    public string Name { getset}
 
    public DateTime CreateAt { getset}
 
    public virtual ICollection<MemberLog> Logs { getset}
}
internal class MemberLog
{
    [Key]
    public Guid Id { getset}
 
    [ForeignKey("Member")]
    public Guid Memebr_Id { getset}
 
    [StringLength(100)]
    public string Name { getset}
 
    public virtual Member Member { getset}
 
}

 

複寫 DbContext.ValidateEntity

internal class ValidationDbContext : DbContext
{

    protected override DbEntityValidationResult ValidateEntity(DbEntityEntry entityEntry,
                                                               IDictionary<objectobject> items)
    {
        var result = base.ValidateEntity(entityEntry, items);
        //TODO:Custom Validation 
 
        return result;
    }
    
}

 

驗證最小日期

把資料的驗證放在這個方法裡面,這裡要做的事很簡單,就是檢查所有的日期欄位

private void ValidateMinDateTime(DbEntityValidationResult result)
{
    var entityEntry = result.Entry;
    var entityType = entityEntry.Entity.GetType();
    var datePropertyInfos = GetDatePropertyInfos(entityType);
    result = new DbEntityValidationResult(entityEntry, new List<DbValidationError>());
    foreach (var datePropertyInfo in datePropertyInfos)
    {
        var name = datePropertyInfo.Name;
        var value = (DateTime) datePropertyInfo.GetValue(entityEntry.Entity, null);
        if (value == DateTime.MinValue)
        {
            result.ValidationErrors.Add(new DbValidationError(name, $"Not support {value} data"));
        }
    }
}
 
private static IEnumerable<PropertyInfo> GetDatePropertyInfos(Type entityType)
{
    List<PropertyInfo> results = null;
 
    if (s_datePropertyInfo.ContainsKey(entityType))
    {
        results = s_datePropertyInfo[entityType].ToList();
    }
    else
    {
        results = new List<PropertyInfo>();
        var propertyInfos = entityType.GetProperties();
 
        foreach (var propertyInfo in propertyInfos)
        {
            if (IsDateTime(propertyInfo.PropertyType))
            {
                results.Add(propertyInfo);
            }
        }
 
        s_datePropertyInfo.TryAdd(entityType, results.ToArray());
    }
 
    return results;
}
 
private static bool IsDateTime(Type sourceType)
{
    var result = false;
    result = sourceType == typeof(DateTime) || sourceType == typeof(DateTime?);
    return result;

 

驗證的執行順序

先執行自訂驗證再執行基本驗證

protected override DbEntityValidationResult ValidateEntity(DbEntityEntry entityEntry,
                                                           IDictionary<objectobject> items)
{
    var result = new DbEntityValidationResult(entityEntry, new List<DbValidationError>());
    this.ValidateMinDateTime(result);
    if (!result.IsValid)
    {
        return result;
    }
 
    return base.ValidateEntity(entityEntry, items);
}

 

先執行基本驗證再執行自訂驗證

protected override DbEntityValidationResult ValidateEntity(DbEntityEntry entityEntry,
                                                           IDictionary<objectobject> items)
{
    var result = base.ValidateEntity(entityEntry, items);
    if (result.IsValid)
    {
        this.ValidateMinDateTime(result);
    }
 
    return result;
}

 

或是兩者一起執行

protected override DbEntityValidationResult ValidateEntity(DbEntityEntry entityEntry,
                                                           IDictionary<objectobject> items)
{
    var result = new DbEntityValidationResult(entityEntry, new List<DbValidationError>());
    this.ValidateMinDateTime(result);
    base.ValidateEntity(entityEntry, items);
    return result;
}

 

以目前情境,個人比較偏好先執行基本驗證。

驗證是否有Log

再來看個例子,異動資料時,若 Logs 欄位沒有資料,則驗證失敗

private void ValidateHasLogIfChangeMode(DbEntityValidationResult result)
{
    var propertyName = "Logs";
    var entityEntry = result.Entry;
    var entityType = entityEntry.Entity.GetType();
    var logPropertyInfo = entityType.GetProperty(propertyName);
    if (logPropertyInfo == null)
    {
        return;
    }
 
    var logs = (IEnumerable<object>) logPropertyInfo.GetValue(entityEntry.Entity, null);
    var state = entityEntry.State;
    if ((state == EntityState.Added ||
         state == EntityState.Modified ||
         state == EntityState.Deleted) & logs.Count() == 0)
    {
        result.ValidationErrors.Add(new DbValidationError(propertyName,
                                                          $"New {entityType.Name} must have a log."));
    }
}

 

完成後的 ValidationEntiry 方法如下

protected override DbEntityValidationResult ValidateEntity(DbEntityEntry entityEntry,
                                                           IDictionary<objectobject> items)
{
    var result = base.ValidateEntity(entityEntry, items);
    if (result.IsValid)
    {
        this.ValidateHasLogIfChangeMode(result);
    }
 
    if (result.IsValid)
    {
        this.ValidateMinDateTime(result);
    }
 
    return result;
}

若有謬誤,煩請告知,新手發帖請多包涵


Microsoft MVP Award 2010~2017 C# 第四季
Microsoft MVP Award 2018~2022 .NET

Image result for microsoft+mvp+logo