dotnet / efcore

EF Core is a modern object-database mapper for .NET. It supports LINQ queries, change tracking, updates, and schema migrations.
https://docs.microsoft.com/ef/
MIT License
13.71k stars 3.17k forks source link

How to resolve DbContext in package-by-feature or other "vertical slice" architectures? #26447

Closed douglasg14b closed 1 year ago

douglasg14b commented 2 years ago

Preface

Similar to feature-folder architecture, package-by-feature vertically slices the codebase to achieve much better cohesion, However, EF Core seems to get right in the way of that since a DbSet can only be of a concrete type, and we can't really "register" our entities in a non-coupled fashion.

This is one (of many) long-standing architectures (This one articled/ written on circa ~2005 by "Uncle Bob") that are relatively easy to use with Asp.Net Core, but sibling projects seems to produce difficulties.

Can EF Core work in a vertically sliced application where the slices are assemblies without forcing the application to break from it's architecture?

Include your code

Say you have this structure, each feature is a different project/assembly.:

App.Feature.Feature1
  -- Feature1.cs
  -- Feature1Service.cs
  -- Feature1Controller.cs
App.Feature.Feature2
  -- Feature2.cs
  -- Feature2Service.cs
  -- Feature2Controller.cs
App.Feature.Feature3
  -- Feature3.cs
  -- Feature3Service.cs
  -- Feature3Controller.cs
... etc

Each feature entity is a DDD styled aggregate, so it is self-contained and defends its own invariants, as opposed to it simply being a POCO.

Each feature service needs a DbContext and may want access to load entities from another feature. However, there is no way to define a "shared" DbContext without creating an assembly that is a circular-reference. An outside assembly would need to reference each feature assembly to use the Feature in each of the context's the DbSet, and each assembly would then need to reference that assembly to use thatDbContext`

This could be solved in a few ways:

I imagine I'm missing some feature/aspect of EF Core that enables this. How can EF Core work with this sort of architecture?

roji commented 2 years ago

Related to (possibly dup of) #16470.

This is unworkable since we are sacrificing nearly all the advantages of having separate assemblies

Am not familiar with this architecture - can you point to a resource detailing the advantages of separating out to different assemblies like this?

douglasg14b commented 2 years ago

Related to (possibly dup of) #16470.

I'm not sure how this was concluded. Other than DI being a marginally shared theme, and my involvement in that thread. Not once in this post did I make mention of interfaces for EF Core classes. I thought I clearly explain the problem space, maybe I didn't, where is the ambiguity?


Edit: unworkable is probably too strong a term, more like undesired.

Am not familiar with this architecture - can you point to a resource detailing the advantages of separating out to different assemblies like this?

I wouldn't say it's my place to justify the architecture, I'm neither an expert on it, nor an advocate. It's an architecture I'm exploring and finding out that EF Core's rigidity to various architectural patterns (Mainly in where they end up converging on a reliance on Dependency Inversion, the D in SOLID) is becoming a significant hindrance to exploring these more advanced patterns that scale better for larger applications. (This also ties in closely with CQRS architecture, which I am not fully exploring in this context).

My grumbling aside though:

Assuming you are already marginally familiar with the advantages of vertical-slice/x-by-feature architecture, Domain Driven Design, and maybe even CQRS. Generally structuring the source code according to domain concepts. Package-by-feature vs folder-by-feature, in addition to the existing advantages, has additional advantages:

Though to be clear, I don't want this to be a discussion on the merits or demerits of various architectural patterns, these have been explored in exhaustive detail in various books, blogs, and boards already. And exist in real-world applications, libraries, toolsets, and implementations


Why am I even making this post?

I was exploring this sort of architecture when EF Core pumped the breaks on a foundational aspect of it, DI. Given this isn't the first time I've found EF Core to be a hindrance in this way, a way it's sibling projects actively encourage and promote, decided to make an issue about it since it's getting ridiculous at this point. Written to try and make more clear the problem caused by this.

My objective is to open up a real discussion on the problem. Not one that gets shutdown by someone running on the platform of "I don't understand the problem space, therefore it's not a valid problem". Which is frustrating, to say the least, especially when real effort is put into these posts/comments.

