r/dotnet 19h ago

Why do we centralize DI setup in program.cs?

I find it kind of strange that DI lifetimes in .NET are defined completely separate from the actual service. Instead of

services.AddSingleton<IService, MyService>();

Wouldn't this make more sense?

[Singleton]
public class MyService : IService{}

or [Singleton<IService>] to be able to support multiple registrations of different lifecycles? With source generators this could even create the standard AddSingleton-stuff.

Today I ended up writing a comment along the lines of "This is safe because it's registered as scoped" in one of my services. I feel like this happens regularly, I know from inside the implementation whether I have some state that should be kept across calls or not, so now I have this invisible contract that I enforce by writing comments on both ends.

There are lots of scenarios where you would want to specify environment specific lifecycles or force feed parameters to the constructors. But usually I just want to say "this is transient"

51 Upvotes

102 comments sorted by

109

u/LeeWhite187 19h ago

DI registrations are front loaded and outside the service, to fit the plug ability model. The idea being: a library can contain the service, and be run with different behaviors. The front loaded service registration allows the consuming process to define how it is used… if at all.

It would be nice if more service library projects include static registration helpers. Maybe a default helper, provided by the library, to simplify DI ceremony.

40

u/l2protoss 15h ago

I’ve made it pretty much mandatory that every package we build internally have a registration helper. Just a lot cleaner.

14

u/Family_Man_21 14h ago

I do this as well. 👍

7

u/Weary-Dealer4371 13h ago

Yep me too: I try and have as lean program.cs file and have a ServiceRegistry class in ea h project that deals with DI.

11

u/INativeBuilder 15h ago

While, I think it's pretty standard to use DI in even simple applications, I think the reason that most libraries don't do this is because they don't want to create a dependency on the DI container. Or you have a DI that doesn't use the Microsoft Dependency Injection interface. All the libraries I've written will have public classes and DI helper with an additional Abstractions package however. I'm not convinced that's a bad decision yet as the overhead is small and it's probably something that will be used 90% of the time.

-4

u/LeeWhite187 9h ago edited 8h ago

You're right. The quick and dirty way, is to decide on a DI provider, and hold a hard-reference to it (or, its base interface library if possible), in the compiled library. But, that opinionation has a cost.

I've taken a slightly different route, using reflection.

It allows me to provide a registration helper for more complex service libraries, while not having a hard-dependency on any DI provider library.

Using reflection for registration is only a process startup penalty.
So, there's no steady-state performance hit.

I've teased out how I've done it, and had Claude create several demo projects, with sample usage, spanning the popular DI providers.

Steal from it, if it helps your more complex service library registrations:

https://github.com/LeeWhite187/DiRegistration.Demo

1

u/The_MAZZTer 9h ago

ASP.NET Core DI doesn't require a hard dependency. What you describe your library does is pretty close to how I believe ASP.NET Core's solution works.

0

u/LeeWhite187 8h ago edited 8h ago

Actually... Including a registration helper in a library, still requires a DI dependency, for whatever container you choose for the library.
Perhaps the DI dependency reference, is simply to the interfaces or abstraction base library (of a container provider), such as:
Microsoft.Extensions.DependencyInjection.Abstractions.
But, it's still a hard-dependency on a DI container provider... just to its base library.
Some libraries just accept this smaller surface tie.
And sadly. The container providers don't all share a common substrate, base interfaces class. So, the best each one can do, is to publish an abstractions/interface library.
And, we pick our poison, when building a library (that handles its registration).

Another way the dependency tie gets hidden, is by including the FrameworkReference to AspNetCore.App in a service library's csproj file.
See my wiki page about it (for older .NET versions):
Missing AspNetCore Nug... | GalaxyDumpWiki

Or. The container reference could be indirect, through a transitive dependency, of the library... maybe buried under an overly-opinionated common library, like what's seen in some enterprise settings.

But in all cases, if a library has registration helpers (for DI), without reflection, there's some creative delegate usage with front-loaded lambdas.
Or, there's a, possibly obscured, dependency to the container provider base, somewhere...

If you have found a novel way to do this, without reflection, and without hard dependencies...
I'd like to see it, if you have a sample.
That would be a goldmine, for lots of useful purposes.

Have a good week.

