Muslim.Inject 10.0.1

dotnet add package Muslim.Inject --version 10.0.1
                    
NuGet\Install-Package Muslim.Inject -Version 10.0.1
                    
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="Muslim.Inject" Version="10.0.1" />
                    
For projects that support PackageReference, copy this XML node into the project file to reference the package.
<PackageVersion Include="Muslim.Inject" Version="10.0.1" />
                    
Directory.Packages.props
<PackageReference Include="Muslim.Inject" />
                    
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 Muslim.Inject --version 10.0.1
                    
#r "nuget: Muslim.Inject, 10.0.1"
                    
#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 Muslim.Inject@10.0.1
                    
#: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=Muslim.Inject&version=10.0.1
                    
Install as a Cake Addin
#tool nuget:?package=Muslim.Inject&version=10.0.1
                    
Install as a Cake Tool

Muslim.Inject

Convention-based dependency registration for Microsoft.Extensions.DependencyInjection.

Mark an interface, and it registers itself:

public interface IUserService : IAutoInjection { }

public class UserService : IUserService { }
builder.Services.AddAutoInjection(typeof(IUserService).Assembly);

That is the whole registration step. No AddScoped per service, no registration file to keep in sync.

dotnet add package Muslim.Inject

Requires .NET 10 (net10.0). It depends on Microsoft.Extensions.DependencyInjection 10.x, which comes in with the package.

Setup

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddAutoInjection(
    typeof(IUserService).Assembly,        // Application
    typeof(UserRepository).Assembly);     // Data

var app = builder.Build();

InjectExtensions.SetServiceProvider(app.Services);

Pass the assemblies that hold your services. Scanning everything loaded is slow and drags in framework types. Calling AddAutoInjection with no assemblies throws ArgumentException.

SetServiceProvider is only needed if you use Inject, InjectRequired, TryInject or RunInScope (below). Registration alone needs just AddAutoInjection.

Pass app.Services, not services.BuildServiceProvider(). The latter builds a second container from the same collection: singletons get duplicated — one set resolved by the framework, another by this one — and the host no longer controls their disposal.

Per-request scopes

If you use Inject<T>() (below), point it at the request's scope:

app.Use(async (context, next) =>
{
    InjectExtensions.SetServiceProvider(context.RequestServices);
    await next();
});

The first call is remembered as the composition root and is never replaced. Later calls apply to the current async flow only, so concurrent requests cannot see each other's scope.

Lifetimes

The marker chooses the lifetime:

Marker Lifetime
IAutoInjection scoped
ITransientInjection transient
ISingletonInjection singleton
public interface IPriceCalculator : ITransientInjection { }
public interface IClock : ISingletonInjection { DateTime UtcNow { get; } }

A singleton must not depend on a scoped service such as a DbContext — the first scope's instance would be captured and reused forever. Use RunInScopeAsync for that.

What gets registered against what

A class is registered against the marked interfaces it declares itself, not those it inherits from a base class. So given:

public interface IBankRepository : IRepository<Bank> { }          // IRepository is marked

internal class BankRepository : Repository<Bank>, IBankRepository { }

BankRepository is registered as IBankRepository only. It does not also claim IRepository<Bank> or the interfaces that composes — those belong to its base class, and every repository in the application would otherwise compete to be the implementation of the same generic contracts.

Things to know

  • The interface is registered, not the class. UserService in the first example resolves as IUserService only. Asking the container for UserService itself returns nothing.

  • Abstract classes are skipped, silently. Only concrete classes are registered.

  • Open generics need the attribute. A class such as Repo<T> : IRepo<T> is not picked up by the marker. Register it explicitly:

    [Injection(typeof(IRepo<>), typeof(Repo<>))]
    public class Repo<T> : IRepo<T> { }
    
  • Several classes for one interface: the last one registered wins. All of them are registered, so IEnumerable<IShape> returns every one, but a plain IShape resolves to the last. Among marker-registered classes you do not control which is last. If it matters, use [Injection] with order, below.

  • Scanning the same type twice is harmless. An identical registration already in the collection is skipped, so overlapping assembly sets do not produce duplicates.

Explicit registration

For pairs that do not match by convention, or where order matters:

[Injection(typeof(IEmailSender), typeof(SmtpEmailSender), order: 10)]
[Injection(typeof(INotifier),    typeof(SmtpEmailSender), order: 20, lifetime: ServiceLifetime.Singleton)]
public class SmtpEmailSender : IEmailSender, INotifier { }

Order defaults to 999, lowest registers first, and the attribute is repeatable. When several registrations target the same service, the last one registered wins — so the highest Order is the one that resolves. In the example above, INotifier and IEmailSender are different services, so the order between them does not change what either resolves to.

Attributes are applied after the marker pass, so one can deliberately override a convention registration.

If an attribute names an implementation that does not satisfy its interface, AddAutoInjection throws at startup naming both types — rather than leaving you a resolution failure at first use. The same applies when the implementation is abstract or not a class, when an open generic is paired with a closed type, when either type is null, or when the lifetime is not a defined ServiceLifetime. An open generic may name an open generic base class, or itself: [Injection(typeof(Cache<>), typeof(Cache<>))].

A class implementing two ISingletonInjection interfaces gets one instance per interface, because each interface is its own registration. If both must share one instance, register the class once yourself and forward each interface to it with a factory.

Resolving without a constructor

Some types cannot take constructor arguments — a base class the container never sees, for example. Inject<T>() resolves from the ambient provider:

public class BaseRepository
{
    protected readonly IMapper _mapper;
    protected readonly ICurrentUser _currentUser;

    public BaseRepository()
    {
        _mapper      = _mapper.InjectRequired();
        _currentUser = _currentUser.InjectRequired();
    }
}

It is an extension on the field being assigned, so the field's type supplies T and you never name the type twice.

For a local rather than a field — nothing to extend — each has a parameterless form instead:

var userRepository = InjectExtensions.InjectRequired<IUserRepository>();
Behaviour when not registered
InjectRequired<T>() throws, naming the missing type — prefer this
Inject<T>() returns the field unchanged, normally null
TryInject<T>(out T) returns false; for genuinely optional services

Inject<T>() with no field to extend returns default instead — null for a reference type.

if (default(IAuditLogger).TryInject(out var logger))
    logger.Write(entry);

// or, with no existing value to extend:
if (InjectExtensions.TryInject<IAuditLogger>(out var logger))
    logger.Write(entry);

Inject and InjectRequired throw a clear error if SetServiceProvider was never called. TryInject does not: with no provider configured it just returns false, the same as for an unregistered service. Use IsConfigured if you need to tell the two apart.

This is a service locator. It hides what a type depends on, makes it hard to test, and moves failures from startup to first use. Use constructor injection wherever it is available.

Scoped services in background work

A hosted service is a singleton, so resolving a DbContext in it would capture one instance for the life of the process and share it across overlapping runs. Take a scope instead:

await InjectExtensions.RunInScopeAsync(async () =>
{
    var repo = default(IUserRepository).InjectRequired();
    await repo.SaveCreateAsync(user);
});

The scope is created from the composition root, is ambient for the duration, and is disposed afterwards. The previous scope is restored, so this nests safely. A synchronous RunInScope exists too. RunInScopeAsync disposes the scope asynchronously, so scoped services that implement only IAsyncDisposable are fine there; RunInScope disposes synchronously and throws for them.

IsConfigured tells you whether a provider has been set, for library code that must work either way.

Resetting between tests

SetServiceProvider latches the composition root on its first-ever call only, and ignores every call after that — the point being that a per-request scope can never overwrite it in production. In a test process the same latch means every test after the first keeps resolving root-level work against the first test's provider, even once it is disposed. Reset() clears both the root and the current scope, so the next test's SetServiceProvider call latches its own root:

public void Dispose() => InjectExtensions.Reset();

The root it clears is a plain static, not an AsyncLocal, so this is not safe under parallel test execution — one test's Reset() can pull the root out from under another test still running. Use it only when tests run sequentially (for example, an xUnit collection with parallelization disabled).

Coding-agent guidance

When you build a project that uses this package, a short guide for AI coding agents is copied to .agents/skills/muslim-inject/ at the root of your repository. Agents that read skills from .agents/skills, such as Codex and GitHub Copilot in VS Code, use it to write correct code against this package. Your own files and other skills are never touched, and nothing is deleted automatically.

To turn it off, add this to your project or Directory.Build.props:

<PropertyGroup>
  <MuslimInjectGenerateSkill>false</MuslimInjectGenerateSkill>
</PropertyGroup>

License

See LICENSE.txt.

Product Compatible and additional computed target framework versions.
.NET net10.0 is compatible.  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. 
Compatible target framework(s)
Included target framework(s) (in package)
Learn more about Target Frameworks and .NET Standard.

NuGet packages (5)

Showing the top 5 NuGet packages that depend on Muslim.Inject:

Package Downloads
Muslim.GenericRepository

EF Core repository for entities with audit columns: reads, writes, soft delete, bulk operations and raw SQL, with CreatedBy/UpdateBy/DeletedBy and UTC timestamps written automatically. Implements Muslim.GenericRepository.Interface.

Muslim.Extensions

Small .NET extension helpers: AutoMapper Map<T>() via Muslim.Inject, empty/default checks, enum parsing, DateOnly conversion, null coalescing, and salted PBKDF2 password hashing with legacy SHA-256 migration. Includes build-delivered agent guidance.

Muslim.Helpers

Small shared helpers for Muslim.* applications: the ICurrentUser contract (auto-registered through Muslim.Inject), a Base64 data URI decoder (Base64Data), and an EF Core comma-separated string list converter with a change-tracking comparer (StringListConverter, StringListComparer, HasStringListConversion).

Muslim.GenericRepository.Interface

Repository abstraction over EF Core: IRepository<TEntity> and the interfaces it composes — reads, writes, soft delete, bulk operations, counts and raw SQL. Reference this to depend on the contract without the implementation.

Muslim.GenericService.Interface

Service abstraction for Muslim.GenericService: IService<TEntity> and the interfaces it composes — reads, finds with paging, create, update, delete, soft delete and restore, counts and raw SQL — all returning Muslim.Result Result<T>. Reference this to depend on the contract without the implementation.

GitHub repositories

This package is not used by any popular GitHub repositories.

Version Downloads Last Updated
10.0.1 166 10/15/2026
8.0.0 255 9/23/2026
2.3.0 109 9/23/2026
2.2.0 249 8/10/2026
2.0.0 566 10/5/2024

Fixes RunInScopeAsync disposing its scope synchronously, and rejects [Injection] pairs the container cannot honour at startup. Ships version-matched agent guidance and generates .agents/skills/muslim-inject during normal consuming builds; preserves user files and supports MuslimInjectGenerateSkill=false. See CHANGELOG.md and README.md.