Skip to content

Commit 7b27543

Browse files
committed
Switch LamarServiceDescriptor to factory-shape base ctor
The (Type, Type, ServiceLifetime) base ctor on LamarServiceDescriptor made the descriptor look like a normal concrete-type registration to external consumers. Wolverine HTTP code-gen reads that as "I can inline-construct this" and walks the impl's ctor chain — which fails as 'NotSupportedException: Cannot build service type ... in any way' whenever a ctor parameter is registered only via Lamar's convention scan (scan-only deps don't surface to the IServiceCollection snapshot Wolverine reads). Per Wolverine's docs, an "opaque lambda factory" descriptor with a Scoped or Transient lifetime makes Wolverine fall back to service location at runtime — exactly the path we want, since service location goes through Lamar's IServiceProvider, which can resolve scan-only deps. Changing the base ctor to (Type, Func, ServiceLifetime) flips the descriptor's external shape to that opaque-factory form, eliminating the inline-construction code-gen path entirely. Lamar's own self-discovery is unaffected — Instance.For checks for the LamarServiceDescriptor subclass first and returns the typed Instance property, bypassing the ImplementationFactory branch. The factory delegate itself throws if invoked: Wolverine service-locates without calling it, and Lamar's resolution path uses the subclass's Instance property, so the only way to reach the delegate is via a non-Lamar IServiceProvider — which is misuse this descriptor isn't designed for. Verified end-to-end against a Wolverine HTTP endpoint that constructor- captures IAsyncDocumentSession through a chain of services including scan-only deps: three concurrent POST requests now succeed with HTTP 200 and distinct session + factory identity hashes (was: same hash and a Raven 'Concurrent usage of async tasks' exception under the legacy Lamar bridge, then 'Cannot build service type' under the intermediate concrete-type-base-ctor fix). All 714 tests in Lamar.Testing pass on net8.0, net9.0, net10.0. All 23 tests in Lamar.AspNetCoreTests pass on all three TFMs.
1 parent 1d64e61 commit 7b27543

1 file changed

Lines changed: 24 additions & 8 deletions

File tree

src/Lamar/LamarServiceDescriptor.cs

Lines changed: 24 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
using System;
12
using Lamar.IoC.Instances;
23
using Microsoft.Extensions.DependencyInjection;
34

@@ -15,21 +16,36 @@ namespace Lamar;
1516
/// resolved Lamar registrations as singleton fields on generated handler
1617
/// classes; see JasperFx/wolverine#1610 and #537 for visible symptoms.
1718
///
18-
/// The subclass routes the Instance through Lamar-specific state, while
19-
/// delegating to the (Type, Type, ServiceLifetime) base constructor so the
20-
/// descriptor's surface-area Lifetime is honest. Lamar's own self-discovery
21-
/// in <see cref="Instance.FindServiceRegistration" /> (and the related
22-
/// Matches helper) prefers the typed Instance when present and otherwise
23-
/// reconstructs from the descriptor's ImplementationType / Lifetime — the
24-
/// two paths produce equivalent results for simple registrations.
19+
/// Descriptor shape: we delegate to the (Type, Func, ServiceLifetime) base
20+
/// constructor, which makes the descriptor look like an "opaque lambda
21+
/// factory" to external consumers. This is the shape Wolverine's docs say
22+
/// triggers service-location at runtime — exactly what we want, because
23+
/// service location calls back into Lamar's IServiceProvider, which can
24+
/// see scan-only deps (whereas Wolverine's inline-construction codegen
25+
/// cannot). The factory delegate itself is never invoked under Lamar
26+
/// resolution: Lamar's <see cref="Instance.For" /> checks for the subclass
27+
/// first and returns the typed Instance directly, bypassing the factory
28+
/// branch. The factory body throws to make any incorrect direct invocation
29+
/// loud and obvious.
2530
/// </summary>
2631
public sealed class LamarServiceDescriptor : ServiceDescriptor
2732
{
2833
public LamarServiceDescriptor(Instance instance)
29-
: base(instance.ServiceType, instance.ImplementationType ?? instance.ServiceType, instance.Lifetime)
34+
: base(
35+
instance.ServiceType,
36+
FactoryShouldNotBeInvoked,
37+
instance.Lifetime)
3038
{
3139
Instance = instance;
3240
}
3341

3442
public Instance Instance { get; }
43+
44+
private static object FactoryShouldNotBeInvoked(IServiceProvider sp) =>
45+
throw new InvalidOperationException(
46+
"LamarServiceDescriptor's ImplementationFactory is a Wolverine/MS-DI codegen marker " +
47+
"and should not be invoked directly. Resolution under Lamar goes through Instance.For " +
48+
"(which prefers the typed Instance on the subclass) and never through this delegate. " +
49+
"If you see this, you are resolving the descriptor outside a Lamar container — build " +
50+
"the container via UseLamar(...) instead of stock MS-DI ServiceProvider.");
3551
}

0 commit comments

Comments
 (0)