Skip to content

Commit 00673a6

Browse files
committed
Minimize diagnostics validator public surface
1 parent 9ecf5d3 commit 00673a6

5 files changed

Lines changed: 108 additions & 56 deletions

File tree

src/Elsa/Diagnostics/Persistence/EXTENSION_POINTS.md

Lines changed: 12 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -37,10 +37,18 @@ that needs fan-out must place it behind one implementation instead of resolving
3737
aggregate bridge; contribution-style `IEnumerable<T>` resolution is not supported.
3838
- **Data rule:** the contract accepts no diagnostic payload, identifier, tenant, or free-form label.
3939

40-
The public surface is intentionally limited to the contracts and models required for concrete
41-
adapter assemblies to compose the shared drain. Provider packages stay in the concrete adapter
42-
projects. A separate `.Core` project would add another public package and dependency layer without
43-
creating an independent domain contract, so this narrow helper-boundary exception is deliberate.
40+
The adapter extension surface is intentionally limited to the contracts and models required for
41+
concrete adapter assemblies to compose the shared drain. Provider packages stay in the concrete
42+
adapter projects. A separate `.Core` project would add another public package and dependency layer
43+
without creating an independent domain contract, so this narrow helper-boundary exception is
44+
deliberate.
45+
46+
`DiagnosticsPersistenceObserverRegistrationValidator` is the constitution-mandated
47+
first-party implementation required by §2.23.3; it is not an adapter extension contract. Its public
48+
constructor accepts only `IServiceCollection`, and its public validation method owns observer
49+
conflict detection and the actionable result. An internal Options adapter delegates .NET
50+
`ValidateOnStart` into that implementation without creating another public implementation or
51+
extension seam.
4452

4553
## Implementable contributor interfaces
4654

Lines changed: 46 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,23 +1,58 @@
1+
using Elsa.Diagnostics.Persistence.Observability;
12
using Microsoft.Extensions.DependencyInjection;
23
using Microsoft.Extensions.Options;
34

45
namespace Elsa.Diagnostics.Persistence.Extensions;
56

6-
public sealed class DiagnosticsPersistenceObserverRegistrationOptions;
7+
/// <summary>
8+
/// Validates that diagnostics persistence composition selects at most one observer implementation.
9+
/// </summary>
10+
public sealed class DiagnosticsPersistenceObserverRegistrationValidator(IServiceCollection services)
11+
{
12+
/// <summary>Returns a failed result that names every conflicting registration, or success.</summary>
13+
public ValidateOptionsResult Validate()
14+
{
15+
var observers = services
16+
.Where(descriptor => descriptor.ServiceType == typeof(IDiagnosticsPersistenceObserver))
17+
.ToArray();
18+
if (observers.Length <= 1)
19+
return ValidateOptionsResult.Success;
20+
21+
var selection = DiagnosticsPersistenceRegistration.FindObserverSelection(services);
22+
var implementations = observers
23+
.Select(descriptor => DescribeObserverDescriptor(descriptor, selection))
24+
.ToArray();
25+
var message =
26+
$"Diagnostics replacement contract '{typeof(IDiagnosticsPersistenceObserver).FullName}' has conflicting " +
27+
$"registrations: {string.Join(", ", implementations)}. Select one observer explicitly through " +
28+
$"'{nameof(DiagnosticsPersistenceRegistration.ReplaceDiagnosticsStore)}'.";
29+
return ValidateOptionsResult.Fail(message);
30+
}
31+
32+
private static string DescribeObserverDescriptor(
33+
ServiceDescriptor descriptor,
34+
(ServiceDescriptor ContractDescriptor, Type ImplementationType)? selection)
35+
{
36+
if (selection is { } selected && ReferenceEquals(descriptor, selected.ContractDescriptor))
37+
return selected.ImplementationType.ToString();
38+
39+
if (descriptor.ImplementationType is { } implementationType)
40+
return implementationType.ToString();
741

8-
public sealed record DiagnosticsPersistenceObserverRegistrationState(IServiceCollection Services);
42+
if (descriptor.ImplementationInstance is { } implementationInstance)
43+
return implementationInstance.GetType().ToString();
944

10-
public sealed class DiagnosticsPersistenceObserverRegistrationValidator(
11-
DiagnosticsPersistenceObserverRegistrationState state)
45+
return $"factory registration for '{typeof(IDiagnosticsPersistenceObserver).FullName}'";
46+
}
47+
}
48+
49+
internal sealed class DiagnosticsPersistenceObserverRegistrationOptions;
50+
51+
internal sealed class DiagnosticsPersistenceObserverRegistrationOptionsAdapter(
52+
DiagnosticsPersistenceObserverRegistrationValidator validator)
1253
: IValidateOptions<DiagnosticsPersistenceObserverRegistrationOptions>
1354
{
1455
public ValidateOptionsResult Validate(
1556
string? name,
16-
DiagnosticsPersistenceObserverRegistrationOptions options)
17-
{
18-
var message = DiagnosticsPersistenceRegistration.GetObserverConflictMessage(state.Services);
19-
return message is null
20-
? ValidateOptionsResult.Success
21-
: ValidateOptionsResult.Fail(message);
22-
}
57+
DiagnosticsPersistenceObserverRegistrationOptions options) => validator.Validate();
2358
}

src/Elsa/Diagnostics/Persistence/Extensions/DiagnosticsPersistenceRegistration.cs

Lines changed: 15 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -103,6 +103,15 @@ public static IServiceCollection AddDiagnosticsPersistenceObservability(this ISe
103103
.Select(item => item.Selection! with { Descriptor = item.Descriptor })
104104
.SingleOrDefault();
105105

106+
internal static (ServiceDescriptor ContractDescriptor, Type ImplementationType)? FindObserverSelection(
107+
IServiceCollection services)
108+
{
109+
var selection = FindSelection<IDiagnosticsPersistenceObserver>(services);
110+
return selection is null
111+
? null
112+
: (selection.ContractDescriptor, selection.ImplementationType);
113+
}
114+
106115
private static void ValidateLifetime(ServiceLifetime lifetime)
107116
{
108117
if (!Enum.IsDefined(lifetime))
@@ -111,55 +120,21 @@ private static void ValidateLifetime(ServiceLifetime lifetime)
111120

112121
private static void RejectObserverConflicts(IServiceCollection services)
113122
{
114-
var message = GetObserverConflictMessage(services);
115-
if (message is not null)
116-
throw new InvalidOperationException(message);
117-
}
118-
119-
internal static string? GetObserverConflictMessage(IServiceCollection services)
120-
{
121-
var observers = services
122-
.Where(descriptor => descriptor.ServiceType == typeof(IDiagnosticsPersistenceObserver))
123-
.ToArray();
124-
if (observers.Length <= 1)
125-
return null;
126-
127-
var selection = FindSelection<IDiagnosticsPersistenceObserver>(services);
128-
var implementations = observers
129-
.Select(descriptor => DescribeObserverDescriptor(descriptor, selection))
130-
.ToArray();
131-
return
132-
$"Diagnostics replacement contract '{typeof(IDiagnosticsPersistenceObserver).FullName}' has conflicting " +
133-
$"registrations: {string.Join(", ", implementations)}. Select one observer explicitly through " +
134-
$"'{nameof(ReplaceDiagnosticsStore)}'.";
135-
}
136-
137-
private static string DescribeObserverDescriptor(
138-
ServiceDescriptor descriptor,
139-
DiagnosticsStoreSelection? selection)
140-
{
141-
if (selection is not null && ReferenceEquals(descriptor, selection.ContractDescriptor))
142-
return selection.ImplementationType.ToString();
143-
144-
if (descriptor.ImplementationType is { } implementationType)
145-
return implementationType.ToString();
146-
147-
if (descriptor.ImplementationInstance is { } implementationInstance)
148-
return implementationInstance.GetType().ToString();
149-
150-
return $"factory registration for '{typeof(IDiagnosticsPersistenceObserver).FullName}'";
123+
var result = new DiagnosticsPersistenceObserverRegistrationValidator(services).Validate();
124+
if (result.Failed)
125+
throw new InvalidOperationException(result.Failures.Single());
151126
}
152127

153128
private static void AddObserverRegistrationValidation(IServiceCollection services)
154129
{
155130
if (services.Any(descriptor =>
156-
descriptor.ServiceType == typeof(DiagnosticsPersistenceObserverRegistrationState)))
131+
descriptor.ServiceType == typeof(DiagnosticsPersistenceObserverRegistrationValidator)))
157132
return;
158133

159-
services.AddSingleton(new DiagnosticsPersistenceObserverRegistrationState(services));
134+
services.AddSingleton(new DiagnosticsPersistenceObserverRegistrationValidator(services));
160135
services.AddSingleton<
161136
IValidateOptions<DiagnosticsPersistenceObserverRegistrationOptions>,
162-
DiagnosticsPersistenceObserverRegistrationValidator>();
137+
DiagnosticsPersistenceObserverRegistrationOptionsAdapter>();
163138
services.AddOptions<DiagnosticsPersistenceObserverRegistrationOptions>().ValidateOnStart();
164139
}
165140

tests/Elsa/Diagnostics/Persistence/Tests/DiagnosticsDocumentationTests.cs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,8 @@ public void Owning_extension_point_catalog_declares_public_seam_semantics_and_he
1919
Assert.Contains("conflict", catalog, StringComparison.OrdinalIgnoreCase);
2020
Assert.Contains("helper boundary", catalog, StringComparison.OrdinalIgnoreCase);
2121
Assert.Contains("DiagnosticsPersistenceObserverRegistrationValidator", catalog, StringComparison.Ordinal);
22-
Assert.Contains("constitution-mandated first-party implementation", catalog, StringComparison.OrdinalIgnoreCase);
22+
Assert.Contains("constitution-mandated", catalog, StringComparison.OrdinalIgnoreCase);
23+
Assert.Contains("first-party implementation", catalog, StringComparison.OrdinalIgnoreCase);
2324
Assert.Contains("internal Options adapter", catalog, StringComparison.Ordinal);
2425
Assert.Contains("not an adapter extension contract", catalog, StringComparison.OrdinalIgnoreCase);
2526
Assert.Contains("## Overridable contracts", catalog, StringComparison.Ordinal);

tests/Elsa/Diagnostics/Persistence/Tests/DiagnosticsPersistenceObserverRegistrationValidatorTests.cs

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,39 @@ public void Validate_WhenObserversConflict_FailsWithActionableDescriptions()
3636
Assert.Contains(nameof(DiagnosticsPersistenceRegistration.ReplaceDiagnosticsStore), message, StringComparison.Ordinal);
3737
}
3838

39+
[Fact]
40+
public void Validate_WhenTrackedDefaultConflictsWithDirectObserver_DescribesSelectedImplementation()
41+
{
42+
var services = new ServiceCollection();
43+
services.AddDiagnosticsPersistenceObservability();
44+
services.AddSingleton<IDiagnosticsPersistenceObserver, SecondObserver>();
45+
var validator = new DiagnosticsPersistenceObserverRegistrationValidator(services);
46+
47+
var result = validator.Validate();
48+
49+
Assert.True(result.Failed);
50+
var message = Assert.Single(result.Failures);
51+
Assert.Contains(typeof(DiagnosticsPersistenceCounters).FullName!, message, StringComparison.Ordinal);
52+
Assert.Contains(typeof(SecondObserver).FullName!, message, StringComparison.Ordinal);
53+
}
54+
55+
[Fact]
56+
public void Validate_WhenInstanceAndFactoryObserversConflict_DescribesBothRegistrationForms()
57+
{
58+
var observer = new FirstObserver();
59+
var services = new ServiceCollection();
60+
services.AddSingleton<IDiagnosticsPersistenceObserver>(observer);
61+
services.AddSingleton<IDiagnosticsPersistenceObserver>(_ => new SecondObserver());
62+
var validator = new DiagnosticsPersistenceObserverRegistrationValidator(services);
63+
64+
var result = validator.Validate();
65+
66+
Assert.True(result.Failed);
67+
var message = Assert.Single(result.Failures);
68+
Assert.Contains(observer.GetType().FullName!, message, StringComparison.Ordinal);
69+
Assert.Contains("factory registration", message, StringComparison.Ordinal);
70+
}
71+
3972
private sealed class FirstObserver : TestObserver;
4073
private sealed class SecondObserver : TestObserver;
4174

0 commit comments

Comments
 (0)