Skip to content

Commit 1bf3533

Browse files
authored
fix: resolve a contracted registration's dependencies without its contract (#336)
1 parent 9679373 commit 1bf3533

61 files changed

Lines changed: 503 additions & 567 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.

‎README.md‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -106,6 +106,8 @@ Register<ILogger, CloudLogger>("cloud");
106106
var fileLogger = resolver.GetService<ILogger>("file");
107107
```
108108

109+
A contract names that one registration. The constructor parameters and injected properties of a registration made under a contract resolve the registrations made without one.
110+
109111
### Initialize the Container
110112

111113
Call `SetupIOC()` once during application startup in each assembly that uses `SplatRegistrations`:

‎src/Splat.DependencyInjection.SourceGenerator/CodeGeneration/CodeGenerator.cs‎

Lines changed: 37 additions & 77 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,6 @@
22
// ReactiveUI and Contributors licenses this file to you under the MIT license.
33
// See the LICENSE file in the project root for full license information.
44

5-
using System;
65
using System.Collections.Immutable;
76
using System.Runtime.CompilerServices;
87

@@ -18,6 +17,11 @@ namespace Splat.DependencyInjection.SourceGenerator.CodeGeneration;
1817
/// others share the one closure the compiler creates for <c>resolver</c>.
1918
/// </para>
2019
/// <para>
20+
/// Dependencies are resolved without the registration's contract. A contract names one registration of the type
21+
/// being registered; a constructor parameter or injected property names no contract, so it gets the default
22+
/// registration of its type.
23+
/// </para>
24+
/// <para>
2125
/// A dependency the resolver does not have throws through a helper written once per file. The factory then holds a
2226
/// call rather than the construction and concatenation of an exception, which keeps the code the factory runs on
2327
/// every resolution small.
@@ -31,7 +35,7 @@ internal static class CodeGenerator
3135
/// <summary>The characters a registration typically takes.</summary>
3236
internal const int RegistrationCapacity = 384;
3337

34-
/// <summary>The name of the helpers that throw for an unregistered dependency.</summary>
38+
/// <summary>The name of the helper that throws for an unregistered dependency.</summary>
3539
internal const string ThrowNotRegistered = "ThrowNotRegistered";
3640

3741
/// <summary>The start of a registration on the resolver, before the registered type.</summary>
@@ -74,7 +78,7 @@ internal static partial class SplatRegistrations
7478
static partial void SetupIOCInternal(Splat.IDependencyResolver resolver)
7579
""";
7680

77-
/// <summary>The helper that throws for a dependency registered without a contract.</summary>
81+
/// <summary>The helper that throws for an unregistered dependency.</summary>
7882
private const string ThrowNotRegisteredMethod = $$"""
7983
/// <summary>Throws for a dependency the resolver has no registration for.</summary>
8084
/// <typeparam name="T">The type of the dependency.</typeparam>
@@ -86,33 +90,6 @@ static partial void SetupIOCInternal(Splat.IDependencyResolver resolver)
8690
}
8791
""";
8892

89-
/// <summary>The helper that throws for a dependency registered under a contract.</summary>
90-
private const string ThrowNotRegisteredWithContractMethod = $$"""
91-
/// <summary>Throws for a dependency the resolver has no registration for under a contract.</summary>
92-
/// <typeparam name="T">The type of the dependency.</typeparam>
93-
/// <param name="typeName">The name of the dependency's type.</param>
94-
/// <param name="contract">The contract the dependency was asked for under.</param>
95-
/// <returns>Never returns.</returns>
96-
private static T {{ThrowNotRegistered}}<T>(string typeName, string contract)
97-
{
98-
throw new global::System.InvalidOperationException("Dependency '" + typeName + "' with contract " + contract + " not registered with Splat resolver.");
99-
}
100-
""";
101-
102-
/// <summary>Which throw helpers the written registrations call.</summary>
103-
[Flags]
104-
internal enum ThrowHelpers
105-
{
106-
/// <summary>No helper is called.</summary>
107-
None = 0,
108-
109-
/// <summary>The helper for a dependency without a contract is called.</summary>
110-
WithoutContract = 1 << 0,
111-
112-
/// <summary>The helper for a dependency under a contract is called.</summary>
113-
WithContract = 1 << 1,
114-
}
115-
11693
/// <summary>Writes the registrations file.</summary>
11794
/// <param name="registrations">The registrations, in source order.</param>
11895
/// <returns>The source.</returns>
@@ -131,7 +108,7 @@ internal static string Generate(ImmutableArray<RegistrationInfo> registrations)
131108
.Lines(MethodDeclaration)
132109
.OpenBlock();
133110

134-
var used = ThrowHelpers.None;
111+
var usesThrowHelper = false;
135112
var written = 0;
136113
var lazySingletons = 0;
137114
foreach (var registration in registrations)
@@ -143,7 +120,7 @@ internal static string Generate(ImmutableArray<RegistrationInfo> registrations)
143120

144121
SeparateRegistration(writer, written);
145122
written++;
146-
AppendTransient(writer, registration, ref used);
123+
AppendTransient(writer, registration, ref usesThrowHelper);
147124
}
148125

149126
foreach (var registration in registrations)
@@ -155,42 +132,42 @@ internal static string Generate(ImmutableArray<RegistrationInfo> registrations)
155132

156133
SeparateRegistration(writer, written);
157134
written++;
158-
AppendLazySingleton(writer, registration, lazySingletons, ref used);
135+
AppendLazySingleton(writer, registration, lazySingletons, ref usesThrowHelper);
159136
lazySingletons++;
160137
}
161138

162139
_ = writer.CloseBlock();
163-
AppendThrowHelpers(writer, used);
140+
AppendThrowHelper(writer, usesThrowHelper);
164141
return writer.CloseBlock().CloseBlock().ToStringAndReturn();
165142
}
166143

167144
/// <summary>Writes a transient registration.</summary>
168145
/// <param name="writer">The writer, at the method body's level.</param>
169146
/// <param name="registration">The registration.</param>
170-
/// <param name="used">The throw helpers called so far, updated with those this registration calls.</param>
147+
/// <param name="usesThrowHelper">Set when this registration calls the throw helper.</param>
171148
[MethodImpl(MethodImplOptions.AggressiveInlining)]
172-
internal static void AppendTransient(SourceWriter writer, RegistrationInfo registration, ref ThrowHelpers used) =>
149+
internal static void AppendTransient(SourceWriter writer, RegistrationInfo registration, ref bool usesThrowHelper) =>
173150
AppendFactoryCall(
174151
writer.Append(RegisterOpen).Append(registration.InterfaceTypeFullName).Append(">("),
175152
registration,
176153
registration.ContractValue,
177-
ref used);
154+
ref usesThrowHelper);
178155

179156
/// <summary>Writes a lazy singleton registration: the lazy value, and the lazy and its value registered.</summary>
180157
/// <param name="writer">The writer, at the method body's level.</param>
181158
/// <param name="registration">The registration.</param>
182159
/// <param name="index">The number of lazy singletons written before this one, which names its local.</param>
183-
/// <param name="used">The throw helpers called so far, updated with those this registration calls.</param>
160+
/// <param name="usesThrowHelper">Set when this registration calls the throw helper.</param>
184161
/// <remarks>
185162
/// Each lazy is a local of the method itself, named by its position, rather than of a block of its own. The
186163
/// compiler keeps every captured local of one scope in one closure object, and <c>resolver</c> already needs one,
187164
/// so the lambdas that read the lazy add no closure; a block per registration would add one each.
188165
/// </remarks>
189-
internal static void AppendLazySingleton(SourceWriter writer, RegistrationInfo registration, int index, ref ThrowHelpers used)
166+
internal static void AppendLazySingleton(SourceWriter writer, RegistrationInfo registration, int index, ref bool usesThrowHelper)
190167
{
191168
var serviceType = registration.InterfaceTypeFullName;
192169
_ = writer.Append("var ").Append(LazyLocal).Append(index).Append(" = new ").Append(LazyTypeOpen).Append(serviceType).Append(">(");
193-
AppendFactoryCall(writer, registration, registration.LazyThreadSafetyMode, ref used);
170+
AppendFactoryCall(writer, registration, registration.LazyThreadSafetyMode, ref usesThrowHelper);
194171

195172
_ = writer.Append(RegisterOpen).Append(LazyTypeOpen).Append(serviceType).Append(">>(() => ").Append(LazyLocal).Append(index);
196173
_ = AppendTrailingArgument(writer, registration.ContractValue).Line(");");
@@ -203,12 +180,12 @@ internal static void AppendLazySingleton(SourceWriter writer, RegistrationInfo r
203180
/// <param name="writer">The writer, after the call's opening parenthesis.</param>
204181
/// <param name="registration">The registration.</param>
205182
/// <param name="lastArgument">The argument after the factory, as C# source; <see langword="null"/> for none.</param>
206-
/// <param name="used">The throw helpers called so far, updated with those the factory calls.</param>
183+
/// <param name="usesThrowHelper">Set when the factory calls the throw helper.</param>
207184
/// <remarks>
208185
/// A factory with nothing to resolve stays on the call's line. Otherwise each argument takes its own line, one
209186
/// level deeper, and the call closes after the last of them.
210187
/// </remarks>
211-
internal static void AppendFactoryCall(SourceWriter writer, RegistrationInfo registration, string? lastArgument, ref ThrowHelpers used)
188+
internal static void AppendFactoryCall(SourceWriter writer, RegistrationInfo registration, string? lastArgument, ref bool usesThrowHelper)
212189
{
213190
if (registration.ConstructorParameters.Length == 0 && registration.PropertyInjections.Length == 0)
214191
{
@@ -217,7 +194,7 @@ internal static void AppendFactoryCall(SourceWriter writer, RegistrationInfo reg
217194
return;
218195
}
219196

220-
AppendFactory(writer.OpenContinuation(), registration, ref used);
197+
AppendFactory(writer.OpenContinuation(), registration, ref usesThrowHelper);
221198
if (lastArgument is not null)
222199
{
223200
_ = writer.ArgumentSeparator().Append(lastArgument);
@@ -229,11 +206,10 @@ internal static void AppendFactoryCall(SourceWriter writer, RegistrationInfo reg
229206
/// <summary>Writes the lambda that constructs a registration's type, with its dependencies one per line.</summary>
230207
/// <param name="writer">The writer, at the start of a line.</param>
231208
/// <param name="registration">The registration.</param>
232-
/// <param name="used">The throw helpers called so far, updated with those the factory calls.</param>
209+
/// <param name="usesThrowHelper">Set when the factory calls the throw helper.</param>
233210
/// <remarks>Leaves the writer on the lambda's last line, for the caller to finish.</remarks>
234-
internal static void AppendFactory(SourceWriter writer, RegistrationInfo registration, ref ThrowHelpers used)
211+
internal static void AppendFactory(SourceWriter writer, RegistrationInfo registration, ref bool usesThrowHelper)
235212
{
236-
var contract = registration.ContractValue;
237213
_ = writer.Append("() => new ").Append(registration.ConcreteTypeFullName).Append('(');
238214

239215
var parameters = registration.ConstructorParameters;
@@ -247,7 +223,7 @@ internal static void AppendFactory(SourceWriter writer, RegistrationInfo registr
247223
_ = writer.ArgumentSeparator();
248224
}
249225

250-
AppendParameter(writer, parameters[i], contract, ref used);
226+
AppendParameter(writer, parameters[i], ref usesThrowHelper);
251227
}
252228

253229
_ = writer.Outdent();
@@ -266,7 +242,7 @@ internal static void AppendFactory(SourceWriter writer, RegistrationInfo registr
266242
{
267243
var property = properties[i];
268244
_ = writer.Append(property.PropertyName).Append(" = ");
269-
AppendService(writer, property.TypeFullName, contract, ref used);
245+
AppendService(writer, property.TypeFullName, ref usesThrowHelper);
270246
_ = writer.ArgumentSeparator();
271247
}
272248

@@ -276,55 +252,39 @@ internal static void AppendFactory(SourceWriter writer, RegistrationInfo registr
276252
/// <summary>Writes the expression that resolves a constructor parameter.</summary>
277253
/// <param name="writer">The writer.</param>
278254
/// <param name="parameter">The parameter.</param>
279-
/// <param name="contract">The registration's contract, as C# source; <see langword="null"/> for none.</param>
280-
/// <param name="used">The throw helpers called so far, updated with the one this parameter calls.</param>
255+
/// <param name="usesThrowHelper">Set when the parameter calls the throw helper.</param>
281256
/// <remarks>A collection resolves to every registration, which may be none, so it never throws.</remarks>
282-
internal static void AppendParameter(SourceWriter writer, in ConstructorParameter parameter, string? contract, ref ThrowHelpers used)
257+
internal static void AppendParameter(SourceWriter writer, in ConstructorParameter parameter, ref bool usesThrowHelper)
283258
{
284259
if (parameter.Kind != DependencyKind.Collection)
285260
{
286-
AppendService(writer, parameter.TypeFullName, contract, ref used);
261+
AppendService(writer, parameter.TypeFullName, ref usesThrowHelper);
287262
return;
288263
}
289264

290-
_ = writer.Append("resolver.GetServices<").Append(parameter.InnerTypeFullName!).Append(">(");
291-
_ = contract is null ? writer.Append(')') : writer.Append(contract).Append(')');
265+
_ = writer.Append("resolver.GetServices<").Append(parameter.InnerTypeFullName!).Append(">()");
292266
}
293267

294268
/// <summary>Writes the expression that resolves one service, throwing when it is not registered.</summary>
295269
/// <param name="writer">The writer.</param>
296270
/// <param name="typeFullName">The fully qualified type of the service.</param>
297-
/// <param name="contract">The registration's contract, as C# source; <see langword="null"/> for none.</param>
298-
/// <param name="used">The throw helpers called so far, updated with the one this service calls.</param>
299-
internal static void AppendService(SourceWriter writer, string typeFullName, string? contract, ref ThrowHelpers used)
271+
/// <param name="usesThrowHelper">Set, as the expression calls the throw helper.</param>
272+
internal static void AppendService(SourceWriter writer, string typeFullName, ref bool usesThrowHelper)
300273
{
301-
_ = writer.Append("resolver.GetService<").Append(typeFullName).Append(">(");
302-
if (contract is null)
303-
{
304-
used |= ThrowHelpers.WithoutContract;
305-
_ = writer.Append(") ?? ").Append(ThrowNotRegistered).Append('<').Append(typeFullName).Append(">(\"").Append(typeFullName).Append("\")");
306-
return;
307-
}
308-
309-
used |= ThrowHelpers.WithContract;
310-
_ = writer.Append(contract).Append(") ?? ").Append(ThrowNotRegistered).Append('<').Append(typeFullName).Append(">(\"")
311-
.Append(typeFullName).Append("\", ").Append(contract).Append(')');
274+
usesThrowHelper = true;
275+
_ = writer.Append("resolver.GetService<").Append(typeFullName).Append(">() ?? ").Append(ThrowNotRegistered)
276+
.Append('<').Append(typeFullName).Append(">(\"").Append(typeFullName).Append("\")");
312277
}
313278

314-
/// <summary>Writes the throw helpers the registrations call, after the registration method.</summary>
279+
/// <summary>Writes the throw helper after the registration method, when a registration calls it.</summary>
315280
/// <param name="writer">The writer, at the class body's level.</param>
316-
/// <param name="used">The helpers the registrations call.</param>
317-
internal static void AppendThrowHelpers(SourceWriter writer, ThrowHelpers used)
281+
/// <param name="usesThrowHelper">Whether a registration calls the helper.</param>
282+
internal static void AppendThrowHelper(SourceWriter writer, bool usesThrowHelper)
318283
{
319-
if ((used & ThrowHelpers.WithoutContract) != 0)
284+
if (usesThrowHelper)
320285
{
321286
_ = writer.BlankLine().Lines(ThrowNotRegisteredMethod);
322287
}
323-
324-
if ((used & ThrowHelpers.WithContract) != 0)
325-
{
326-
_ = writer.BlankLine().Lines(ThrowNotRegisteredWithContractMethod);
327-
}
328288
}
329289

330290
/// <summary>Writes a trailing argument when there is one.</summary>

0 commit comments

Comments
 (0)