Skip to content

[BUG] Marten audit projections unregisterable: IServiceProvider parameter rejected by JasperFx validation #949

@dlrivada

Description

@dlrivada

Description

AuditEntryProjection.Create and ReadAuditEntryProjection.Create in Encina.Audit.Marten accept IServiceProvider as their second parameter. Marten/JasperFx projection validation rejects this type at store initialization, so ConfigureMartenAuditProjections.Configure(StoreOptions) throws before the store can be used.

As a result, any application calling services.AddEncinaAuditMarten(...) will fail at boot. This bug has gone undetected because Encina.Audit.Marten has no integration tests (no tests/Encina.IntegrationTests/AuditMarten/ folder exists) — only unit tests that call projection.Create(evt, services) directly, bypassing Marten's projection graph validation.

Steps to Reproduce

  1. Install Encina.Audit.Marten in any app with a real PostgreSQL connection.
  2. Register the audit store:
    services.AddEncinaAuditMarten(options =>
    {
        options.TemporalGranularity = TemporalKeyGranularity.Monthly;
    });
  3. Boot the host — Marten runs ConfigureMartenAuditProjections.Configure(StoreOptions).
  4. JasperFx.Events.Projections.InvalidProjectionException is thrown.

Expected Behavior

Marten store initialization completes successfully and both audit projections are registered as async projections on the async daemon.

Actual Behavior

JasperFx.Events.Projections.InvalidProjectionException :
Projection Encina.Audit.Marten.Projections.AuditEntryProjection has validation errors:
Create(AuditEntryRecordedEvent, IServiceProvider) : Task<AuditEntryReadModel>
 - Parameter of type 'System.IServiceProvider' is not supported.
   Valid options are:
     - System.Threading.CancellationToken
     - Marten.IDocumentOperations
     - Encina.Audit.Marten.Events.AuditEntryRecordedEvent
     - JasperFx.Events.IEvent
     - JasperFx.Events.IEvent<AuditEntryRecordedEvent>

Environment

  • Encina Version: 0.13.x (main, current HEAD)
  • .NET Version: .NET 10.0
  • OS: Any (logic bug, not platform-specific)
  • Package(s) Affected:
    • Encina.Audit.Marten (AuditEntryProjection, ReadAuditEntryProjection, ConfigureMartenAuditProjections)

Code Sample

// Minimal reproduction without Marten store boot
using Encina.Audit.Marten.Projections;
using Marten;

var sut = new ConfigureMartenAuditProjections();
var storeOptions = new StoreOptions();
storeOptions.Connection("Host=localhost;Port=5432;Database=test;Username=postgres;Password=postgres");

// Throws InvalidProjectionException on Projections.Add(...)
sut.Configure(storeOptions);

Source (src/Encina.Audit.Marten/Projections/AuditEntryProjection.cs:60-62):

public async Task<AuditEntryReadModel> Create(
    AuditEntryRecordedEvent @event,
    IServiceProvider services)   // ← Marten rejects this parameter
{
    var keyProvider = services.GetRequiredService<ITemporalKeyProvider>();
    // ...
}

Same shape in src/Encina.Audit.Marten/Projections/ReadAuditEntryProjection.cs.

Stack Trace

   at JasperFx.Events.Projections.EventProjectionApplication`1.AssertMethodValidity()
   at JasperFx.Events.Projections.JasperFxEventProjectionBase`2.AssembleAndAssertValidity()
   at JasperFx.Events.Projections.ProjectionGraph`3.Register(...)
   at JasperFx.Events.Projections.ProjectionGraph`3.Add(...)
   at Encina.Audit.Marten.Projections.ConfigureMartenAuditProjections.Configure(StoreOptions options)
     in src/Encina.Audit.Marten/Projections/ConfigureMartenAuditProjections.cs:line 40

Root Cause

The Marten projection pipeline injects per-event services through IDocumentOperations, not IServiceProvider. Temporal key providers and options must be resolved via a projection-scoped pattern (custom factory + constructor injection), not by taking the raw DI container as a method parameter.

Proposed Fix

Refactor both projections to one of the supported Marten patterns:

  1. Preferred — Custom projection factory via IProjectionSource: resolve ITemporalKeyProvider, IOptions<MartenAuditOptions>, and the logger via constructor injection, and register the projection with a factory that pulls them from DI at store boot.
  2. Alternative — IDocumentOperations + session-scoped service lookup: query temporal keys through Marten-managed sessions rather than a raw IServiceProvider.
  3. Add an integration test under tests/Encina.IntegrationTests/AuditMarten/ that boots a real Marten store (Testcontainers PostgreSQL) and exercises AddEncinaAuditMarten() end-to-end. This test would have caught the bug on introduction.

Out of scope for this issue but recommended follow-up: audit all Encina.Marten / Encina.Marten.GDPR projections for the same anti-pattern.

Related

Additional Context

  • Discovered while adding unit/guard coverage for Encina.Audit.Marten in PR test(audit-marten): close UNIT and GUARD coverage gaps #948. The Configure_ValidOptions_RegistersProjectionsAndIndexes test had to be removed; only the null-guard path remains testable at unit level.
  • No Encina.IntegrationTests/AuditMarten/ folder exists, so the Marten store has never been booted with these projections in CI. Any production app consuming the package would fail immediately.

Metadata

Metadata

Assignees

No one assigned

    Labels

    area-auditAudit trails and change trackingarea-complianceGDPR, EU Laws, and Regulatory Compliance patternsarea-event-sourcingEvent Sourcing patterns and infrastructurearea-martenMarten event sourcing providerbugSomething isn't workingcomplexity-mediumComplexity: Mediummarten-integrationMarten library integrationpriority-criticalPriority: Critical (⭐⭐⭐⭐⭐)

    Projects

    Status
    Done

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions