Oddhouse.Attic
1.0.1
dotnet add package Oddhouse.Attic --version 1.0.1
NuGet\Install-Package Oddhouse.Attic -Version 1.0.1
<PackageReference Include="Oddhouse.Attic" Version="1.0.1" />
<PackageVersion Include="Oddhouse.Attic" Version="1.0.1" />
<PackageReference Include="Oddhouse.Attic" />
paket add Oddhouse.Attic --version 1.0.1
#r "nuget: Oddhouse.Attic, 1.0.1"
#:package Oddhouse.Attic@1.0.1
#addin nuget:?package=Oddhouse.Attic&version=1.0.1
#tool nuget:?package=Oddhouse.Attic&version=1.0.1
Oddhouse.Attic
A dependency injection container with support for constructing objects from string representations. Useful when dependency graphs need to be configured through text — config files, databases, or user input — without writing registration code for each combination.
Basic Usage
Register types against the static root container and resolve them anywhere:
// Register
Attic.Register<ILogger, ConsoleLogger>();
Attic.Register<IDatabase, SqlDatabase>();
Attic.Register<IAppService, AppService>();
// Resolve — AppService is constructed with ILogger and IDatabase injected
var result = Attic.Resolve<IAppService>();
if (result.Success)
{
result.Value.Run();
}
Constructor parameters are resolved automatically. AppService only needs to declare them:
public class AppService : IAppService
{
public AppService(ILogger logger, IDatabase database) { ... }
}
Registration
Three ways to register a dependency:
// Map an interface to an implementation type (constructed via injection)
Attic.Register<IService, ServiceImpl>();
// Use a factory delegate
Attic.Register<IService>(() => new ServiceImpl(Environment.GetEnvironmentVariable("KEY")));
// Register a pre-existing instance
Attic.Register<IService>(myInstance);
The default scope is SingleInstance — one instance is created and reused. Pass an InstanceScope to change this:
Attic.Register<IService, ServiceImpl>(InstanceScope.Transient);
String-based Registration
Dependencies can also be described as strings and registered without writing factory code:
Attic.RegisterString<IService>("new MyApp.ServiceImpl('connection-string', timeout: 30)");
The string is parsed and validated at registration time. At resolution time the container constructs the object, injecting any container-registered dependencies via $:
// 'logger' is resolved from the container; the timeout is a literal value
Attic.RegisterString<IService>("new MyApp.ServiceImpl($, timeout: 30)");
Strings can come from any text source:
string value = configuration["Service"];
Attic.RegisterString<IService>(value);
See the DependencyString Format chapter at the end of this document for more information
Lifetime Scopes
A lifetime scope inherits all registrations from its parent container and can add its own. Disposing it disposes the instances it owns:
using var scope = Attic.BeginLifetimeScope();
// Register a per-request instance on the scope
scope.Register<ICurrentUser>(currentUser);
// Resolves AppService with ICurrentUser from the scope, everything else from the root
var result = scope.Resolve<IAppService>();
Use InstanceScope.SingleInstancePerLifetimeScope to give each scope its own instance of a type:
Attic.Register<IUnitOfWork, UnitOfWork>(InstanceScope.SingleInstancePerLifetimeScope);
Validation
Check the entire registration graph at startup to catch missing or misconfigured dependencies early:
var result = Attic.CanCreateRegisteredTypes();
if (!result.Success)
throw new Exception($"Container misconfigured: {result.Error}");
You can also check a specific type or string without resolving it:
OperationResult canResolve = Attic.CanResolve<IService>();
OperationResult canCreate = Attic.CanCreateWithInjection<ServiceImpl>();
OperationResult canBuild = Attic.CanCreateFromString<IService>("new MyApp.ServiceImpl()");
OperationResult
All methods return OperationResult or OperationResult<T> instead of throwing on failure:
var result = Attic.Resolve<IService>();
if (result.Success)
Use(result.Value);
else
Log(result.Error);
// Implicit bool conversion works too
if (Attic.CanResolve<IService>())
...
Multiple Containers
Attic.Root is a convenience for the common single-container case. For more control, create independent containers directly:
IContainer container = Attic.CreateContainer();
container.Register<IService, ServiceImpl>();
ILifetimeScope scope = container.BeginLifetimeScope();
DependencyString Format
A DependencyString is a textual representation of a value or object that the container can construct. It is parsed from a plain string and supports primitives, object construction, collection initializers, property initializers, and dependency injection.
Whitespace between tokens is always ignored.
Primitives
Boolean
true
false
Assigns to any bool or bool? target.
Integer
42
-7
Parsed as long. Assignable to byte, sbyte, short, ushort, int, uint, long, ulong, decimal, float, double, and any enum whose underlying type is an integer type.
Decimal
3.14
-0.5
Parsed as decimal. Assignable to decimal, float, and double.
String
'hello world'
"hello world"
Both single and double quotes are supported. Use \ to escape any character, including the quote character itself.
'it\'s a string'
"say \"hello\""
A single-character string is also assignable to char. String values can additionally be assigned to any type that has a TypeConverter capable of converting from string (e.g. Guid, Uri).
DateTime
Dates are parsed using CultureInfo.InvariantCulture. The following formats are supported:
| Format | Kind |
|---|---|
2025-01-31 |
Local |
2025-01-31 14:30 |
Local |
2025-01-31 14:30:00 |
Local |
2025-01-31 14:30:00.123456 |
Local |
2025-01-31T14:30:00+01:00 |
Local with offset |
2025-01-31T14:30:00.123456+01:00 |
Local with offset |
2025-01-31T14:30:00Z |
UTC |
2025-01-31T14:30:00.123456Z |
UTC |
Assignable to DateTime and DateTimeOffset.
TimeSpan
1.02:03:04
00:30:00
Parsed using the standard .NET TimeSpan.Parse format with CultureInfo.InvariantCulture. Assignable to TimeSpan.
Null
null
Assignable to any reference type or nullable value type (T?).
Dependency Injection
$
Resolves the value from the container. The type to resolve is inferred from context (e.g. the constructor parameter type or registered dependency type). This cannot be used when creating a DependencyString outside of a container context.
Object Construction
Basic construction
TypeName(arg1, arg2)
new TypeName(arg1, arg2)
The new keyword is optional when arguments are present; both forms behave identically. Each argument is itself a DependencyString and follows the same format rules recursively.
Omitting the type name
When the target type can be inferred from context (e.g. a constructor parameter type), the type name may be omitted:
(arg1, arg2)
new (arg1, arg2)
Named arguments
Arguments can be named using name: value syntax:
TypeName(width: 800, height: 600)
Positional arguments must come before named arguments, matching the same rules as C#.
Type-only reference (no new)
A bare type name without parentheses or braces resolves the value from the container. If no registration is found, constructor injection is attempted:
TypeName
Collection initializer
new TypeName { element1, element2, element3 }
Elements are added by calling the type's public Add method after construction. The element type is inferred from the type's IEnumerable<T> interface. Can be combined with constructor arguments:
new TypeName(arg1) { element1, element2 }
Property initializer
Named entries in { } set public writable properties after construction:
new TypeName { PropertyA = value1, PropertyB = value2 }
Combined
Constructor arguments, properties, and collection elements can all be combined. Properties and elements are distinguished by whether a name is present:
new TypeName(arg1) { element1, element2, PropertyA = value1 }
Type Names
Built-in keywords
The C# primitive type keywords are supported: bool, byte, sbyte, char, decimal, double, float, int, uint, long, ulong, short, ushort, string, object.
Additionally, List and Dictionary can be used as shorthand for System.Collections.Generic.List<T> and System.Collections.Generic.Dictionary<TKey, TValue>.
Fully qualified names
System.DateTime()
System.Text.StringBuilder()
Nested types use + as the separator (or . is also accepted by the parser):
Outer+Inner()
With assembly name
An assembly name can be appended after a comma:
My.Namespace.MyType, MyAssembly
Alternatively, the type can be wrapped in square brackets to disambiguate in nested positions:
[My.Namespace.MyType, MyAssembly]()
Generic types
Generic arguments are specified with <>:
new List<string>()
new Dictionary<string, int>()
In nested positions (e.g. a generic argument that itself has an assembly name), wrap the inner type in square brackets:
new [System.Collections.Generic.List<[System.Int32, mscorlib]>, mscorlib]()
Arrays
Append [] to any type name:
new string[] { 'a', 'b', 'c' }
new int[][] { }
When the element type can be inferred from context, the type name can be omitted:
new [] { 'a', 'b', 'c' }
Full Examples
// Primitives
42
3.14
true
'hello'
2025-06-01
00:15:00
null
// Simple construction
new System.Text.StringBuilder()
new System.Text.StringBuilder(64)
// Named arguments
new System.Text.StringBuilder(capacity: 64)
// Resolve from container
MyApp.Services.ILogger
// Generic collection
new List<string> { 'one', 'two', 'three' }
// Array
new string[] { 'one', 'two', 'three' }
// Constructor + properties + elements
new MyApp.Config(true) { Timeout = 30, 'item1', 'item2' }
// Assembly-qualified type
[MyApp.Services.Logger, MyApp]()
// Inject a dependency from the container as a constructor argument
new MyApp.Service($, 'config-value')
| Product | Versions Compatible and additional computed target framework versions. |
|---|---|
| .NET | net9.0 is compatible. 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. |
-
net9.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.
Support for open generics