Yak 1.2.0
dotnet add package Yak --version 1.2.0
NuGet\Install-Package Yak -Version 1.2.0
<PackageReference Include="Yak" Version="1.2.0" />
<PackageVersion Include="Yak" Version="1.2.0" />
<PackageReference Include="Yak" />
paket add Yak --version 1.2.0
#r "nuget: Yak, 1.2.0"
#:package Yak@1.2.0
#addin nuget:?package=Yak&version=1.2.0
#tool nuget:?package=Yak&version=1.2.0
Yak
Yak is simple inversion of control for humans. It leverages Source Generators in Roslyn to create compile time IoC containers with simple, declarative interface.
The name comes from Yet Another Inversion of Control (YAIC resembles word YAK).
Quick start
Let's say you have a couple of classes in your project:
public interface ISingletonComponent { }
public class SingletonComponent : ISingletonComponent { }
public class TransientComponent
{
public ISingletonComponent SingletonComponent { get; }
public TransientComponent(ISingletonComponent singletonComponent)
{
SingletonComponent = singletonComponent;
}
}
To make your own IoC container, create a partial class annotated with [Module]. Mark properties with lifetime attributes ([Singleton] or [Transient]) and use partial properties — Yak automatically generates getters that resolve dependencies via constructors at compile time.
using Yak;
[Module]
public partial class MyModule
{
[Singleton<SingletonComponent>]
public partial ISingletonComponent SingletonComponent { get; }
[Transient<TransientComponent>]
public partial TransientComponent TransientComponent { get; }
}
Usage is straightforward:
using MyModule myModule = new MyModule();
ISingletonComponent singleton1 = myModule.SingletonComponent;
ISingletonComponent singleton2 = myModule.SingletonComponent;
// True — singletons always return the same instance
Console.WriteLine("Singleton is identical: {0}", ReferenceEquals(singleton1, singleton2));
TransientComponent transient1 = myModule.TransientComponent;
TransientComponent transient2 = myModule.TransientComponent;
// False — transients return a new instance each time
Console.WriteLine("Transient is identical: {0}", ReferenceEquals(transient1, transient2));
// True — transient instances share the same singleton dependency
Console.WriteLine("Singleton dependencies are identical: {0}",
ReferenceEquals(transient1.SingletonComponent, transient2.SingletonComponent));
Module imports
Modules can depend on other modules via constructor parameters. Properties from imported modules are available for dependency resolution:
[Module]
public partial class InfraModule
{
[Singleton<DatabaseService>]
public partial IDatabaseService Database { get; }
}
[Module]
public partial class AppModule(InfraModule infra)
{
[Transient<UserRepository>]
public partial UserRepository UserRepository { get; }
// UserRepository(IDatabaseService) is resolved from infra.Database
}
using InfraModule infra = new InfraModule();
using AppModule app = new AppModule(infra);
UserRepository repo = app.UserRepository;
Module interfaces
Interfaces annotated with [Module] define reusable, composable sets of services. A concrete [Module] class that implements such interfaces gets generated property implementations for all lifetime-attributed properties in the interface hierarchy. Properties without lifetime attributes are requirements the concrete class must provide.
[Module]
public interface IInfrastructure
{
Config Config { get; } // no attribute — consumer must provide
[Singleton]
Database Database { get; }
[Singleton]
Logger Logger { get; }
}
[Module]
public interface IMessaging : IInfrastructure
{
[Singleton]
MessageBus MessageBus { get; }
}
[Module]
public interface IAnalytics : IInfrastructure
{
[Singleton]
Tracker Tracker { get; }
}
Compose interfaces via standard C# interface inheritance. Diamond inheritance resolves naturally — shared services are generated once:
[Module]
public interface IFullStack : IMessaging, IAnalytics { }
[Module]
partial class AppModule : IFullStack
{
public Config Config { get; } = new Config { Environment = "production" };
[Singleton]
public partial OrderService OrderService { get; }
}
using AppModule app = new AppModule();
app.OrderService.PlaceOrder("widget");
// Database is created once and shared — diamond inheritance just works.
Console.WriteLine(ReferenceEquals(app.Database, app.OrderService.Tracker.Database)); // True
If the concrete class declares a property that matches an interface property, the class declaration wins — this lets you override specific registrations.
Lifetimes
- Singleton — one instance per module, created on first access
- Transient — new instance on every access
Use the generic form ([Singleton<T>], [Transient<T>]) to specify a concrete implementation type when the property type is an interface.
Callbacks
Methods annotated with [OnActivate] or [OnDispose] are called when dependencies are created or when the module is disposed. Callbacks are matched by parameter type:
[Module]
public partial class MyModule
{
[Singleton<SingletonComponent>]
public partial ISingletonComponent SingletonComponent { get; }
[OnActivate]
protected void OnComponentCreated(ISingletonComponent component)
{
// Called when SingletonComponent is first created
}
[OnDispose]
protected void OnComponentDisposed(ISingletonComponent component)
{
// Called when the module is disposed
}
}
Callbacks propagate across module imports. If a provider module defines callbacks, they also fire for matching services created by importing modules:
[Module]
public partial class InfraModule
{
public List<IUpdatable> Updatables { get; } = new();
[OnActivate]
void Register(IUpdatable u) => Updatables.Add(u);
[OnDispose]
void Deregister(IUpdatable u) => Updatables.Remove(u);
}
[Module]
public partial class AppModule(InfraModule infra)
{
[Singleton<PlayerSystem>]
public partial IUpdatable PlayerSystem { get; }
// PlayerSystem is automatically registered/deregistered via InfraModule's callbacks
}
Factories
Mark a property with [Factory<T>] or [StaticFactory<T>] to opt out of constructor injection. Yak locates a matching method on T, calls it, and wraps the result with lifetime management, callbacks, and dispose tracking. The attribute name declares the dispatch:
[Factory<T>]— calls a public instance method onT.Tmust be in the module graph (a registered property, an imported module, or the current module class itself).[StaticFactory<T>]— calls a public static method onT.Tcan be any named type.
Use [Factory<CurrentModule>] (or [StaticFactory<CurrentModule>]) when the factory method lives on the module itself. Private helpers are allowed on self-reference:
[Module]
public partial class MyModule
{
[Singleton]
public partial Config Config { get; }
[Singleton]
[Factory<MyModule>]
public partial Connection Connection { get; }
private Connection Build(Config config) => Connection.Open(config);
}
Use [Factory<TSource>] when the factory is an instance method on another registered service — Yak resolves the source through the graph and calls the method on that instance:
[Module]
public partial class MyModule
{
[Singleton]
public partial ConnectionFactory ConnectionFactory { get; }
[Singleton]
[Factory<ConnectionFactory>]
public partial Connection Connection { get; }
// emits: ConnectionFactory.Open(...)
}
Use [StaticFactory<TSource>] when the factory is a public static method on any type — including the service's own type, which is the idiomatic way to wire up types whose only construction path is a static Create/Open:
public class Connection : IDisposable
{
private Connection(Config config) { /* ... */ }
public static Connection Open(Config config) => new Connection(config);
public void Dispose() { /* ... */ }
}
[Module]
public partial class MyModule
{
[Singleton]
public partial Config Config { get; }
[Singleton]
[StaticFactory<Connection>]
public partial Connection Connection { get; }
// emits: Connection.Open(Config)
}
public static class Helpers
{
public static Connection Make(Config config) => Connection.Open(config);
}
[Module]
public partial class OtherModule
{
[Singleton]
public partial Config Config { get; }
[Singleton]
[StaticFactory<Helpers>]
public partial Connection Connection { get; }
}
Factory method parameters are resolved from the module graph exactly like constructor parameters. Candidate methods must not be generic, async, or extension methods, and must have a return type matching the property's declared (or [Singleton<TImpl>]/[Transient<TImpl>]) type. If multiple candidates match, pass an explicit method name: [Factory<MyModule>("Build")] or [StaticFactory<Helpers>(nameof(Helpers.Make))].
Diagnostics
| ID | Severity | Description |
|---|---|---|
| YAK001 | Error | A module type is imported more than once across constructors |
| YAK002 | Error | A property has a constructor dependency that cannot be resolved |
| YAK003 | Error | A lifetime property is missing the partial modifier |
| YAK007 | Error | A factory property has multiple matching candidates |
| YAK008 | Error | A factory property has no matching candidate |
| YAK009 | Error | A factory type argument is not a named class, interface, or struct |
| YAK010 | Error | A [Factory<T>] source type is not in the module graph (use [StaticFactory<T>] for public statics) |
How it works
Yak is a source generator that detects classes annotated with [Module]. For each module, it generates a partial class with property getter implementations that handle lifetime management and constructor-based dependency resolution. When a module class implements [Module] interfaces, the generator also produces implementations for interface-declared properties. Everything is handled at compile time without any run-time reflection.
| 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
- 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.