【问题标题】:Is it possible to prevent EntityFramework 4 from overwriting customized properties?是否可以防止 Entity Framework 4 覆盖自定义属性?
【发布时间】:2024-01-04 22:19:01
【问题描述】:

我首先使用 EF 4 数据库 + POCO。因为 EF 没有简单的方法来说明传入的 DateTimes 是 UTC 类型的,所以我将属性从自动生成的文件移动到另一个文件中的部分类。

    private DateTime _createdOn;
    public virtual System.DateTime CreatedOn
    {
        get { return _createdOn; }
        set
        {
            _createdOn =
                (value.Kind == DateTimeKind.Unspecified)
                    ? _createdOn = DateTime.SpecifyKind(value, DateTimeKind.Utc)
                    : value;
        }
    }

但是,现在每次我更新模型时,自动属性都会在 T4 代中再次创建。当然这会导致以下编译错误:“'Foo' 类型已经包含 'CreatedOn' 的定义”。

有什么方法可以告诉 EF 不要生成该属性并让我自己处理它?

更新

感谢大家的回答...

我创建了一个具有不同名称的新自定义属性。

    public virtual System.DateTime CreatedOnUtc
    {
        get
        {
            return (CreatedOn.Kind==DateTimeKind.Unspecified)
                ? DateTime.SpecifyKind(CreatedOn, DateTimeKind.Utc)
                : CreatedOn;
        }
        set
        {
            CreatedOn =
                (value.Kind == DateTimeKind.Unspecified)
                    ? CreatedOn = DateTime.SpecifyKind(value, DateTimeKind.Utc)
                    : value;
        }
    }

我还将自动生成的属性的所有 setter 和 getter 设置为 Private,但我需要在 Linq-to-Entities 查询中使用的那些属性除外(叹气)。在这些情况下,我将这些 getter 设置为内部。

我当然希望在 DateTime 类型上有一个下拉菜单,以指定 EF 应将其视为什么“种类”的 DateTime。这将节省时间和额外的复杂性。

