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

Allow entity type instance to implement/inherit from type in model #13344

Open
Tracked by #240
ajcvickers opened this issue Sep 17, 2018 · 9 comments
Open
Tracked by #240

Allow entity type instance to implement/inherit from type in model #13344

ajcvickers opened this issue Sep 17, 2018 · 9 comments

Comments

@ajcvickers
Copy link
Member

Allow interfaces or base types to be mapped by specifying the type to use when materializing an instance of the interface or base class. For example, something like this:

public interface IBlog
{
    int Id { get; set; }
    ICollection<IPost> Posts { get; set; }
}

public interface IPost
{
    int Id { get; set; }
    IBlog Blog { get; set; }
}

public class Blog : IBlog
{
    public int Id { get; set; }
    public ICollection<IPost> Posts { get; set; }
}

public class Post : IPost
{
    public int Id { get; set; }
    public IBlog Blog { get; set; }
}

Currently this can be mapped like this:

public class BloggingContext : DbContext
{
    protected override void OnModelCreating(ModelBuilder modelBuilder)
    {
        modelBuilder.Entity<Blog>()
            .HasMany(e => (ICollection<Post>) e.Posts)
            .WithOne(e => (Blog) e.Blog);
    }
}

Instead, this would be mapped using something like this:

public class BloggingContext : DbContext
{
    protected override void OnModelCreating(ModelBuilder modelBuilder)
    {
        modelBuilder.Entity<IBlog>().ImplementedBy<Blog>();
        modelBuilder.Entity<IPost>().ImplementedBy<Post>();
    }
}

Note that there is significant overlap here with #10789. For example, it should be possible to use a factory method for creating the implementation instance, in which case the factory rather than the explicit implementation types would be in the model.

This issue is also related to #757, except that here each interface/base type is constrained to only have one implementation type at runtime.

@sjb-sjb
Copy link

sjb-sjb commented Sep 18, 2018

I experienced a challenge that might be addressed by this enhancement. However I view it from a slightly different perspective.

Let's first talk about base classes rather than interfaces. When considering a base class, since EF already has a database implementation for inheritance, the question is really about how to execute queries from the base class. Specifically if there is a navigation relationship that specifies the base class as the dependent type, then how should we execute a query (such as Include) based on that navigation? To me it seems clear that we should find the table implementing the class hierarchy and load all entities derived from that navigation type, resulting in a list of heterogeneous type (but all derived from the base type). This would be consistent with normal object design concepts. Finding the table should be pretty easy if the base class was included in the model (either explicitly or implicitly). If the base class was not included in the model then, for the query to be meaningful there must be some class that was included in the model and that is derived from the base class. What one might be able to do is, whenever a class is included in the model take note of all of its base classes. Then when a navigation type occurs we can identify the set of classes in the model which are derived from that navigation type. These will all be in one table (due to table per hierarchy). Just execute the query on that table and select the ones that are derived from the navigation type.

The comment by @ajcvickers mentions both base classes and interfaces, and the example is for interfaces. Extending the comments above, suppose that whenever a class is included in the model, we take note of all the interfaces that it implements, in addition to all the base classes. The difference is that when a navigation type is an interface, there could be multiple tables containing entities that implement that interface. Actually even within one table representing a type hierarchy there could be distinct classes that implement the interface. There are a couple of ways you could go with this. You could throw an error if there are multiple entities in the model that implement the interface. You could require configuration such as what @ajcvickers described. Or you could conceivably query all the entity types that implement the interface and return them all in one navigation list.

In terms of the configuration approach, I feel that the selected class should be specifiable on a per-navigation level rather than per-model. You could easily have two navigations in different parts of the model that are represented by the same interface type but where the selected entities are meant to be different. It would be nice to be able to specify for example

public class Post: IPost
{
    public int Id { get; set; }
    [RestrictTo(typeof(Blog)] public IBlog Blog { get; set; }
}

The entities returned from the navigation would be all entities in the model that are derived from Blog; it is a heterogenous list. We could also specify a derived type such as [RestrictTo(typeof(RssBlog))], or for that matter any other entity type in the model that implements IBlog.

One thing I wouldn't agree with would be if only one entity type could implement the interface type or be derived from the base type. That would sharply limit the use. After all, one of the main reasons for having base types or interfaces is that there can be multiple implementations or derived classes; one encapsulates re-usable functionality in the base class, or a re-usable design in the interface. This would be disabled in the entity model if there could only be one derived or implementing class.

sjb

P.S. Caveat emptor: I really don't know how EF works on the inside!

@douglasg14b
Copy link

douglasg14b commented Aug 9, 2019

This would be a very welcome feature!

The casting example during configuration is extremely helpful for the time being.

@douglasg14b
Copy link

douglasg14b commented Aug 9, 2019

@ajcvickers Side question, if you don't mind.

How is this proposed to work with DbSet /context.Set<>? The use of model interfaces would also imply we can use said interfaces in a DbContext compatible interface as well. Which would allow for the DbSet to be defined in said interface, using the interfaces of the models. ie:

An Example of how this would be in an ideal world, with DDD:

// Example DDD-style entity interface defined in Project.Core
public interface IBlog
{
    int Id { get; set; }
    ICollection<IPost> Posts { get; }

