Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Feature: Make it easier to handle growing models by supporting graph hydration and/or something like MyContext.AllEntities<EntityType>() -- or a better approach you can suggest #3507

Closed
MatthewMWR opened this issue Oct 22, 2015 · 4 comments

Comments

@MatthewMWR
Copy link

In a scenario where a hub & spoke 1:Many domain model grows over time, it becomes cumbersome and fragile to modify the hub entity class with new navigations and modify the dbcontext class with new DBSet properties for every new spoke entity type. For example imagine an extensible model where various devs may add new spoke types (which all derive from... say SpokeTypeBase), but the main hub type and dbcontext are expected to be stable.

Today this can handled for saving to db with TPC by:

  1. Avoid placing navigation properties on the hub class, instead just have nav and FK properties on the spoke classes
  2. Avoid placing something like public virtual DBSet<SpokeTypeBase> AllSpokeEntities on the dbcontext else OnModelCreating will try to create an entity type for SpokeTypeBase and insist on a PK.
  3. Avoid placing DBSet properties on the dbcontext for any of the spoke types (as we don't want to maintain the list)
  4. Add object graphs to the db context not via dbset properties, but like MyContext.Add(each entity)

While this works for saving to DB, creating a nice TPC layout with correct relationships, it leaves no way that I have found to retrieve spoke entities from the db context.

It would be nice if EF could allow for this pattern by either:
A) Supporting automatic object graph hydration so I only need the hub type exposed as a dbset on the dbcontext, and/or
B) Support something along the lines of public virtual DBSet<SpokeTypeBase> AllSpokeEntities on the db context to be used like MyContext.AllSpokeEntities.OfType() or some private analog to be exposed like MyContext.AllSpokeEntities<SpokeType>() .
C) or a better approach to the same problem space that someone else might suggest.

Thanks.

@rowanmiller
Copy link
Contributor

Hey,

I may be missing what you are trying to achieve... but you can already have a DbSet<BaseType> and then run a LINQ query against it and get all derived types included in the query. That works in EF6 and will work in EF7.

~Rowan

@MatthewMWR
Copy link
Author

Thanks. Your comment made me rethink this-- and I believe I was fighting the wrong battle. By default/convention when I added a DBSet then BaseType was added to the Model as an EntityType and required a Key. I didn't want this as I assumed it was trying to do TPH.

Workaround 1 was to remove the DBSet which is described above which is not ideal

Workaround 2 looks like if I go ahead and give BaseType a Key we no longer fail on model creation with the "requires a key" exception. The db it creates looks like TPC, but including BaseType as a table as it if were a concrete type. This isn't ideal as I think the table would always be empty, but not terrible, except see IndexOutOfRangeException paragraph below.

Workaround 3 was to remove the EntityType for BaseType from the model by using RemoveEntityType(). This gets us past model creation at runtime, but....

When using workarounds 2 or 3 all runtime calls to MyDbContext.Add() fail with a mysterious IndexOutOfRangeException. Also TrackGraph() appears to be broken (nothing happens).

If I can't figure out the IndexOutOfRangeException and TrackGraph behavior I'll post the repro code. I suspect I'm doing something wrong defining the relationships.

@MatthewMWR
Copy link
Author

This may be heading in the direction of a separate bug, but I'll continue here for now. For some reason EF7beta8 hates this model. Depending on various choices you can make with the model (see comments) it will always fail (but in different places) with NullReferenceException or IndexOutOfRangeException.

using System;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
using System.Threading;
using Microsoft.Data.Entity;

namespace ConsoleApplication2
{
    internal class Program
    {
        private static void Main(string[] args)
        {
            //  build hub/spoke object graph
            var toolboxInstance = new Toolbox() {Title = "This toolbox is red"};
            var hammerInstance = new Hammer() {Description = "My favorite hammer"};
            var wrenchInstance = new Wrench() {Description = "My least favorite wrench"};
            toolboxInstance.Tools.Add(hammerInstance);
            toolboxInstance.Tools.Add(wrenchInstance);

            using (var dbc = new ToolsContext())
            {
                //  Note: fails unless we either give ToolBase a Key (explicitly or by convention), or exclude ToolBase from
                //  the model using Model.RemoveEntityType() or Model.Ignore()
                dbc.Database.EnsureCreated();
                dbc.Database.EnsureDeleted();
                dbc.Database.EnsureCreated();

                //  Note: If ToolBase has been removed from the model (not current code state)
                //  These will fail with IndexOutOfRangeException
                //  Also if ToolboxId is a CLR property on any of the Tool classes
                //  (As opposed to auto-generated shadow) then these fail with NullReferenceException
                dbc.Add(toolboxInstance);
                dbc.Add(hammerInstance);
                dbc.Add(wrenchInstance);
                Console.WriteLine(dbc.SaveChanges());
            }

            using (var dbc = new ToolsContext())
            {
                var toolboxesBackFromDb = dbc.Toolboxes.Include(x => x.Tools);
                Console.WriteLine(toolboxesBackFromDb.Count());
                //
                //  Fails with IndexOutOfRangeException
                //
                var toolbox1 = toolboxesBackFromDb.AsEnumerable().FirstOrDefault();
                Console.WriteLine(toolbox1.Tools.Any());
            }
            Thread.Sleep(10000);
        }
    }

    public class ToolsContext : DbContext
    {
        protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
        {
            optionsBuilder.UseSqlServer(@"server=(localdb)\MsSqlLocalDB;database=HubSpokeExperiment");
        }
        protected override void OnModelCreating(ModelBuilder modelBuilder)
        {
            //  using reflection here to pick up new tool types as they are created
            //  rather than maintaining static list
            foreach (var t in Assembly.GetExecutingAssembly().DefinedTypes.Where(t => t.IsSubclassOf(typeof(ToolBase))))
            {
                var eb = modelBuilder.Entity(t.AsType());
                eb.HasKey("Id");
            }
        }
        public DbSet<Toolbox> Toolboxes { get; set; } 
        public DbSet<ToolBase> Tools { get; set; }
    }

    public class Toolbox
    {
        public Toolbox()
        {
            Tools = new List<ToolBase>();
        }
        public int Id { get; set; }
        public string Title { get; set; }
        public ICollection<ToolBase> Tools { get; set; }
    }

    public abstract class ToolBase
    {
        public string Description { get; set; }
        public int Id { get; set; }
        public Toolbox Toolbox { get; set; }
        //public int ToolboxId { get; set; }
    }

    public class Hammer : ToolBase
    {
        public string SomethingAboutHammers { get; set; }
    }

    public class Wrench : ToolBase
    {
        public string SomethingAboutWrenches { get; set; }
    }
}

@MatthewMWR
Copy link
Author

Since the issues I'm hitting now are significantly removed from the issue title, I am going to open separate more narrowly scoped issues for those as potential bugs. Thanks.

@ajcvickers ajcvickers reopened this Oct 16, 2022
@ajcvickers ajcvickers closed this as not planned Won't fix, can't repro, duplicate, stale Oct 16, 2022
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

3 participants