3

I implemented a class EUMemberChecker which is responsible of checking if a country is a member of the EU. In order to do its job, the class contains a method public bool IsEUMember(string country). The data used to check if a country is a member of the EU is stored in a PostgreSQL database table.

I want to make this class available via DI by adding it as a singleton service via AddSingleton. The reason for this is that it should only load the EU members from the database once on application startup. If I would add this class via AddScoped, every single instance of it would need to have its own list of EU members loaded from the database, which would be quite an overhead in my opinion.

My problem is that I cannot add this class as a singleton because it uses my DbContext which is added as a scoped service. By doing it anyway, it causes the runtime error Dependency ...DatabaseContext {ReturnDefault} as parameter "context" reuse CurrentScopeReuse {Lifespan=100} lifespan shorter than its parent's: singleton ... {ReturnDefault} as parameter ....

Therefore, it seems like I have to add my class as a scoped service, resulting in an additional database call to load the EU member countries for every instance of the class.

Am I applying the Single Responsibility Principle incorrectly here or how can I get around this problem? How can I ensure that the EU members are not loaded from the database multiple times for no good reason?

Chris
  • 1,417
  • 4
  • 21
  • 53
  • https://stackoverflow.com/a/58989132/3181933 – ProgrammingLlama Jul 05 '22 at 16:04
  • @DavidG: in this case, it seems that the DbContext is used once to initialize a cache; I would say that as long as it's guaranteed to be used just once (and access is synchronized using a lock), this is actually safe. Still, as this code would raise some eyebrows, it might be beneficial to explore other options first, before injecting a DbContext into that singleton (I would say at the very least a fat comment block is required to explain why this is done, why it is safe, and why it's the best solution). – Steven Jul 05 '22 at 16:18
  • @Steven Even though, I'd still use the "inject service provider and create scope and get DbContext from that" method since you would need to change the lifetime when configuring the context in DI – DavidG Jul 05 '22 at 16:19
  • @DavidG: sure, injecting a scoped into a singleton is certainly something I would try to prevent in all cases. See my answer where I give alternative solutions – Steven Jul 05 '22 at 16:22
  • Shouldn't it be possible to inject `IServiceProvider` in `EUMemberChecker` and then get the context using `GetRequiredService()` on it? – Good Night Nerd Pride Jul 05 '22 at 16:42

1 Answers1

5

There are several solutions to solve this. Here are the options I could think of:

  • Make the EUMemberChecker scoped, but pull the cache part out of the class, and inject it as a Singleton service.
  • Make EUMemberChecker part of your Composition Root to allow injecting the Container instance (e.g. IServiceProvider) into that class. This allows creating a scope instance from which you can resolve, for instance, the DbContext. If the class contains business logic, it'd be good to extract that from the class and inject that into the class as well; you wish to keep the amount of code inside your Composition Root as small as possible.
  • Ignore and suppress the warning, as you seem to be sure that this doesn't cause any problem in your specific case. Note that with MS.DI, there likely isn't an easy way to suppress this error.
  • Construct the DbContext manually inside the EUMemberChecker at the time you create the cache. This might mean you need to inject configuration values into EUMemberChecker, such as the connection string, which is something DbContext obviously needs.
  • Load the data from the database before making your container registrations and supply this cache manually upon registration. e.g.: services.AddSingleton(c => new EUMemberChecker(loadedMembers)).
  • Initialize EUMemberChecker directly at startup by calling some sort of Initialize method. Either the loaded members can be supplied to the method, or you can pass in the DbContext so that Initialize can do the querying internally. This DbContext can be resolved from the container at startup, probably by resolving it from a manually created scope.

Which option is best, depends on a lot of implementation details, so you will have to decide which one best suits your needs.

marc_s
  • 732,580
  • 175
  • 1,330
  • 1,459
Steven
  • 166,672
  • 24
  • 332
  • 435
  • Thanks for the suggestions! I had some trouble figuring out which option would be ideal in my situation but I ended up going with your last suggestion (Initialize method). Most of the other options would've also worked, though. – Chris Jul 06 '22 at 12:27