Skip to content

Natural key on an aggregate in an ancillary Marten store: [WriteModel] / [WriteAggregate] cannot determine the aggregate id #4439

Description

@syserr500

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) => [];
}
  1. dotnet run -- codegen preview → the CodeGenerationException above.
  2. 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.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions