entity-framework-coretable-per-hierarchy

Is it possible to access a shared TPH column in EF Core without using intermediate classes?


When using shared columns in an EF Core TPH setup, is it possible to access the shared column during projection?

    class Program
    {
        public static readonly ILoggerFactory MyLoggerFactory
            = LoggerFactory.Create(builder => { 
                builder.AddConsole(); 
            });

        static async Task Main(string[] args)
        {
            using (var context = new ClientContext())
            {
                context.Database.EnsureDeleted();
                context.Database.EnsureCreated();

                var actions = await context.Actions
                    .Select(a => new
                    {
                        Id = a.Id,

                        // this works - but really messy and complex in real world code
                        Message = (a as ActionA).Message ?? (a as ActionB).Message,

                        // this throws "Either the query source is not an entity type, or the specified property does not exist on the entity type."
                        // is there any other way to access the shared column Message?
                        // Message = EF.Property<string>(a, "Message"),
                    })
                    .ToListAsync();

                actions.ForEach(a => Console.WriteLine(a.Id + a.Message));
            }
        }

        public class ActionBase
        {
            public int Id { get; set; }

            // ... other shared properties
        }

        public class ActionA : ActionBase
        {
            // shared with B
            [Required]
            [Column("Message")]
            public string Message { get; set; }

            // ... other specific properties
        }

        public class ActionB : ActionBase
        {
            // shared with A
            [Required]
            [Column("Message")]
            public string Message { get; set; }

            // ... other specific properties
        }

        public class ActionC : ActionBase
        {
            public string SomethingElse { get; set; }

            // ... other specific properties
        }

        class ClientContext : DbContext
        {
            protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
            {
                // TO USE SQL
                //optionsBuilder
                //    .UseLoggerFactory(MyLoggerFactory)
                //    .UseSqlServer("Server=(localdb)\\mssqllocaldb;Database=TPHSharedColumn;Trusted_Connection=True;MultipleActiveResultSets=true;Connect Timeout=30")
                //    .EnableSensitiveDataLogging(false);
                // TO USE INMEMORY
                optionsBuilder
                   .UseLoggerFactory(MyLoggerFactory)
                   .UseInMemoryDatabase(Guid.NewGuid().ToString());
            }

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

                builder.Entity<ActionA>().HasData(new ActionA()
                {
                    Id = 1,
                    Message = "A"
                });
                builder.Entity<ActionB>().HasData(new ActionB()
                {
                    Id = 2,
                    Message = "B"
                });
                builder.Entity<ActionC>().HasData(new ActionC()
                {
                    Id = 3,
                    SomethingElse = "C"
                });
            }

            public DbSet<ActionBase> Actions { get; set; }
        }
    }

In this simple example, it would of course be possible to move Message to the base class - but that would make it possible to accidentally add an ActionC with a Message since I would need to remove the Required attribute.

I also know I could add a ActionWithRequiredMessage intermediate class to inherit ActionA and ActionB with, but again - in the much more complex real world example this is not feasible since there are also other shared columns and C# does not allow inheriting from multiple classes - and EF Core does not seem to like to use interfaces for this.

I simply would like to find a way to directly access the shared column - and use it in a projection.

Anyone know if this is possible?


Solution

  • I can't find it documented, but in EF Core 5.x you can access the shared column using any of the derived entities having a property mapped to it, e.g. all these work

    Message = (a as ActionA).Message,
    
    Message = (a as ActionB).Message,
    
    Message = ((ActionA)a).Message,
    
    Message = ((ActionB)a).Message,