-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathModuleGraphValidator.cs
More file actions
125 lines (108 loc) · 4.11 KB
/
ModuleGraphValidator.cs
File metadata and controls
125 lines (108 loc) · 4.11 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
using System.Reflection;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;
namespace SimpleModule.Hosting;
/// <summary>
/// Boot-time validator that catches the most common "missing peer module" failure mode:
/// a module's <c>SimpleModule.X.Contracts</c> assembly is loaded (because some other
/// module references it) but no service satisfies the contract interface, meaning the
/// implementing <c>SimpleModule.X</c> package was never installed.
/// </summary>
#pragma warning disable CA1812 // Instantiated by DI as IHostedService
internal sealed class ModuleGraphValidator : IHostedService
#pragma warning restore CA1812
{
private const string SimpleModulePrefix = "SimpleModule.";
private const string ContractsSuffix = ".Contracts";
private readonly IServiceProviderIsService _isService;
private readonly ILogger<ModuleGraphValidator> _logger;
private readonly SimpleModuleOptions _options;
private readonly IHostEnvironment _environment;
public ModuleGraphValidator(
IServiceProviderIsService isService,
ILogger<ModuleGraphValidator> logger,
SimpleModuleOptions options,
IHostEnvironment environment
)
{
_isService = isService;
_logger = logger;
_options = options;
_environment = environment;
}
public Task StartAsync(CancellationToken cancellationToken)
{
if (!_options.ValidateModuleGraph)
{
return Task.CompletedTask;
}
// In Production this check is too noisy — downstream apps may legitimately
// ship a contracts assembly without the impl (e.g. their own implementation).
if (_environment.IsProduction())
{
return Task.CompletedTask;
}
var unsatisfied = FindUnsatisfiedContracts();
if (unsatisfied.Count == 0)
{
return Task.CompletedTask;
}
foreach (var (contractFullName, suspectedModulePackage) in unsatisfied)
{
_logger.LogWarning(
"Module graph: contract {Contract} is referenced but no implementation is registered. The module providing it ({Package}) appears to be missing — add a PackageReference (or call its registration extension) to fix runtime resolution failures.",
contractFullName,
suspectedModulePackage
);
}
return Task.CompletedTask;
}
public Task StopAsync(CancellationToken cancellationToken) => Task.CompletedTask;
private List<(string Contract, string Package)> FindUnsatisfiedContracts()
{
var unsatisfied = new List<(string Contract, string Package)>();
foreach (var assembly in EnumerateContractsAssemblies())
{
var assemblyName = assembly.GetName().Name!;
var implPackage = assemblyName[..^ContractsSuffix.Length];
foreach (var iface in EnumerateContractInterfaces(assembly))
{
if (!_isService.IsService(iface))
{
unsatisfied.Add((iface.FullName ?? iface.Name, implPackage));
}
}
}
return unsatisfied;
}
private static IEnumerable<Assembly> EnumerateContractsAssemblies()
{
return AppDomain
.CurrentDomain.GetAssemblies()
.Where(a =>
{
var name = a.GetName().Name;
return name is not null
&& name.StartsWith(SimpleModulePrefix, StringComparison.Ordinal)
&& name.EndsWith(ContractsSuffix, StringComparison.Ordinal);
});
}
private static IEnumerable<Type> EnumerateContractInterfaces(Assembly assembly)
{
Type[] exported;
try
{
exported = assembly.GetExportedTypes();
}
catch (ReflectionTypeLoadException)
{
return [];
}
return exported.Where(t =>
t.IsInterface
&& t.Name.StartsWith('I')
&& t.Name.EndsWith("Contracts", StringComparison.Ordinal)
);
}
}