Dependency Injection (DI) has become a fundamental pattern in modern application development, and .NET has had excellent support for DI since .NET Core. With .NET 9, Microsoft has introduced several performance enhancements, API improvements, and developer experience features that make it even easier to work with the built-in DI container.
In this article, we’ll go through what’s actually new in Dependency Injection for .NET 9, why you should care about these improvements, and how to use them in real-world applications. We'll also cover best practices and share examples to help you get the most out of these features.
Dependency Injection in .NET: A Quick Recap
Dependency Injection is a design pattern used to decouple dependencies from your classes. Instead of manually creating dependencies (services, objects, etc.) inside a class, DI allows you to inject those dependencies through constructors, properties, or methods. This results in code that is easier to test, maintain, and modify over time.
In previous versions of .NET, you might already be familiar with core DI features such as:
- Scoped, Transient, and Singleton service lifetimes.
- Service registration using
AddSingleton
,AddScoped
, orAddTransient
. - Constructor injection for providing dependencies.
- Open Generics support for services.
What’s Actually New in .NET 9’s DI?
With .NET 9, there are some targeted improvements to the built-in DI container. While the core concepts of DI haven’t changed much, performance optimizations and new API features have been introduced to make DI more efficient and easier to use.
1. Improved TryAdd
Methods for Service Registration
The TryAdd
pattern is a useful way to register a service only if one hasn’t already been registered. In previous versions of .NET, developers could use TryAddSingleton
, TryAddScoped
, and TryAddTransient
, but it wasn’t always clear whether these methods were the most optimal way to add conditional services.
.NET 9 improves upon this by streamlining the process of checking for existing registrations and only adding services when necessary. Additionally, there’s better support for multiple registrations with fewer side effects.
Example:
csharp services.TryAddSingleton<IMyService, MyService>();
If IMyService
hasn’t already been registered, it will be added as a singleton. This prevents overwriting existing registrations and ensures better control over service lifetimes.
2. Improved Diagnostics for DI
.NET 9 introduces better logging and diagnostic tools for debugging DI issues. In large applications, misconfigured services can be hard to track, and with new diagnostic features, you can more easily troubleshoot and monitor DI-related issues.
For example, when registering services, debug logging can now give you more insightful information about the DI container's behavior:
- Which services are being resolved.
- How long each service resolution takes.
- Whether there are cyclic dependencies.
This information is essential for tracking down issues in complex systems, especially during production debugging.
3. Improved Source-Generated Dependency Injection
.NET 9 introduces source generation to make DI faster and more efficient at compile time. This feature reduces the runtime overhead of resolving dependencies by using source generators to create DI code during compilation, which eliminates the need for reflection at runtime. This is a major performance booster for applications that rely heavily on DI.
Source generation was first introduced for other areas in earlier .NET versions, but with .NET 9, the source-generated DI container can generate optimized code to handle service resolutions faster. This is particularly helpful in scenarios where performance is critical, such as high-load APIs or real-time applications.
Example of how to enable source generation:
csharp // Add this in your csproj file <PropertyGroup> <UseSourceGenerators>true</UseSourceGenerators> </PropertyGroup>
Once source generation is enabled, the compiler will generate the necessary DI boilerplate code, allowing you to get more efficient runtime behavior.
4. Native Support for AOT (Ahead-of-Time) Compilation
.NET 9’s DI container is now optimized to work more efficiently with AOT. AOT compilation enables your application to be compiled directly into machine code before it runs, offering significant performance improvements.
This is particularly relevant for scenarios like Blazor WebAssembly or mobile applications, where runtime performance and startup times are critical. By making the DI container AOT-friendly, .NET 9 makes it easier to use DI in these contexts without sacrificing performance.
5. Improved Support for Minimal APIs
In .NET 9, the DI container integrates even more seamlessly with Minimal APIs, a feature introduced in .NET 6 that simplifies the way developers build HTTP APIs. With better DI integration, you can now inject services into Minimal API handlers more easily.
Example of DI in a Minimal API:
csharp var app = WebApplication.CreateBuilder(args).Build(); app.MapGet("/hello", (IMyService myService) => myService.GetGreeting()); app.Run();
Minimal APIs now allow for constructor-less injection in handlers, reducing boilerplate code and making your APIs cleaner and easier to maintain.
Why You Should Start Using These Features
1. Performance Gains
Source generation and AOT support provide significant performance improvements, especially in large-scale applications where DI is heavily used. By reducing the overhead of service resolution and optimizing service lifetimes, your applications can start up faster and run more efficiently.
2. Better Debugging and Diagnostics
The improved diagnostics for DI in .NET 9 give you more insight into how your services are being resolved. This reduces the time spent debugging dependency issues, making it easier to maintain large applications.
3. Improved Developer Experience
With better tooling, simpler service registration, and minimal boilerplate code, .NET 9 makes DI easier to work with. Whether you’re building APIs, web apps, or mobile applications, the new features streamline the development process and make your code cleaner.
Best Practices for Dependency Injection in .NET 9
- Leverage Source Generation: Enable source generation to take advantage of faster service resolution.
- Use TryAdd for Conditional Registration: Use
TryAdd
methods to avoid overwriting existing service registrations. - Avoid Service Locators: Let the DI container handle service resolution automatically, rather than resolving services manually using
GetService
. - Monitor with DI Diagnostics: Use the improved diagnostics to monitor and troubleshoot service resolutions in production.