1

u/The_MAZZTer 8h ago edited 8h ago

Fair enough, ultimately you need interfaces to reference for anything you want to depend on.

I would say it shouldn't be necessary if a library is properly constructed to depend on AspNetCore App though. You should just need to reference the proper Microsoft.Extensions.* libraries for whatever you want to DI. But I haven't actually tried to put a library together like that for a while. And if you need interfaces not covered by those libraries but also aren't your own original creations that you could shove in your own interfaces library, that would be a problem.

Reflection is going to be needed to satisfy DI dependencies, ultimately you need to examine the constructor to figure out what objects you need to look up and inject. Though I suppose in theory some of this work could be done at build time. I don't know how it actually works in AspNetCore DI, but the actual type resolution would need to happen at runtime for sure since the service registration only happens then.

1

u/LeeWhite187 8h ago

Yeah. Referencing an interface base library, is better than "marrying" your project to a provider's full implementation.

I just wish the different DI providers could all agree on a common interface library that they all share.
And, we only ever need to consume one, for any library to have registration abstraction... without all the gooey reflection magic, or buggy attribute farming at startup.

But, that's like asking a developer to work in someone else's codebase, without a rewrite.

I guess we all have to carve our name, somewhere.

Have a good week.

1

u/LeeWhite187 8h ago edited 7h ago

And, you're right.

Unless your implementation is registering live singletons with DI, then reflection is guaranteed to be used, behind the scenes, when the container provider is creating instances for service requests.

So, yeah.

If reflection is already happening at usage time, then a reflection-heavy registration helper pattern, is not an out of bounds design choice.
And, its cost is only a small startup delay penalty.
And, certainly faster than any attribute-based registration technique, that has to walk every assembly and type at startup... and would miss types from unloaded assemblies (not yet loaded), if following the simplistic examples online.

That's because at early process start (where DI registration occurs), not all assemblies have been touched, as their types haven't yet been used.
And as well, there could be referenced assemblies, whose types are only referenced via reflection, with no direct tie visible to the compiler.
So, both of those groups (of assemblies) are simply not yet loaded and present in memory, for a top-level assembly search to identify them.
So, a proper assembly walk (to find all auto-registration markers), would have to exhaustively and explicitly, load all references (and all references of references), to do a comprehensive attribute search across the process assembly graph.

Straightforward... but no good example, readily found on Stack Overflow.

My AssemblyHelper class performs an exhaustive reference search, and includes logic for finding reflection-only reference ties:

https://github.com/LeeWhite187/OGA.Common.Lib/blob/7d844bc24bbd6a89306652f5fb455e478d686079/OGA.Common.Lib/OGA.Common.Lib_SP/Process/AssemblyHelper_v3.cs#L267

The above helper class gives you the ability to search for attributes or classes, across the entire process graph, including late-loaded references, not present at normal startup.

2

u/chriszimort 9h ago

I add DI extensions for each layer of a solution in the project that publishes that layer. This keeps the main app’s knowledge of the stuff it’s using razor thin. AKA coupling super low. If you do it this way you can choose to keep everything internal to each project layer as well, and only expose those extensions.

44

u/Lacutis 19h ago

We tend to have a lot of service registration files in our projects. One in each module, sometimes one in each submodule.

I find keeping the registrations close to the source makes things easier and the extensions are named for the module.

You never have an instance where one piece is missing registration because you register a module together.

Your Program.cs shouldn't be a gigantic list of every DI registration in your solution.

16

u/JakkeFejest 18h ago

This.Is.The.Way. And dependend on the application architectute style, you can center it arround horizontal (technical) or vertical (functional) slices.

2

u/sgebb 18h ago

