【问题标题】:How to use interface properties with CodeFirst如何在 CodeFirst 中使用接口属性
【发布时间】:2012-03-21 13:12:09
【问题描述】:

我有以下实体:

public interface IMyEntity
{
    [Key]
    int Id { get; set; }
    IMyDetail MyDetail { get; set; }
    ICollection<IMyDetail> CollectionOfReferences { get; set; }
}

public interface IMyDetail
{
    [Key]
    int Id { get; set; }
    int IntValue { get; set; }
}

public class MyEntity : IMyEntity
{
    [Key]
    public virtual int Id { get; set; }
    public virtual IMyDetail MyDetail { get; set; }
    public virtual ICollection<IMyDetail> CollectionOfReferences { get; set; }
}

public class MyDetail : IMyDetail
{
    [Key]
    public virtual int Id { get; set; }
    public virtual int IntValue { get; set; }
}

我想使用 EF CodeFirst 来访问数据库并创建数据库架构。但是 CodeFirst 不允许对实体之间的关系使用接口类型。因此它不会在 MyEntity 和 MyDetail 之间创建关系。我无法更改接口,因此我无法将属性类型更改为 MyDetail 而不是 IMyDetail。但是我知道这个模型的客户端只会使用每个接口的一个实现。

我找到了 IMyDetail 类型属性的解决方法。我可以创建一个 MyDetail 类型的属性并显式实现接口的属性:

    private MyDetail _myDetail;

    public virtual MyDetail MyDetail
    {
        get
        {
            return this._myDetail;
        }
        set
        {
            this._myDetail = value;
        }
    }

    IMyDetail IMyEntity.MyDetail
    {
        get
        {
            return this._myDetail;
        }
        set
        {
            this._myDetail = (MyDetail)value;
        }
    }

它工作正常。但此解决方案不适用于 ICollection&lt;IMyDetail&gt;,因为我无法将其转换为 ICollection&lt;MyDetail&gt;

有解决办法吗?

【问题讨论】:

  • EF 不支持接口,所以不要在模型中使用它们或不要使用 EF。
  • 我知道。但我必须使用它们。所以,我尝试做一个解决方法。我希望有人帮助我:)
  • @LadislavMrnka 对于我的一生,我在 EF 官方网站或各种开发人员博客上找不到列出 EF 中不支持的所有属性类型的单个列表(接口,版本 5 之前的枚举, ETC...)。你知道我可以参考的这样一份清单吗?
  • Ladislavs 上面的评论不正确。在模型中使用接口是非常可取的。即使 EF 不支持,您也可以使用它们。请参阅此处找到的 Bogeys 答案:stackoverflow.com/questions/25385161/…
  • 您可以使用 Linq 将集合与以下接口相互转换: IEnumerable IMyEntity.MyDetails { get { return MyDetails.Select(i => i as IMyDetail); } set { MyDetails = (ICollection)value.Select(i => i as MyDetail); } }

标签: entity-framework


【解决方案1】:

我遇到了同样的问题,并找到了像 Nathan 一样的解决方案,但您甚至可以更进一步,通过显式定义接口,将属性命名为相同(此处为 ExtensionsIAddress.Extensions) :

public interface IAddress
{
    string Address { get; set; }
    IEnumerable<IAddressExtension> Extensions { get; set; }
}

public interface IAddressExtension
{
    string Key { get; set; }
    string Value { set; }
}

[Table("AddressExtensions")]
public class AddressExtension : IAddressExtension
{
    [Key]
    public string Id { get; set; }
    public string Key { get; set; }
    public string Value { get; set; }
}

[Table("Addresses")]
public class Address : IAddress
{
    [Key]
    public string Id { get; set; }
    public string Address { get; set; }

    public IEnumerable<AddressExtension> Extensions { get; set; }

    [NotMapped]
    IEnumerable<IAddressExtension> IAddress.Extensions
    {
        get { return Extensions; }
        set { Extensions = value as IEnumerable<AddressExtension>; }
    }
}

Code First 忽略 interface-property 并使用具体类,而您仍然可以将此类作为IAddress 的接口访问。