    void AddPost(IPost post);
}

// DDD-styled entity implementation defined in Project.Domain
public class Blog : IBlog
{
    public int Id { get; private set; }
    private HashSet<IPost> _posts { get; private set; }
    public IEnumerable<IPost> Posts => _posts ?.ToList();
    
    // Entity mutator method, missing logic ofc
    public void AddPost(IPost post)
    {
        _posts .Add(post);
    }
}


// Interface in Project.Core
public interface IAppDbContext 
{
    public DbSet<IBlog> Blogs {get; set;}
    public DbSet<IPost> Blogs {get; set;}
}

// Concrete implementation in Project.Infrastructure
// Not sure how this part could be made possible?
public class AppDbContext : DbContext, IAppDbContext 
{
    public DbSet<Blog> Blogs {get; set;}
    public DbSet<Post> Blogs {get; set;}
}

//Somewhere In DI config
service.AddScoped<IAppDbContext, AppDbContext>();
service.AddScoped<DbContext>(x => x.GetRequiredService<AppDbContext>());
service.AddScoped<IMyService, MyService >();

// Domain service interface Project.Core
public interface IMyService
{
    IBlog GetABlog(int blogId);
}

// Domain service implementation under Project.Domain
public class MyService : IMyService
{
    IAppDbContext  _context;
    
    public MyService (IAppDbContext  context)
    {
        _context= context;
    }

    public IBlog GetABlog(int blogId)
    {
        return _context.Blogs.Find(blogId);
    }
}

// Controller in web project that has no knowledge of concrete implementations
public class MyController: Controller
{
    IMyService _myService;
    
    public MyController (IMyService myService)
    {
        _myService = myService;
    }

    public IBlog GetBlog(int id)
    {
        return _myService .GetABlog(id);
    }
}

Example Hierarchy:  Core -> Domain -> Infrastructure

In this example, the control is inverted. Interfaces are defined in core, and the application will rely on injected interfaces, and not reference concrete implementations. This allows a clean separation between where the contracts are defined, where they are implemented, and where they are used. With higher layer assemblies being able to provide concrete instances that are used by lower layer assemblies through their interfaces.

If you took out the ability to use an IBlog from a DbSet (Without performing casts every time you use a Set) This quickly breaks down, as now Project.Core cannot define a service interface, because that interface must return a Blog, which is defined in a higher layer, neither can it define the IAppDbContext interface. And this quickly becomes a mess.


Ideally one could configure EF in such a way that consumers need only know of model interfaces, and not their concrete types. EF is aware of the concrete types, but happily returns the configured interface instead (And the generic type for the IQueryable...etc is that interface, vs the concrete type). How that can be achieved though, I hope you can comment on.

This is less valuable to an anemic model, but to a DDD model where domain logic & invariant guarding exists with aggregates/entities. It's extremely valuable.

@ajcvickers
Copy link
Member Author

@douglasg14b An implementation of this feature would allow public DbSet<IBlog> Blogs {get; set;}. However, EF would still need to be configured with a factory or some other mechanism to create a concrete instance.

@douglasg14b
Copy link

douglasg14b commented Aug 9, 2019

@ajcvickers Thanks for the info!

Are those factories something that EF can handle internally? Ie. in a configuration of some sort telling EF that IBlog is implemented as Blog? Or is it some external code?

If it's external code, is that something I can read up on right now to gain a deeper understanding of how EF interfaces with factories?

Similarly, if I found myself motivated, I wouldn't mind digging into EF to better understand it's source. Are there design specs for this already? It could be a fun distraction project to try and create an implementation of that...

@ajcvickers
Copy link
Member Author

@douglasg14b Hard to be sure what this will look like without designing/implementing it. We don't have any design specs around this.

@douglasg14b
Copy link

douglasg14b commented Aug 9, 2019

Ah, I see.

If I was to do some leg work on that over the next few weeks/months, would a design proposal be something that should be put into a separate issue to be discussed and picked at?

I'm not necessarily making a commitment, but if my free time ever clears up it's something I'm relatively passionate about, and would like to know how you/your team would prefer a spec/design/proposal.

@ajcvickers
Copy link
Member Author

@douglasg14b Yes, a proposal we can discuss would be great! However, to be transparent, we won't be able to take the PR for 3.0, and it's looking like management is only going to allow bug fixes in 3.1, so I expect the next time we will be able to release something (that is more than a preview) with new features will likely be over a year from now. 🤷‍♂

@douglasg14b
Copy link

Sounds like that's giving me lots of time then! Definitely a shame that it will be a year or so, but it's understandable.

I have a hell of a lot of EF source to read through anyways...

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests

4 participants