Могу ли я использовать интерфейс с внешним ключом в EF Core и установить его в качестве внешнего ключа с помощью Fluent API? - PullRequest
0 голосов
/ 06 июля 2018

Я пытаюсь ограничить использование нескольких generic методов только Entities, что inherit из IParentOf<TChildEntity> interface, а также доступа к Entity's Foreign Key (ParentId) Generically.

Для демонстрации;

public void AdoptAll<TParentEntity, TChildEntity>(TParentEntity parent,
   TParentEntity adoptee) 
    where TParentEntity : DataEntity, IParentOf<TChildEntity> 
    where TChildEntity : DataEntity, IChildOf<TParentEntity>
{
    foreach (TChildEntity child in (IParentOf<TChildEntity>)parent.Children)
    {
        (IChildOf<TParentEntity)child.ParentId = adoptee.Id;
    }
}

Модель класса дочернего объекта будет выглядеть так:

public class Account : DataEntity, IChildOf<AccountType>, IChildOf<AccountData>
{
    public string Name { get; set; }

    public string Balance { get; set; }

    // Foreign Key and Navigation Property for AccountType
    int IChildOf<AccountType>.ParentId{ get; set; }
    public virtual AccountType AccountType { get; set; }

    // Foreign Key and Navigation Property for AccountData
    int IChildOf<AccountData>.ParentId{ get; set; }
    public virtual AccountData AccountData { get; set; }
}

Прежде всего, это возможно сделать? Или это будет пробой в EF?

Во-вторых, поскольку внешние ключи не соответствуют соглашению (а их несколько), как мне установить их через Fluent Api? Я вижу, как это сделать, в аннотациях данных.

Надеюсь, это понятно, я некоторое время обдумывал это и пытался обойти его, чтобы я мог следовать своему аргументу, но он может быть нечетко передан, поэтому, пожалуйста, попросите разъяснений, если это необходимо. Моя причина желания сделать это состоит в том, чтобы сделать код безопасным, а также автоматизировать большую часть ручного изменения классов, необходимых для добавления новых ассоциаций и сущностей.

Спасибо.

Редактировать

Я решил создать несколько базовых классов для реализации этой идеи и проверить ее, мой код выглядит следующим образом.

public abstract class ChildEntity : DataEntity
{
    public T GetParent<T>() where T : ParentEntity
    {
        foreach (var item in GetType().GetProperties())
        {
            if (item.GetValue(this) is T entity)
                return entity;
        }

        return null;
    }
}

public abstract class ParentEntity : DataEntity
{
    public ICollection<T> GetChildren<T>() where T : ChildEntity
    {
        foreach (var item in GetType().GetProperties())
        {
            if (item.GetValue(this) is ICollection<T> collection)
                return collection;
        }

        return null;
    }
}

public interface IParent<TEntity> where TEntity : ChildEntity
{
    ICollection<T> GetChildren<T>() where T : ChildEntity;
}

public interface IChild<TEntity> where TEntity : ParentEntity
{
    int ForeignKey { get; set; }

    T GetParent<T>() where T : ParentEntity;
}

public class ParentOne : ParentEntity, IParent<ChildOne>
{
    public string Name { get; set; }
    public decimal Amount { get; set; }

    public virtual ICollection<ChildOne> ChildOnes { get; set; }
}

public class ParentTwo : ParentEntity, IParent<ChildOne>
{
    public string Name { get; set; }
    public decimal Value { get; set; }

    public virtual ICollection<ChildOne> ChildOnes { get; set; }
}

public class ChildOne : ChildEntity, IChild<ParentOne>, IChild<ParentTwo>
{
    public string Name { get; set; }
    public decimal Balance { get; set; }

    int IChild<ParentOne>.ForeignKey { get; set; }
    public virtual ParentOne ParentOne { get; set; }

    int IChild<ParentTwo>.ForeignKey { get; set; }
    public virtual ParentTwo ParentTwo { get; set; }
}

Data Entity просто дает каждому entity Id property.

У меня есть стандартные универсальные репозитории, настроенные с классом Unit of Work для посредничества. Метод AdoptAll выглядит в моей программе следующим образом.

