The Entry Point to Needlr's Configuration
Every Needlr-powered application begins with a single object: the Syringe class. If you have ever used the builder pattern in C# to construct a complex object step by step, the Syringe class will feel immediately familiar. It is the central configuration hub that lets you chain method calls together to describe how your dependency injection container should be assembled, which discovery strategy to use, how types should be filtered and registered, and what post-build behavior you want applied.
Rather than scattering configuration across multiple files or relying on a monolithic startup block, Needlr consolidates everything behind a fluent API rooted in new Syringe(). This article takes a deep dive into the Syringe class itself: its builder methods, the method chaining model, configuration options for discovery and registration, and the specialized WebApplicationSyringe for ASP.NET Core projects. If you are looking for hello-world setup instructions, the introductory guide covers that ground. This article assumes you already have Needlr installed and want to understand the full surface area of its configuration API.
Before we begin, it helps to understand why a builder-style entry point matters for dependency injection configuration. When you register services manually against IServiceCollection in C#, you write imperative code that calls AddTransient, AddSingleton, and AddScoped in sequence. There is no structure enforcing which decisions come first or how they relate to each other. The Syringe class imposes that structure. Each method call returns an object that exposes only the methods valid for the next step, guiding you through configuration in a logical order and catching mistakes at compile time rather than runtime.
Anatomy of the Fluent Chain
The Syringe class uses method chaining to walk you through a series of decisions. At each step, the return type changes to expose only the methods appropriate for that phase of configuration. This is a deliberate design choice that prevents you from calling methods out of order or skipping required steps.
Here is a comprehensive example that touches every major configuration point in a single fluent chain:
using NexusLabs.Needlr;
var provider = new Syringe()
.UsingReflection() // Step 1: Choose discovery strategy
.UsingAdditionalAssemblies([ // Step 2: Configure assembly scanning
typeof(Program).Assembly,
typeof(IMyService).Assembly])
.UsingPreRegistrationCallback(services => // Step 3: Configure services before auto-registration
{
// Add manual registrations or configure services
services.AddSingleton<IConfiguration>(/* ... */);
})
.UsingPostPluginRegistrationCallback(services => // Step 4: Add post-build behavior
{
// Validate or configure after all plugins run
// This runs after automatic discovery and plugin registration
})
.BuildServiceProvider(); // Step 5: Build the service provider
Each method in this chain serves a distinct purpose. The call to UsingReflection() selects the discovery strategy. The UsingAdditionalAssemblies call determines which assemblies Needlr scans beyond the default. The UsingPreRegistrationCallback allows you to add manual registrations or configure services before Needlr's automatic discovery runs. The UsingPostPluginRegistrationCallback hooks in behavior that runs after all automatic discovery and plugin registration completes. Finally, BuildServiceProvider() executes the entire pipeline and returns a configured service provider.
Configuration Steps in Order
- Discovery Strategy: Choose
.UsingSourceGen()or.UsingReflection() - Additional Assemblies: Configure which assemblies to scan beyond defaults (optional)
- Pre-Registration Callback: Add manual registrations before auto-discovery (optional)
- Post-Plugin Registration Callback: Add behavior after all registration completes (optional)
- Build Service Provider: Execute discovery and build the service provider
This structure mirrors how the builder pattern in C# is typically implemented: each method mutates or extends internal state and returns either the same builder or a new builder with a narrower interface. The result is code that reads like a sentence describing what you want, rather than a sequence of disconnected imperative statements.
Discovery Strategies
The first decision in any Syringe configuration is which discovery strategy to use. Needlr provides three options, each selected by a single method call on the Syringe instance.
Calling .UsingSourceGen() selects the source generation strategy. A Roslyn source generator runs at compile time and emits the registration code directly into your assembly. This means zero reflection at runtime, full AOT compatibility, and compile-time visibility into exactly what gets registered. This is the recommended strategy for new projects.
Calling .UsingReflection() selects the reflection-based strategy. Needlr inspects your assemblies at application startup, using runtime reflection to discover types and their interfaces. This adds a small amount of initialization time but works in every .NET runtime environment, including scenarios where assemblies are loaded dynamically.
Discovery Strategy Comparison
Source Generation (.UsingSourceGen()):
- Compile-time code generation
- Zero runtime reflection overhead
- AOT and trimming compatible
- Recommended for new projects
Reflection (.UsingReflection()):
- Runtime type inspection
- Works with dynamic assemblies
- Compatible with all .NET runtimes
- Required for plugin loading scenarios
Calling .UsingAutoConfiguration() selects a bundled configuration that combines a discovery strategy with sensible defaults for assembly scanning and type filtering. This is the quickest way to get a working configuration when you do not need to customize individual steps. Under the hood, auto-configuration selects either source generation or reflection depending on which packages you have installed, and applies default assembly providers and type filters.
Each of these methods returns a new object whose type exposes the next phase of configuration. After choosing a strategy, you can configure additional assemblies and use callbacks to customize registration. If you skip the optional steps and call BuildServiceProvider() directly, Needlr applies reasonable defaults for each.
Additional Assembly Configuration
After selecting a discovery strategy, you can control which assemblies Needlr scans. By default, Needlr scans the assemblies it considers relevant based on your project references. For many applications this default is sufficient, but larger solutions with shared libraries, third-party packages, or plugin architectures may need explicit control.
The UsingAdditionalAssemblies method accepts an array of assemblies to include in the scan:
var provider = new Syringe()
.UsingSourceGen()
.UsingAdditionalAssemblies([
typeof(Program).Assembly, // Include the assembly containing your entry point
typeof(IOrderService).Assembly, // Include a specific referenced assembly by type
typeof(ExternalLib.SomeClass).Assembly // Include an assembly by direct reference
])
.BuildServiceProvider();
Using typeof(T).Assembly is the most common way to reference an assembly. You provide any type from the assembly you want included, and Needlr resolves the assembly at scan time. This approach is refactoring-friendly: if you move the type to a different namespace, the assembly reference still works because it is tied to the type itself rather than a string.
Additional assembly configuration is especially important when your solution contains class libraries that define service interfaces and implementations in separate projects. Without explicit assembly configuration, Needlr might scan only the entry-point assembly and miss implementations defined elsewhere. By adding each relevant assembly, you ensure that the scanner sees every type that should be registered.
If you are working with a modular architecture where assemblies are loaded at runtime, the reflection strategy combined with explicit assembly configuration gives you full control over what enters the container. This is the same kind of concern that arises in inversion of control discussions: the framework needs to know where to look for implementations, and the additional assemblies configuration is how you tell it.
Pre-Registration Callbacks
Needlr uses built-in conventions to automatically register discovered types. Each concrete class is registered as itself and against every interface it implements, using a singleton lifetime by default. You can override the default lifetime using the [Transient] or [Scoped] attributes on your classes.
If you need to add manual registrations or configure services before Needlr's automatic discovery runs, use the UsingPreRegistrationCallback method:
var provider = new Syringe()
.UsingReflection()
.UsingPreRegistrationCallback(services =>
{
// Add manual registrations before auto-discovery
services.AddSingleton<IConfiguration>(sp =>
new ConfigurationBuilder()
.AddJsonFile("appsettings.json")
.Build());
// Add framework services
services.AddLogging();
})
.BuildServiceProvider();
The pre-registration callback runs before Needlr's automatic discovery, so any services you register here are available when Needlr scans and registers your types. This is useful for adding configuration, logging, or other framework services that your discovered types might depend on.
If you need more granular control over how types are registered, you can use the [DoNotAutoRegister] attribute on specific classes and register them manually in the pre-registration callback with custom configuration.
WebApplicationSyringe for ASP.NET Core
Web applications have additional configuration needs beyond service registration. They need middleware, endpoint routing, static file handling, and host configuration. Needlr addresses this with the WebApplicationSyringe, a specialized builder returned by calling .ForWebApplication() on a configured Syringe.
The WebApplicationSyringe wraps the standard ASP.NET Core WebApplicationBuilder and exposes methods for configuring the web host alongside the DI container. When you call .BuildWebApplication(), it returns a fully configured WebApplication that you can use exactly like one created through the standard WebApplication.CreateBuilder() API.
using NexusLabs.Needlr;
var app = new Syringe()
.UsingSourceGen()
.ForWebApplication()
.UsingPreRegistrationCallback(services =>
{
// Configure framework services
services.AddControllers();
services.AddEndpointsApiExplorer();
services.AddSwaggerGen();
})
.BuildWebApplication();
// Standard ASP.NET Core middleware pipeline
app.UseSwagger();
app.UseSwaggerUI();
app.UseHttpsRedirection();
app.UseAuthorization();
app.MapControllers();
app.Run();
The UsingPreRegistrationCallback method allows you to add framework services like controllers, Swagger, authentication, or anything else that the standard builder supports. Needlr handles the auto-discovery registration, and you handle the framework-specific registrations through the callback.
This separation is important. Needlr's auto-discovery is excellent at finding and registering your application's business services, but framework services like AddControllers() or AddAuthentication() require explicit calls with specific configuration. The pre-registration callback gives you a clean place to make those calls without losing the benefits of automatic registration for your own types.
After BuildWebApplication() returns, you have a standard WebApplication instance. The middleware pipeline, endpoint mapping, and server startup all follow the normal ASP.NET Core patterns. Nothing about Needlr changes how you write middleware or define routes. The library's scope is limited to service registration, and the ForWebApplication() method is simply a convenience that integrates that registration into the web host builder lifecycle.
Post-Plugin Registration Callbacks
After Needlr's automatic discovery and any plugin registrations complete, you sometimes need to run additional logic. Debugging a missing registration, writing diagnostic endpoints, or building admin dashboards that show the application's service graph are all scenarios where you might need to inspect or modify the final service collection.
Needlr provides the UsingPostPluginRegistrationCallback method that runs after all automatic discovery and plugin registration completes:
var provider = new Syringe()
.UsingReflection()
.UsingPostPluginRegistrationCallback(services =>
{
// Verify that critical services are registered
var hasEmailService = services.Any(sd =>
sd.ServiceType == typeof(IEmailService));
if (!hasEmailService)
{
throw new InvalidOperationException(
"IEmailService must be registered. " +
"Ensure the service class is in a scanned assembly.");
}
// Log the total number of registrations
Console.WriteLine($"Total services registered: {services.Count}");
})
.BuildServiceProvider();
The post-plugin registration callback is particularly useful during development and testing. If a service fails to resolve at runtime, the first troubleshooting step is often to check whether it was registered at all. You can iterate over the IServiceCollection and inspect ServiceDescriptor objects to understand what was registered.
The callback does not affect the registration process itself. It runs after all registration is complete, so you can use it for validation, diagnostics, or logging without any impact on how your services are resolved at runtime.
Post-Plugin Registration Callbacks
The Syringe pipeline follows a defined order: choose a strategy, configure additional assemblies, run pre-registration callbacks, execute automatic discovery, run plugin registrations, and finally run post-plugin registration callbacks. The post-plugin registration callback provides a hook to run logic after all registration is complete.
Common uses for post-plugin registration callbacks include validating that certain critical services are present, logging the registration summary, or applying cross-cutting decorators that depend on the full set of registered types being known.
using NexusLabs.Needlr;
var provider = new Syringe()
.UsingSourceGen()
.UsingPostPluginRegistrationCallback(services =>
{
// Verify that critical services are registered
var hasAuth = services.Any(sd =>
sd.ServiceType == typeof(IAuthenticationService));
if (!hasAuth)
{
throw new InvalidOperationException(
"IAuthenticationService must be registered. " +
"Ensure the auth assembly is included in the scan.");
}
// Log the total number of registrations
Console.WriteLine($"Total services registered: {services.Count}");
})
.BuildServiceProvider();
The UsingPostPluginRegistrationCallback method accepts a delegate that receives the IServiceCollection after all automatic discovery and plugin registration completes. This means the callback sees the complete set of registrations and can make decisions based on the full picture.
Post-plugin registration callbacks are distinct from IServiceCollectionPlugin classes that are discovered during scanning. Discovered plugins run as part of the registration phase and are intended for adding manual registrations. Post-plugin registration callbacks run after all registration is complete and are intended for validation, diagnostics, or transformation of the completed registration set. Keeping these two concepts separate makes the pipeline easier to reason about: plugins add services, callbacks verify or modify the result.
Putting It All Together
To illustrate how the pieces connect, here is a complete Program.cs for a console application that uses every major configuration option on the Syringe class:
using NexusLabs.Needlr;
var provider = new Syringe()
.UsingReflection()
.UsingAdditionalAssemblies([
typeof(Program).Assembly,
typeof(MyApp.Services.IOrderService).Assembly
])
.UsingPreRegistrationCallback(services =>
{
// Add manual registrations before auto-discovery
services.AddSingleton<IConfiguration>(/* ... */);
})
.UsingPostPluginRegistrationCallback(services =>
{
// Validate registrations after all plugins run
var serviceCount = services.Count;
Console.WriteLine($"Registered {serviceCount} services.");
// Verify critical services are present
if (!services.Any(sd => sd.ServiceType == typeof(IOrderService)))
{
throw new InvalidOperationException("IOrderService must be registered.");
}
})
.BuildServiceProvider();
var orderService = provider.GetRequiredService<IOrderService>();
orderService.ProcessPendingOrders();
This example starts with a new Syringe(), selects the reflection strategy, configures which additional assemblies to scan, adds manual registrations in the pre-registration callback, validates the final service collection in the post-plugin registration callback, and builds the service provider. The entire DI configuration is a single fluent statement that reads top to bottom.
Compare this to a traditional approach where you might have dozens of services.AddSingleton<>() calls, a separate method for configuring each module, and scattered logic for validation. The Syringe class consolidates all of that into a single, structured pipeline. If you are familiar with how other builder patterns work in .NET, the article on the builder pattern in C# explains the underlying principles that make this design effective.
Frequently Asked Questions
What is the Syringe class in Needlr?
The Syringe class is the main entry point for configuring Needlr's dependency injection pipeline. You create an instance with new Syringe() and chain method calls on it to select a discovery strategy, configure additional assemblies, set up pre-registration callbacks, and add post-plugin registration callbacks. It follows the builder pattern, where each method returns an object that exposes the next valid set of configuration options. The final call in the chain, typically BuildServiceProvider() or BuildWebApplication(), executes the pipeline and returns a configured service provider or web application.
How does method chaining work in the Syringe API?
Each method on the Syringe class returns either the same builder instance or a new object with a narrower interface. This means you can write the entire configuration as a single chained expression. The return types are designed so that your IDE's IntelliSense guides you through the valid options at each step. If you try to call a method out of order, the compiler will catch it because the method simply will not exist on the current return type. This pattern is common in fluent APIs and is the same technique described in articles about the builder pattern.
Can I use WebApplicationSyringe for non-web projects?
No. The ForWebApplication() method is specifically designed for ASP.NET Core applications. It produces a WebApplication instance, which is part of the ASP.NET Core hosting model. For console applications, background services, or other non-web hosts, use the standard Syringe pipeline with BuildServiceProvider() to get a configured service provider directly.
How does Needlr register discovered types?
Needlr uses built-in conventions to automatically register discovered types. Each concrete class is registered as itself and against its implemented interfaces with a singleton lifetime by default. You can override the default lifetime using the [Transient] or [Scoped] attributes on your classes. If you need more control, you can use the [DoNotAutoRegister] attribute to exclude specific types from automatic discovery and register them manually in a pre-registration callback.
How do post-plugin registration callbacks differ from IServiceCollectionPlugin?
The two mechanisms serve different purposes and run at different times. An IServiceCollectionPlugin is discovered during scanning and runs as part of the registration phase. Its job is to add manual registrations to the service collection, such as factory delegates or third-party types that cannot be auto-discovered. A post-plugin registration callback runs after all automatic discovery and plugin registration is complete and sees the finished service collection. Its job is to validate, log, or transform the final set of registrations. Keeping them separate ensures that manual registrations are in place before any validation logic runs.
Can I inspect the service collection in production code?
Yes, but use it judiciously. You can inspect the IServiceCollection in post-plugin registration callbacks to see what was registered. This is most useful for diagnostics: health-check endpoints that verify critical services are present, admin dashboards that display the service graph, or startup validation that fails fast when expected registrations are missing. Avoid using the service collection as a service locator in application logic. Resolving services should always happen through constructor injection or IServiceProvider, not by querying the service collection at runtime.
Does the Syringe class support mixing discovery strategies?
Each Syringe instance uses a single discovery strategy. You select it with .UsingSourceGen(), .UsingReflection(), or .UsingAutoConfiguration(), and that choice applies to the entire build. If you need to combine strategies, for example scanning some assemblies with reflection and using source generation for others, you can create multiple Syringe instances and use their pre-registration callbacks to populate a shared IServiceCollection. Because IServiceCollection is just a list of service descriptors, multiple builds can contribute to it without conflict.
Wrapping Up
The Syringe class is the backbone of Needlr's configuration model. By structuring the API as a fluent builder, Needlr makes the configuration process discoverable, type-safe, and readable. Each step in the chain represents a clear decision: which strategy discovers your types, which assemblies are scanned, which types are filtered, how registrations are mapped, and what happens after the scan completes.
Understanding each configuration point gives you the ability to tailor Needlr's behavior to your project's specific needs. The default settings work well for straightforward applications, but the assembly providers, type filters, registrar options, and plugin hooks give you precise control when your architecture demands it. The WebApplicationSyringe extends this model cleanly into ASP.NET Core without requiring you to learn a separate API.
For more details and additional examples, the Needlr documentation and the source code on GitHub are the best next steps. If you are exploring the broader landscape of design patterns that inform how builder APIs and dependency injection containers are designed, the big list of design patterns provides a comprehensive reference to build on.