The problem of not being able to invert entity dependencies with EF Core is a real problem that affects things on a very abstract level, which can make it difficult to "get" for many. The gist of it being that many design & architectural options that are used in applications stop being available here.


Preemptively addressing questions along the lines of List/Justify pattern XYZ type questions.

This is a red herring, intentional or not, that moves the discussion from a problem & solution, to one of endless justification & retort. That requires a significant depth of knowledge to answer well, and no depth to keep endlessly prodding without investing energy into understanding.

Why is it difficult to describe/answer re: patterns & architecture?

douglasg14b commented 2 years ago

Sorry for the long post, I'm being sincere here, and hope you will be too.

douglasg14b commented 2 years ago

A brief example of a conversation I had recently (With someone about feature-packages) that further highlights the problem space.

Me: So, features are to avoid circular dependencies. However, it's likely you'll have some features that must depend on each other... Other Person: In that case you would extract interfaces into a common/shared project, and reference those Me: So, just DI? Other Person: Yep. Pretty straightforward.

Solving problems in this manner is commonplace, but the moment you consider doing this with your entities, you suddenly have an awkward situation. You can no longer apply a common solution to a common problem.

ajcvickers commented 2 years ago

@douglasg14b I think it would help us understand what it is you are looking for if we could see something a bit more concrete. Two possibilities for this are:

Would it be possible to provide one or both of these things?

douglasg14b commented 2 years ago

I can do that!

I'm kinda hammered right now, so it will take a bit of time to circle back around to this. Thank you for both the response, and your patience.


As for the 2nd request though, that gets difficult as I only know of proprietary ones (From current and past employers and clients). The best I could suggest is reading material, a quick google search on feature by folder, feature by package, and/or vertical slice architecture will reveal plenty of discussions on the topic.

roji commented 2 years ago

@douglasg14b sure thing, and thanks for your time on this. FWIW my comment above wasn't meant to cast doubt or to ask you to justify anything - just curiosity about the pattern and a desire to know more.

In any case, a simple skeleton solution would go a long way to help us understand exactly what's being requested and how we can help address it from the EF Core side.

Ciantic commented 2 years ago

we can't really "register" our entities in a non-coupled fashion

I think I know what you mean, but I found solution that works for splitting entities between packages, but still having just one DbContext in the main application.

Note that DbSet property is not required for querying anymore, you can just add the entities with Fluent API:

In feature package:

public class Thing
{
    public Guid Id { get; set; }
    public string Name { get; set; } = "";
}

public class Other
{
    public Guid Id { get; set; }
    public string Name { get; set; } = "";
}

public static class ModelBuilderExtension {

    public static void AddMyStuff(this ModelBuilder modelBuilder)
    {
        modelBuilder.Entity<Thing>(); // Notice that DbSet is not required!
        modelBuilder.Entity<Other>();
    }
}

In main app package:

public class AppDbContext : DbContext
{
    public AppDbContext(DbContextOptions<AppDbContext> options)
    : base(options)
    {
    }

    protected override void OnModelCreating(ModelBuilder modelBuilder)
    {
        modelBuilder.AddMyStuff(); // <-- This registers the entities
        base.OnModelCreating(modelBuilder);
    }
}

You can even make Repositories in the feature package if you make a decision e.g. to register AppDbContext as a DbContext service.

Notice that you don't need the DbSet as properties to make queries, adding or changing the entities:

dbContext.Add(new Thing() {
    Name = "Foo"
});
dbContext.SaveChanges();
dbContext.Set<Thing>().Where(p => p.Name == "Foo").First();

In above the dbContext.Set creates DbSet from thin air, you don't need the properties usually in the EF docs.

ajcvickers commented 2 years ago

EF Team Triage: Closing this issue as the requested additional details have not been provided and we have been unable to reproduce it.

BTW this is a canned response and may have info or details that do not directly apply to this particular issue. While we'd like to spend the time to uniquely address every incoming issue, we get a lot traffic on the EF projects and that is not practical. To ensure we maximize the time we have to work on fixing bugs, implementing new features, etc. we use canned responses for common triage decisions.