Why Dependency Injection Boilerplate Is a Problem
If you have worked on any non-trivial .NET application, you have almost certainly written dozens of lines that look like services.AddTransient<IFoo, Foo>(). For small projects this is manageable, but as a codebase grows the registration block becomes a maintenance burden. Every new class means another line, every refactored interface means hunting through startup code, and the risk of forgetting a registration grows with every pull request. Automatic dependency injection in C# is the idea that the container should discover and register your services for you, eliminating that boilerplate entirely.
This is where Needlr enters the picture. Needlr is an opinionated, fluent dependency injection library for .NET that scans your assemblies, finds your classes and interfaces, and registers them automatically. It supports both source generation for AOT-compatible scenarios and a reflection-based fallback, so you can choose the approach that fits your project. In this guide we will walk through the core concepts at a survey level: how the fluent API works, how auto-discovery registers your types, how to exclude types or add manual registrations, and how advanced features like decorators and keyed services fit in.
If you are new to the concept of dependency injection itself, you may want to start with a primer on what is inversion of control before continuing. For readers who already understand constructor injection and the role of a DI container, this article will show how Needlr removes the tedious parts so you can focus on the code that matters.
What Is Needlr?
Needlr is an open-source library published as a set of NuGet packages under the NexusLabs.Needlr namespace. Its core promise is simple: you describe how you want scanning to work through a fluent API, and it handles what gets registered. The main entry point is the Syringe class, which you instantiate and chain method calls on to configure your container.
At a high level, Needlr provides two scanning strategies. The first is source generation, where a Roslyn source generator runs at compile time to produce the registration code. This strategy is AOT-compatible and avoids runtime reflection. The second is a reflection-based scanner that inspects assemblies at startup. Both strategies share the same fluent API surface, so switching between them requires changing a single method call rather than rewriting your configuration.
The library also ships an auto-configuration bundle that combines the most common settings into a single call. For web applications there is a dedicated ASP.NET integration that produces a configured WebApplication directly. For console applications or other hosts, Needlr works directly with IServiceCollection in C#, the same abstraction that the built-in Microsoft container uses.
Key Features of Needlr
- Automatic Service Discovery: Scans assemblies and registers types based on conventions, eliminating manual registration boilerplate
- Source Generation First: Compile-time type discovery for AOT compatibility and optimal performance
- Reflection Fallback: Runtime discovery available for dynamic scenarios and plugin loading
- Fluent API: Chain-able configuration methods for clean, readable setup
- ASP.NET Core Integration: Seamless web application creation and configuration
- Plugin System: Extensible architecture for modular applications
- Decorator Support: Automatic decorator wiring with
[DecoratorFor]attribute - Keyed Services: Support for multiple implementations of the same interface (.NET 8+)
Setting Up Needlr With Source Generation
Source generation is the recommended approach for new projects. It produces registration code at compile time, which means no reflection overhead at startup and full compatibility with Native AOT. To get started, install the following NuGet packages:
NexusLabs.Needlr.Injection-- the core abstractionsNexusLabs.Needlr.Injection.SourceGen-- the Roslyn source generator
Once the packages are installed, the setup code is minimal. Here is a basic example for a console application:
using NexusLabs.Needlr;
// Create a new Syringe instance, which is the main entry point
var syringe = new Syringe();
// Configure the syringe to use source generation for scanning
IServiceCollection services = new ServiceCollection();
syringe
.UsingSourceGen() // Use compile-time source generation
.Scan(services); // Populate the IServiceCollection
var provider = services.BuildServiceProvider();
This snippet shows the minimal three-step pattern: create a Syringe, choose a scanning strategy, and call Scan to populate a service collection. The source generator inspects the assemblies referenced by your project and emits registration calls for every discovered type. Because this happens at compile time, the resulting code is identical to what you would have written by hand, but it is generated for you automatically.
If you are working with console applications and want to understand how IServiceCollection fits into non-web hosts, the article on how to use IServiceCollection in console applications covers the fundamentals in detail.
Setting Up Needlr With Reflection
Not every project can use source generation. You might be working with dynamically loaded plugins, targeting a runtime that does not support the source generator, or simply prototyping quickly. In those cases Needlr provides a reflection-based scanner that works at runtime. Install these packages instead:
NexusLabs.Needlr.InjectionNexusLabs.Needlr.Injection.Reflection
The code is almost identical to the source generation example:
using NexusLabs.Needlr;
var syringe = new Syringe();
IServiceCollection services = new ServiceCollection();
syringe
.UsingReflection() // Use runtime reflection for scanning
.Scan(services); // Populate the IServiceCollection
var provider = services.BuildServiceProvider();
The only difference is the call to .UsingReflection() instead of .UsingSourceGen(). The fluent API remains the same, and the auto-discovery behavior is identical. The trade-off is that reflection scanning happens at application startup rather than at compile time, which adds a small amount of initialization time. For most applications this is negligible, but it is worth knowing the distinction when you are evaluating performance-sensitive or AOT scenarios.
When to Choose Source Generation vs Reflection
Use Source Generation When:
- Building AOT-compiled applications
- Targeting trimmed/self-contained deployments
- You want faster startup times
- All plugins are known at compile time
Use Reflection When:
- Loading plugins dynamically at runtime
- Scanning assemblies not known at compile time
- Using Scrutor for advanced registration patterns
- Prototyping quickly without waiting for source generator tooling
If you want the simplest possible setup and do not need to choose between strategies, Needlr also provides a bundle package called NexusLabs.Needlr.Injection.Bundle. With this package installed you can call .UsingAutoConfiguration() to get a sensible default configuration in a single method call.
How Auto-Discovery Works
The core value proposition of Needlr is that you do not need to manually register most of your services. When Needlr scans your assemblies, it follows a straightforward convention: every concrete class is registered as itself, and if that class implements one or more interfaces, it is also registered against each of those interfaces.
Consider a typical service class and its interface:
// Define an interface for the service
public interface IEmailService
{
Task SendAsync(string to, string subject, string body);
}
// Implement the interface in a concrete class
public class EmailService : IEmailService
{
public async Task SendAsync(string to, string subject, string body)
{
// Send the email
await Task.CompletedTask;
}
}
With Needlr configured, you do not need to write services.AddTransient<IEmailService, EmailService>() anywhere. The scanner discovers EmailService, sees that it implements IEmailService, and registers both mappings automatically. Any class in your scanned assemblies that requests IEmailService through constructor injection will receive an instance of EmailService without any manual wiring.
This convention-based approach scales well. Whether you have ten services or ten thousand, the registration block stays the same size: zero lines of manual registration code. The convention is intentionally opinionated. Rather than requiring you to annotate every class with an attribute, Needlr assumes that if a class is public and concrete, it should be available for injection. This keeps the noise in your codebase low and lets you focus on writing the actual business logic.
Assembly scanning also supports filtering. You can tell Needlr which assemblies to include or exclude, which is helpful when you have third-party libraries in your dependency graph that you do not want scanned. This filtering happens through the same fluent API, keeping all configuration in one place.
Excluding Types With DoNotAutoRegister
Not every class should be registered in the container. Data transfer objects, entity classes, static helpers, and other types that are not services should be left out. Needlr provides the [DoNotAutoRegister] attribute for this purpose:
using NexusLabs.Needlr;
// This class will NOT be registered by Needlr's auto-discovery
[DoNotAutoRegister]
public class EmailMessage
{
public string To { get; set; }
public string Subject { get; set; }
public string Body { get; set; }
}
When the scanner encounters a type decorated with [DoNotAutoRegister], it skips that type entirely. This is a simple opt-out mechanism that keeps the default behavior permissive while giving you precise control where you need it.
The attribute approach is useful when you have a small number of types to exclude. If you find yourself applying [DoNotAutoRegister] to many types in a particular namespace or assembly, it may be more efficient to adjust your assembly scanning filters instead. Both approaches achieve the same result, and choosing between them is a matter of which feels cleaner for your particular codebase.
Manual Registration With IServiceCollectionPlugin
Auto-discovery handles the common case, but some registrations require manual control. You might need to register a type with a specific lifetime, provide a factory delegate, or register a third-party class that you cannot annotate. Needlr supports this through the IServiceCollectionPlugin interface.
When Manual Registration Is Needed
- Specific Service Lifetimes: Registering services as singleton or scoped when the default transient lifetime is not appropriate
- Factory Functions: Creating instances with complex initialization logic or conditional creation
- Third-Party Classes: Registering types from external libraries that you cannot modify or annotate
- Conditional Registration: Registering services based on configuration or environment variables
- Multiple Implementations: Registering several implementations of the same interface with different keys or contexts
A plugin is a class that implements IServiceCollectionPlugin and contains a method to register services directly against the IServiceCollection. Needlr discovers these plugins during scanning and invokes them as part of the registration process:
using NexusLabs.Needlr;
// A plugin for manual registrations that Needlr discovers automatically
public class InfrastructurePlugin : IServiceCollectionPlugin
{
public void Register(IServiceCollection services)
{
// Register with a specific lifetime
services.AddSingleton<ICacheProvider, RedisCacheProvider>();
// Register with a factory delegate
services.AddTransient<IDbConnection>(sp =>
{
var config = sp.GetRequiredService<IConfiguration>();
return new SqlConnection(config.GetConnectionString("Default"));
});
}
}
This plugin class is itself discovered by Needlr during scanning, so you do not need to call it manually. The scanner finds every class implementing IServiceCollectionPlugin, instantiates it, and invokes its Register method. This pattern keeps your manual registrations organized into cohesive units rather than scattered across a monolithic startup file.
If you have used a plugin-based architecture before, this concept will feel familiar. The article on plugin architecture design pattern explains the broader pattern and why organizing code into self-contained modules improves maintainability.
The Decorator Pattern and Keyed Services
Needlr includes support for the decorator pattern through the [DecoratorFor<T>] attribute. Decorators let you wrap an existing service with additional behavior, such as logging, caching, or validation, without modifying the original implementation. If you have worked with decorators in other DI libraries, the article on the decorator pattern in C# using Autofac provides a solid foundation for the concept.
With Needlr, you annotate a class with [DecoratorFor<T>] and the library handles the registration plumbing. The decorator receives the inner service through its constructor and delegates to it as needed. This is a powerful way to apply cross-cutting concerns without cluttering your core service implementations.
Common Decorator Use Cases
- Logging: Adding method entry/exit logging without modifying service implementations
- Caching: Wrapping services with caching logic to improve performance
- Validation: Adding input validation before service methods execute
- Retry Logic: Implementing automatic retry for transient failures
- Performance Monitoring: Tracking execution time and performance metrics
On .NET 8 and later, Needlr also supports keyed services. Keyed services allow you to register multiple implementations of the same interface and resolve them by a key at injection time. This is useful when you have strategies, providers, or handlers that share an interface but differ by context. Needlr integrates with the built-in keyed services API in Microsoft.Extensions.DependencyInjection, so the registrations it produces are fully compatible with the standard resolution mechanisms.
Web Application Integration
For ASP.NET Core applications, Needlr provides the NexusLabs.Needlr.AspNet package. This integration offers a streamlined builder that produces a configured WebApplication directly:
var app = new Syringe()
.UsingSourceGen()
.ForWebApplication() // Configure for ASP.NET Core
.BuildWebApplication(); // Returns a WebApplication instance
app.MapGet("/", () => "Hello World");
app.Run();
The ForWebApplication() method configures Needlr to work with the ASP.NET Core hosting model, and BuildWebApplication() returns the standard WebApplication that you use to map endpoints, add middleware, and start the server. This keeps the Needlr configuration contained to a few lines at the top of your Program.cs while the rest of your web app code remains unchanged.
Needlr also supports hosted services and BackgroundService registrations. If your application uses long-running background tasks, the scanner discovers types that derive from BackgroundService or implement IHostedService and registers them appropriately.
How Needlr Compares to Other Approaches
The .NET ecosystem has several options for reducing DI boilerplate. Libraries like Autofac offer module-based registration and advanced features like property injection. Scrutor provides assembly scanning on top of the built-in Microsoft container. If you are evaluating your options, the comparison article on Scrutor vs Autofac in C# is worth reading for context.
Needlr distinguishes itself in a few ways. First, its source-generation-first design means that the registration code is visible in your build output, making it easier to debug and fully compatible with AOT compilation. Second, the fluent API provides a single, chainable entry point rather than requiring you to learn a separate module system. Third, the convention-over-configuration approach means that most services require zero configuration at all.
That said, Needlr is opinionated by design. If you need fine-grained control over every registration or rely heavily on features specific to another container, it may not be the right fit. The library works best when you are comfortable with its conventions and want to minimize the amount of registration code you maintain.
For a broader view of design patterns that relate to dependency injection and modular architecture, the big list of design patterns provides an overview of the patterns that inform how DI containers are designed and used.
Choosing the Right NuGet Packages
Needlr is distributed as several NuGet packages, each serving a specific purpose. Here is a quick reference for which packages to install based on your scenario.
Package Selection Guide
Core Package (Required):
NexusLabs.Needlr.Injection-- Core abstractions and theSyringeentry point
Discovery Strategy (Choose One):
NexusLabs.Needlr.Injection.SourceGen-- Source generation for compile-time discovery (recommended)NexusLabs.Needlr.Injection.Reflection-- Reflection-based runtime discoveryNexusLabs.Needlr.Injection.Bundle-- Auto-configuration bundle with sensible defaults
Web Applications (Optional):
NexusLabs.Needlr.AspNet-- ASP.NET Core integration for web applications
For source-generation projects, install NexusLabs.Needlr.Injection and NexusLabs.Needlr.Injection.SourceGen. For reflection-based projects, swap the source gen package for NexusLabs.Needlr.Injection.Reflection. If you want a bundle that includes sensible defaults, install NexusLabs.Needlr.Injection.Bundle. For ASP.NET Core web applications, add NexusLabs.Needlr.AspNet on top of whichever scanning package you chose.
All packages are available on NuGet and the source code is on GitHub. The official documentation includes additional examples and configuration options beyond what this overview covers.
Frequently Asked Questions
What is automatic dependency injection in C#?
Automatic dependency injection in C# refers to the practice of using a library or framework to discover and register your services in the DI container without writing explicit registration code for each type. Instead of manually calling services.AddTransient<IFoo, Foo>() for every service, a scanner examines your assemblies and registers types based on conventions. Needlr is one such library, and it uses either source generation or reflection to perform this scanning. The result is less boilerplate, fewer missed registrations, and a startup configuration that stays small regardless of how many services your application has.
Does Needlr work with the built-in Microsoft DI container?
Yes. Needlr works directly with IServiceCollection, which is the standard service registration abstraction in Microsoft.Extensions.DependencyInjection. It does not replace the built-in container. Instead, it populates the same IServiceCollection that the Microsoft container uses, so all of your existing code that resolves services through IServiceProvider continues to work exactly as before. This also means that Needlr is compatible with any host that uses IServiceCollection, including ASP.NET Core, Generic Host, and custom hosts.
When should I use source generation versus reflection?
Use source generation when you want compile-time safety, AOT compatibility, and zero reflection overhead at startup. This is the recommended default for new projects. Use reflection when you are loading assemblies dynamically at runtime, working in an environment where the Roslyn source generator is not supported, or prototyping quickly and do not want to wait for source generator tooling to catch up. Both strategies produce the same registrations and share the same fluent API, so switching between them later is straightforward.
How do I register a service with a specific lifetime like singleton?
Needlr's auto-discovery registers services with a default lifetime. When you need a specific lifetime, such as singleton or scoped, use the IServiceCollectionPlugin interface to register that service manually. Inside the plugin's Register method you have full access to IServiceCollection and can call AddSingleton, AddScoped, or AddTransient as needed. You can also apply [DoNotAutoRegister] to the type if you want to prevent the auto-discovery from registering it with the default lifetime.
Can I use Needlr alongside other DI libraries like Autofac?
Needlr populates a standard IServiceCollection, so it can coexist with other libraries that also work with that abstraction. If you are migrating from dependency injection with Autofac and want to move incrementally, you could use Needlr for auto-discovery while keeping some Autofac modules for registrations that require Autofac-specific features. However, mixing containers adds complexity, so in most cases it is simpler to commit to one approach for the entire application.
What happens if two classes implement the same interface?
When Needlr discovers multiple implementations of the same interface, it registers all of them. The last registration wins when you resolve a single instance through IServiceProvider.GetRequiredService<T>(), following the standard Microsoft DI behavior. If you need all implementations, you can inject IEnumerable<T> to receive every registered instance. For cases where you need to select a specific implementation by key, .NET 8 keyed services provide a clean solution, and Needlr supports keyed registrations.
Is Needlr suitable for large enterprise applications?
Needlr is designed to scale with your codebase. Assembly scanning with filtering lets you control exactly which projects and namespaces are included. The IServiceCollectionPlugin pattern keeps manual registrations organized into focused modules. The [DoNotAutoRegister] attribute provides fine-grained exclusion. For large applications, the source generation approach is particularly valuable because registration errors surface at compile time rather than at runtime. The library handles thousands of types without meaningful performance impact, especially with the source generation strategy where all work is done during compilation.
Wrapping Up
Needlr addresses a real pain point in .NET development: the growing wall of services.Add... calls that every non-trivial application accumulates. By scanning assemblies and registering types automatically, it keeps your startup code minimal and your focus on business logic. The source generation strategy gives you compile-time safety and AOT compatibility, while the reflection fallback covers scenarios where runtime discovery is necessary.
The fluent API built around the Syringe class provides a clean, discoverable entry point. Features like [DoNotAutoRegister], IServiceCollectionPlugin, [DecoratorFor<T>], and keyed services give you escape hatches for the cases where conventions are not enough. And because everything ultimately populates a standard IServiceCollection, you are not locked into a proprietary container.
Whether you are starting a new project or looking to reduce boilerplate in an existing one, Needlr is worth evaluating as part of your dependency injection strategy.
