SharpServiceCollection 1.0.0
See the version list below for details.
dotnet add package SharpServiceCollection --version 1.0.0
NuGet\Install-Package SharpServiceCollection -Version 1.0.0
<PackageReference Include="SharpServiceCollection" Version="1.0.0" />
<PackageVersion Include="SharpServiceCollection" Version="1.0.0" />
<PackageReference Include="SharpServiceCollection" />
paket add SharpServiceCollection --version 1.0.0
#r "nuget: SharpServiceCollection, 1.0.0"
#:package SharpServiceCollection@1.0.0
#addin nuget:?package=SharpServiceCollection&version=1.0.0
#tool nuget:?package=SharpServiceCollection&version=1.0.0
SharpServiceCollection
SharpServiceCollection is a lightweight C# package that provides a declarative, comipile-time and AOT-friendly way to work with IServiceCollection.
Table of contents
- Installation
- How it works
- Your first registration
- Registration options
- Choose how services are discovered
- Register services from multiple projects
- Opt out of source generation
- Diagnostics
- Package contents
Installation
Install one package. You do not need to install separate packages for the runtime library, dependency types, or source generator:
dotnet add package SharpServiceCollection
You can also install it from NuGet.
The package contains:
- The runtime library and public attributes
- The dependency types used by the attributes
- The Roslyn source generator under
analyzers/dotnet/cs
The source generator runs automatically when you build a project that references the package.
How it works
The basic workflow is:
- Install
SharpServiceCollection. - Add an
InjectableDependencyattribute to each service implementation. - Call the generated
AddAttributedServices()extension method. - Resolve the service through the normal .NET dependency injection APIs.
For example:
using Microsoft.Extensions.DependencyInjection;
using SharpServiceCollection;
using SharpServiceCollection.Attributes;
using SharpServiceCollection.Enums;
using SharpServiceCollection.Generated;
public interface IEmailSender
{
Task SendAsync(string address, string message);
}
[InjectableDependency(InstanceLifetime.Scoped, ResolveBy.MatchingInterface)]
public sealed class EmailSender : IEmailSender
{
public Task SendAsync(string address, string message)
{
Console.WriteLine($"Sending to {address}: {message}");
return Task.CompletedTask;
}
}
var builder = WebApplication.CreateBuilder(args);
// The source generator creates this method for the current assembly.
builder.Services.AddAttributedServices();
var app = builder.Build();
app.MapGet("/email", async (IEmailSender sender) =>
{
await sender.SendAsync("user@example.com", "Welcome!");
return Results.Ok();
});
app.Run();
EmailSender is registered as IEmailSender, and its lifetime is scoped. The generated code uses the normal Microsoft.Extensions.DependencyInjection registration APIs; there is no runtime type scanning for source-generated registration.
Your first registration
Register a class by itself
Use ResolveBy.Self when consumers should resolve the concrete class:
[InjectableDependency(InstanceLifetime.Transient, ResolveBy.Self)]
public sealed class TokenGenerator
{
public string Create() => Guid.NewGuid().ToString("N");
}
This is equivalent to:
services.TryAddTransient<TokenGenerator>();
Register by interface
Use ResolveBy.MatchingInterface when the class follows the I{ClassName} naming convention:
public interface IOrderService
{
Task PlaceAsync(int orderId);
}
[InjectableDependency(InstanceLifetime.Scoped, ResolveBy.MatchingInterface)]
public sealed class OrderService : IOrderService
{
public Task PlaceAsync(int orderId) => Task.CompletedTask;
}
This registers:
services.TryAddScoped<IOrderService, OrderService>();
The class must implement the matching interface. For example, OrderService must implement IOrderService.
Register an explicit service type
Use the generic attribute when the service type does not follow a naming convention:
public interface IMessageHandler
{
Task HandleAsync(string message);
}
[InjectableDependency<IMessageHandler>(InstanceLifetime.Singleton)]
public sealed class WelcomeMessageHandler : IMessageHandler
{
public Task HandleAsync(string message) => Task.CompletedTask;
}
This registers WelcomeMessageHandler as IMessageHandler.
Registration options
Lifetimes
InstanceLifetime supports the standard dependency injection lifetimes:
| Lifetime | Behavior |
|---|---|
Singleton |
One instance for the application lifetime |
Scoped |
One instance per service scope, such as an HTTP request |
Transient |
A new instance each time the service is requested |
Example:
[InjectableDependency(InstanceLifetime.Singleton, ResolveBy.Self)]
public sealed class ApplicationClock
{
public DateTimeOffset Now => DateTimeOffset.UtcNow;
}
Registration target
ResolveBy determines which service type is registered:
| Value | Registers as | Example |
|---|---|---|
Self |
The concrete class | TokenGenerator |
MatchingInterface |
I{ClassName} |
OrderService → IOrderService |
ImplementedInterface |
Every interface implemented by the class | PaymentService → ICardPayment, IRefunds |
For example:
[InjectableDependency(
InstanceLifetime.Scoped,
ResolveBy.ImplementedInterface)]
public sealed class PaymentService : ICardPayment, IRefunds
{
// Registered for both ICardPayment and IRefunds.
}
Keys, enumerable services, and ordering
All registration attributes support these options:
| Option | Default | Purpose |
|---|---|---|
TryAdd |
true |
Avoid replacing an existing registration when true; use the regular Add* API when false |
Key |
null |
Register a keyed service |
Enumerable |
false |
Add the implementation to an enumerable service collection |
Order |
0 |
Control processing order when registrations compete |
Keyed services
[InjectableDependency<IMessageHandler>(
InstanceLifetime.Transient,
Key = "welcome")]
public sealed class WelcomeMessageHandler : IMessageHandler
{
public Task HandleAsync(string message) => Task.CompletedTask;
}
The service can then be resolved using the standard keyed-service APIs:
var handler = serviceProvider.GetRequiredKeyedService<IMessageHandler>("welcome");
Multiple implementations
Set Enumerable = true when you want all matching implementations available through IEnumerable<T>:
public interface IValidator<T>
{
bool IsValid(T value);
}
public sealed record Order(int Id);
[InjectableDependency<IValidator<Order>>(
InstanceLifetime.Singleton,
Enumerable = true)]
public sealed class PaymentValidator : IValidator<Order>
{
}
[InjectableDependency<IValidator<Order>>(
InstanceLifetime.Singleton,
Enumerable = true)]
public sealed class AddressValidator : IValidator<Order>
{
}
Inject them together:
public sealed class OrderService(IEnumerable<IValidator<Order>> validators)
{
public bool IsValid(Order order) => validators.All(v => v.IsValid(order));
}
Enumerable = true requires TryAdd = true.
Duplicate registrations and Order
Registrations are processed by Order ascending, followed by class name ascending.
- With
TryAdd = true, the first matching registration wins. - With
TryAdd = false, the regularAdd*method is used, so later registrations can replace the default service resolution.
[InjectableDependency<IWorker>(InstanceLifetime.Scoped, Order = 1)]
public sealed class PrimaryWorker : IWorker
{
}
[InjectableDependency<IWorker>(InstanceLifetime.Scoped, Order = 2)]
public sealed class BackupWorker : IWorker
{
}
Choose how services are discovered
Source-generated registration
Source generation is the recommended option when the assemblies are known at build time. It is fast, avoids runtime assembly scanning, and is a good fit for trimming and AOT scenarios.
Add the attribute to your services, then call:
using SharpServiceCollection.Generated;
services.AddAttributedServices();
The generated method is internal, so it is intended for use inside the same assembly that contains the attributed services.
When a host needs to register services from another assembly, the generator also creates a public method based on that assembly name:
// For an assembly named My.Module.Application:
services.AddAttributedServicesFrom_MyModuleApplication();
The complete method name follows this pattern:
AddAttributedServicesFrom_{SanitizedAssemblyName}
For example:
Assembly name: My.Module.Application
Generated call: services.AddAttributedServicesFrom_MyModuleApplication();
The generator uses the consuming project's AssemblyName, removes dots and other non-alphanumeric characters, and adds the AddAttributedServicesFrom_ prefix. For example, the test project assembly SharpServiceCollection.Tests generates:
services.AddAttributedServicesFrom_SharpServiceCollectionTests();
The generated method is public, so a host project can call the method generated in a referenced module assembly.
Reflection-based registration
Use reflection when an assembly is only known at runtime, when you are loading plugins, or while migrating an existing application to source generation.
using SharpServiceCollection.Extensions;
services.AddServicesFromCurrentAssembly();
services.AddServicesFromAssembly(pluginAssembly);
services.AddServicesFromAssemblyContaining<PluginMarker>();
services.AddServicesFromAssemblyContaining(typeof(PluginMarker));
Reflection scans the selected assembly at runtime. It is more flexible than source generation, but source generation is usually preferable when the set of assemblies is known during the build.
Register services from multiple projects
In a modular solution, each project can own its service registrations. A host project can then discover and execute those registrations through generated code.
This is useful when your solution looks like this:
MyApp.Api # host project
MyApp.Orders # feature module
MyApp.Payments # feature module
Create a module registration
Mark a sealed class with [ServiceRegistrationItem] and implement IServiceRegistration:
using Microsoft.Extensions.DependencyInjection;
using SharpServiceCollection.Attributes;
using SharpServiceCollection.Generated;
using SharpServiceCollection.Interfaces;
[ServiceRegistrationItem]
public sealed class OrdersRegistration : IServiceRegistration
{
public Task RegisterAsync(IServiceCollection services)
{
services.AddAttributedServices();
services.AddSingleton<OrderNumberGenerator>();
return Task.CompletedTask;
}
}
The generator discovers classes that:
- Have the
[ServiceRegistrationItem]attribute - Are
sealed - Implement
IServiceRegistrationorIServiceRegistration<TContext> - Provide the matching
RegisterAsyncmethod
The class can have any name. It does not need to be named ServiceRegistration.
Order controls execution order. Lower order values run first. Registrations with the same order are sorted by implementation type name.
Enable the host project
Set ServiceRegistrationRoot in the host project's .csproj file:
<PropertyGroup>
<ServiceRegistrationRoot>true</ServiceRegistrationRoot>
</PropertyGroup>
The host must reference the module projects or packages. Then execute the generated registrations during startup:
using SharpServiceCollection.Generated;
var builder = WebApplication.CreateBuilder(args);
await builder.Services.ExecuteServiceRegistrationItemsAsync();
var app = builder.Build();
app.Run();
The source generator creates a small public aggregator in every module. The root project finds those aggregators through its project or package references and combines them into ExecuteServiceRegistrationItemsAsync.
Use a registration context
Implement IServiceRegistration<TContext> when a module needs configuration or environment information:
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using SharpServiceCollection.Attributes;
using SharpServiceCollection.Interfaces;
public sealed record AppContext(
IConfiguration Configuration,
IHostEnvironment Environment);
[ServiceRegistrationItem(Order = 20)]
public sealed class PaymentsRegistration : IServiceRegistration<AppContext>
{
public Task RegisterAsync(
IServiceCollection services,
AppContext context)
{
services.AddSingleton(context.Configuration);
return Task.CompletedTask;
}
}
Pass the matching context from the host:
var context = new AppContext(
builder.Configuration,
builder.Environment);
await builder.Services.ExecuteServiceRegistrationItemsAsync(context);
| Host call | Executes |
|---|---|
ExecuteServiceRegistrationItemsAsync() |
Registrations implementing IServiceRegistration |
ExecuteServiceRegistrationItemsAsync(context) |
Registrations implementing IServiceRegistration<TContext> where TContext matches |
Opt out of source generation
The package contains two independent source generators. You can disable either one, or both, in a consuming project's .csproj file.
Disable attributed DI generation
Set DisableInjectableDependencyGenerator to true when you want to use the reflection-based registration APIs instead of generated AddAttributedServices() methods:
<PropertyGroup>
<DisableInjectableDependencyGenerator>true</DisableInjectableDependencyGenerator>
</PropertyGroup>
This disables generated attributed-service registration only. Reflection APIs such as AddServicesFromAssembly(...) remain available.
Disable service-registration orchestration
Set DisableServiceRegistrationGenerator to true when you do not use [ServiceRegistrationItem] and IServiceRegistration:
<PropertyGroup>
<DisableServiceRegistrationGenerator>true</DisableServiceRegistrationGenerator>
</PropertyGroup>
This disables generated service-registration aggregators and root methods such as ExecuteServiceRegistrationItemsAsync(...).
To disable both generators:
<PropertyGroup>
<DisableInjectableDependencyGenerator>true</DisableInjectableDependencyGenerator>
<DisableServiceRegistrationGenerator>true</DisableServiceRegistrationGenerator>
</PropertyGroup>
Diagnostics
The source generator reports clear diagnostics when an attribute or registration class is invalid:
| ID | Severity | Meaning |
|---|---|---|
| SSC001 | Error | Enumerable = true requires TryAdd = true |
| SSC002 | Error | ResolveBy.MatchingInterface requires an I{ClassName} interface |
| SSC003 | Error | Invalid InstanceLifetime value |
| SSC004 | Error | Invalid ResolveBy value |
| SSC005 | Error | A type marked with [ServiceRegistrationItem] must be sealed |
| SSC006 | Error | A type marked with [ServiceRegistrationItem] must implement IServiceRegistration or IServiceRegistration<TContext> |
Fix the reported source code and rebuild. The generator will run again automatically.
Package contents
SharpServiceCollection is distributed as a single NuGet package:
SharpServiceCollection.nupkg
├── lib/netstandard2.0/
│ ├── SharpServiceCollection.dll
│ └── SharpServiceCollection.Dependencies.dll
└── analyzers/dotnet/cs/
├── SharpServiceCollection.Generators.dll
└── SharpServiceCollection.Dependencies.dll
The dependency assembly is included in the package so consumers can use the public attribute, enum, and interface types without installing another package. The generator copy is placed in the analyzer folder so it runs automatically during builds.
| Product | Versions Compatible and additional computed target framework versions. |
|---|---|
| .NET | net5.0 was computed. net5.0-windows was computed. net6.0 was computed. net6.0-android was computed. net6.0-ios was computed. net6.0-maccatalyst was computed. net6.0-macos was computed. net6.0-tvos was computed. net6.0-windows was computed. net7.0 was computed. net7.0-android was computed. net7.0-ios was computed. net7.0-maccatalyst was computed. net7.0-macos was computed. net7.0-tvos was computed. net7.0-windows was computed. net8.0 was computed. net8.0-android was computed. net8.0-browser was computed. net8.0-ios was computed. net8.0-maccatalyst was computed. net8.0-macos was computed. net8.0-tvos was computed. net8.0-windows was computed. net9.0 was computed. net9.0-android was computed. net9.0-browser was computed. net9.0-ios was computed. net9.0-maccatalyst was computed. net9.0-macos was computed. net9.0-tvos was computed. net9.0-windows was computed. net10.0 was computed. net10.0-android was computed. net10.0-browser was computed. net10.0-ios was computed. net10.0-maccatalyst was computed. net10.0-macos was computed. net10.0-tvos was computed. net10.0-windows was computed. |
| .NET Core | netcoreapp2.0 was computed. netcoreapp2.1 was computed. netcoreapp2.2 was computed. netcoreapp3.0 was computed. netcoreapp3.1 was computed. |
| .NET Standard | netstandard2.0 is compatible. netstandard2.1 was computed. |
| .NET Framework | net461 was computed. net462 was computed. net463 was computed. net47 was computed. net471 was computed. net472 was computed. net48 was computed. net481 was computed. |
| MonoAndroid | monoandroid was computed. |
| MonoMac | monomac was computed. |
| MonoTouch | monotouch was computed. |
| Tizen | tizen40 was computed. tizen60 was computed. |
| Xamarin.iOS | xamarinios was computed. |
| Xamarin.Mac | xamarinmac was computed. |
| Xamarin.TVOS | xamarintvos was computed. |
| Xamarin.WatchOS | xamarinwatchos was computed. |
-
.NETStandard 2.0
- SharpServiceCollection.Dependencies (>= 1.0.0)
- SharpServiceCollection.Generators (>= 1.0.0)
NuGet packages
This package is not used by any NuGet packages.
GitHub repositories
This package is not used by any popular GitHub repositories.