3

I understand the basics of DI in .NET Core, but I'm having trouble figuring out how to use it with multiple projects. Imagine I'm setting up a database context in the Startup class of ASP.NET Core:

public void ConfigureServices(IServiceCollection services)
{
  services.AddDbContext<GalleryDb>();
}

I know how to access that context in an API controller:

public class AlbumController : Microsoft.AspNetCore.Mvc.Controller
{
  private GalleryDb _ctx;

  public AlbumController(GalleryDb ctx)
  {
    _ctx = ctx;
  }
}

But what does one do when there are many layers and functions between the API controller and the data access class? Eventually the code reaches my repository class, which is the one that actually requires the context. It looks like this:

public class AlbumRepository
{
  private GalleryDb _ctx;
  public AlbumRepository(GalleryDb ctx)
  {
    _ctx = ctx;
  }

  public void Save(AlbumEntity entity)
  {
    // Use _ctx to persist to DB.
  }
}

I understand that I could pass the context from the API entry point all the way down, but that seems like an anti-pattern because it means passing it as a parameter through multiple classes and functions that have no interest in it.

Instead, I'd like to do something like this at the point where I invoke the repository class:

public void Save(AlbumEntity album)
{
  var ctx = DependencyResolver.GetInstance<GalleryDb>();
  var repo = new AlbumRepository(ctx);
  repo.Save(album);
}

I believe some DI frameworks have something like this, but I'm trying to figure out how to do it with native .NET Core 2.0. Is this possible? What is the best practice? I found one thread (ASP.NET Core DependencyResolver) talk about using IServiceProvider but the implication was that this was not a desirable solution.

I'm hoping whatever the solution is, I can extend it to apply to other DI classes like ASP.NET Identity's RoleManager and SignInManager.

Roger
  • 2,118
  • 1
  • 20
  • 25
  • 1
    You should (almost) never need to call the container itself. Just use construction injection everywhere. So you shouldn't ever have a need to `new` any instance of an object (there are some exceptions, i.e. when you need parameters only know at runtime, the you usually use an abstract factory) – Tseng Oct 07 '17 at 04:04

2 Answers2

5

The key breakthrough chris-pratt helped me understand is that the only way this works is to use DI through all the layers. For example, down in the data layer I get a DB context through DI:

public class AlbumRepository
{
  private GalleryDb _ctx;
  public AlbumRepository(GalleryDb ctx)
  {
    _ctx = ctx;
  }
}

In the business layer I use DI to get a reference to the data layer:

public class Album
{
  private AlbumRepository _repo;
  public Album(AlbumRepository repo)
  {
    _repo = repo;
  }
}

Then, in the web layer, I use DI to get a reference to the business layer class:

[Route("api/[controller]")]
public class AlbumController : Microsoft.AspNetCore.Mvc.Controller
{
  private Album _album;
  public AlbumController (Album album)
  {
    _album = album;
  }
}

By using DI through every layer, the DI system is able to construct all the necessary classes at the point where they are needed.

This requirement has a profound impact on the architecture of an application, and I now realize that my initial hope to tweak an existing, non-DI app to start using DI for the DB context is a major undertaking.

Roger
  • 2,118
  • 1
  • 20
  • 25
4

I understand that I could pass the context from the API entry point all the way down, but that seems like an anti-pattern because it means passing it as a parameter through multiple classes and functions that have no interest in it.

No, that's not an anti-pattern. That's how you should do it. However, the bit about "classes and functions that have no interest in it" makes no sense.

Simply, if you're working with something like a repository that wraps a DbContext (a horrible idea, by the way, but we'll put a pin in that), then you shouldn't ever be dealing directly with that DbContext. Instead, you should be injecting your repository into your controllers and then simply let the context be injected into that:

public void ConfigureServices(IServiceCollection services)
{
    services.AddDbContext<GalleryDb>();
    services.AddScoped<AlbumRepository>();
}

Since ASP.NET Core knows how to inject GalleryDb, and AlbumRepository takes GalleryDb as a constructor param, you simply register AlbumRepository for injection as well (using a "scoped" or request lifetime).

Now, you can inject AlbumRepository the same way you're currently injecting the context:

public class AlbumController : Microsoft.AspNetCore.Mvc.Controller
{
    private AlbumRepository _repo;

    public AlbumController(AlbumRepository repo)
    {
        _repo = repo;
    }
}

Where this starts to get tricky is when you have many repositories, especially if you have controllers that need to interact with several repositories. Eventually, your code will become a rat's nest of service config and injection boilerplate. However, at that point, you should really be employing the unit of work pattern as well, encapsulating all your repositories in one class that you can inject instead. But wait, oh yeah, that's what DbContext is already. It's a unit of work encapsulating multiple repositories, or DbSets. This is why you shouldn't being using the repository pattern in conjunction with Entity Framework. It's a pointless abstraction that does nothing but add additional and unnecessary entropy to your code.

If you want to abstract DbContext, then you should use something like the service layer pattern (not to be confused with the RPC bull excrement Microsoft refers to as the "service pattern") or the CQRS (Command Query Responsibility Segregation) pattern. The repository pattern is for one thing: abstracting away raw SQL. If you don't have raw SQL, you should not be implementing that pattern.