public void AdoptAll<TParentEntity, TChildEntity>(TParentEntity parent,
    TParentEntity adoptee, UoW uoW)
    where TParentEntity : DataEntity, IParent<TChildEntity>
    where TChildEntity : DataEntity, IChild<TParentEntity>
{
    var currentParent = uoW.GetRepository<TParentEntity>().Get(parent.Id);
        foreach (TChildEntity child in currentParent.GetChildren<TChildEntity>())
    {
        child.ForeignKey = adoptee.Id;
    }
}

Кажется, это работает правильно и без сбоев (минимальное тестирование), есть ли какие-либо серьезные недостатки в этом?

Спасибо.

Редактировать два

Это метод OnModelCreating в DbContext, который устанавливает внешний ключ для каждого объекта. Это проблематично?

protected override void OnModelCreating(ModelBuilder modelBuilder)
{
    base.OnModelCreating(modelBuilder);

    modelBuilder.Entity<ChildOne>()
        .HasOne(p => p.ParentOne)
        .WithMany(c => c.ChildOnes)
        .HasForeignKey(fk => ((IChild<ParentOne>)fk).ForeignKey);

    modelBuilder.Entity<ChildOne>()
        .HasOne(p => p.ParentTwo)
        .WithMany(c => c.ChildOnes)
        .HasForeignKey(fk => ((IChild<ParentTwo>)fk).ForeignKey);
}

1 Ответ

0 голосов
/ 09 июля 2018

Согласно обновленному примеру, вы хотите скрыть явный FK от открытого интерфейса класса сущностей и все же позволить ему быть видимым для EF Core и сопоставляться со столбцом FK в базе данных.

Первая проблема заключается в том, что явно реализованный элемент интерфейса не может быть непосредственно обнаружен EF. Также у него нет хорошего имени, поэтому соглашения по умолчанию не применяются.

Например, без плавной конфигурации EF Core правильно создаст одну-много ассоциаций между Parent и Child сущностями, но, поскольку он не обнаружит свойства int IChild<Parent>.ForeignKey { get; set; }, он будет поддерживать значения свойств FK через ParentOneId / ParentTwoId теневые свойства , а не через явные свойства интерфейса. Другими словами, эти свойства не будут заполняться EF Core, а также не будут учитываться трекером изменений.

Чтобы позволить EF Core использовать их, необходимо отобразить как свойство FK, так и имя столбца базы данных, используя соответственно HasForeignKey и HasColumnName плавные перегрузки метода API, принимающие string имя свойства. Обратите внимание, что имя свойства строки должно быть полностью определено с пространством имен. Хотя Type.FullName предоставляет эту строку для неуниверсальных типов, для универсальных типов, таких как IChild<ParentOne>, такого свойства / метода не существует (результат должен быть "Namespace.IChild<Namespace.ParentOne>"), поэтому давайте сначала создадим несколько помощников для этого:

static string ChildForeignKeyPropertyName<TParent>() where TParent : ParentEntity
    => $"{typeof(IChild<>).Namespace}.IChild<{typeof(TParent).FullName}>.{nameof(IChild<TParent>.ForeignKey)}";

static string ChildForeignKeyColumnName<TParent>() where TParent : ParentEntity
    => $"{typeof(TParent).Name}Id";

Следующим будет создание вспомогательного метода для выполнения необходимой конфигурации:

static void ConfigureRelationship<TChild, TParent>(ModelBuilder modelBuilder)
    where TChild : ChildEntity, IChild<TParent>
    where TParent : ParentEntity, IParent<TChild>
{
    var childEntity = modelBuilder.Entity<TChild>();

    var foreignKeyPropertyName = ChildForeignKeyPropertyName<TParent>();
    var foreignKeyColumnName = ChildForeignKeyColumnName<TParent>();
    var foreignKey = childEntity.Metadata.GetForeignKeys()
        .Single(fk => fk.PrincipalEntityType.ClrType == typeof(TParent));

    // Configure FK column name
    childEntity
        .Property<int>(foreignKeyPropertyName)
        .HasColumnName(foreignKeyColumnName);


    // Configure FK property
    childEntity
        .HasOne<TParent>(foreignKey.DependentToPrincipal.Name)
        .WithMany(foreignKey.PrincipalToDependent.Name)
        .HasForeignKey(foreignKeyPropertyName);
}