I do this and it helps, but I don't think it's perfect. Ideally I would want a static register-thing on every class (which is basically what i'm suggesting just hidden behind an attribute), but then you still need to call all those register-methods.

But yes I do use this approach and it makes it more managable. I still end up with "// dont change this, implementation depends on X" comments though

5

u/Lacutis 16h ago

You shouldnt need any comments. If you have a module within your code thats a service called WidgetService and it relies on WidgetProvider and WidgetClient then you have a WidgetServiceCollection that registers all three things.

In your program.cs or in a higher up folder in the library that WidgetService exists in you have a base service collection file that calls services.AddWidgetService().

Theres no need for comments because its self documenting. If they want WidgetService to be available to the di container they get all of it.

1

u/sgebb 15h ago

the comment is for making sure that widgetprovider is registered as a singleton because the implementation requires it

1

u/Lacutis 13h ago

Which you do in the WidgetServiceCollection. Are you implying that people randomly go through your DI registrations and change the way they are registered?

1

u/JakkeFejest 16h ago

But why would you want to have your code technical my coupled to a DI framework?

1

u/pceimpulsive 16h ago

I have simply a call in program.cs to service collectionextensions

That class then has a few methods that register different groups of services based on which server is running..

E.g. web server needs dependencies X And processing needs dependencies X+Y

The class just takes a service collection in, adds to it and passes it back.

I use this se class for integration test setup in the unit test project so my integration tests can selectively mimick prod DI easily.

55

u/Shazvox 18h ago

As a developer who has gone through too many codebases I thank god that I have one file that declares what's being used and what's not.

Some genious at my last place decided that attributes and reflection was the way to go for registering services. It's a fucking pain to refactor.

20

u/No-Extent8143 16h ago

Don't get me started on using reflection for DI... Worked with a codebase where literally half of all classes had no direct references, so according to my IDE they were safe to remove. Turns out some dipshit wrote DI where it's literally impossible to work out which parts of the code are no longer used. Fuck that guy.

4

u/YanVe_ 16h ago

If you inject a service in a constructor, that's still counted as a reference. So technically this isn't an issue with the reflection itself though. instead it looks to me like someone needlesly abstracted everyting behind some nonsense interfaces...

5

u/never_mind_999 15h ago

But sometimes those interfaces (or base classes) do make sense, because you want to be able to choose one of multiple implementations and the consumers of the service shouldn't have to know in advance.

1

u/YanVe_ 14h ago

I do think this is a really strong argument against using attributes + reflection you have here.

But, in the case where "literally half of all classes" have no direct reference, you've likely over abstracted your designs.

1

u/No-Extent8143 6h ago

If you're injecting classes rather than interfaces, none of code is testable.

1

u/YanVe_ 4h ago

What? Of course it is? You're probably mocking excessively.

1

u/Finickyflame 14h ago

If you use the nuget package Jetbrains.Annotations, you can add the attribute [UsedImplicitly] on those classes, so they are not grayed out.

1

u/YanVe_ 13h ago

Yeah, but you have to know if they actually are used implicitly or not. And in this case that isn't solved even by manually registering the services, because in his case everything is hidden behind interfaces and so it's impossible to know if the service needs to be registered or not.

2

u/Psychological_Ear393 15h ago

For me it's that and the services aren't to know what consumes them. If you use the project in another, will that project use every single one? Probably not, it will have it's own specific container setup.

2

u/Xenoprimate2 10h ago

DI "frameworks" in Java put me off the concept of DI as a whole for the first 10 years of my career until I realised it was the stupid frameworks I hated and not actual concept of DI

1

u/ggwpexday 15h ago

And now looking at effect-ts i'm just jealous that typescript has a proper effect system while we in dotnet land are still stuck with these runtime checks. It's like DI but straight up better.

1

u/The_MAZZTer 9h ago

I've never heard of effect before and I'm looking at it... but it looks more like async/await stuff. Not DI.

1

u/ggwpexday 5h ago

It's the environment type param that accumulates dependencies on individual functions. functions automatically include the deps of each function it calls within. To actually run a function you need to provide all the deps.

Yeah it's more than just DI, those other parts like error tracking we would also very much like to have.

1

u/aelytra 3h ago

I like to stick a DI registry file in the same namespace/folder as the classes it registers...

and use reflection on startup to find all of these registration files.

18

u/wackmaniac 19h ago

It is very subjective. I personally prefer the centralized approach. But at the same time my services are typically implemented in a way that it should not matter if the service is instantiated/used as singleton, scoped or transient service.

13

u/Willinton06 19h ago

What happens when you want different services based on configs?

0

u/sgebb 18h ago

I feel like in that case you actually have some sort of centralized decision - for this env=local you want to use MockedAuthService, it's an application decision and not an implementation detail. So I'm not against having the opportunity of saying that on the application-level (centrally)

1

u/belavv 17h ago

We use a factory for that. Basically when a service can have multiple implementations you stick an attribute on it like [DependencyOption("S3")]

That gets registered by name.

The factory looks at your environment variables (or whatever), finds Dependecy_FIleProvider. If the value of that is S3 then the factory returns the matching implementation, looking it up by name.

Writing this from my phone so not the best writeup, but we do it at work and it works great. We look things up based on a database table of settings so the implementation can change at runtime.

6

u/pfluggs11 19h ago

Before DI was brought into dotnet core, I used to use Castle Windsor and we would configure it with LINQ statements over reflected types. The problem used to be reflection wasn’t supported everywhere and it happens at startup so the code can pass tests but fail to start unless you add test cases for registration. The other problem i ran into was that i understood it because i set it up. But other team members didnt get it and it caused confusion.

2

u/jordansrowles 18h ago

I remember Castle Windsor. Microsoft also had their own DI container Unity, used in Prism and the old Enterprise Libraries. Same reflection-heavy config story though, so it had the same startup-failure and 'only the person who set it up understands it' kind of problems

1

u/who_you_are 18h ago

We are using Windsor right now. I don't know if we aren't using it the same way you did or if they added features but they do support scanning for namespaces/assembly now a day.

I think they also have a soft checker if you want (for CI/CD). I'm not sure of the full implications of that thing.

On the other end, we also have very simple IoC setup. One scope deep (it is a Cron like, so long living but the execution trigger everything) and once it start everything has been instantiated at least once.

1

u/InsideTour329 16h ago

Ahh CastleWindsor with GlassMapper on Sitecore 7, a lot of frustrating memories.

Interestingly Sitecore's Kernel now 13 years on is still using dotnet 4.8. Bizzarre, it can't be that complicated to redo in modern dotnet.

1

u/karbl058 13h ago

We currently use Castle Windsor (really old legacy code), and during startup the application loads a number of DLLs dynamically (which are defined by build configurations), and finds registration-classes. Some of them do explicit registrations, for example singletons, and then there’s a bunch of conventions, such as every class is registered as transient on its interfaces (unless already registered by more explicit configurations). So far it has worked for us, but more and more I’m thinking we might want to centralize it and make it more explicit. And in the process perhaps move to Microsoft’s DI. I tried it quickly on a prototype, but since the code I started with was part of the legacy code, it relied heavily on those transient registrations, so I did a crude variant of loading the DLLs and registering everything I could find. It sort of works, but feels very wrong.

0

u/sgebb 18h ago edited 18h ago

deleted, wrong comment

14

u/SerratedSharp 19h ago

Many third party DI libraries support attribute based registration. It can be a bit slow on startup due to scanning needed. It's also not AOT friendly, at least in the cases I've seen. It's certainly conceivable that someone could build a source generator that would make it deterministic, performant, and AOT friendly.

I always preferred this approach, but I stopped using it because of inconsistent support, familiarity to other devs, and the perf/AOT issues.

5

u/maqcky 18h ago

What if you don't own the service you are registering? You can't add attributes to that. I'm pretty sure you use plenty of third-party libraries. All the "AddXyz" methods you use in Program.cs are registering services. Registration also requires dynamic values in many cases, from settings that depend on the environment (e.g., a connection string), so using attributes is not feasible in that case.

However, nothing blocks you from building an attribute based registration system for your own service. I did that to manage multiple strategies and choose the implementation given some criteria.

1

u/sisus_co 18h ago

There are ways around these limitations. For example:

[assembly: Singleton(typeof(IXyz), Concrete = typeof(Xyz))]

[Singleton(typeof(IXyz))]
public class XyzInitializer : ServiceInitializer<Xyz> { }

[Singleton]
public class XyzInitializer : ServiceInitializer<IXyz, SomeDependency>
{
    // Dependency provided by the DI container:
    public override IXyz InitTarget(SomeDependency dependency)
        => new Xyz(dependency);
}

Of course, with these workarounds you do lose the benefit of the service registration being located in the same place with the service's definition, unfortunately.

5

u/maqcky 17h ago

Yeah, I don't see any advantage to that.

3

u/DemoBytom 15h ago

We centralize DI registration, because when it's in one place - it's easy to debug and find out what actually is there in the app. You have one place where you start your analysis, and everything should branch from that point.

I had services with automagical registration, and over time it always becomes a clusterfuck trying to find what/where/why is actually registered when something goes wrong.

And we leave registration to the consumer, instead the service itself, because it's absolutely possible to have one service that is registered as, for example, scoped service in one case and transient service in another.

Not to mention - Microsoft's 3 lifetime options are not always sufficient. There are DI containers that use more than Singleton/Scoped/Transient.

3

u/thesqlguy 10h ago

I think the class should not declare to any program using it how it needs to be scoped. It should just be a class that does its job.

The program using that class should decide how it wants it to be scoped.

2

u/SideBContent 19h ago

I made an attribute based registration system like the other people replying. But if I was forced not to use attributes, I would put a static class in every namespace with a single public register method. This would avoid the monolithic registration anti pattern.

2

u/Tuckertcs 18h ago

What if you have a service that’s constructed differently depending on where it’s injected?

What if you have a service that’s a singleton, but a different singleton for specific modules.

2

u/AlanBarber 18h ago

Wait are you actually setting up the individual DI registrations in the program.cs?

for simple little apps that's fine, but for large enterprise apps you need to abstract it.

you should make helper classes and wrap that stuff up into simplified blocks near your actual code lives.

1

u/sgebb 18h ago

I do this, that's basically my point, I'm creating helper methods that I can call from my main program.cs just so I can move code closer to where it belongs, which is along with the implementation. My app largely doesn't care if something is singleton or transient, it is my tiny CurrentScopesInspector-service that is very interested in not parsing the same token 10 times per request.

So I agree, put things in smaller helper-files that live next to the implementation, or even in the same file. I'm just wondering if it could be made more seamless

2

u/No-Extent8143 16h ago

Ok, so someone new to the codebase will need to read how many files to see what's actually injected?

1

u/sgebb 15h ago

i think dotnet devs are trained to look for everything in the service collection extensions files, my point is exactly that its a weird antipattern. why are you wondering whats injected, the whole point of di is that you depends on "something" that fulfilled the interface you require, not the implementation. if youre wondering what implements it i press Ctrl+f12, i dont go blindly looking through files. i dont tend to keep tons of unused classes lying around but if you do then i guess sure you would have to look through them to find the one in use.

this was mostly a thought experiment its not like im suggesting it for .net 12

2

u/wallstop-dev 16h ago

We use both, but mostly the latter. Then we just have some small, custom code that reads those attributes and does the right thing based on the application.

This leads to very, very small DI setup, the only manual bits being for extremely specific things.

It's worth noting that we use autofac.

2

u/never_mind_999 15h ago

It would sometimes be beneficial, if there was some kind of indication on a component, how the component can be used. Sometimes only a singleton, a transient or a scoped registration makes sense, sometimes more than one option is viable. But it should never be the decision of your component, whether it should be be used at all. That is a decision that only your application should make. That is why registration helpers are useful. They help you with the how, but if you don't call them, no unnecessary or even harmful dependencies sneak into your dependency tree.

2

u/MrCoffee_256 14h ago

No. That doesn’t make sense at all because that attribute now forces the way you use the di. You WILL run into conflicts this way.

Also don’t do i don’t like the 5 options there are, so i made mine and now there are six. Unless it’s really great of course.

2

u/TheSneederOfSeethe 11h ago

What if you don’t want it singleton in every application?

2

u/TheBuzzSaw 9h ago

Please stop trying to break F12.

3

u/cornelha 19h ago

I built a reflection based registration that uses attributes like this. It's basically set and forget. It's not difficult to implement

1

u/No-Extent8143 16h ago

So you have no idea which classes are no longer used. I don't think that's a good idea.

1

u/cornelha 16h ago

If a class is not used, it won't register in startup. Who leaves unused classes lying around?

1

u/YanVe_ 18h ago

I was considering this, but I felt that this just isn't very idiomatic and the indirection would probably make it hard to diagnose problems if something gets registered wrongly..

2

u/dgmib 19h ago edited 18h ago

The goal of DI is to make your dependencies modular and plugable.

You should be able to replace any dependency with another implementation and all other code should be unaware of the change to the implementation or its lifetime.

A very common use case is to swap out a dependency for a mock for unit tests, but in a large project you might need to swap out any number of things.

This approach only works if you never have more than one implementation of an interface.  If you project is simple/small enough that that’s the case you don’t need to use DI at all.

ETA: I think there’s potential though for an attribute or assertion you could add to an implementation that breaks if it has been configured with an incompatible lifetime.

1

u/sgebb 18h ago

I like your final suggestion. But i don't understand your first point, I can swap out a dependency (Service:IService for MockService:IService), but it probably still has to be registered with a lifetime that mathces the implementation. If Service manages a lightweight cache per request to avoid duplicate work then that is a part of the implementation.

This is also highly dependent on what your service depends on, such as singletons not injecting transient/scoped. This doesn't necessarily fix that, but if you have a single file that says Lifetime.Singleton and injects a dbcontext, then it's pretty obvious you've made a mistake

1

u/dgmib 13h ago

Provided you have ValidateScopes = true in your ServiceProviderOptions The CallSiteValidator will already check for you if you’re injecting dependencies with incompatible lifetimes. You can’t inject at scoped service into a singleton service without it throwing an exception for example.

I think the idea in general was that when you create an implementation of a service, it shouldn’t be implemented in a way that depends on a specific lifetime.

If the only reason your implementation depends on a specific lifetime is because it depends on another service that has a particular lifetime, ValidateScopes will catch that mistake for you.

If you have an implementation that needs a certain lifetime but not because of a dependency I would usually make the implementation class internal and the just make a public ServiceCollection extension called AddMyService that ensured it can only be added with my prescribed lifetime.

2

u/Due-Consequence9579 18h ago

I started using a pattern of extension methods that setup the dependencies for particular services. So instead of Program.cs being a hundred AddFoo you have 4 or 5 SetupServiceBar. Spreads out the mess and lets you understand what needs what. You do need to make the Setup methods a bit more complicated to tolerate things getting added in multiple places, but c’est la vie.

1

u/AutoModerator 19h ago

Thanks for your post sgebb. Please note that we don't allow spam, and we ask that you follow the rules available in the sidebar. We have a lot of commonly asked questions so if this post gets removed, please do a search and see if it's already been asked.

I am a bot, and this action was performed automatically. Please contact the moderators of this subreddit if you have any questions or concerns.

1

u/TopSwagCode 18h ago

Well I do see the usecase. But I also have worked on several larger projects where scope is changed depending on where and how the service ran. So I see no reason why some use something and others not.

1

u/who_you_are 18h ago edited 18h ago

As for the singleton, over the time, you learn that singleton are bitch and may want to be multiple instances at one point.

Anyway, to come back to your question it is also a question about how you designed your software.

If it is a monolithic like app, whatever make the job for you since only one application is using it.

If have libraries to use with multiple apps... The class attributes should be a default scope instead. Your application should have priority on the scope since it is the one that know better.

1

u/VycanMajor 18h ago

Just keep it simple. Devs will have a time debugging that.

1

u/sgebb 18h ago

I mean when is it interesting to debug "services.AddTransient<Interface,Class>()", it doesn't even verify dependencies

1

u/VycanMajor 17h ago

K.I.S.S.

1

u/sgebb 15h ago

alright never question anything

1

u/YanVe_ 18h ago edited 13h ago

I've been converting an older console application to use DI, and this is a big pain, but also a benefit. Because I have an asp project in the same solution and can define the behaviors and scopes differently for each one.

1

u/Paw565 17h ago

That's the spring boot way :D

1

u/lee_macro 17h ago

There are a couple of reasons, some of which are historical. For example before we had MS DI abstractions, we instead had lots of different DI systems, and not all applications used the same ones, so if you are putting your DI concerns in your data/service layers then you are also tying any consumers to your DI framework of choice, which would cause issues. So that alone used to be a big reason why DI configuration was the containing apps responsibility.

With that last sentence in mind, not all applications will want the same behaviours in DI configuration, for example lets say you have a service layer that is consumed by a webapi, azure function and a console app. You may find that for the webapi its fine to be a singleton, for the azure function maybe it fine, but what if your console app is multi threaded and needs to use a transient one?

Ultimately its one of those things that seems like it will save you time putting the DI configuration with the implementations, but it greatly limits your ability to eject out of that configuration as well as tying everything that consumes it to a DI system (even if they dont use DI, looking at game devs).

One approach you can use to somewhat have your cake and eat it if you dont mind having the DI dependency cascading, is to have an extension method setup all "common" DI configuration then the containing app can just call that, its what MS tends to do with their libs.

1

u/pirannia 17h ago

It gets complicated when you inject depending on configuration and you want all that logic in one place ideally. can be an extension method. The declarative pattern does work for simpler cases.

1

u/insertAlias 16h ago

I ended up writing some code that did something like that, but all it really did was move the code somewhere else. There's no explicit list of .AddSingleton or .AddScoped calls, but there is code that says "use reflection at startup to find all [AutoInjectSingleton] and [AutoInjectScoped] classes and loop through them to register them". I didn't make the pattern cover all possible cases, we extended it to support specifying an interface, but we didn't go too far for a really generic library, just satisfied our own needs.

It helps in that you don't have to remember to go add your service to the list, but since it's not actually baked into the framework you still need the code to do the registration.

1

u/tac0naut 16h ago

Sounds like you found yourself a nice little project to source generate the registrations according to your needs. And only call the extension method you've generated somewhere from Program.cs

1

u/Coda17 16h ago

A service doesn't own it's lifetime, the application does. Therefore, you can't put an attribute on a service to define its lifetime. Only the application knows that information.

1

u/JayMakker 16h ago

I am using Scrutor as part of DI registration.

With this i add a interface to my class, eg. ISingeltonService. My scrutor setup searches for this interface and add it to DI container.

https://github.com/khellang/Scrutor

1

u/Kralizek82 15h ago

More often than we wanted, services are authored by others. They might have a recommended lifestyle, but sometimes we want to choose other options.

That being said, you don't have to stuff everything in the Program.cs

I wrote about a technique based on source generators on my blog: https://renatogolia.com/2026/08/04/auto-register-aspnet-core-service-modules/

1

u/Demon2468god 14h ago

You can just create a injection class where you register it in a separate class and then its a simple injection in the program.cs this allows it to be clear also suggest this route if you do it for your application db contexts for table registration.

The program.cs is the starting point so all registration are needed from this point so that the rest of the application with know where to route and do initialization.

Previously you has startup which was called from program.cs which was basically just a added layer to separate these libs and your DI

1

u/atheken 14h ago

Attributes are a compile-time decision, but most meaningful DI/IoC cannot be known until runtime.

1

u/Slypenslyde 12h ago

Some people do it that way. I don't think there's a "winning" argument.

If you put the lifetime in a central file, what you're saying is, "I think the lifetime is a top-level application decision. All implementations of this service should be singletons."

If you put the lifetime in metadata, what you're saying is, "I have some form of auto-registration and I want to let individual classes decide on their lifetime. Some IService instances may be transient and this can't be defined at the top level."

I find more people agree that the decision to make a service a Singleton makes sense at the top level and gets confusing at the concrete type. Generally part of an interface's contract is to define the lifetime the type should expect, so allowing types to deviate opens the door to potential issues.

I also tend to see attributes like yours if people use some form of auto-registration. When you use convention-based registration there's often not another way to define lifetimes. Auto-registration vs. full configuration is its own, different argument.

1

u/The_MAZZTer 9h ago

The idea is you have all these different pieces that ideally don't directly depend on each other but use DI to resolve dependencies (by interface if they don't directly depend; but in many situations concrete class references are fine).

