Repository navigation
Expand file tree
/
Copy pathServiceCollectionExtensions.Decorators.GraphPlanning.Tests.cs
More file actions
291 lines (278 loc) · 14.6 KB
/
Copy pathServiceCollectionExtensions.Decorators.GraphPlanning.Tests.cs
File metadata and controls
291 lines (278 loc) · 14.6 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
using Mammoth.Extensions.DependencyInjection.Configuration;
using Microsoft.Extensions.DependencyInjection;
namespace Mammoth.Extensions.DependencyInjection.Tests;
[TestClass]
public class DecoratedGraphPlanningRegressionTests
{
public static IEnumerable<object[]> GraphCases()
{
foreach (var type in new[] { typeof(Rejected), typeof(RejectedEnumerable), typeof(Selected), typeof(SelectedEnumerable) })
foreach (var kind in new[] { "native", "snapshot", "diagnostics" })
foreach (var lifetime in new[] { ServiceLifetime.Transient, ServiceLifetime.Scoped, ServiceLifetime.Singleton })
foreach (var keyed in new[] { false, true })
foreach (var layers in new[] { 1, 2 })
yield return [type, kind, lifetime, keyed, layers];
}
[TestMethod]
[DynamicData(nameof(GraphCases))]
public void OriginalGraphsFailBeforeActivationAndValidGraphsActivateOnlyOnce(
Type type, string kind, ServiceLifetime lifetime, bool keyed, int layers)
{
foreach (var closed in new[] { false, true })
foreach (var valid in new[] { false, true })
{
var nativeCounts = new Counts();
var counts = new Counts();
var nativeServices = Configure(nativeCounts);
var services = Configure(counts);
using var native = nativeServices.BuildServiceProvider();
for (var index = 0; index < layers; index++) services.Decorate<IWork, Forwarder>();
// Native type originals remain visible to ValidateOnBuild, which
// also closes their open-generic dependencies while planning the root.
if (!valid && kind == "diagnostics")
{
var nativeError = Assert.ThrowsExactly<AggregateException>(() => nativeServices.BuildServiceProvider(
new ServiceProviderOptions { ValidateOnBuild = true }));
var error = Assert.ThrowsExactly<AggregateException>(() => Build(services, kind));
StringAssert.Contains(nativeError.ToString(), nameof(MissingOther));
StringAssert.Contains(error.ToString(), nameof(MissingOther));
Assert.AreEqual(0, counts.Parts + counts.Broken + counts.Roots + counts.Wrappers);
continue;
}
using var provider = Build(services, kind);
// A caller's later mutation must not repair the already-built graph.
services.AddSingleton<MissingOther>();
using var nativeScope = native.CreateScope();
using var scope = provider.CreateScope();
for (var repeat = 0; repeat < 2; repeat++)
{
if (!valid)
{
var nativeError = Assert.ThrowsExactly<InvalidOperationException>(() => Resolve(nativeScope.ServiceProvider, keyed));
var error = Assert.ThrowsExactly<InvalidOperationException>(() => Resolve(scope.ServiceProvider, keyed));
StringAssert.Contains(nativeError.Message, nameof(MissingOther));
StringAssert.Contains(error.Message, nameof(MissingOther));
Assert.AreEqual(0, nativeCounts.Parts + nativeCounts.Broken + nativeCounts.Roots);
Assert.AreEqual(0, counts.Parts + counts.Broken + counts.Roots + counts.Wrappers,
"Graph errors must be detected before executing any application factories or constructors.");
}
else
{
var expected = Resolve(nativeScope.ServiceProvider, keyed);
var result = Resolve(scope.ServiceProvider, keyed);
Assert.AreEqual(expected.Value, result.Value);
Assert.AreEqual(nativeCounts.Parts, counts.Parts);
Assert.AreEqual(nativeCounts.Broken, counts.Broken);
Assert.AreEqual(nativeCounts.Roots, counts.Roots);
Assert.AreEqual(counts.Roots * layers, counts.Wrappers);
}
}
IServiceCollection Configure(Counts tracker)
{
var collection = Services(tracker);
if (valid) collection.AddSingleton<MissingOther>();
collection.Add(closed ? ServiceDescriptor.Transient<IBroken<int>, Broken<int>>()
: ServiceDescriptor.Transient(typeof(IBroken<>), typeof(Broken<>)));
collection.Add(keyed ? ServiceDescriptor.DescribeKeyed(typeof(IWork), "blue", type, lifetime)
: ServiceDescriptor.Describe(typeof(IWork), type, lifetime));
return collection;
}
}
}
[TestMethod]
[DataRow("native", false)]
[DataRow("native", true)]
[DataRow("snapshot", false)]
[DataRow("snapshot", true)]
[DataRow("diagnostics", false)]
[DataRow("diagnostics", true)]
public void NonemptyDependsOnFactoriesKeepTheirSeparateRejectedCandidatePolicy(string kind, bool keyed)
{
var counts = new Counts();
var services = Services(counts);
services.AddTransient(typeof(IBroken<>), typeof(Broken<>));
Dependency[] map = [Dependency.OnValue("unused", 1)];
if (keyed) services.AddKeyedTransient<IWork, Rejected>("blue", map);
else services.AddTransient<IWork, Rejected>(map);
services.Decorate<IWork, Forwarder>();
using var provider = Build(services, kind);
using var scope = provider.CreateScope();
Assert.AreEqual("valid", Resolve(scope.ServiceProvider, keyed).Value);
Assert.AreEqual(1, counts.Parts);
Assert.AreEqual(0, counts.Broken);
}
[TestMethod]
[DataRow("native", false)]
[DataRow("native", true)]
[DataRow("snapshot", false)]
[DataRow("snapshot", true)]
[DataRow("diagnostics", false)]
[DataRow("diagnostics", true)]
public void OpaqueDependencyFactoriesStillRunOnlyAtRealResolutionAndKeepTheirException(string kind, bool keyed)
{
var nativeCounts = new Counts();
var counts = new Counts();
var failure = new InvalidOperationException("Application dependency failure");
using var native = Configure(nativeCounts).BuildServiceProvider();
var services = Configure(counts);
services.Decorate<IWork, Forwarder>();
using var provider = Build(services, kind);
using var nativeScope = native.CreateScope();
using var scope = provider.CreateScope();
Assert.AreSame(failure, Assert.ThrowsExactly<InvalidOperationException>(() => Resolve(nativeScope.ServiceProvider, keyed)));
Assert.AreSame(failure, Assert.ThrowsExactly<InvalidOperationException>(() => Resolve(scope.ServiceProvider, keyed)));
Assert.AreEqual(1, counts.Parts);
Assert.AreEqual(nativeCounts.Parts, counts.Parts);
Assert.AreEqual(1, counts.Broken);
Assert.AreEqual(nativeCounts.Broken, counts.Broken);
Assert.AreEqual(0, counts.Roots + counts.Wrappers);
IServiceCollection Configure(Counts tracker)
{
var collection = Services(tracker);
collection.AddTransient<IBroken<int>>(_ => { tracker.Broken++; throw failure; });
if (keyed) collection.AddKeyedTransient<IWork, Selected>("blue");
else collection.AddTransient<IWork, Selected>();
return collection;
}
}
[TestMethod]
[DataRow("native", false)]
[DataRow("native", true)]
[DataRow("snapshot", false)]
[DataRow("snapshot", true)]
[DataRow("diagnostics", false)]
[DataRow("diagnostics", true)]
public void EarlierInvalidEnumerableRegistrationIsNotHiddenByLastValidBinding(string kind, bool keyed)
{
var nativeCounts = new Counts();
var counts = new Counts();
using var native = Configure(nativeCounts).BuildServiceProvider();
var services = Configure(counts);
services.Decorate<IWork, Forwarder>();
services.Decorate<IWork, Forwarder>();
if (kind == "diagnostics")
{
Assert.ThrowsExactly<AggregateException>(() => Configure(nativeCounts).BuildServiceProvider(
new ServiceProviderOptions { ValidateOnBuild = true }));
StringAssert.Contains(Assert.ThrowsExactly<AggregateException>(() => Build(services, kind)).ToString(), nameof(MissingOther));
Assert.AreEqual(0, counts.Parts + counts.Broken + counts.Roots + counts.Wrappers);
return;
}
using var provider = Build(services, kind);
using var scope = provider.CreateScope();
StringAssert.Contains(Assert.ThrowsExactly<InvalidOperationException>(() => Resolve(native, keyed)).Message, nameof(MissingOther));
StringAssert.Contains(Assert.ThrowsExactly<InvalidOperationException>(() => Resolve(scope.ServiceProvider, keyed)).Message, nameof(MissingOther));
Assert.AreEqual(0, nativeCounts.Parts + nativeCounts.Broken + nativeCounts.Roots);
Assert.AreEqual(0, counts.Parts + counts.Broken + counts.Roots + counts.Wrappers);
IServiceCollection Configure(Counts tracker)
{
var collection = Services(tracker);
collection.AddTransient(typeof(IBroken<>), typeof(Broken<>));
collection.AddTransient(typeof(IBroken<>), typeof(Healthy<>));
if (keyed) collection.AddKeyedTransient<IWork, SelectedEnumerable>("blue");
else collection.AddTransient<IWork, SelectedEnumerable>();
return collection;
}
}
[TestMethod]
[DataRow("native", false)]
[DataRow("native", true)]
[DataRow("snapshot", false)]
[DataRow("snapshot", true)]
[DataRow("diagnostics", false)]
[DataRow("diagnostics", true)]
public void SuccessfulGraphCheckDoesNotLeakAcrossProvidersSharingTheOriginalDescriptor(string kind, bool keyed)
{
var original = keyed ? ServiceDescriptor.KeyedTransient<IWork, Selected>("blue")
: ServiceDescriptor.Transient<IWork, Selected>();
var validCounts = new Counts();
var invalidCounts = new Counts();
using var valid = Build(Configure(validCounts, complete: true), kind);
if (kind == "diagnostics")
{
StringAssert.Contains(Assert.ThrowsExactly<AggregateException>(() => Build(Configure(invalidCounts, complete: false), kind)).ToString(), nameof(MissingOther));
Assert.AreEqual(0, invalidCounts.Parts + invalidCounts.Broken + invalidCounts.Roots + invalidCounts.Wrappers);
using var validOnlyScope = valid.CreateScope();
Assert.AreEqual("selected", Resolve(validOnlyScope.ServiceProvider, keyed).Value);
return;
}
using var invalid = Build(Configure(invalidCounts, complete: false), kind);
using var validScope = valid.CreateScope();
using var invalidScope = invalid.CreateScope();
Assert.AreEqual("selected", Resolve(validScope.ServiceProvider, keyed).Value);
StringAssert.Contains(Assert.ThrowsExactly<InvalidOperationException>(() => Resolve(invalidScope.ServiceProvider, keyed)).Message, nameof(MissingOther));
Assert.AreEqual(0, invalidCounts.Parts + invalidCounts.Broken + invalidCounts.Roots + invalidCounts.Wrappers);
IServiceCollection Configure(Counts tracker, bool complete)
{
var collection = Services(tracker);
if (complete) collection.AddSingleton<MissingOther>();
collection.AddTransient(typeof(IBroken<>), typeof(Broken<>));
collection.Add(original);
collection.Decorate<IWork, Forwarder>();
return collection;
}
}
public sealed class Healthy<T> : IBroken<T>
{
public Healthy(Counts counts) => counts.Broken++;
}
private static IServiceCollection Services(Counts counts)
{
IServiceCollection services = new ServiceCollection();
services.AddSingleton(counts);
services.AddTransient<Part>(_ => { counts.Parts++; return new Part(); });
services.AddSingleton<Other>();
return services;
}
private static IWork Resolve(IServiceProvider provider, bool keyed) => keyed
? provider.GetRequiredKeyedService<IWork>("blue") : provider.GetRequiredService<IWork>();
private static ServiceProvider Build(IServiceCollection services, string kind) => kind switch
{
"snapshot" => ServiceProviderFactory.CreateServiceProvider(services),
"diagnostics" => ServiceProviderFactory.CreateServiceProvider(services,
new ExtendedServiceProviderOptions { ValidateOnBuild = true, DetectIncorrectUsageOfTransientDisposables = true }),
_ => services.BuildServiceProvider()
};
public sealed class Counts { public int Parts; public int Broken; public int Roots; public int Wrappers; }
public sealed class Part;
public sealed class Other;
public sealed class Missing;
public sealed class MissingOther;
public interface IBroken<T>;
public sealed class Broken<T> : IBroken<T>
{
public Broken(MissingOther missing, Counts counts) => counts.Broken++;
}
public interface IWork { string Value { get; } }
public abstract class Work : IWork
{
public string Value { get; }
protected Work(string value, Counts counts) { Value = value; counts.Roots++; }
}
public sealed class Rejected : Work
{
public Rejected(Part part, Other other, Counts counts) : base("valid", counts) { }
public Rejected(IBroken<int> broken, Missing missing, Counts counts) : base("invalid", counts) { }
}
public sealed class RejectedEnumerable : Work
{
public RejectedEnumerable(Part part, Other other, Counts counts) : base("valid", counts) { }
public RejectedEnumerable(IEnumerable<IBroken<int>> broken, Missing missing, Counts counts) : base("invalid", counts) { }
}
public sealed class Selected(Part part, IBroken<int> broken, Counts counts) : Work("selected", counts)
{
public Part Part { get; } = part;
public IBroken<int> Broken { get; } = broken;
}
public sealed class SelectedEnumerable(Part part, IEnumerable<IBroken<int>> broken, Counts counts) : Work("selected", counts)
{
public Part Part { get; } = part;
public IEnumerable<IBroken<int>> Broken { get; } = broken;
}
public sealed class Forwarder : IWork
{
public IWork Inner { get; }
public string Value => Inner.Value;
public Forwarder(IWork inner, Counts counts) { Inner = inner; counts.Wrappers++; }
}
}