Правильный способ навигации для общего объекта

Новая награда 2017/10/31

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

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

Оригинальный вопрос ниже

У меня есть родительский объект, который имеет отношение один к одному с несколькими возможными таблицами (FK находится на дочерних таблицах). Поскольку свойство навигации для дочернего элемента определяется интерфейсом, у меня нет навигации к другому концу отношения.

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

Это мой код и его можно легко скопировать/вставить в тестовое приложение:

Изменить (в ответ на ответ Ивана Стоева): Когда я попытался реализовать ваше решение, я получил эту ошибку при попытке создать миграцию:

The association 'SoftwareApplicationData_CreatedBy' between entity types 'SoftwareApplicationData' and 'AppUser' is invalid. In a TPC hierarchy independent associations are only allowed on the most derived types.

Итак, мне кажется, что мне нужно отредактировать исходный код, чтобы отразить более сложную модель, которую я первоначально пропустил для краткости. Мои извинения, поскольку я не думал, что дополнительный код будет актуальным до сих пор.

Обратите внимание, что я сделал все сущности, которые теперь наследуются от MyEntity.

Редактирование конца

public abstract class MyEntity
{
    public int Id { get; set; }

    public AppUser CreatedBy { get; set; }
}

public class AppUser : MyEntity { }

public interface ISoftwareApplicationData
{
    SoftwareApplicationBase Application { get; set; }
}

//Parent entity representing a system installation and the software installed on it.
//The collection property is *not* the generic entity I mentioned earlier.
public class SystemConfiguration : MyEntity
{
    public ICollection<SoftwareApplicationBase> Applications { get; set; }
}

//Represents the software itself. Has other generic attributes that I've ommitted for brevity.
//The Data property represents additional, application-specific attributes. I need to be able
//to navigate from SoftwareApplicationBase to whatever may be on the other end
public class SoftwareApplicationBase : MyEntity
{
    public SystemConfiguration Configuration { get; set; }

    public string ApplicationName { get; set; }

    public ISoftwareApplicationData Data { get; set; }
}

//This is a generic, catch-all application class that follows a basic Application/Version
//convention. Most software will use this class
public class SoftwareApplication : MyEntity, ISoftwareApplicationData
{
    public SoftwareApplicationBase Application { get; set; }

    public string Version { get; set; }
}

//Operating systems have special attributes, so they get their own class.
public class OperatingSystem : MyEntity, ISoftwareApplicationData
{
    public SoftwareApplicationBase Application { get; set; }

    public string Version { get; set; }

    public string ServicePack { get; set; }
}

//Yet another type of software with its own distinct attributes
public class VideoGame : MyEntity, ISoftwareApplicationData
{
    public SoftwareApplicationBase Application { get; set; }

    public string Publisher { get; set; }

    public string Genre { get; set; }
}

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

Ответ 1

Поскольку свойство навигации для дочернего элемента определяется интерфейсом, у меня нет навигации к другому концу отношения.

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

Основная проблема в этом дизайне - это интерфейс, поскольку EF работает только с классами. Но если вы можете заменить его на абстрактный класс, и если FK в дочерних таблицах также является PK (т.е. Следуйте Shared Primary Key Asociation для представления взаимно-однозначного отношения), вы можете использовать EF Таблица для конкретного типа (TPC) наследование стратегии для сопоставления существующих дочерних таблиц, что, в свою очередь, позволит EF обеспечить необходимую навигацию автоматически для вас.

Вот пример модифицированной модели (исключая ISoftwareApplicationBase и SystemConfiguration, которые не имеют значения):

public class SoftwareApplicationBase
{
    public int Id { get; set; }
    public string ApplicationName { get; set; }
    public SoftwareApplicationData Data { get; set; }
}

public abstract class SoftwareApplicationData
{
    public int ApplicationId { get; set; }
    public SoftwareApplicationBase Application { get; set; }
}

public class SoftwareApplication : SoftwareApplicationData
{
    public string Version { get; set; }
}

public class OperatingSystem : SoftwareApplicationData
{
    public string Version { get; set; }
    public string ServicePack { get; set; }
}

public class VideoGame : SoftwareApplicationData
{
    public string Publisher { get; set; }
    public string Genre { get; set; }
}

конфигурации:

modelBuilder.Entity<SoftwareApplicationBase>()
    .HasOptional(e => e.Data)
    .WithRequired(e => e.Application);

modelBuilder.Entity<SoftwareApplicationData>()
    .HasKey(e => e.ApplicationId);

modelBuilder.Entity<SoftwareApplication>()
    .Map(m => m.MapInheritedProperties().ToTable("SoftwareApplication"));

modelBuilder.Entity<OperatingSystem>()
    .Map(m => m.MapInheritedProperties().ToTable("OperatingSystem"));

modelBuilder.Entity<VideoGame>()
    .Map(m => m.MapInheritedProperties().ToTable("VideoGame"));

Сгенерированные таблицы и отношения:

CreateTable(
    "dbo.SoftwareApplicationBase",
    c => new
        {
            Id = c.Int(nullable: false, identity: true),
            ApplicationName = c.String(),
        })
    .PrimaryKey(t => t.Id);

CreateTable(
    "dbo.SoftwareApplication",
    c => new
        {
            ApplicationId = c.Int(nullable: false),
            Version = c.String(),
        })
    .PrimaryKey(t => t.ApplicationId)
    .ForeignKey("dbo.SoftwareApplicationBase", t => t.ApplicationId)
    .Index(t => t.ApplicationId);

CreateTable(
    "dbo.OperatingSystem",
    c => new
        {
            ApplicationId = c.Int(nullable: false),
            Version = c.String(),
            ServicePack = c.String(),
        })
    .PrimaryKey(t => t.ApplicationId)
    .ForeignKey("dbo.SoftwareApplicationBase", t => t.ApplicationId)
    .Index(t => t.ApplicationId);

CreateTable(
    "dbo.VideoGame",
    c => new
        {
            ApplicationId = c.Int(nullable: false),
            Publisher = c.String(),
            Genre = c.String(),
        })
    .PrimaryKey(t => t.ApplicationId)
    .ForeignKey("dbo.SoftwareApplicationBase", t => t.ApplicationId)
    .Index(t => t.ApplicationId);

Тест навигации:

var test = db.Set<SoftwareApplicationBase>()
    .Include(e => e.Data)
    .ToList();

EF генерировал SQL-запрос из вышеперечисленного:

SELECT
    [Extent1].[Id] AS [Id],
    [Extent1].[ApplicationName] AS [ApplicationName],
    CASE WHEN ([UnionAll4].[ApplicationId] IS NULL) THEN CAST(NULL AS varchar(1)) WHEN ([UnionAll4].[C5] = 1) THEN '2X0X' WHEN ([UnionAll4].[C6] = 1) THEN '2X1X' ELSE '2X2X' END AS [C1],
    [UnionAll4].[ApplicationId] AS [C2],
    CASE WHEN ([UnionAll4].[ApplicationId] IS NULL) THEN CAST(NULL AS varchar(1)) WHEN ([UnionAll4].[C5] = 1) THEN [UnionAll4].[C1] WHEN ([UnionAll4].[C6] = 1) THEN CAST(NULL AS varchar(1)) END AS [C3],
    CASE WHEN ([UnionAll4].[ApplicationId] IS NULL) THEN CAST(NULL AS varchar(1)) WHEN ([UnionAll4].[C5] = 1) THEN [UnionAll4].[C2] WHEN ([UnionAll4].[C6] = 1) THEN CAST(NULL AS varchar(1)) END AS [C4],
    CASE WHEN ([UnionAll4].[ApplicationId] IS NULL) THEN CAST(NULL AS varchar(1)) WHEN ([UnionAll4].[C5] = 1) THEN CAST(NULL AS varchar(1)) WHEN ([UnionAll4].[C6] = 1) THEN [UnionAll4].[Version] END AS [C5],
    CASE WHEN ([UnionAll4].[ApplicationId] IS NULL) THEN CAST(NULL AS varchar(1)) WHEN ([UnionAll4].[C5] = 1) THEN CAST(NULL AS varchar(1)) WHEN ([UnionAll4].[C6] = 1) THEN CAST(NULL AS varchar(1)) ELSE [UnionAll4].[C3] END AS [C6],
    CASE WHEN ([UnionAll4].[ApplicationId] IS NULL) THEN CAST(NULL AS varchar(1)) WHEN ([UnionAll4].[C5] = 1) THEN CAST(NULL AS varchar(1)) WHEN ([UnionAll4].[C6] = 1) THEN CAST(NULL AS varchar(1)) ELSE [UnionAll4].[C4] END AS [C7]
    FROM   [dbo].[SoftwareApplicationBase] AS [Extent1]
    LEFT OUTER JOIN  (SELECT
        [Extent2].[ApplicationId] AS [ApplicationId]
        FROM [dbo].[SoftwareApplication] AS [Extent2]
    UNION ALL
        SELECT
        [Extent3].[ApplicationId] AS [ApplicationId]
        FROM [dbo].[VideoGame] AS [Extent3]
    UNION ALL
        SELECT
        [Extent4].[ApplicationId] AS [ApplicationId]
        FROM [dbo].[OperatingSystem] AS [Extent4]) AS [UnionAll2] ON [Extent1].[Id] = [UnionAll2].[ApplicationId]
    LEFT OUTER JOIN  (SELECT
        [Extent5].[ApplicationId] AS [ApplicationId],
        CAST(NULL AS varchar(1)) AS [C1],
        CAST(NULL AS varchar(1)) AS [C2],
        [Extent5].[Version] AS [Version],
        CAST(NULL AS varchar(1)) AS [C3],
        CAST(NULL AS varchar(1)) AS [C4],
        cast(0 as bit) AS [C5],
        cast(1 as bit) AS [C6]
        FROM [dbo].[SoftwareApplication] AS [Extent5]
    UNION ALL
        SELECT
        [Extent6].[ApplicationId] AS [ApplicationId],
        CAST(NULL AS varchar(1)) AS [C1],
        CAST(NULL AS varchar(1)) AS [C2],
        CAST(NULL AS varchar(1)) AS [C3],
        [Extent6].[Publisher] AS [Publisher],
        [Extent6].[Genre] AS [Genre],
        cast(0 as bit) AS [C4],
        cast(0 as bit) AS [C5]
        FROM [dbo].[VideoGame] AS [Extent6]
    UNION ALL
        SELECT
        [Extent7].[ApplicationId] AS [ApplicationId],
        [Extent7].[Version] AS [Version],
        [Extent7].[ServicePack] AS [ServicePack],
        CAST(NULL AS varchar(1)) AS [C1],
        CAST(NULL AS varchar(1)) AS [C2],
        CAST(NULL AS varchar(1)) AS [C3],
        cast(1 as bit) AS [C4],
        cast(0 as bit) AS [C5]
        FROM [dbo].[OperatingSystem] AS [Extent7]) AS [UnionAll4] ON [Extent1].[Id] = [UnionAll4].[ApplicationId]

Не самый красивый, но грязная работа для вас:)

Изменить: MyEntity базовый класс и требование, которое должен унаследовать каждый класс сущности, сильно ограничивает параметры. TPC больше не применим из-за отношения, определяющего свойство навигации внутри базового класса (другое ограничение EF). Следовательно, единственной жизнеспособной автоматической опцией EF является использование некоторых из двух других стратегий наследования EF, но они требуют изменения структуры базы данных.

Если вы можете позволить себе ввести промежуточную таблицу, содержащую общие свойства и отношения SoftwareApplicationData, вы можете использовать Таблица для каждого типа (TPT) следующим образом:

Модель:

public class SoftwareApplicationBase : MyEntity
{
    public string ApplicationName { get; set; }
    public SoftwareApplicationData Data { get; set; }
}

public abstract class SoftwareApplicationData : MyEntity
{
    public SoftwareApplicationBase Application { get; set; }
}

public class SoftwareApplication : SoftwareApplicationData
{
    public string Version { get; set; }
}

public class OperatingSystem : SoftwareApplicationData
{
    public string Version { get; set; }
    public string ServicePack { get; set; }
}

public class VideoGame : SoftwareApplicationData
{
    public string Publisher { get; set; }
    public string Genre { get; set; }
}

Конфигурация:

modelBuilder.Entity<SoftwareApplicationBase>()
    .HasOptional(e => e.Data)
    .WithRequired(e => e.Application);

modelBuilder.Entity<SoftwareApplicationData>()
    .ToTable("SoftwareApplicationData");

modelBuilder.Entity<SoftwareApplication>()
    .ToTable("SoftwareApplication");

modelBuilder.Entity<OperatingSystem>()
    .ToTable("OperatingSystem");

modelBuilder.Entity<VideoGame>()
    .ToTable("VideoGame");

Соответствующие таблицы:

CreateTable(
    "dbo.SoftwareApplicationData",
    c => new
        {
            Id = c.Int(nullable: false),
            CreatedBy_Id = c.Int(),
        })
    .PrimaryKey(t => t.Id)
    .ForeignKey("dbo.AppUser", t => t.CreatedBy_Id)
    .ForeignKey("dbo.SoftwareApplicationBase", t => t.Id)
    .Index(t => t.Id)
    .Index(t => t.CreatedBy_Id);

CreateTable(
    "dbo.SoftwareApplication",
    c => new
        {
            Id = c.Int(nullable: false),
            Version = c.String(),
        })
    .PrimaryKey(t => t.Id)
    .ForeignKey("dbo.SoftwareApplicationData", t => t.Id)
    .Index(t => t.Id);

CreateTable(
    "dbo.OperatingSystem",
    c => new
        {
            Id = c.Int(nullable: false),
            Version = c.String(),
            ServicePack = c.String(),
        })
    .PrimaryKey(t => t.Id)
    .ForeignKey("dbo.SoftwareApplicationData", t => t.Id)
    .Index(t => t.Id);

CreateTable(
    "dbo.VideoGame",
    c => new
        {
            Id = c.Int(nullable: false),
            Publisher = c.String(),
            Genre = c.String(),
        })
    .PrimaryKey(t => t.Id)
    .ForeignKey("dbo.SoftwareApplicationData", t => t.Id)
    .Index(t => t.Id);

Желаемая навигация по-прежнему как таковая, с бонусом, обеспечивающим высокую загрузку базовых навигационных свойств:

var test = db.Set<SoftwareApplicationBase>()
    .Include(e => e.Data)
    .Include(e => e.Data.CreatedBy)
    .ToList();

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