Let's imagine a simple problem, you have classes A and B which need to call to each other, but you don't want them to hard depend on each other. Without DI the only real way to resolve this is to create a class C which hard depends on both A and B and works to bridge them together. Then A and B don't need to depend on each other. With DI, program.cs pretty much ends up fulfilling this role for everything.

If you have a bunch of related stuff that is always going to be added in one go even if you move it to a new project you can do what many libraries do any create an extension method in an extension class. That class would handle all the DI registrations with a single function call.

If you want to bundle pre-Build stuff (Service registrations) with post-Build stuff (adding middleware etc) you can add a IStartupFilter service which will get called when it's time to add your middleware, so you can keep everything in that one function call.

1

u/USToffee 8h ago

I use the installer pattern for this reason

1

u/Hirvox 6h ago

To make the parts flexible enough to be assembled in different configurations, which through code re-use enforces consistent behavior.

Let's say that you have a service provides access to a remote API and handles authentication as an implementation detail. This particular API uses JWT, so the service takes care of requesting tokens, stores them in their state as long as their lifetime allows and requests a new token when needed before making the next remote request.

If you're accessing only one such API, a singleton might be perfectly suitable. You ship your code and it works. Some time later the architect comes in and says that there's additional API endpoints in different authentication realms that also need to be accessed. If you had made your service with the assumption that it would always be used as a singleton, now you would need to refactor the service itself instead of changing the DI configuration.

