Skip to content

Commit 54885ad

Browse files
author
Nikolay Pianikov
committed
Enhance generated comments for roots, constructors, and Dispose methods with detailed descriptions. Add debugging and API reference guides to documentation. Improve Resolve/ResolveByTag behavior clarifications when handling root arguments.
1 parent 9408a62 commit 54885ad

8 files changed

Lines changed: 245 additions & 11 deletions

File tree

.gitignore

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
**/*.DotSettings.user
66
**/*.csproj.user
77
**/*.log
8+
/tests/Pure.DI.GeneratedCodeReview/Generated
89
benchmarks/data/*.log
910
benchmarks/data/results/*.md
1011
*.suo
@@ -24,4 +25,3 @@ _ReSharper.Caches/
2425
/samples/UnityApp/*.csproj
2526
/samples/UnityApp/*.sln
2627
/samples/UnityApp/*.slnx
27-

Pure.DI.sln

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -127,6 +127,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "EF", "samples\EF\EF.csproj"
127127
EndProject
128128
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "HugeComposition", "samples\HugeComposition\HugeComposition.csproj", "{E1B0FCD1-B34B-4707-A2A2-276D30FC5593}"
129129
EndProject
130+
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Pure.DI.GeneratedCodeReview", "tests\Pure.DI.GeneratedCodeReview\Pure.DI.GeneratedCodeReview.csproj", "{C31B21FA-0339-4184-8A22-BD4B8A78730A}"
131+
EndProject
130132
Global
131133
GlobalSection(SolutionConfigurationPlatforms) = preSolution
132134
Debug|Any CPU = Debug|Any CPU
@@ -257,6 +259,10 @@ Global
257259
{E1B0FCD1-B34B-4707-A2A2-276D30FC5593}.Debug|Any CPU.Build.0 = Debug|Any CPU
258260
{E1B0FCD1-B34B-4707-A2A2-276D30FC5593}.Release|Any CPU.ActiveCfg = Release|Any CPU
259261
{E1B0FCD1-B34B-4707-A2A2-276D30FC5593}.Release|Any CPU.Build.0 = Release|Any CPU
262+
{C31B21FA-0339-4184-8A22-BD4B8A78730A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
263+
{C31B21FA-0339-4184-8A22-BD4B8A78730A}.Debug|Any CPU.Build.0 = Debug|Any CPU
264+
{C31B21FA-0339-4184-8A22-BD4B8A78730A}.Release|Any CPU.ActiveCfg = Release|Any CPU
265+
{C31B21FA-0339-4184-8A22-BD4B8A78730A}.Release|Any CPU.Build.0 = Release|Any CPU
260266
EndGlobalSection
261267
GlobalSection(NestedProjects) = preSolution
262268
{7C9E056B-CBA9-4548-9CDB-C5CE03C491B0} = {8163CDD7-7018-4301-A984-803C3807A6A6}
@@ -289,5 +295,6 @@ Global
289295
{006A6849-86DC-43C7-8297-67249A807439} = {FA80D231-C641-4A49-99C6-0C065D818B07}
290296
{658990AA-FA40-4B28-8E14-2AB8BBD9C701} = {FA80D231-C641-4A49-99C6-0C065D818B07}
291297
{E1B0FCD1-B34B-4707-A2A2-276D30FC5593} = {FA80D231-C641-4A49-99C6-0C065D818B07}
298+
{C31B21FA-0339-4184-8A22-BD4B8A78730A} = {9DF1D09E-863F-4800-9E28-954440EE1601}
292299
EndGlobalSection
293300
EndGlobal

readme/FooterTemplate.md

Lines changed: 207 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,78 @@ The _compositionTypeName_ parameter can be omitted:
5858

5959
</details>
6060

61+
<details>
62+
<summary>How to read generated code</summary>
63+
64+
Generated code is regular C# code. Read it in two passes:
65+
66+
1. First inspect the generated public API of the composition.
67+
2. Then inspect implementation details only when debugging lifetimes, scopes, or performance.
68+
69+
The public API answers the main questions:
70+
71+
- Which composition class was generated from `DI.Setup(...)`.
72+
- Which roots are available as properties or methods.
73+
- Which constructor arguments are required by the composition.
74+
- Whether `Resolve`/`ResolveByTag` methods, scopes, `Dispose`, or `DisposeAsync` were generated.
75+
76+
For example, this setup:
77+
78+
```c#
79+
DI.Setup("Composition")
80+
.Arg<string>("connectionString")
81+
.RootArg<Guid>("userId")
82+
.Bind<IRepository>().To<Repository>()
83+
.Bind<IService>().To<Service>()
84+
.Root<IService>("CreateService");
85+
```
86+
87+
produces a composition API shaped like this:
88+
89+
```c#
90+
partial class Composition
91+
{
92+
public Composition(string connectionString) { ... }
93+
94+
public IService CreateService(Guid userId) { ... }
95+
}
96+
```
97+
98+
The implementation body shows how Pure.DI creates the graph:
99+
100+
- `new Implementation(...)` calls show constructor injection.
101+
- Private fields usually represent cached singleton or scoped instances.
102+
- Lock statements appear when thread-safe access is required.
103+
- Local variables named for per-resolve or per-block lifetimes show reuse inside one generated root body.
104+
- `Dispose` and `DisposeAsync` release tracked singleton and scoped disposable instances.
105+
106+
Use `Hint.FormatCode` only when reading or presenting generated code. It can increase compilation time and memory usage.
107+
108+
</details>
109+
110+
<details>
111+
<summary>Generated API reference</summary>
112+
113+
| Setup element | Generated API |
114+
|----------------------------------------|---------------------------------------------------------------------------------------------------------------------------------------|
115+
| `DI.Setup("Composition")` | A partial class named `Composition`, unless the setup kind prevents class generation. |
116+
| `.Root<T>("Name")` | A public property named `Name`, or a method named `Name` when the root uses root arguments or generic type arguments. |
117+
| `.Root<T>()` | An anonymous private root. It is available only through generated `Resolve`/`ResolveByTag` methods when those methods can resolve it. |
118+
| `.Arg<T>("name")` | A composition constructor parameter when the argument is used by at least one root graph. |
119+
| `.RootArg<T>("name")` | A parameter on root methods that use this value. Roots with root arguments cannot be resolved by `Resolve`/`ResolveByTag`. |
120+
| `Lifetime.Transient` | A new instance is created at each injection site. |
121+
| `Lifetime.Singleton` | A private cached field is generated and reused by the composition. |
122+
| `Lifetime.Scoped` | Scope-related constructors/members are generated and the instance is reused inside a scope. |
123+
| `Lifetime.PerResolve` | A local value is reused during one root or `Resolve` call. |
124+
| `Lifetime.PerBlock` | A local value is reused inside a generated code block. |
125+
| Disposable singleton/scoped dependency | `Dispose` and/or `DisposeAsync` are generated on the composition. |
126+
| `Hint.Resolve = Off` | `Resolve`/`ResolveByTag` methods and anonymous roots are not generated. Use named roots directly. |
127+
| `Hint.ToString = On` | `ToString()` returns a Mermaid class diagram of the composition. |
128+
| `Hint.Comments = Off` | XML documentation comments are not generated for the composition API. |
129+
| `Hint.FormatCode = On` | Generated code is formatted for easier reading. |
130+
131+
</details>
132+
61133
<details>
62134
<summary>Setup arguments</summary>
63135

@@ -1523,6 +1595,17 @@ See also:
15231595
<summary>Comments</summary>
15241596

15251597
Pure.DI can copy comments from setup calls into generated documentation comments for the composition class, composition arguments, and composition roots.
1598+
When no user comment is provided, Pure.DI generates documentation for the generated API so the composition can be inspected from IntelliSense.
1599+
1600+
Generated comments describe:
1601+
1602+
- The composition roots exposed by the generated composition class.
1603+
- The root contract, implementation type, tag, and lifetime.
1604+
- Whether a root is a property or a method.
1605+
- Composition constructor parameters created from used `Arg<T>(...)` values.
1606+
- Root method parameters created from used `RootArg<T>(...)` values.
1607+
- `Resolve`/`ResolveByTag` limitations for roots that require root arguments.
1608+
- `Dispose`/`DisposeAsync` behavior for tracked singleton and scoped disposable instances.
15261609

15271610
Use regular `//` comments before API calls when you want Pure.DI to include the text in the generated documentation:
15281611

@@ -1561,6 +1644,15 @@ public IService Service
15611644

15621645
For other setup calls, such as `Arg<T>(...)`, comments are used as documentation text in the generated constructor documentation. Use regular `//` comments there unless you want XML markup to be shown as text.
15631646

1647+
To suppress generated documentation comments, turn comments off for the setup:
1648+
1649+
```c#
1650+
DI.Setup("Composition")
1651+
.Hint(Hint.Comments, "Off")
1652+
.Bind<IService>().To<Service>()
1653+
.Root<IService>("Service");
1654+
```
1655+
15641656
</details>
15651657

15661658
<details>
@@ -1599,6 +1691,65 @@ flowchart TD
15991691

16001692
</details>
16011693

1694+
<details>
1695+
<summary>Debugging generated code</summary>
1696+
1697+
Use this workflow when you need to inspect or debug the generated composition:
1698+
1699+
1. Save generated files in the project.
1700+
1701+
```xml
1702+
<PropertyGroup>
1703+
<EmitCompilerGeneratedFiles>true</EmitCompilerGeneratedFiles>
1704+
<CompilerGeneratedFilesOutputPath>$(BaseIntermediateOutputPath)Generated</CompilerGeneratedFilesOutputPath>
1705+
</PropertyGroup>
1706+
```
1707+
1708+
2. Enable formatting only while debugging.
1709+
1710+
```c#
1711+
DI.Setup("Composition")
1712+
.Hint(Hint.FormatCode, "On")
1713+
.Bind<IService>().To<Service>()
1714+
.Root<IService>("Service");
1715+
```
1716+
1717+
3. Rebuild the project and open the generated `Composition.g.cs` file under the configured generated files folder.
1718+
1719+
4. Start with the generated public API:
1720+
1721+
- composition constructors;
1722+
- root properties and root methods;
1723+
- `Resolve`/`ResolveByTag` methods;
1724+
- scope factory methods;
1725+
- `Dispose`/`DisposeAsync`.
1726+
1727+
5. Then inspect the root body that creates the graph. Constructor calls show the exact dependency path, private fields show cached singleton/scoped values, and local variables show per-resolve/per-block reuse.
1728+
1729+
6. For a structural view, enable `Hint.ToString` and render the Mermaid diagram:
1730+
1731+
```c#
1732+
DI.Setup("Composition")
1733+
.Hint(Hint.ToString, "On")
1734+
.Bind<IService>().To<Service>()
1735+
.Root<IService>("Service");
1736+
1737+
var diagram = new Composition().ToString();
1738+
```
1739+
1740+
7. If an anonymous root is hard to step through, disable lightweight anonymous roots for debugging:
1741+
1742+
```c#
1743+
DI.Setup("Composition")
1744+
.Hint(Hint.LightweightAnonymousRoot, "Off")
1745+
.Bind<IService>().To<Service>()
1746+
.Root<IService>();
1747+
```
1748+
1749+
Turn `FormatCode`, `ToString`, and extra debugging hints off again when they are no longer needed.
1750+
1751+
</details>
1752+
16021753
## Project template
16031754

16041755
Install the DI template [Pure.DI.Templates](https://www.nuget.org/packages/Pure.DI.Templates)
@@ -1676,6 +1827,62 @@ You can set project properties to save generated files and control their storage
16761827

16771828
</details>
16781829

1830+
<details>
1831+
<summary>Generated code troubleshooting</summary>
1832+
1833+
### Generated files are not visible
1834+
1835+
Set `EmitCompilerGeneratedFiles` to `true`, rebuild the project, and check the generated files folder configured by `CompilerGeneratedFilesOutputPath`. If the folder is empty, run `dotnet build-server shutdown`, rebuild, and check that the project references the `Pure.DI` package.
1836+
1837+
### The root was generated as a method, not a property
1838+
1839+
A root becomes a method when it needs runtime data, such as `RootArg<T>(...)`, or when the root itself is generic. Call it directly and pass the required arguments:
1840+
1841+
```c#
1842+
var service = composition.CreateService(userId);
1843+
```
1844+
1845+
### Resolve methods were not generated
1846+
1847+
Check `Hint.Resolve`. When it is `Off`, `Resolve`/`ResolveByTag` methods are intentionally omitted and anonymous roots are not generated. Use named roots instead:
1848+
1849+
```c#
1850+
var service = composition.Service;
1851+
```
1852+
1853+
### Resolve cannot create a root with root arguments
1854+
1855+
`Resolve`/`ResolveByTag` methods do not have a place to pass root arguments. Use the generated root method directly, or disable `Resolve` with `Hint.Resolve = Off` to avoid warnings and keep the generated API explicit.
1856+
1857+
### A lock appears in generated code
1858+
1859+
Pure.DI generates synchronization for thread-safe access to cached instances when needed. To remove it only when composition access is known to be single-threaded, use:
1860+
1861+
```c#
1862+
DI.Setup("Composition")
1863+
.Hint(Hint.ThreadSafe, "Off");
1864+
```
1865+
1866+
### Dispose or DisposeAsync was generated
1867+
1868+
The composition tracks singleton and scoped instances that implement `IDisposable` or `IAsyncDisposable`. Dispose the composition when it owns such instances:
1869+
1870+
```c#
1871+
using var composition = new Composition();
1872+
```
1873+
1874+
or:
1875+
1876+
```c#
1877+
await using var composition = new Composition();
1878+
```
1879+
1880+
### Generated code is hard to read
1881+
1882+
Temporarily enable `Hint.FormatCode` and save generated files with `EmitCompilerGeneratedFiles`. For graph-level inspection, enable `Hint.ToString` and use the Mermaid diagram.
1883+
1884+
</details>
1885+
16791886
<details>
16801887
<summary>Performance profiling</summary>
16811888

src/Pure.DI.Core/Core/Code/ClassCommenter.cs

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -136,7 +136,19 @@ IReadOnlyCollection<string> CreateRootTerms(Root root)
136136
IReadOnlyCollection<string> CreateRootDescriptions(Root root) =>
137137
root.Source.Comments.Count > 0
138138
? root.Source.Comments.Select(comment => comments.Escape(comments.GetText(comment))).ToList()
139-
: [$"Provides a composition root of type {formatter.FormatRef(root.Node.Type)}."];
139+
: [CreateRootDescription(root)];
140+
141+
string CreateRootDescription(Root root)
142+
{
143+
var description = new StringBuilder();
144+
description.Append($"Provides a composition root of type {formatter.FormatRef(root.Node.Type)}.");
145+
if (root.RootArgs.Length > 0)
146+
{
147+
description.Append($" This root uses root arguments and cannot be resolved by generated {hints.ResolveMethodName}/{hints.ResolveByTagMethodName} methods.");
148+
}
149+
150+
return description.ToString();
151+
}
140152
}
141153

142154
var root = orderedRoots.Find(i => i.IsPublic);

src/Pure.DI.Core/Core/Code/ParameterizedConstructorCommenter.cs

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ public void AddComments(CompositionCode composition, Unit unit)
1313

1414
var code = composition.Code;
1515
code.AppendLine("/// <summary>");
16-
code.AppendLine($"/// This parameterized constructor creates a new instance of <see cref=\"{composition.Name.ClassName}\"/> with arguments.");
16+
code.AppendLine($"/// This parameterized constructor creates a new instance of <see cref=\"{composition.Name.ClassName}\"/> with composition arguments used by the object graph.");
1717
code.AppendLine("/// </summary>");
1818
foreach (var arg in composition.ClassArgs.GetArgsOfKind(ArgKind.Composition))
1919
{
@@ -34,13 +34,13 @@ public void AddComments(CompositionCode composition, Unit unit)
3434
}
3535
else
3636
{
37-
code.AppendLine($"/// <param name=\"{mdArg.ArgName}\">The composition argument of type {formatter.FormatRef(mdArg.Type)}.</param>");
37+
code.AppendLine($"/// <param name=\"{mdArg.ArgName}\">The composition argument of type {formatter.FormatRef(mdArg.Type)}. Only arguments used by roots or their dependencies appear in this constructor.</param>");
3838
}
3939
}
4040

4141
foreach (var arg in composition.SetupContextArgs.Where(i => i.Kind == SetupContextKind.Argument))
4242
{
43-
code.AppendLine($"/// <param name=\"{arg.Name}\">The setup context of type {formatter.FormatRef(arg.Type)}.</param>");
43+
code.AppendLine($"/// <param name=\"{arg.Name}\">The setup context argument of type {formatter.FormatRef(arg.Type)} copied from a dependent setup.</param>");
4444
}
4545
}
4646
}

src/Pure.DI.Core/Core/Code/Parts/ApiMembersBuilder.cs

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -9,8 +9,8 @@ sealed class ApiMembersBuilder(
99
: IClassPartBuilder
1010
{
1111
private const string CommentSummaryStart = "/// <summary>";
12-
private const string CommentSummary = "/// Resolves the composition root.";
13-
private const string CommentSummaryByTag = "/// Resolves the composition root by tag.";
12+
private const string CommentSummary = "/// Resolves a generated composition root that does not require root arguments.";
13+
private const string CommentSummaryByTag = "/// Resolves a generated composition root by tag when the root does not require root arguments.";
1414
private const string CommentSummaryFinish = "/// </summary>";
1515
private const string CommentParamType = "/// <param name=\"type\">The type of the composition root.</param>";
1616
private const string CommentParamTag = "/// <param name=\"tag\">The tag of a composition root.</param>";
@@ -176,7 +176,7 @@ public CompositionCode Build(CompositionCode composition)
176176
private static void FinishComments(Lines apiCode)
177177
{
178178
apiCode.AppendLine("/// <returns>An instance of a composition root.</returns>");
179-
apiCode.AppendLine($"/// <exception cref=\"{Names.CannotResolveExceptionTypeName}\">Will be thrown if the corresponding composition root was not specified. To specify a composition root use API method such as <see cref=\"{Names.IConfigurationTypeName}.Root{{T}}\"/>.</exception>");
179+
apiCode.AppendLine($"/// <exception cref=\"{Names.CannotResolveExceptionTypeName}\">Will be thrown if the corresponding composition root was not specified or if the root requires root arguments. To specify a composition root use API method such as <see cref=\"{Names.IConfigurationTypeName}.Root{{T}}\"/>.</exception>");
180180
apiCode.AppendLine($"/// <seealso cref=\"{Names.IConfigurationTypeName}.RootBind{{T}}\"/>");
181181
apiCode.AppendLine($"/// <seealso cref=\"{Names.IConfigurationTypeName}.Roots{{T}}\"/>");
182182
apiCode.AppendLine($"/// <seealso cref=\"{Names.IConfigurationTypeName}.Builder{{T}}\"/>");

src/Pure.DI.Core/Core/Code/Parts/DisposeMethodBuilder.cs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@ public CompositionCode Build(CompositionCode composition)
2727
if (isCommentsEnabled)
2828
{
2929
code.AppendLine("/// <summary>");
30-
code.AppendLine("/// <inheritdoc/>");
30+
code.AppendLine("/// Disposes tracked singleton and scoped instances created by this composition.");
3131
code.AppendLine("/// </summary>");
3232
}
3333

@@ -79,7 +79,7 @@ public CompositionCode Build(CompositionCode composition)
7979
if (isCommentsEnabled)
8080
{
8181
code.AppendLine("/// <summary>");
82-
code.AppendLine("/// <inheritdoc/>");
82+
code.AppendLine("/// Asynchronously disposes tracked singleton and scoped instances created by this composition.");
8383
code.AppendLine("/// </summary>");
8484
}
8585

src/Pure.DI.Core/Core/Code/RootMethodsCommenter.cs

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,8 @@ sealed class RootMethodsCommenter(
77
{
88
public void AddComments(CompositionCode composition, Root root)
99
{
10-
if (!composition.Hints.IsCommentsEnabled)
10+
var hints = composition.Hints;
11+
if (!hints.IsCommentsEnabled)
1112
{
1213
return;
1314
}
@@ -47,6 +48,13 @@ public void AddComments(CompositionCode composition, Root root)
4748
code.AppendLine("/// </para>");
4849
}
4950

51+
if (root.RootArgs.Length > 0)
52+
{
53+
code.AppendLine("/// <para>");
54+
code.AppendLine($"/// This root requires root arguments and is generated as a method. It cannot be resolved by generated {hints.ResolveMethodName}/{hints.ResolveByTagMethodName} methods.");
55+
code.AppendLine("/// </para>");
56+
}
57+
5058
if (!root.IsPublic)
5159
{
5260
return;

0 commit comments

Comments
 (0)