Как использовать свойства интерфейса с CodeFirst - PullRequest
30 голосов
/ 21 марта 2012

У меня есть следующие объекты:

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<IMyDetail>, потому что я не могу привести его к ICollection<MyDetail>.

Есть ли какие-то решения для этого?

Ответы [ 6 ]

13 голосов
/ 20 августа 2012

Несовершенное решение состоит в том, чтобы просто объединить эти интерфейсы, которые вы хотите сохранить, в базовые классы и разбить базовые объекты на подклассы. EF поддерживает это, и если вы используете Table Per Hierarchy (по умолчанию), вы можете отсортировать все нижележащие подклассы объектов по общему свойству, используя обычный запрос LINQ из EF, вместо того, чтобы быть хитрым и делать что-то наподобие write raw SQL или получить несколько списков в памяти и отсортировать объединение без помощи БД, как если бы вы использовали решение Cel для интерфейсов и адаптеров.

Вы также можете использовать дочерние / родительские типы интерфейсов в качестве обобщенных, так что, когда разработчик использует конкретные классы в БД, они могут в основном использовать ваши интерфейсы, но при этом указывайте 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();

Поскольку универсальный объект помечен out, универсальный шаблон является ковариантным и, следовательно, может принимать все, что соответствует ограничениям универсального шаблона, включая приведенное выше приведение дерева типов к интерфейсу IChild.

Тем не менее, если вы действительно хотите сохранить интерфейсы, правильный ответ, вероятно, заключается в использовании NHibernate: Как отобразить интерфейс в nhibernate?

И некоторые кодировщики рекомендуют хранить интерфейсы на объектах в любом ORM, ограниченные несколькими общими свойствами, или риск злоупотребления: Программирование интерфейсов при отображении с помощью Fluent NHibernate

5 голосов
/ 12 июня 2012

Обходной путь - создать специальную реализацию для каждого интерфейса, который вы хотите использовать с Entity Framework, используя шаблон адаптера:

Оболочка на интерфейс

// 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 - см. Конфликт изменений с первым внешним ключом кода в Entity Framework для реализации, которая работает с такими иерархиями объектов

4 голосов
/ 01 августа 2016

После нескольких бессонных ночей я считаю, что нашел решение этой проблемы.Я проверил этот подход (немного), и он, кажется, работает, но ему, вероятно, нужно еще несколько глазных яблок, чтобы разорвать его и объяснить, почему этот подход может сломаться.Я использовал 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;
        }
    }
}
1 голос
/ 26 сентября 2018

Если вам действительно нужна абстракция, предлагаемая с помощью интерфейсов, тогда рассмотрите возможность добавления доменного уровня в ваше приложение . Уровень домена предназначен для представления объектов без бремени логики постоянства, что приводит к более чистой и расширяемой архитектуре. (Не ясно, что это цель 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. 1010 *

1 голос
/ 05 апреля 2018

У меня была та же проблема, и я нашел решение точно так же, как Натан, но вы даже можете сделать еще один шаг вперед и получить свойства с одинаковыми именами (здесь Extensions и IAddress.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 игнорирует свойство интерфейса и использует конкретный класс, в то время как вы все еще можете обращаться к этому классу как к интерфейсу IAddress.

1 голос
/ 13 мая 2017

У меня тоже была эта проблема на некоторых моделях, а на других нет, и я пытался использовать принятый ответ.Затем я немного углубился в то, что отличалось на моделях, которые работали.

Исправлено изменение в использовании ICollection вместо использования IEnumerable, и проблемы исчезли.

Этоустранена необходимость использования приведенного ниже кода в принятом ответе:

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

и он стал

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

, что намного проще.

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...