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

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

Closed
douglasg14b opened this issue Oct 24, 2021 · 9 comments
Labels
closed-no-further-action The issue is closed and no further action is planned. customer-reported

Comments

@douglasg14b
Copy link

douglasg14b commented Oct 24, 2021

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 that DbContext`

This could be solved in a few ways:

  • Use interfaces for the DbSet models of each of the feature's entities, and configure the concrete type with Ef Core within each of the assemblies
    • You can now have a shared assembly that defines all the interfaces, and each of the other feature assemblies need not even hold references to each other.
    • Each assembly is responsible for configuring itself with EF Core
    • This is Dependency Inversion
  • Move all the features into a single project and separate them by folder.
    • This is unworkable since we are sacrificing nearly all the advantages of having separate assemblies
  • Move all the feature entities into an assembly
    • This is also unworkable, this would be a kludge that sacrifices the advantages of having the features in separate assemblies in the first place

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

@douglasg14b douglasg14b changed the title How to resolve DbContext in package-by-feature or other "vertical slice" architecture? How to resolve DbContext in package-by-feature or other "vertical slice" architectures? Oct 24, 2021
@roji
Copy link
Member

roji commented Oct 24, 2021

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
Copy link
Author

douglasg14b commented Oct 25, 2021

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:

  • Cleaner vertical slices/bounded contexts
  • Better bounded context boundaries
    • Enforcement on not having bounded contexts circularly dependent
    • Internal bounded context members are hidden from the rest of the application, better enforcing/enabling several DDD concepts
  • Better modularity / self-contained / independent
    • Leading into microservices is very much possible, depending on the restrictiveness of dependencies
  • Easier to test
  • Many, many, more things I have yet to learn or realize as I explore this type of architecture

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?

  • It's abstract
    • The problem space is abstract and as broad as ideas are common, which makes it easy for others not already in the space to consider it negligible.
    • It can take weeks, months, or years to really absorb some of these concepts, especially when the build up on top of other concepts. (This is why I'm exploring these architectural patterns, to better understand them)
  • Attention span of readers
    • The more abstract the problem, the more foundational things have to be described, the longer everything gets
    • The longer the post, the more likely it will be skimmed, and not understood. The more likely that nonsense comments will be made that clearly didn't read the whole thing.
  • My time, energy, and holy hell my wrists
    • It takes time & energy to write this sort of stuff. And if long enough, sometimes a bit of pain.

@douglasg14b
Copy link
Author

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

@douglasg14b
Copy link
Author

douglasg14b commented Oct 26, 2021

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
Copy link
Contributor

@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:

  • A skeleton set of projects and code that assume EF Core is implemented/factored the way you want it so that we can see exactly what the D.I. boundaries looks like, and understand how the services interact.
  • A pointer to a real application implemented using this kind of architecture, regardless of whether that application uses EF Core or not.

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

@ajcvickers ajcvickers modified the milestone: Backlog Nov 3, 2021
@douglasg14b
Copy link
Author

douglasg14b commented Nov 7, 2021

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
Copy link
Member

roji commented Nov 8, 2021

@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
Copy link

Ciantic commented Nov 29, 2021

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
Copy link
Contributor

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.

@ajcvickers ajcvickers added the closed-no-further-action The issue is closed and no further action is planned. label Jan 11, 2022
@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
closed-no-further-action The issue is closed and no further action is planned. customer-reported
Projects
None yet
Development

No branches or pull requests

4 participants