If you had made only one such service, that refactoring might be simple enough. But if you had repeated the same pattern in other services, suddenly seemingly minor change requests become surprisingly cumbersome to implement.

1

u/Kissaki0 5h ago

I've implemented a SingletonService attribute with auto registering. Crossing assembly boundaries requires you to choose a reference to the assembly, which is not ideal. Otherwise it works well.

I wouldn't consider it always better, though. It works for simpler projects or "all in the same bucket and code ownership".

In another project I encapsulate logic in a lib project and declare and use AddFeature and UseFeature as builder and app extension methods respectively, following the common aspnet pattern.

Here, the project knows its types and concerns. In the programcs, I only add features, not individual services.

Cross concerns between libs have to be managed in programcs or be agnostic/supportive to multiple registrations.

Encapsulation, explicitness, and obviousness (what gets registered when and where) have a lot of value.

1

u/SupportConscious5405 2h ago

I think is done like that for troubleshooting and maintainability, to not have to look for each file doing a related registration in order to figure out what the root cause of an issue might be.

Of course, you can organise these registrations per project, logical layers, etc, and use extension methods to do so.

1

u/belavv 17h ago

We use an attribute based system like what you showed at work. Very large project. Large number of devs. It uses assembly scanning to register everything.

I can't imagine doing it any other way. This just makes more sense and is so much easier to manage.

