Skip to content

Commit c3b8330

Browse files
author
Nikolay Pianikov
committed
Update README
1 parent 63c524a commit c3b8330

68 files changed

Lines changed: 654 additions & 413 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

AGENTS.md

Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1642,6 +1642,85 @@ The key points are:
16421642
- The `Dependency` (or `Ordinal`) attribute is used to mark the property for injection
16431643
- The DI automatically injects the dependency when resolving the object graph
16441644

1645+
## Nullable reference types
1646+
1647+
Pure.DI preserves nullable reference type annotations when it reads dependency contracts, builds the graph, and generates composition members.
1648+
Use nullable dependencies for values that are allowed to be absent. A nullable root or composition argument does not get a generated null check, while a non-null reference argument still does.
1649+
A non-null binding can satisfy a nullable dependency request. This is useful for optional constructor parameters, nullable factory results, and nullable collection elements.
1650+
>[!TIP]
1651+
>`T?` means that the consumer can handle `null`; it does not mean that a missing binding is ignored. If no binding or auto-binding can provide the type, Pure.DI still reports the graph error.
1652+
1653+
```c#
1654+
using Shouldly;
1655+
using Pure.DI;
1656+
using System.Collections.Generic;
1657+
using System.Linq;
1658+
1659+
DI.Setup(nameof(Composition))
1660+
.Hint(Hint.Resolve, "Off")
1661+
.Bind<IDatabase>().To<Database>()
1662+
.Bind<IReportService>().To<ReportService>()
1663+
1664+
// Nullable composition argument: no generated null check
1665+
.Arg<string?>("defaultTitle", "title")
1666+
1667+
// Nullable root argument: no generated null check
1668+
.RootArg<string?>("connectionString", "connection")
1669+
1670+
// Composition root
1671+
.Root<IReportService>("CreateReportService");
1672+
1673+
var composition = new Composition(defaultTitle: null);
1674+
var reportService = composition.CreateReportService(connectionString: null);
1675+
1676+
reportService.DefaultTitle.ShouldBeNull();
1677+
reportService.ConnectionString.ShouldBeNull();
1678+
reportService.OptionalDatabase.ShouldNotBeNull();
1679+
reportService.Databases.Count.ShouldBe(1);
1680+
1681+
interface IDatabase;
1682+
1683+
class Database : IDatabase;
1684+
1685+
interface IReportService
1686+
{
1687+
string? DefaultTitle { get; }
1688+
1689+
string? ConnectionString { get; }
1690+
1691+
IDatabase? OptionalDatabase { get; }
1692+
1693+
IReadOnlyList<IDatabase?> Databases { get; }
1694+
}
1695+
1696+
class ReportService(
1697+
[Tag("title")] string? defaultTitle,
1698+
[Tag("connection")] string? connectionString,
1699+
IDatabase? optionalDatabase,
1700+
IEnumerable<IDatabase?> databases)
1701+
: IReportService
1702+
{
1703+
public string? DefaultTitle { get; } = defaultTitle;
1704+
1705+
public string? ConnectionString { get; } = connectionString;
1706+
1707+
public IDatabase? OptionalDatabase { get; } = optionalDatabase;
1708+
1709+
public IReadOnlyList<IDatabase?> Databases { get; } = databases.ToList();
1710+
}
1711+
```
1712+
1713+
To run the above code, the following NuGet packages must be added:
1714+
- [Pure.DI](https://www.nuget.org/packages/Pure.DI)
1715+
- [Shouldly](https://www.nuget.org/packages/Shouldly)
1716+
1717+
Limitations: nullable annotations describe compile-time contracts. They are not runtime validation rules and do not replace explicit domain validation.
1718+
Common pitfalls:
1719+
- Using `T?` to hide a missing binding instead of modelling an optional value.
1720+
- Forgetting tags for nullable primitive values when several values of the same type exist.
1721+
- Assuming `IEnumerable<T?>` changes the lifetime of elements; lifetime still comes from the matched bindings.
1722+
See also: [Composition arguments](composition-arguments.md), [Root arguments](root-arguments.md), [Injection on demand](injection-on-demand.md).
1723+
16451724
## Default values
16461725

16471726
This example shows how to use default values in dependency injection when explicit injection is not possible.

AGENTS_MEDIUM.md

Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1642,6 +1642,85 @@ The key points are:
16421642
- The `Dependency` (or `Ordinal`) attribute is used to mark the property for injection
16431643
- The DI automatically injects the dependency when resolving the object graph
16441644

1645+
## Nullable reference types
1646+
1647+
Pure.DI preserves nullable reference type annotations when it reads dependency contracts, builds the graph, and generates composition members.
1648+
Use nullable dependencies for values that are allowed to be absent. A nullable root or composition argument does not get a generated null check, while a non-null reference argument still does.
1649+
A non-null binding can satisfy a nullable dependency request. This is useful for optional constructor parameters, nullable factory results, and nullable collection elements.
1650+
>[!TIP]
1651+
>`T?` means that the consumer can handle `null`; it does not mean that a missing binding is ignored. If no binding or auto-binding can provide the type, Pure.DI still reports the graph error.
1652+
1653+
```c#
1654+
using Shouldly;
1655+
using Pure.DI;
1656+
using System.Collections.Generic;
1657+
using System.Linq;
1658+
1659+
DI.Setup(nameof(Composition))
1660+
.Hint(Hint.Resolve, "Off")
1661+
.Bind<IDatabase>().To<Database>()
1662+
.Bind<IReportService>().To<ReportService>()
1663+
1664+
// Nullable composition argument: no generated null check
1665+
.Arg<string?>("defaultTitle", "title")
1666+
1667+
// Nullable root argument: no generated null check
1668+
.RootArg<string?>("connectionString", "connection")
1669+
1670+
// Composition root
1671+
.Root<IReportService>("CreateReportService");
1672+
1673+
var composition = new Composition(defaultTitle: null);
1674+
var reportService = composition.CreateReportService(connectionString: null);
1675+
1676+
reportService.DefaultTitle.ShouldBeNull();
1677+
reportService.ConnectionString.ShouldBeNull();
1678+
reportService.OptionalDatabase.ShouldNotBeNull();
1679+
reportService.Databases.Count.ShouldBe(1);
1680+
1681+
interface IDatabase;
1682+
1683+
class Database : IDatabase;
1684+
1685+
interface IReportService
1686+
{
1687+
string? DefaultTitle { get; }
1688+
1689+
string? ConnectionString { get; }
1690+
1691+
IDatabase? OptionalDatabase { get; }
1692+
1693+
IReadOnlyList<IDatabase?> Databases { get; }
1694+
}
1695+
1696+
class ReportService(
1697+
[Tag("title")] string? defaultTitle,
1698+
[Tag("connection")] string? connectionString,
1699+
IDatabase? optionalDatabase,
1700+
IEnumerable<IDatabase?> databases)
1701+
: IReportService
1702+
{
1703+
public string? DefaultTitle { get; } = defaultTitle;
1704+
1705+
public string? ConnectionString { get; } = connectionString;
1706+
1707+
public IDatabase? OptionalDatabase { get; } = optionalDatabase;
1708+
1709+
public IReadOnlyList<IDatabase?> Databases { get; } = databases.ToList();
1710+
}
1711+
```
1712+
1713+
To run the above code, the following NuGet packages must be added:
1714+
- [Pure.DI](https://www.nuget.org/packages/Pure.DI)
1715+
- [Shouldly](https://www.nuget.org/packages/Shouldly)
1716+
1717+
Limitations: nullable annotations describe compile-time contracts. They are not runtime validation rules and do not replace explicit domain validation.
1718+
Common pitfalls:
1719+
- Using `T?` to hide a missing binding instead of modelling an optional value.
1720+
- Forgetting tags for nullable primitive values when several values of the same type exist.
1721+
- Assuming `IEnumerable<T?>` changes the lifetime of elements; lifetime still comes from the matched bindings.
1722+
See also: [Composition arguments](composition-arguments.md), [Root arguments](root-arguments.md), [Injection on demand](injection-on-demand.md).
1723+
16451724
## Default values
16461725

16471726
This example shows how to use default values in dependency injection when explicit injection is not possible.

README.md

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -306,6 +306,7 @@ dotnet run
306306
- [Field injection](readme/field-injection.md)
307307
- [Method injection](readme/method-injection.md)
308308
- [Property injection](readme/property-injection.md)
309+
- [Nullable reference types](readme/nullable-reference-types.md)
309310
- [Default values](readme/default-values.md)
310311
- [Required properties or fields](readme/required-properties-or-fields.md)
311312
- [Overrides](readme/overrides.md)
@@ -2146,8 +2147,8 @@ AI needs to understand the situation it’s in (context). This means knowing det
21462147
| AI context file | Size | Tokens |
21472148
| --------------- | ---- | ------ |
21482149
| [AGENTS_SMALL.md](AGENTS_SMALL.md) | 62KB | 16K |
2149-
| [AGENTS_MEDIUM.md](AGENTS_MEDIUM.md) | 108KB | 27K |
2150-
| [AGENTS.md](AGENTS.md) | 406KB | 104K |
2150+
| [AGENTS_MEDIUM.md](AGENTS_MEDIUM.md) | 111KB | 28K |
2151+
| [AGENTS.md](AGENTS.md) | 409KB | 104K |
21512152

21522153
For different IDEs, you can use the _AGENTS.md_ file as is by simply copying it to the root directory. For use with _JetBrains Rider_ and _Junie_, please refer to [these instructions](https://www.jetbrains.com/help/junie/customize-guidelines.html). For example, you can copy any _AGENTS.md_ file into your project (using _Pure.DI_) as _.junie/guidelines.md._
21532154
## How to contribute to Pure.DI

readme/async-disposable-scope.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -174,13 +174,13 @@ partial class Composition: IDisposable, IAsyncDisposable
174174
[MethodImpl(MethodImplOptions.AggressiveInlining)]
175175
get
176176
{
177-
Func<Session> perBlockFunc596 = new Func<Session>(
177+
Func<Session> perBlockFunc602 = new Func<Session>(
178178
[MethodImpl(MethodImplOptions.AggressiveInlining)]
179179
() =>
180180
{
181181
return new Session(this);
182182
});
183-
return new Program(perBlockFunc596);
183+
return new Program(perBlockFunc602);
184184
}
185185
}
186186

readme/async-enumerable.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -89,14 +89,14 @@ partial class Composition
8989
get
9090
{
9191
[MethodImpl(MethodImplOptions.AggressiveInlining)]
92-
async IAsyncEnumerable<IHealthCheck> EnumerationOf_transientIAsyncEnumerable376()
92+
async IAsyncEnumerable<IHealthCheck> EnumerationOf_transientIAsyncEnumerable382()
9393
{
9494
yield return new MemoryCheck();
9595
yield return new ExternalServiceCheck();
9696
await Task.CompletedTask;
9797
}
9898

99-
return new HealthService(EnumerationOf_transientIAsyncEnumerable376());
99+
return new HealthService(EnumerationOf_transientIAsyncEnumerable382());
100100
}
101101
}
102102
}

readme/auto-scoped.md

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -155,18 +155,18 @@ partial class Composition
155155
[MethodImpl(MethodImplOptions.AggressiveInlining)]
156156
get
157157
{
158-
Func<IListeningSession> perBlockFunc604 = new Func<IListeningSession>(
158+
Func<IListeningSession> perBlockFunc610 = new Func<IListeningSession>(
159159
[MethodImpl(MethodImplOptions.AggressiveInlining)]
160160
() =>
161161
{
162-
IListeningSession transientIListeningSession605;
162+
IListeningSession transientIListeningSession611;
163163
Composition localParentScope = this;
164164
// Create a child scope so scoped services (PlaybackQueue) are unique per session.
165165
var localScope = new Composition(localParentScope);
166-
transientIListeningSession605 = localScope.Session;
167-
return transientIListeningSession605;
166+
transientIListeningSession611 = localScope.Session;
167+
return transientIListeningSession611;
168168
});
169-
return new MusicApp(perBlockFunc604);
169+
return new MusicApp(perBlockFunc610);
170170
}
171171
}
172172

readme/automapper.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -335,7 +335,7 @@ classDiagram
335335
Mapper o-- "Singleton" LoggerFactory : LoggerFactory
336336
Program *-- StudentService : IStudentService
337337
Program o-- "Singleton" ILogger : ILogger
338-
FuncᐸStudentˏPersonᐳ o-- "Singleton" PersonFormatter : IPersonFormatter
338+
FuncᐸStudentˏPersonᐳ o-- "Singleton" PersonFormatter : IPersonFormatterɁ
339339
FuncᐸStudentˏPersonᐳ o-- "Singleton" Mapper : IMapper
340340
namespace AutoMapper {
341341
class IMapper {
@@ -384,7 +384,7 @@ classDiagram
384384
namespace System {
385385
class FuncᐸStudentˏPersonᐳ {
386386
<<delegate>>
387-
+IPersonFormatter Formatter
387+
+IPersonFormatterɁ Formatter
388388
}
389389
class IDisposable {
390390
<<abstract>>

readme/bind-attribute-with-lifetime-and-tag.md

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -89,28 +89,28 @@ partial class Composition
8989
private readonly Object _lock = new Object();
9090
#endif
9191

92-
private IGpu? _singletonIGpu2147483116;
92+
private IGpu? _singletonIGpu2147483112;
9393
private GraphicsAdapter? _singletonGraphicsAdapter62;
9494

9595
public IRenderer Renderer
9696
{
9797
[MethodImpl(MethodImplOptions.AggressiveInlining)]
9898
get
9999
{
100-
if (_singletonIGpu2147483116 is null)
100+
if (_singletonIGpu2147483112 is null)
101101
lock (_lock)
102-
if (_singletonIGpu2147483116 is null)
102+
if (_singletonIGpu2147483112 is null)
103103
{
104104
if (_singletonGraphicsAdapter62 is null)
105105
{
106106
_singletonGraphicsAdapter62 = new GraphicsAdapter();
107107
}
108108

109109
GraphicsAdapter localInstance_1182D1279 = _singletonGraphicsAdapter62;
110-
_singletonIGpu2147483116 = localInstance_1182D1279.HighPerfGpu;
110+
_singletonIGpu2147483112 = localInstance_1182D1279.HighPerfGpu;
111111
}
112112

113-
return new RayTracer(_singletonIGpu2147483116);
113+
return new RayTracer(_singletonIGpu2147483112);
114114
}
115115
}
116116
}

readme/build-up-of-an-existing-generic-object.md

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -106,16 +106,16 @@ partial class Composition
106106
public IFacade<Guid> GetFacade(string userName)
107107
{
108108
if (userName is null) throw new ArgumentNullException(nameof(userName));
109-
UserContext<Guid> transientUserContext511;
109+
UserContext<Guid> transientUserContext517;
110110
// The "BuildUp" method injects dependencies into an existing object.
111111
// This is useful when the object is created externally (e.g., by a UI framework
112112
// or an ORM) or requires specific initialization before injection.
113113
UserContext<Guid> localContext = new UserContext<Guid>();
114-
Guid transientGuid513 = Guid.NewGuid();
114+
Guid transientGuid519 = Guid.NewGuid();
115115
localContext.UserName = userName;
116-
localContext.SetId(transientGuid513);
117-
transientUserContext511 = localContext;
118-
return new Facade<Guid>(transientUserContext511);
116+
localContext.SetId(transientGuid519);
117+
transientUserContext517 = localContext;
118+
return new Facade<Guid>(transientUserContext517);
119119
}
120120
}
121121
```

readme/builder-with-arguments.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -125,7 +125,7 @@ classDiagram
125125
TelemetrySystem --|> ITelemetrySystem
126126
Composition ..> Satellite : Satellite Initialize(Pure.DI.UsageTests.Basics.BuilderWithArgumentsScenario.Satellite buildingInstance, System.Guid id)
127127
Satellite o-- Guid : Argument "id"
128-
Satellite *-- TelemetrySystem : ITelemetrySystem
128+
Satellite *-- TelemetrySystem : ITelemetrySystemɁ
129129
namespace Pure.DI.UsageTests.Basics.BuilderWithArgumentsScenario {
130130
class Composition {
131131
<<partial>>
@@ -136,7 +136,7 @@ classDiagram
136136
}
137137
class Satellite {
138138
<<record>>
139-
+ITelemetrySystem Telemetry
139+
+ITelemetrySystemɁ Telemetry
140140
+SetId(Guid id) : Void
141141
}
142142
class TelemetrySystem {

0 commit comments

Comments
 (0)