SharpServiceCollection 1.0.2

There is a newer version of this package available.
See the version list below for details.
dotnet add package SharpServiceCollection --version 1.0.2
                    
NuGet\Install-Package SharpServiceCollection -Version 1.0.2
                    
This command is intended to be used within the Package Manager Console in Visual Studio, as it uses the NuGet module's version of Install-Package.
<PackageReference Include="SharpServiceCollection" Version="1.0.2" />
                    
For projects that support PackageReference, copy this XML node into the project file to reference the package.
<PackageVersion Include="SharpServiceCollection" Version="1.0.2" />
                    
Directory.Packages.props
<PackageReference Include="SharpServiceCollection" />
                    
Project file
For projects that support Central Package Management (CPM), copy this XML node into the solution Directory.Packages.props file to version the package.
paket add SharpServiceCollection --version 1.0.2
                    
#r "nuget: SharpServiceCollection, 1.0.2"
                    
#r directive can be used in F# Interactive and Polyglot Notebooks. Copy this into the interactive tool or source code of the script to reference the package.
#:package SharpServiceCollection@1.0.2
                    
#:package directive can be used in C# file-based apps starting in .NET 10 preview 4. Copy this into a .cs file before any lines of code to reference the package.
#addin nuget:?package=SharpServiceCollection&version=1.0.2
                    
Install as a Cake Addin
#tool nuget:?package=SharpServiceCollection&version=1.0.2
                    
Install as a Cake Tool

SharpServiceCollection

Build status

SharpServiceCollection is a lightweight C# package that provides a declarative, comipile-time and AOT-friendly way to work with IServiceCollection.

Table of 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:

  1. Install SharpServiceCollection.
  2. Add an InjectableDependency attribute to each service implementation.
  3. Call the generated AddAttributedServices() extension method.
  4. 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} OrderServiceIOrderService
ImplementedInterface Every interface implemented by the class PaymentServiceICardPayment, 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 regular Add* 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 IServiceRegistration or IServiceRegistration<TContext>
  • Provide the matching RegisterAsync method

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 can contain registrations itself, split across any number of files, and/or reference 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 project that contains service registrations. A project with ServiceRegistrationRoot also gets the ExecuteServiceRegistrationItemsAsync orchestration extension. It combines that project's local registrations with aggregators discovered through its project or package references. The orchestration method is internal, because it is intended to be called from the root project itself.

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 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. 
Compatible target framework(s)
Included target framework(s) (in package)
Learn more about Target Frameworks and .NET Standard.
  • .NETStandard 2.0

    • No dependencies.

NuGet packages

This package is not used by any NuGet packages.

GitHub repositories

This package is not used by any popular GitHub repositories.

Version Downloads Last Updated
1.1.1 49 7/16/2026
1.1.0 81 7/15/2026
1.0.6 95 7/13/2026
1.0.5 93 7/13/2026
1.0.4 90 7/12/2026
1.0.3 102 7/11/2026
1.0.2 90 7/11/2026
1.0.1 94 7/11/2026
1.0.0 96 7/11/2026
0.1.0 135 6/27/2026
0.0.6 953 9/27/2025
0.0.5 184 9/26/2025
0.0.4 631 4/6/2025