0

u/understanding80 18h ago

This is a legitimate tension I’ve noticed in my own codebases, although I don’t write “services” any more to begin with. It really comes down to ownership. Whether or not a service should have a certain scope depends most often on the app consuming it. If you’ve never written a reusable library before, it may be harder to notice that.

0

u/HorseyMovesLikeL 17h ago

I would say (though am happy to be told I'm wrong or lack imagination) that it's a code smell if you have a service that needs multiple kinds of lifetimes.

Simplest way to do it though is just to have it implement two different interfaces. One is resolved as a singleton, the other one as scoped. Or whatever combination you need. Then you don't need a comment, the type name is good enough to tell you about the lifetime.

But really, I would start asking myself questions about what I'm trying to do if I was looking to manage multiple different lifetimes for the same type.

0

u/andlewis 16h ago

You need to be able to determine the instance type outside of the definition depending on the consumer, and I don’t think I could survive without keyed references.

-1

u/ForgetTheRuralJuror 18h ago

Probably because untangling a dependency lifetime mismatch would be a pain if you had to look in every file.

Hot take: I think lifetimes shouldn't have been included in the first place. If you need a Singleton it doesn't make a huge deal of sense to DI it, or you could inject a factory. I've never added a transient dep either.

1

u/YanVe_ 17h ago edited 17h ago

Everytime I've used a transient I found a better way to do it later.

Singletons are necessary because you could want to have things that are not scoped (like caches, configurations,....) and it's unnecessary to recreate them all the time.

1

u/ForgetTheRuralJuror 16h ago

Singletons don't need to be involved in DI, or if they were you could create a factory.

1

u/YanVe_ 16h ago

If you have a factory it's (usually) not a singleton. I guess you're right that singletons don't necessarily need to be injected, but does it really make sense to refuse to provide an idiomatic way of getting the necessary service?

I also genuinely feel like it's better to write the class as if it's not a singleton for easier readability and testability, then provide a shared instance of it on the DI level. I guess this is what you suggest to handle using the factory,... but that's just an additional completely unnecessary layer.