Chris Pratt
  • 232,153
  • 36
  • 385
  • 444
  • 1
    chris-pratt, I did follow your arguments against the repository with EF (https://softwareengineering.stackexchange.com/questions/180851/why-shouldnt-i-use-the-repository-pattern-with-entity-framework) in my .NET Core web api project and had quite a headache mocking DbContext and DbSet. Introducing a repository just made things so much easier to test. If you need multiple repositories in a conroller why not just have a single interface exposing all that you need and inject that. – alwayslearning Oct 07 '17 at 15:57
  • Not sure why you had a headache. What's so difficult about mocking DbContext? Regardless, unless you don't test your repositories, you'd still have to mock it to test those. So really you're just doubling to your test load. – Chris Pratt Oct 07 '17 at 16:12
  • I actually did manage to mock the DbContext, so ignore the 'headache'. What if I want to replace EF with some other data retrieval mechanism.It would be much easier if it were abstract behind an interface; that way I wouldn't need to update the tests even after replacing the underlying data fetching mechanism. Tests would just test my interface/repository/service whatever you call it. – alwayslearning Oct 07 '17 at 18:57
  • Well, yeah. No one's arguing against that. The point is that that abstraction should actually do something for you. With a repo, you're still bleeding logic into your application code, whereas a service layer pattern would abstract that all away – Chris Pratt Oct 07 '17 at 19:01
  • Agree on that point. The abstraction shouldn't be just call forwarding but should be doing something more meaningful. – alwayslearning Oct 07 '17 at 19:05
  • chris-pratt, thanks for your thoughtful answer. You are basically suggesting passing DbContext down from the entry point to the data layer. But the stack trace might be 30 levels deep by the time I'm saving to the DB and it doesn't seem right to add DbContext to all those classes when they are doing nothing with it except for passing it down. That sure feels like a code smell. Maybe you would say a stack trace 30 levels deep is itself a code smell? In a highly refactored, complex app I would argue that is to be expected. – Roger Oct 09 '17 at 23:12
  • The code smell would be going 30 levels deep. Why would you possibly need that much abstraction? Further, a class should not be passed anything it doesn't use. If it uses a repository, you pass it that, not the DbContext. The DbContext would be passed to the repository when it's instantiated. Again, not clear what you're doing here, but I don't thinking you're getting the concept of dependency injection. – Chris Pratt Oct 09 '17 at 23:31
  • Your statement that "a class should not be passed anything it doesn't use" implies that the class the API controller invokes is the one that interacts directly with the DB. That is, the class receiving the DB context from the API controller be the one that uses it. But that is not possible in even a simple 4-tier app. E.g. REST layer => service bus => business logic => data layer. How does the context get from the REST layer to the business layer? If you have examples of complex GitHub projects demonstrating what you mean, I'd love to look at them. – Roger Oct 10 '17 at 16:02
  • Sorry but I'm still not getting you. The only thing that needs your DbContext is the DI container: in this case, the ConfigureServices method in your StartUp class. That's it. Literally everything else would only get passed your repo, service, etc. – Chris Pratt Oct 10 '17 at 20:56
  • IDbContext.SaveChanges() is invoked in the business or data layer, so the reference to DbContext needs to pass through the service bus layer (given my example of a 4-tier app above). Since this layer doesn't care about DbContext, I don't know how to pass it through the classes in this layer while following your advice that one should never pass a reference to a class where it's not needed. – Roger Oct 11 '17 at 15:08
  • In your DI container, you register the IDbContext with the implementation. That's it. Then, as long as the DI container is handling the instantiation of your other classes, any time one of them has a dependency on IDbContext (via ctor), the DI container will inject an IDbContext instance according to the defined lifetime scope (either using an existing instance or creating a new one). You don't have to pass it around class to class. You just need to ensure that all classes that utilize it, are themselves being injected, which should be the case anyways. – Chris Pratt Oct 11 '17 at 15:14
  • I think I see what you are saying. The business or data layer is where I'll have a class that takes DbContext in the constructor, NOT the API controller. The API controller would specify a service bus class in its constructor. And the service bus class specifies a business layer class in its constructor, and so on. The end result is that the injected class at the top layer starts a chain reaction that creates all the necessary classes throughout the layers. Thanks for your patience. – Roger Oct 11 '17 at 19:40
  • Yes, that's exactly it. – Chris Pratt Oct 11 '17 at 21:21
  • This is an old comment but I used to agree with not using the repository method with EF Core. Personally I don't get what Microsoft has done with EF Core. Their own examples show the repository method, as much as I hate the repository method I just still end up using it. DbSet works as a repository, except you don't get the added flexibility of changing your data access, and you basically end up implementing a repository except you call it something else. It just felt silly kidding myself I had freed myself from repositories while abstracting away queries into extension methods. – perustaja Jan 21 '20 at 06:58
  • I'm not sure why Microsoft persists in showing the repository pattern in EF examples. It's been a long point of contention, and has done a disservice to a great number of new developers. As someone who fell in that trap myself and actually works in the trenches, I can tell you it's 100% wrong. By the time you've worked with it enough, you can *feel* it's wrong. Don't start with abstraction; grow into it. As you develop your app, you'll feel out areas that will benefit from that, but you're always best off with a single project with no abstraction at all, if you can get by with it. – Chris Pratt Jan 21 '20 at 09:52
  • That's not most real world projects, but the point is that simple is better than complex. Only add complexity, when it adds more benefit than the simplicity. – Chris Pratt Jan 21 '20 at 09:53