Controlling What Gets Discovered
By default, Needlr scans all assemblies referenced by your application and discovers every concrete type it finds. For small applications, this works perfectly. But as your codebase grows, you may want more control: scanning only specific assemblies, excluding certain namespaces, or filtering types based on custom criteria. Assembly scanning in Needlr provides fine-grained control over what gets discovered and registered, allowing you to organize your dependency injection configuration precisely.
Needlr uses assembly providers to control which assemblies are scanned and type filters to control which types within those assemblies are registered. This article explains how to configure assembly scanning, how to filter types, and how to organize discovery in large applications. If you are new to automatic service discovery, the guide on automatic service discovery in C# with Needlr covers the basics.
This article focuses specifically on controlling and organizing assembly scanning. Topics like the Syringe API, source generation vs reflection, and automatic discovery conventions are covered in other articles. Here we are concerned with fine-tuning what gets discovered.
Understanding Assembly Providers
Assembly providers control which assemblies Needlr scans for types. By default, Needlr scans all assemblies referenced by your application, but you can configure it to scan only specific assemblies or filter assemblies based on naming patterns.
Here is a basic example that shows default scanning:
using NexusLabs.Needlr.Injection;
using NexusLabs.Needlr.Injection.SourceGen;
// Scans all referenced assemblies by default
var serviceProvider = new Syringe()
.UsingSourceGen()
.BuildServiceProvider();
To control which assemblies are scanned, use an assembly provider:
using NexusLabs.Needlr.Injection;
using NexusLabs.Needlr.Injection.SourceGen;
var serviceProvider = new Syringe()
.UsingSourceGen()
.UsingAssemblyProvider(builder => builder
.MatchingAssemblies(x => x.Contains("MyApp"))
.Build())
.BuildServiceProvider();
The MatchingAssemblies method filters assemblies based on a predicate. In this example, only assemblies whose name contains "MyApp" are scanned.
Filtering Assemblies by Name
The most common way to control assembly scanning is by filtering assemblies based on their names. This is useful when you have multiple projects in a solution and want to scan only specific ones.
Common Filtering Strategies
- By Prefix: Scan only assemblies starting with a specific prefix (e.g., "MyApp")
- By Pattern: Include assemblies matching multiple patterns (e.g., "MyApp" OR "Shared")
- By Exclusion: Exclude specific assemblies (e.g., exclude "Tests" and "Mocks")
- By Namespace: Filter types within assemblies by namespace patterns
- By Type Name: Exclude types with specific naming patterns (e.g., "Internal", "Helper")
Here is an example that shows various filtering strategies:
using NexusLabs.Needlr.Injection;
using NexusLabs.Needlr.Injection.SourceGen;
// Scan only assemblies that start with "MyApp"
var serviceProvider = new Syringe()
.UsingSourceGen()
.UsingAssemblyProvider(builder => builder
.MatchingAssemblies(x => x.StartsWith("MyApp"))
.Build())
.BuildServiceProvider();
// Scan assemblies that match multiple patterns
var serviceProvider2 = new Syringe()
.UsingSourceGen()
.UsingAssemblyProvider(builder => builder
.MatchingAssemblies(x =>
x.Contains("MyApp") ||
x.Contains("Shared") ||
x.Contains("Infrastructure"))
.Build())
.BuildServiceProvider();
// Exclude specific assemblies
var serviceProvider3 = new Syringe()
.UsingSourceGen()
.UsingAssemblyProvider(builder => builder
.MatchingAssemblies(x =>
x.Contains("MyApp") &&
!x.Contains("Tests") &&
!x.Contains("Mocks"))
.Build())
.BuildServiceProvider();
These examples show different ways to filter assemblies: by prefix, by multiple patterns, or by exclusion patterns. Choose the approach that best fits your project structure.
Including Additional Assemblies
Sometimes you need to scan assemblies that are not directly referenced by your application. This can happen with dynamically loaded plugins or assemblies loaded at runtime.
Here is an example that shows how to include additional assemblies:
using NexusLabs.Needlr.Injection;
using NexusLabs.Needlr.Injection.Reflection; // Reflection required for dynamic loading
using System.Reflection;
// Load assemblies dynamically
var pluginAssembly = Assembly.LoadFrom("path/to/plugin.dll");
var serviceProvider = new Syringe()
.UsingReflection() // Must use reflection for dynamic assemblies
.UsingAdditionalAssemblies([pluginAssembly])
.BuildServiceProvider();
The UsingAdditionalAssemblies method allows you to include assemblies that are not part of the default scanning set. Note that this requires using reflection-based discovery, as source generation cannot scan assemblies that are not known at compile time.
Filtering Types Within Assemblies
Once you have selected which assemblies to scan, you can further control which types within those assemblies get registered. Needlr provides type filters for this purpose.
Here is an example that shows type filtering:
using NexusLabs.Needlr.Injection;
using NexusLabs.Needlr.Injection.SourceGen;
var serviceProvider = new Syringe()
.UsingSourceGen()
.UsingTypeFilter(type =>
{
// Only register types from specific namespaces
return type.Namespace?.StartsWith("MyApp.Services") == true;
})
.BuildServiceProvider();
The UsingTypeFilter method allows you to provide a predicate that determines whether a type should be registered. Types that do not match the filter are skipped during discovery.
Combining Assembly and Type Filtering
You can combine assembly filtering with type filtering to achieve precise control over what gets discovered:
using NexusLabs.Needlr.Injection;
using NexusLabs.Needlr.Injection.SourceGen;
var serviceProvider = new Syringe()
.UsingSourceGen()
.UsingAssemblyProvider(builder => builder
.MatchingAssemblies(x => x.Contains("MyApp"))
.Build())
.UsingTypeFilter(type =>
{
// Only register types that:
// 1. Are in the Services namespace
// 2. Are not abstract
// 3. Have a public constructor
return type.Namespace?.StartsWith("MyApp.Services") == true &&
!type.IsAbstract &&
type.GetConstructors().Any(c => c.IsPublic);
})
.BuildServiceProvider();
This example shows how to combine multiple filtering criteria to precisely control discovery. The assembly provider filters which assemblies are scanned, and the type filter further refines which types within those assemblies are registered.
Organizing Discovery by Namespace
A common pattern is to organize services by namespace and scan only specific namespaces. This helps maintain clear boundaries between different parts of your application.
Here is an example that demonstrates namespace-based organization:
using NexusLabs.Needlr.Injection;
using NexusLabs.Needlr.Injection.SourceGen;
// Scan only services from specific namespaces
var serviceProvider = new Syringe()
.UsingSourceGen()
.UsingTypeFilter(type =>
{
var namespace = type.Namespace ?? "";
return namespace.StartsWith("MyApp.Services") ||
namespace.StartsWith("MyApp.Repositories") ||
namespace.StartsWith("MyApp.Infrastructure");
})
.BuildServiceProvider();
This approach allows you to clearly define which parts of your application participate in automatic discovery, making it easier to understand and maintain the dependency injection configuration.
Excluding Types with DoNotAutoRegister
The [DoNotAutoRegister] attribute provides a way to exclude specific types from automatic discovery, even if they would otherwise match your filters. This is useful when you need manual control over specific registrations.
Here is an example:
using NexusLabs.Needlr;
// This type will be excluded from automatic discovery
[DoNotAutoRegister]
public class ManuallyConfiguredService : IService
{
public void DoWork()
{
// Implementation
}
}
// This type will be discovered automatically
public class AutoRegisteredService : IService
{
public void DoWork()
{
// Implementation
}
}
// Setup with filtering
var serviceProvider = new Syringe()
.UsingSourceGen()
.UsingTypeFilter(type => type.Namespace?.StartsWith("MyApp") == true)
.BuildServiceProvider();
// ManuallyConfiguredService is excluded even though it matches the filter
// AutoRegisteredService is discovered automatically
The [DoNotAutoRegister] attribute takes precedence over type filters, so types marked with this attribute are always excluded from automatic discovery.
Real-World Example: Multi-Project Solution
Let's look at a complete example that demonstrates scanning configuration for a multi-project solution:
using NexusLabs.Needlr.Injection;
using NexusLabs.Needlr.Injection.SourceGen;
var serviceProvider = new Syringe()
.UsingSourceGen()
.UsingAssemblyProvider(builder => builder
// Scan only application assemblies, exclude test and infrastructure
.MatchingAssemblies(x =>
x.StartsWith("MyApp") &&
!x.Contains("Tests") &&
!x.Contains("Infrastructure.External"))
.Build())
.UsingTypeFilter(type =>
{
// Only register types from service and repository namespaces
var namespace = type.Namespace ?? "";
return (namespace.StartsWith("MyApp.Services") ||
namespace.StartsWith("MyApp.Repositories")) &&
// Exclude internal implementation details
!type.Name.Contains("Internal") &&
!type.Name.Contains("Helper");
})
.BuildServiceProvider();
// Services in MyApp.Services namespace are discovered
namespace MyApp.Services
{
public interface IOrderService
{
Task<Order> CreateOrderAsync(OrderRequest request);
}
public class OrderService : IOrderService
{
public async Task<Order> CreateOrderAsync(OrderRequest request)
{
return await Task.FromResult(new Order());
}
}
}
// Services in MyApp.Repositories namespace are discovered
namespace MyApp.Repositories
{
public interface IOrderRepository
{
Task<Order> GetByIdAsync(int id);
}
public class SqlOrderRepository : IOrderRepository
{
public async Task<Order> GetByIdAsync(int id)
{
return await Task.FromResult(new Order { Id = id });
}
}
}
// Types in other namespaces are excluded
namespace MyApp.Infrastructure.External
{
public class ExternalService
{
// Not discovered due to namespace filter
}
}
// Types with Internal in the name are excluded
namespace MyApp.Services
{
public class OrderServiceInternal
{
// Not discovered due to name filter
}
}
This example shows how to configure scanning for a multi-project solution:
- Assembly filtering limits scanning to application assemblies
- Type filtering further restricts discovery to specific namespaces
- Name-based exclusions prevent internal types from being discovered
Ordering and Sorting Assemblies
Needlr allows you to control the order in which assemblies are processed, which can be important for deterministic registration order or when using custom ordering logic.
Here is an example that shows assembly ordering:
using NexusLabs.Needlr.Injection;
using NexusLabs.Needlr.Injection.SourceGen;
var serviceProvider = new Syringe()
.UsingSourceGen()
.UsingAssemblyProvider(builder => builder
.MatchingAssemblies(x => x.Contains("MyApp"))
.UseLibTestEntryOrdering() // Use standard .NET ordering
.Build())
.BuildServiceProvider();
The UseLibTestEntryOrdering method applies standard .NET assembly ordering, which can help ensure deterministic discovery order across different environments.
Performance Considerations
Assembly scanning performance depends on several factors:
- Number of assemblies scanned
- Number of types in those assemblies
- Complexity of type filters
- Whether you use source generation or reflection
Source generation performs scanning at compile time, so it has no runtime performance impact. Reflection-based scanning happens at application startup, so complex filters or large numbers of assemblies can impact startup time.
Here are some tips for optimizing scanning performance:
Limit Assembly Scanning: Only scan assemblies that contain services you need. Exclude test assemblies, infrastructure assemblies that don't contain services, and third-party libraries.
Use Efficient Filters: Keep type filters simple and efficient. Avoid complex logic or reflection-heavy operations in filters.
Prefer Source Generation: Use source generation when possible, as it eliminates runtime scanning overhead.
Cache Filter Results: If you have expensive filter logic, consider caching results or precomputing filter decisions.
Best Practices
When configuring assembly scanning with Needlr, follow these best practices:
Be Explicit About Boundaries: Clearly define which assemblies and namespaces participate in automatic discovery. This makes the configuration easier to understand and maintain.
Use Namespace Organization: Organize your code by namespace and scan only the namespaces that contain services. This provides clear boundaries and makes filtering easier.
Exclude Test Assemblies: Always exclude test assemblies from scanning. Test assemblies should not be part of production dependency injection configuration.
Document Filtering Logic: Document why certain assemblies or types are included or excluded. This helps future developers understand the configuration.
Start Broad, Narrow Gradually: Start with broad scanning and narrow it down as needed. It is easier to exclude things than to discover what is missing.
Use DoNotAutoRegister for Exceptions: Use [DoNotAutoRegister] for types that need manual registration rather than trying to exclude them with complex filters.
Comparison with Manual Registration
Assembly scanning with filtering provides a middle ground between fully automatic discovery and fully manual registration:
Fully Automatic: Scans everything, registers everything. Simple but can include unwanted types.
Filtered Scanning: Scans selected assemblies, registers filtered types. Provides control while maintaining automation.
Manual Registration: Explicitly register each type. Maximum control but requires maintenance.
Filtered scanning gives you the benefits of automatic discovery while maintaining control over what gets registered, making it ideal for larger applications.
Conclusion
Assembly scanning configuration gives you fine-grained control over what Needlr discovers and registers. By combining assembly providers with type filters, you can precisely control which types participate in automatic discovery while maintaining the benefits of convention-based registration.
Whether you are organizing a multi-project solution, excluding test assemblies, or filtering by namespace, Needlr's scanning configuration provides the flexibility you need to structure your dependency injection setup exactly as you want it.
Frequently Asked Questions
How do I control which assemblies are scanned?
Use the UsingAssemblyProvider method with MatchingAssemblies to filter assemblies by name. You can use predicates to include or exclude assemblies based on naming patterns.
Can I scan dynamically loaded assemblies?
Yes, use UsingAdditionalAssemblies to include dynamically loaded assemblies. Note that this requires using reflection-based discovery, as source generation cannot scan assemblies unknown at compile time.
How do I filter which types get registered?
Use the UsingTypeFilter method to provide a predicate that determines which types should be registered. Types that do not match the filter are skipped during discovery.
Can I combine assembly and type filtering?
Yes, you can combine UsingAssemblyProvider with UsingTypeFilter to achieve precise control over both which assemblies are scanned and which types within those assemblies are registered.
How do I exclude specific types from discovery?
Use the [DoNotAutoRegister] attribute on types you want to exclude. This attribute takes precedence over type filters, so marked types are always excluded.
Does filtering affect performance?
Filtering has minimal performance impact with source generation, as filtering happens at compile time. With reflection-based discovery, complex filters can impact startup time, so keep filters simple and efficient.
Can I organize discovery by namespace?
Yes, use type filters to check the Namespace property and include only types from specific namespaces. This is a common pattern for organizing services in larger applications.