Как видите, я использую службы метаданных EF Core, чтобы найти имена соответствующих свойств навигации.

Но этот общий метод на самом деле показывает ограничение этого дизайна. Общие ограничения позволяют нам использовать

childEntity.Property(c => c.ForeignKey)

, который прекрасно компилируется, но не работает во время выполнения. Это не только для беглых методов API, но в основном для любого общего метода, включающего деревья выражений (например, запрос LINQ to Entities). Нет такой проблемы, когда свойство интерфейса неявно реализовано с открытым свойством.

Мы вернемся к этому ограничению позже. Чтобы завершить сопоставление, добавьте следующее к вашему OnModelCreating переопределению:

ConfigureRelationship<ChildOne, ParentOne>(modelBuilder);
ConfigureRelationship<ChildOne, ParentTwo>(modelBuilder);

И теперь EF Core будет корректно загружать / учитывать ваши явно реализованные свойства FK.

Теперь вернемся к ограничениям. Нет проблем в использовании общих объектных сервисов, таких как ваш AdoptAll метод или LINQ to Objects. Но вы не можете получить доступ к этим свойствам в общем случае в выражениях, используемых для доступа к метаданным EF Core или внутри запросов LINQ to Entities. В последнем случае вы должны получить к нему доступ через свойство навигации, или в обоих сценариях вы должны получить доступ по имени, возвращенному методом ChildForeignKeyPropertyName<TParent>(). На самом деле запросы будут работать, но будут оцениваться локально , что приведет к проблемам с производительностью или неожиданному поведению.

* Э.Г. 1045. *

static IEnumerable<TChild> GetChildrenOf<TChild, TParent>(DbContext db, int parentId)
    where TChild : ChildEntity, IChild<TParent>
    where TParent : ParentEntity, IParent<TChild>
{
    // Works, but causes client side filter evalution
    return db.Set<TChild>().Where(c => c.ForeignKey == parentId);

    // This correctly translates to SQL, hence server side evaluation
    return db.Set<TChild>().Where(c => EF.Property<int>(c, ChildForeignKeyPropertyName<TParent>()) == parentId);
}

Если коротко, то это возможно, но используйте с осторожностью и убедитесь, что оно того стоит для ограниченных общих сценариев обслуживания, которые оно допускает. Альтернативные подходы будут использовать не интерфейсы, а (комбинацию) метаданных EF Core, отражения или Func<...> / Expression<Func<..>> общих аргументов метода, аналогичных Queryable методам расширения.

Редактировать: По второму вопросу редактировать, свободная конфигурация

modelBuilder.Entity<ChildOne>()
    .HasOne(p => p.ParentOne)
    .WithMany(c => c.ChildOnes)
    .HasForeignKey(fk => ((IChild<ParentOne>)fk).ForeignKey);

modelBuilder.Entity<ChildOne>()
    .HasOne(p => p.ParentTwo)
    .WithMany(c => c.ChildOnes)
    .HasForeignKey(fk => ((IChild<ParentTwo>)fk).ForeignKey);

производит следующую миграцию для ChildOne

migrationBuilder.CreateTable(
    name: "ChildOne",
    columns: table => new
    {
        Id = table.Column<int>(nullable: false)
            .Annotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn),
        ForeignKey = table.Column<int>(nullable: false),
        Name = table.Column<string>(nullable: true),
        Balance = table.Column<decimal>(nullable: false)
    },
    constraints: table =>
    {
        table.PrimaryKey("PK_ChildOne", x => x.Id);
        table.ForeignKey(
            name: "FK_ChildOne_ParentOne_ForeignKey",
            column: x => x.ForeignKey,
            principalTable: "ParentOne",
            principalColumn: "Id",
            onDelete: ReferentialAction.Cascade);
        table.ForeignKey(
            name: "FK_ChildOne_ParentTwo_ForeignKey",
            column: x => x.ForeignKey,
            principalTable: "ParentTwo",
            principalColumn: "Id",
            onDelete: ReferentialAction.Cascade);
    });

Обратите внимание на один столбец ForeignKey и попытку использовать его в качестве внешнего ключа для ParentOne и ParentTwo. Он сталкивается с теми же проблемами, что и прямое использование ограниченного свойства интерфейса, поэтому я предполагаю, что оно не работает.

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