【问题讨论】:

    标签: c# entity-framework entity-framework-4 poco database-first


    【解决方案1】:

    另一种方法是挂钩 DbContext 中的 ObjectMaterialized 事件并在那里设置类型。

    在我的 DbContext 构造函数中,我这样做:

        ((IObjectContextAdapter)this).ObjectContext.ObjectMaterialized += new ObjectMaterializedEventHandler(ObjectMaterialized);
    

    然后方法是这样的:

    private void ObjectMaterialized(object sender, ObjectMaterializedEventArgs e)
            {
                Person person = e.Entity as Person;
                if (person != null) // the entity retrieved was a Person
                {
                    if (person.BirthDate.HasValue)
                    {
                        person.BirthDate = DateTime.SpecifyKind(person.BirthDate.Value, DateTimeKind.Utc);
                    }
                    person.LastUpdatedDate = DateTime.SpecifyKind(person.LastUpdatedDate, DateTimeKind.Utc);
                    person.EnteredDate = DateTime.SpecifyKind(person.EnteredDate, DateTimeKind.Utc);
                }
            }
    

    缺点是您需要确保为您关心的每个属性设置它,但至少将其设置在可能的最低级别。

    【讨论】:

    • 如果我再次遇到这个问题,我想我会倾向于这种方法。谢谢。
    • 我尝试使用这种方法,但发现它根本不适合我。我必须做的是指定 DateTime 对象在连接字符串中被指定为 Ticks,并添加:“datetimeformat=Ticks;” .之后,它确实起作用了。
    • 这考虑从数据库中检索实体,但不存储它们。
    【解决方案2】:

    我使用了与 Michael 相同的方法,然后我再深入一点,并使用反射来搜索 DateTime 和 DateTime?

    我确保所有 DateTime 值都被读取为 Utc DateTimes 的解决方案如下:

    首先我在我的 DbContext Extensions 方法类中编写了三个方法。因为我需要将它用于多个 DbContexts

    public static void ReadAllDateTimeValuesAsUtc(this DbContext context)
    {
            ((IObjectContextAdapter)context).ObjectContext.ObjectMaterialized += ReadAllDateTimeValuesAsUtc;
    }
    
    private static void ReadAllDateTimeValuesAsUtc(object sender, ObjectMaterializedEventArgs e)
    {
        //Extract all DateTime properties of the object type
        var properties = e.Entity.GetType().GetProperties()
            .Where(property => property.PropertyType == typeof (DateTime) ||
                               property.PropertyType == typeof (DateTime?)).ToList();
        //Set all DaetTimeKinds to Utc
        properties.ForEach(property => SpecifyUtcKind(property, e.Entity));
    }
    
    private static void SpecifyUtcKind(PropertyInfo property, object value)
    {
        //Get the datetime value
        var datetime = property.GetValue(value, null);
    
        //set DateTimeKind to Utc
        if (property.PropertyType == typeof(DateTime))
        {
            datetime = DateTime.SpecifyKind((DateTime) datetime, DateTimeKind.Utc);
        }
        else if(property.PropertyType == typeof(DateTime?))
        {
            var nullable = (DateTime?) datetime;
            if(!nullable.HasValue) return;
            datetime = (DateTime?)DateTime.SpecifyKind(nullable.Value, DateTimeKind.Utc);
        }
        else
        {
            return;
        }
    
        //And set the Utc DateTime value
        property.SetValue(value, datetime, null);
    }
    

    然后我转到我的 WebsiteReadModelContext 的构造函数,它是一个 DbContext 对象并调用 ReadAllDateTimeValuesAsUtc 方法

    public WebsiteReadModelContext()
    {
          this.ReadAllDateTimeValuesAsUtc();
    }
    

    【讨论】:

    • 聪明,但我会担心使用反射的性能。没有什么可以帮助您了解是否值得担心。
    • 很好的快速修复,相反,存储日期和时间需要另一个钩子来断言日期时间类型。
    • 我认为ObjectContext.SavingChanges 可以用于此。 (需要添加有关如何处理“本地”日期的其他逻辑)。我认为@Jason 对他的性能问题是正确的,但现在这对我来说已经足够了。更好的方法是编辑 TT 模板,无需反射。
    • 事实证明,这样做的真正成本实际上是访问IObjectContextAdapter.ObjectContext 属性,因为这将触发 EF 内部的昂贵操作。仅 400 个 DbContext 创建就需要 6 秒以上。
    • 我相信使用ObjectContext应该只在第一次调用时很昂贵,此时它将建立数据库连接。这与您第一次读取或写入时产生的费用相同。是吗?
    【解决方案3】:

    我将使用 edmx 并为 CreatedOn 属性指定另一个名称(例如 CreatedOnInternal)。然后将生成的访问修饰符设置为 Internal 而不是 Public。然后您可以在部分类中实现您的自定义属性,而不必担心这一点。

    【讨论】:

      【解决方案4】:

      我认为如果您尝试手动修改 EF 生成的类,事情就会变得一团糟。

      我建议采用两种选择:

      1. 不要修改现有属性,而是向部分类添加一个新属性,CreatedOnUTC 或类似的东西。
      2. 修改 T4 模板以更改它为这些日期属性生成访问器的方式(如果每个 DateTime 属性都以相同的方式工作,则更容易)。这不是微不足道的,因为它依赖于类型,但至少可以让您在未来使用生成器。

      【讨论】:

      • 谢谢@steve。我选择了选项 1。我只是对 DateTime 值的语义如此不可或缺的东西不是 edmx 中的简单下拉选项感到沮丧。
      • 好答案。 @All:如果您想了解 2. 的详细工作原理,请查看 this link
      【解决方案5】:

      当涉及到它生成的东西时,EF 有时会非常讨厌。
      在 Database First 应用程序中,当我遇到类似的问题时,我通常会采用这种方法:

      • 保留自动生成的属性,但将它们设为 private 并更改其名称;
      • 添加对业务代码有意义的公共“包装”属性。

      例如,我会将CreatedOn 重命名为其他名称,例如CreatedOnInternal(归功于Jeff)并在设计器中将其标记为私有。在部分类中,我将添加一个 public CreatedOn 包装器属性,用于来回转换。

      【讨论】:

      • 我刚刚意识到 Jeff 已经发布了这个想法,并且由于他的命名建议比我的好,我编辑了我的答案。
      【解决方案6】:

      我知道这是一个老问题,但这是一个不需要反思或为每个新属性编辑 DbContext 的解决方案。

      它包括编辑Context.tt:

      首先在 tt 文件顶部添加以下内容(大约第 40 行):

      using System.Data.Entity.Core.Objects;
      

      然后在构造函数的正下方(在我的例子中是第 86 行),添加以下生成代码:

      <#
      var entitiesWithDateTime = typeMapper.GetItemsToGenerate<EntityType>(itemCollection)
                                      .Where(e => e.Properties.Any(p => typeMapper.GetTypeName(p.TypeUsage) == "System.DateTime"));
      
      if(entitiesWithDateTime.Count() > 0)
      {
      #>
          private void ObjectMaterialized(object sender, ObjectMaterializedEventArgs e)
          {
      <#
          var count = 0;
          foreach (var entityType in entitiesWithDateTime)
          {
      #>
              <#=count != 0 ? "else " : String.Empty#>if(e.Entity is <#=entityType.Name#>)
              {
                  var entity = e.Entity as <#=entityType.Name#>;
      <#
                  foreach(var property in entityType.Properties.Where(p => typeMapper.GetTypeName(p.TypeUsage) == "System.DateTime"))
                  {
      #>
                  entity.<#=property.Name#> = DateTime.SpecifyKind(entity.<#=property.Name#>, DateTimeKind.Utc);
      <#
                  }
      #>
              }
      <#
              count++;
          }
      #>
          }
      <#
      }
      #>
      

      这将在编译时围绕 DbContext 的所有实体进行迭代,并在每个 DateTimeProperties 上调用 DateTime.SpecifyKind

      此代码将生成与 michael.aird 相同的代码,但无需对每个新属性进行手动编辑!

      【讨论】: