Describe the bug
An aggregate with a [NaturalKey] property is registered only on an ancillary Marten store (AddMartenStore<IOrdersStore>()), and its handler chain is routed to that store with [MartenStore(typeof(IOrdersStore))]. A handler taking [WriteModel] Order (same with [WriteAggregate]) whose command carries only the natural key fails at codegen:
JasperFx.CodeGeneration.CodeGenerationException: Error trying to generate the code for file AddItemHandler668202374 of type HandlerChain
System.InvalidOperationException: Unable to determine an aggregate id for the parameter 'order' on method HandlerType: AddItemHandler, Method: System.Object[] Handle(AddItem, Order)
at Wolverine.Persistence.EventSourcing.WriteModelAttribute.Modify(IChain chain, ParameterInfo parameter, IServiceContainer container, GenerationRules rules)
The identical handler works when the aggregate is registered on the default store: the generated chain then contains
var stream_order = await documentSession.Events.FetchForWriting<Order, OrderNumber>(((AddItem)context.Envelope.Message).Number, cancellation);
Cause, from the V6.35.0 source: WriteModelAttribute falls back to provider.TryDetermineNaturalKeyType(aggregateType, container) when the standard identity search finds nothing, and MartenEventSourcingFrameProvider.TryDetermineNaturalKeyType is
if (container.GetInstance<IDocumentStore>().Options is not StoreOptions storeOptions) return null;
return storeOptions.Projections.FindNaturalKeyDefinition(aggregateType)?.OuterType;
It always asks the default IDocumentStore. Marten's FindNaturalKeyDefinition is per store (TryFindAggregate over that store's registered projections), so for an aggregate that exists only on an ancillary store it returns null and the natural-key path is never taken. The chain does know its store (chain.AncillaryStoreType), and LoadAggregateFrame.NaturalKeyFetchForWriting writes the fetch against that store's session once IsNaturalKey is set, so only the detection lacks the store. [WriteModel] reaches the same provider through FindEventSourcingFrameProvider (Marten's CanPersist is catch-all), so both spellings behave the same.
To Reproduce
Console project, net10.0, packages WolverineFx.Marten 6.35.0, Marten 9.33.0, Microsoft.Extensions.Hosting 10.0.12. No database is needed; codegen never opens the connection.
using JasperFx;
using JasperFx.Events.Aggregation;
using Marten;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using Wolverine;
using Wolverine.Marten;
using Wolverine.Persistence.EventSourcing;
// Build with -p:DefineConstants=ON_DEFAULT to register the aggregate on the default store instead (the control).
const string cs = "Host=localhost;Port=5432;Database=nk;Username=postgres;Password=postgres";
var builder = Host.CreateApplicationBuilder(args);
builder.Services.AddMarten(o =>
{
o.Connection(cs);
#if ON_DEFAULT
o.Projections.LiveStreamAggregation<Order>();
#endif
}).IntegrateWithWolverine();
builder.Services.AddMartenStore<IOrdersStore>(o =>
{
o.Connection(cs);
o.DatabaseSchemaName = "orders";
#if !ON_DEFAULT
o.Projections.LiveStreamAggregation<Order>();
#endif
}).IntegrateWithWolverine();
builder.UseWolverine(opts => opts.Policies.AutoApplyTransactions());
return await builder.Build().RunJasperFxCommands(args);
public record OrderNumber(string Value);
public sealed record Order
{
public Guid Id { get; init; }
[NaturalKey]
public OrderNumber Number { get; init; } = null!;
[NaturalKeySource]
public static OrderNumber KeyFor(OrderPlaced e) => e.Number;
public static Order Create(OrderPlaced e) => new() { Id = e.Id, Number = e.Number };
}
public record OrderPlaced(Guid Id, OrderNumber Number);
public record AddItem(OrderNumber Number, string Sku);
public interface IOrdersStore : IDocumentStore;
#if !ON_DEFAULT
[MartenStore(typeof(IOrdersStore))]
#endif
public static class AddItemHandler
{
public static object[] Handle(AddItem command, [WriteModel(Required = false)] Order? order) => [];
}
dotnet run -- codegen preview → the CodeGenerationException above.
dotnet run -p:DefineConstants=ON_DEFAULT -- codegen preview → succeeds and prints the FetchForWriting<Order, OrderNumber> chain quoted above.
Calling session.Events.FetchForWriting<Order, OrderNumber>(command.Number, ct) by hand on the injected IDocumentSession of the routed chain works, which is the workaround we use in the meantime.
Expected behavior
TryDetermineNaturalKeyType consults the store the chain is routed to (chain.AncillaryStoreType, falling back to the default IDocumentStore), or all registered stores, before giving up. That needs the chain in the signature, e.g. TryDetermineNaturalKeyType(Type aggregateType, IChain chain, IServiceContainer container).
Additional context
- WolverineFx.Marten 6.35.0, Marten 9.33.0 (JasperFx.Events 2.67.0), .NET SDK 10.0.401, macOS.
- Marten's side is fine:
FetchForWriting<T, TKey> by natural key on the ancillary store works, and the lookup table is created in that store's schema.
- Happy to turn the repro into a failing test in
MartenTests/AggregateHandlerWorkflow if that helps.
Describe the bug
An aggregate with a
[NaturalKey]property is registered only on an ancillary Marten store (AddMartenStore<IOrdersStore>()), and its handler chain is routed to that store with[MartenStore(typeof(IOrdersStore))]. A handler taking[WriteModel] Order(same with[WriteAggregate]) whose command carries only the natural key fails at codegen:The identical handler works when the aggregate is registered on the default store: the generated chain then contains
Cause, from the V6.35.0 source:
WriteModelAttributefalls back toprovider.TryDetermineNaturalKeyType(aggregateType, container)when the standard identity search finds nothing, andMartenEventSourcingFrameProvider.TryDetermineNaturalKeyTypeisIt always asks the default
IDocumentStore. Marten'sFindNaturalKeyDefinitionis per store (TryFindAggregateover that store's registered projections), so for an aggregate that exists only on an ancillary store it returns null and the natural-key path is never taken. The chain does know its store (chain.AncillaryStoreType), andLoadAggregateFrame.NaturalKeyFetchForWritingwrites the fetch against that store's session onceIsNaturalKeyis set, so only the detection lacks the store.[WriteModel]reaches the same provider throughFindEventSourcingFrameProvider(Marten'sCanPersistis catch-all), so both spellings behave the same.To Reproduce
Console project,
net10.0, packagesWolverineFx.Marten 6.35.0,Marten 9.33.0,Microsoft.Extensions.Hosting 10.0.12. No database is needed; codegen never opens the connection.dotnet run -- codegen preview→ theCodeGenerationExceptionabove.dotnet run -p:DefineConstants=ON_DEFAULT -- codegen preview→ succeeds and prints theFetchForWriting<Order, OrderNumber>chain quoted above.Calling
session.Events.FetchForWriting<Order, OrderNumber>(command.Number, ct)by hand on the injectedIDocumentSessionof the routed chain works, which is the workaround we use in the meantime.Expected behavior
TryDetermineNaturalKeyTypeconsults the store the chain is routed to (chain.AncillaryStoreType, falling back to the defaultIDocumentStore), or all registered stores, before giving up. That needs the chain in the signature, e.g.TryDetermineNaturalKeyType(Type aggregateType, IChain chain, IServiceContainer container).Additional context
FetchForWriting<T, TKey>by natural key on the ancillary store works, and the lookup table is created in that store's schema.MartenTests/AggregateHandlerWorkflowif that helps.