【讨论】:

    【解决方案2】:

    一个不完美的解决方案是将这些你想要持久化的接口合并到基类中,并用子类分解底层对象。 EF 确实支持这一点,如果您使用 Table Per Hierarchy(默认),您可以使用来自 EF 的常规 LINQ 查询通过共享属性对所有底层子类对象进行排序,而不必变得狡猾并执行诸如 write raw SQL 或将多个列表放入内存并在没有数据库帮助的情况下对联合进行排序,就像使用 Cel 的接口和适配器解决方案一样。

    您还可以将接口的子/父类型作为泛型,这样当实现者在 Db 中使用具体类时,他们可以主要使用您的接口,但告诉 EF 使用具体类:

    public interface IParent<out TChild>
        where TChild : IChild
    {
        ICollection<TChild> Children { get; set; }
    

    有人可以创建他们的 Db 类,例如:

    public class Parent : IParent<Child>
    . . .
    

    但仍然像这样使用它们:

    IParent<IChild> parents = db.Parents.Include(p => p.Children).ToArray();
    

    因为泛型被标记为outthe generic is covariant,因此可以采用任何符合泛型限制的内容,包括将上述类型树转换为 IChild 接口。

    也就是说,如果你真的想持久化接口,正确的答案可能是使用 NHibernate: How to map an interface in nhibernate?

    并且一些编码人员建议您将任何 ORM 中实体上的接口限制为一些共享属性,否则可能会被误用: Programming to interfaces while mapping with Fluent NHibernate

    【讨论】:

    • 假设您使用接口System.Collections.Generic.ICollection&lt;T&gt; 不应该TChildChildren 上始终有效,因为TChild 是协变的?因此,您需要将其更改为 IReadOnlyCollection&lt;T&gt;,但无法通过 IParent&lt;IChild&gt; parents 访问修改集合。
    • 在这里找到的柏忌答案stackoverflow.com/questions/25385161/… 是正确的。
    • 我不确定它是如何工作的...ICollection 是两种方式,所以这在技术上应该抛出一个协变错误,除非它更改为IEnumerable。由于您不能同时声明出入,这根本行不通。
    • 这很好,直到您拥有不同接口类型或任何类型关系的多个接口属性,然后它变得一团糟,即。 Order&lt;OrderDetails&lt;Product, Person&gt;&gt;&gt;
    【解决方案3】:

    如果您确实需要使用接口提供的抽象,那么考虑将域层添加到您的应用程序。领域层旨在表示实体而没有持久性逻辑的负担,这导致了更清洁和更可扩展的架构。 (目前尚不清楚这是 OP 的目标,但它似乎适用于在其他地方讨论过相同问题的其他人。)这可能是唯一不会像其他解决方案那样引入不直观约束的解决方案(显式接口实现,类型转换问题...)如果你走这条路,你可能甚至不需要接口——域类就足够了。

    就类/命名空间结构而言,最终结果可能如下所示:

    namespace Domain.Entities  // not EF
        class MyDomainEntity
    namespace DataAccess.Entities  // EF entities
        class MyDataAccessEntity // no relation to MyDomainEntity
    namespace DataAccess.Entities.Mappers
        class MyDataAccessEntityMapper // responsible for mapping MyDataAccessEntity to and from MyDomainEntity
    

    诚然,这种方法需要做更多的工作。您将需要 2 组实体(1 组用于域,1 组用于持久性)和类以在域和持久性实体之间进行映射。因此,只有在有令人信服的理由时,这种方法才值得。否则,对于小型应用程序和持久层不太可能更改的应用程序,如果您继续使用 EF 实体,可能会减少工作量和混淆。

    但是,如果您确实走这条路,那么您会发现在应用程序中使用域(非 EF)实体变得更加容易,并且将域逻辑排除在 EF 实体之外也使 EF 模型更易于使用。

    【讨论】:

      【解决方案4】:

      我有一个类似的案例,我是这样解决的:

      public class Order : IOrder
      {
           public string FirstName { get; set; }
           public string LastName { get; set; }
           public List<OrderItem> Items {get; set;} = new List<OrderItem>(); 
           IEnumerable<IOrderItem> IOrder.Items
              {
                  get { return Items; }
                  set { Items = value as List<OrderItem>; }
              }
      }
      
      public class OrderItem: IOrderItem
      {
          public string Name {get; set;}
          public decimal Price {get; set;}
      }
      

      【讨论】:

      • 如果您希望IOrderItem 始终是OrderItem,您不妨放弃该界面。
      • 在这个具体实现中,总是会是一个OrderItem,但是这个接口也会被InvoiceItem等其他类实现。
      【解决方案5】:

      经过几个不眠之夜,我相信我已经找到了解决这个问题的方法。我已经测试了这种方法(一点点),它似乎有效,但它可能需要更多的眼球才能将其撕开并解释为什么这种方法可能会失败。我使用 FluentAPI 来设置我的数据库属性,而不是装饰实体类属性。我已经从实体类成员中删除了虚拟属性(我更喜欢使用显式包含而不是依赖子实体的延迟加载)。我还稍微重命名了示例类和属性,以便我更清楚。我假设您正在尝试表达实体与其详细信息之间的一对多关系。您正在尝试为存储库层中的实体实现接口,以便上层与实体类无关。更高层只知道接口而不是实体本身...

      public interface IMyEntity
      {
          int EntityId { get; set; }
      
          //children
          ICollection<IMyDetailEntity> Details { get; set; }
      }
      
      public interface IMyDetailEntity
      {
          int DetailEntityId { get; set; }
          int EntityId { get; set; }
          int IntValue { get; set; }
      
          //parent
          IEntity Entity { get; set; }
      }
      
      public class MyEntity : IMyEntity
      {
          public int EntityId { get; set; }
          private ICollection<IMyDetailEntity> _Details;
      
          public ICollection<MyDetailEntity> Details {
              get 
              {
                  if (_Details == null)
                  {
                      return null;
                  }
      
                  return _Details.Select(x => (MyDetailEntity) x).ToList();
              }
              set 
              {
                  _Details = value.Select(x => (IMyDetailEntity) x).ToList();
              }
          }
      
          ICollection<IMyDetailEntity> IMyEntity.Details
          {
              get
              {
                  return _Details;
              }
              set
              {
                  _Details = value;
              }
          }
      }
      
      public class MyDetailEntity : IMyDetailEntity
      {
          public int DetailEntityId { get; set; }
          public int EntityId { get; set; }
          public int IntValue { get; set; }
      
          private IMyEntity _Entity;
      
          public MyEntity Entity
          {
              get
              {
                  return (Entity)_Entity;
              }
              set
              {
                  _Entity = (Entity)value;
              }
          }
      
          IEntity IMyDetailEntity.Entity
          {
              get
              {
                  return _Entity;
              }
              set
              {
                  _Entity = value;
              }
          }
      }
      

      【讨论】:

      • 使用集合一次作为抽象,一次作为具体将返回列表的另一个实例。因此,当在 IMyEntity 上编辑详细信息时,更改会持续存在并且它们将消失。
      【解决方案6】:

      我的一些模型上也有这个问题,而其他模型上没有,并尝试使用接受的答案。然后我更深入地研究了这些模型的不同之处。

      修复是从使用 ICollection 改为使用 IEnumerable,到目前为止问题已经消失。

      这消除了在接受的答案中使用以下代码的需要:

      public interface IParent<out TChild>
      where TChild : IChild
      {
      ICollection<TChild> Children { get; set; }       
      

      它变成了

      public interface IParent
      {
      IEnumerable<IChild> Children { get; set; } 
      

      这要简单得多。

      【讨论】:

        【解决方案7】:

        一种解决方法是使用适配器模式为您要与实体框架一起使用的每个接口创建一个特殊的实现:

        每个接口的包装器

        // Entity Framework will recognize this because it is a concrete type
        public class SecondLevelDomainRep: ISecondLevelDomain
        {
            private readonly ISecondLevelDomain _adaptee;
        
            // For persisting into database
            public SecondLevelDomainRep(ISecondLevelDomain adaptee)
            {
                _adaptee = adaptee;
            }
        
            // For retrieving data out of database
            public SecondLevelDomainRep()
            {
                // Mapping to desired implementation
                _adaptee = new SecondLevelDomain();
            }
        
            public ISecondLevelDomain Adaptee
            {
                get { return _adaptee; }
            }
        
            public string Id
            {
                get { return _adaptee.Id; }
                set { _adaptee.Id = value; }
            }
        
            // ... whatever other members the interface defines
        }
        

        保存和加载示例

            // Repositor is your DbContext
        
            public void SubmitDomain(ISecondLevelDomain secondLevelDomain)
            {
                 Repositor.SecondLevelDomainReps.Add(new SecondLevelDomainRep(secondLevelDomain));
                 Repositor.SaveChanges();
            }
        
            public IList<ISecondLevelDomain> RetrieveDomains()
            {
                 return Repositor.SecondLevelDomainReps.Select(i => i.Adaptee).ToList();
            }
        

        使用导航属性/外键/父子映射

        对于更复杂的接口/类,您可能会遇到 InvalidOperationException - 请参阅 Conflicting changes with code first foreign key in Entity Framework 了解适用于此类对象层次结构的实现

        【讨论】:

        • 那么,我应该将导航属性声明为ICollection&lt;SecondLevelDomainRep&gt; 而不是ICollection&lt;ISecondLevelDomain&gt;
        • @PavelSurmenok 是的,这就是他的建议。 Adapter 封装了它持有的接口对象。不过,加载关联实体将比正常使用 EF 更复杂。
        • 在属性上定义 setter/getter 会抛出 The specified type member 'Id' is not supported in LINQ to Entities. Only initializers, entity members, and entity navigation properties are supported....
        猜你喜欢
        • 1970-01-01
        • 2014-08-22
        • 1970-01-01
        • 2010-12-08
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        相关资源
        最近更新 更多