Muslim.Inject
10.0.1
dotnet add package Muslim.Inject --version 10.0.1
NuGet\Install-Package Muslim.Inject -Version 10.0.1
<PackageReference Include="Muslim.Inject" Version="10.0.1" />
<PackageVersion Include="Muslim.Inject" Version="10.0.1" />
<PackageReference Include="Muslim.Inject" />
paket add Muslim.Inject --version 10.0.1
#r "nuget: Muslim.Inject, 10.0.1"
#:package Muslim.Inject@10.0.1
#addin nuget:?package=Muslim.Inject&version=10.0.1
#tool nuget:?package=Muslim.Inject&version=10.0.1
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, notservices.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.
UserServicein the first example resolves asIUserServiceonly. Asking the container forUserServiceitself 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 plainIShaperesolves to the last. Among marker-registered classes you do not control which is last. If it matters, use[Injection]withorder, 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 | Versions 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. |
-
net10.0
- Microsoft.Extensions.DependencyInjection (>= 10.0.13)
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.
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.