Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
45 changes: 45 additions & 0 deletions src/Marten.AotSmoke/Marten.AotSmoke.csproj
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
<Project Sdk="Microsoft.NET.Sdk">

<PropertyGroup>
<OutputType>Exe</OutputType>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<IsPackable>false</IsPackable>

<!--
AOT smoke test (marten#4349 / JasperFx/jasperfx#213). Builds in CI and
verifies that a consumer of Marten through the AOT-clean surfaces
(AddMarten + document mapping + event registration + projection
registration + DI resolution of IDocumentStore) compiles with no
IL2026 / IL2046 / IL2055 / IL2065 / IL2067 / IL2070 / IL2072 / IL2075
/ IL2090 / IL2091 / IL2111 / IL3050 / IL3051 warnings.

If a previously-AOT-clean API in Marten later gains a
[RequiresUnreferencedCode] / [RequiresDynamicCode] annotation, this
project's build fails in CI and the regression is caught before
downstream consumers see it. New IL warnings introduced by edits to
Program.cs are equally caught.

Pattern mirrors src/JasperFx.AotSmoke/ in JasperFx/jasperfx
(commit d4077d8) and src/Weasel.Core.AotSmoke/ in JasperFx/weasel
(PR #283 / commit 5844c9f).

Deliberately NOT a test project — no xUnit, no test discovery, no
dotnet test. It's a build-time consumer that exits cleanly when the
smoke surface analyzes clean under the AOT rule set.

Static TypeLoadMode consumer — no JasperFx.RuntimeCompiler reference,
no services.AddRuntimeCompilation() call. Marten 9 retired the
runtime-codegen path entirely; this project is the regression guard
that Marten itself stays AOT-clean under that contract.
-->
<IsAotCompatible>true</IsAotCompatible>
<TrimMode>full</TrimMode>
<WarningsAsErrors>IL2026;IL2046;IL2055;IL2065;IL2067;IL2070;IL2072;IL2075;IL2090;IL2091;IL2111;IL3050;IL3051</WarningsAsErrors>
</PropertyGroup>

<ItemGroup>
<ProjectReference Include="..\Marten\Marten.csproj" />
</ItemGroup>

</Project>
111 changes: 111 additions & 0 deletions src/Marten.AotSmoke/Program.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,111 @@
// AOT smoke test (marten#4349 / JasperFx/jasperfx#213).
//
// This program touches a representative cross-section of the AOT-clean
// Marten surface for a Static-TypeLoadMode consumer. The csproj sets
// IsAotCompatible=true, TrimMode=full, and promotes the AOT analyzer warning
// codes to errors, so any change that adds [RequiresDynamicCode] /
// [RequiresUnreferencedCode] to an API exercised here — or any change to
// this file that calls into a reflective Marten surface — fails the build
// in CI.
//
// What's exercised:
// - services.AddMarten(opts => { ... }) — the DI-facing entry point.
// - One document type with an Id property — exercises the closed-shape
// IDocumentStorage / IIdentification path (post-#4404, no Roslyn emit).
// - One event type + one SingleStreamProjection<,> registration — the
// primary projection-apply surface that Marten 9 routes through
// JasperFx.Events.SourceGenerator's [GeneratedEvolver] discovery
// (Options.Projections.DiscoverGeneratedEvolvers is called at startup
// in DocumentStore.cs).
// - StoreOptions.GeneratedCodeMode = TypeLoadMode.Static — pins the
// consumer profile that AOT-publishing apps would set.
// - Host.CreateApplicationBuilder + Build + DI resolution of
// IDocumentStore — the boot path consumers actually hit.
//
// What's deliberately NOT exercised:
// - Connecting / querying / persisting anything — this is a build-time
// analyzer test, not a runtime test. The connection string is a
// placeholder; the host is built but never actually opens a connection.
// - Compiled queries — Marten.SourceGenerator covers that surface and has
// its own analyzer-level tests in Marten.SourceGenerator.Tests.
// - JasperFx.RuntimeCompiler / services.AddRuntimeCompilation() — Marten 9
// retired that path (#4454 / #4461). This smoke deliberately does not
// pull it back in.

using JasperFx.CodeGeneration;
using JasperFx.Events;
using JasperFx.Events.Projections;
using Marten;
using Marten.Events.Aggregation;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;

const string placeholderConnectionString =
"Host=localhost;Port=5432;Database=marten_aot_smoke;Username=postgres;password=postgres";

var builder = Host.CreateApplicationBuilder();

builder.Services.AddMarten(opts =>
{
opts.Connection(placeholderConnectionString);

// Pin the consumer profile an AOT-publishing app would set. Marten 9
// retired the runtime-codegen path entirely, so this setting is a
// documented no-op kept for source-compatibility — exercising it here
// pins that compile-time contract.
opts.GeneratedCodeMode = TypeLoadMode.Static;

// Exercise the document-mapping surface — registers the closed-shape
// IDocumentStorage path (#4404). DocumentMapping infers the Id member
// and the Id type from SmokeDoc.Id reflectively at registration time,
// which is part of Marten's [DynamicallyAccessedMembers]-annotated
// contract; the AOT analyzer is happy when the type is fed as
// typeof(T) literal as it is here.
opts.Schema.For<SmokeDoc>();

// Exercise the event-registration surface — RegisterEventType is the
// explicit alias-registration path the JasperFx.Events source generator
// also feeds at module-init time. Done explicitly here so the smoke
// test doesn't rely on assembly scanning.
opts.Events.AddEventType<SmokeEvent>();

// Exercise the projection-registration surface — Projections.Add<T>
// closes over the user's SingleStreamProjection<,> type, which the
// JasperFx.Events.SourceGenerator handles via [GeneratedEvolver]
// discovery (DocumentStore.cs calls DiscoverGeneratedEvolvers at
// startup post-#4454). Inline lifecycle keeps the smoke test free of
// the async-daemon surface, which has its own AOT story.
opts.Projections.Add<SmokeProjection>(ProjectionLifecycle.Inline);
});

using var host = builder.Build();
var store = host.Services.GetRequiredService<IDocumentStore>();

if (store is null)
{
Console.Error.WriteLine("DI resolved a null IDocumentStore — regression in AddMarten.");
return 1;
}

Console.WriteLine("Marten AOT smoke OK — AddMarten + Schema.For + AddEventType + Projections.Add + DI resolve clean.");
return 0;


public sealed class SmokeDoc
{
public Guid Id { get; set; }
public string Name { get; set; } = string.Empty;
public int Count { get; set; }
}

public sealed record SmokeEvent(int Delta);

public sealed class SmokeProjection : SingleStreamProjection<SmokeDoc, Guid>
{
public override SmokeDoc Evolve(SmokeDoc? snapshot, Guid id, IEvent e)
{
snapshot ??= new SmokeDoc { Id = id };
if (e.Data is SmokeEvent inc) snapshot.Count += inc.Delta;
return snapshot;
}
}
1 change: 1 addition & 0 deletions src/Marten.AspNetCore/Marten.AspNetCore.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
<GenerateAssemblyInformationalVersionAttribute>true</GenerateAssemblyInformationalVersionAttribute>
<GenerateDocumentationFile>true</GenerateDocumentationFile>
<nullable>enable</nullable>
<IsAotCompatible>true</IsAotCompatible>
</PropertyGroup>

<ItemGroup>
Expand Down
5 changes: 4 additions & 1 deletion src/Marten.EntityFrameworkCore/EfCoreDbContextFactory.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
using System;
using System.Diagnostics.CodeAnalysis;
using Microsoft.EntityFrameworkCore;
using Npgsql;

Expand All @@ -13,7 +14,9 @@ namespace Marten.EntityFrameworkCore;
/// </summary>
internal static class EfCoreDbContextFactory
{
public static (TDbContext DbContext, NpgsqlConnection InitialConnection) Create<TDbContext>(
public static (TDbContext DbContext, NpgsqlConnection InitialConnection) Create<
[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicConstructors)]
TDbContext>(
this Storage.IMartenDatabase database,
Action<DbContextOptionsBuilder<TDbContext>>? configure = null,
string? schemaName = null)
Expand Down
5 changes: 4 additions & 1 deletion src/Marten.EntityFrameworkCore/EfCoreEventProjection.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using System.Threading;
using System.Threading.Tasks;
using JasperFx.Events;
Expand All @@ -15,7 +16,9 @@ namespace Marten.EntityFrameworkCore;
/// in the same database transaction.
/// </summary>
/// <typeparam name="TDbContext">The EF Core DbContext type to use</typeparam>
public abstract class EfCoreEventProjection<TDbContext>: IProjection
public abstract class EfCoreEventProjection<
[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicConstructors)]
TDbContext>: IProjection
where TDbContext : DbContext
{
/// <summary>
Expand Down
19 changes: 18 additions & 1 deletion src/Marten.EntityFrameworkCore/EfCoreMultiStreamProjection.cs
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
using System;
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using System.Threading;
using System.Threading.Tasks;
using JasperFx.Core;
Expand All @@ -23,7 +24,23 @@ namespace Marten.EntityFrameworkCore;
/// <typeparam name="TDoc">The aggregate document type persisted by EF Core</typeparam>
/// <typeparam name="TId">The aggregate identity type</typeparam>
/// <typeparam name="TDbContext">The EF Core DbContext type to use</typeparam>
public abstract class EfCoreMultiStreamProjection<TDoc, TId, TDbContext>
/// <remarks>
/// AOT: TDoc carries the DAM flag set required by the wrapped
/// <see cref="EfCoreProjectionStorage{TDoc, TId, TDbContext}"/> — propagating
/// the EF Core <c>FindEntityType</c> / <c>Find&lt;TEntity&gt;</c> trim contract
/// up to consumers.
/// </remarks>
public abstract class EfCoreMultiStreamProjection<
[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicConstructors
| DynamicallyAccessedMemberTypes.NonPublicConstructors
| DynamicallyAccessedMemberTypes.PublicFields
| DynamicallyAccessedMemberTypes.NonPublicFields
| DynamicallyAccessedMemberTypes.PublicProperties
| DynamicallyAccessedMemberTypes.NonPublicProperties
| DynamicallyAccessedMemberTypes.Interfaces)]
TDoc, TId,
[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicConstructors)]
TDbContext>
: MultiStreamProjection<TDoc, TId>, IValidatedProjection<StoreOptions>
where TDoc : class where TId : notnull where TDbContext : DbContext
{
Expand Down
53 changes: 48 additions & 5 deletions src/Marten.EntityFrameworkCore/EfCoreProjectionExtensions.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
using System;
using System.Diagnostics.CodeAnalysis;
using System.Linq;
using JasperFx.Events.Projections;
using Marten.Events.Projections;
Expand All @@ -20,7 +21,17 @@ public static class EfCoreProjectionExtensions
/// Automatically sets up EF Core-based aggregate persistence and Weasel schema migration
/// for all entity types in the DbContext.
/// </summary>
public static void Add<TDoc, TId, TDbContext>(this StoreOptions options,
public static void Add<
[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicConstructors
| DynamicallyAccessedMemberTypes.NonPublicConstructors
| DynamicallyAccessedMemberTypes.PublicFields
| DynamicallyAccessedMemberTypes.NonPublicFields
| DynamicallyAccessedMemberTypes.PublicProperties
| DynamicallyAccessedMemberTypes.NonPublicProperties
| DynamicallyAccessedMemberTypes.Interfaces)]
TDoc, TId,
[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicConstructors)]
TDbContext>(this StoreOptions options,
EfCoreSingleStreamProjection<TDoc, TId, TDbContext> projection,
ProjectionLifecycle lifecycle)
where TDoc : class where TId : notnull where TDbContext : DbContext
Expand All @@ -35,7 +46,17 @@ public static void Add<TDoc, TId, TDbContext>(this StoreOptions options,
/// Automatically sets up EF Core-based aggregate persistence and Weasel schema migration
/// for all entity types in the DbContext.
/// </summary>
public static void Add<TDoc, TId, TDbContext>(this StoreOptions options,
public static void Add<
[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicConstructors
| DynamicallyAccessedMemberTypes.NonPublicConstructors
| DynamicallyAccessedMemberTypes.PublicFields
| DynamicallyAccessedMemberTypes.NonPublicFields
| DynamicallyAccessedMemberTypes.PublicProperties
| DynamicallyAccessedMemberTypes.NonPublicProperties
| DynamicallyAccessedMemberTypes.Interfaces)]
TDoc, TId,
[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicConstructors)]
TDbContext>(this StoreOptions options,
EfCoreMultiStreamProjection<TDoc, TId, TDbContext> projection,
ProjectionLifecycle lifecycle)
where TDoc : class where TId : notnull where TDbContext : DbContext
Expand All @@ -49,7 +70,17 @@ public static void Add<TDoc, TId, TDbContext>(this StoreOptions options,
/// Add an <see cref="EfCoreMultiStreamProjection{TDoc,TId,TDbContext}"/> to a composite projection.
/// Registers EF Core-based aggregate persistence and Weasel schema migration.
/// </summary>
public static void Add<TDoc, TId, TDbContext>(this CompositeProjection composite,
public static void Add<
[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicConstructors
| DynamicallyAccessedMemberTypes.NonPublicConstructors
| DynamicallyAccessedMemberTypes.PublicFields
| DynamicallyAccessedMemberTypes.NonPublicFields
| DynamicallyAccessedMemberTypes.PublicProperties
| DynamicallyAccessedMemberTypes.NonPublicProperties
| DynamicallyAccessedMemberTypes.Interfaces)]
TDoc, TId,
[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicConstructors)]
TDbContext>(this CompositeProjection composite,
StoreOptions options,
EfCoreMultiStreamProjection<TDoc, TId, TDbContext> projection,
int stageNumber = 1)
Expand All @@ -64,7 +95,17 @@ public static void Add<TDoc, TId, TDbContext>(this CompositeProjection composite
/// Add an <see cref="EfCoreSingleStreamProjection{TDoc,TId,TDbContext}"/> to a composite projection.
/// Registers EF Core-based aggregate persistence and Weasel schema migration.
/// </summary>
public static void Add<TDoc, TId, TDbContext>(this CompositeProjection composite,
public static void Add<
[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicConstructors
| DynamicallyAccessedMemberTypes.NonPublicConstructors
| DynamicallyAccessedMemberTypes.PublicFields
| DynamicallyAccessedMemberTypes.NonPublicFields
| DynamicallyAccessedMemberTypes.PublicProperties
| DynamicallyAccessedMemberTypes.NonPublicProperties
| DynamicallyAccessedMemberTypes.Interfaces)]
TDoc, TId,
[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicConstructors)]
TDbContext>(this CompositeProjection composite,
StoreOptions options,
EfCoreSingleStreamProjection<TDoc, TId, TDbContext> projection,
int stageNumber = 1)
Expand All @@ -80,7 +121,9 @@ public static void Add<TDoc, TId, TDbContext>(this CompositeProjection composite
/// Weasel migration pipeline. Tables defined in the DbContext's model will be created
/// and migrated automatically alongside Marten's own schema objects.
/// </summary>
public static void AddEntityTablesFromDbContext<TDbContext>(this StoreOptions options,
public static void AddEntityTablesFromDbContext<
[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicConstructors)]
TDbContext>(this StoreOptions options,
Action<DbContextOptionsBuilder<TDbContext>>? configure = null)
where TDbContext : DbContext
{
Expand Down
18 changes: 17 additions & 1 deletion src/Marten.EntityFrameworkCore/EfCoreProjectionStorage.cs
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
using System;
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using System.Threading;
using System.Threading.Tasks;
using JasperFx.Events;
Expand All @@ -17,7 +18,22 @@ namespace Marten.EntityFrameworkCore;
/// <see cref="DbContextTransactionParticipant{TDbContext}.BeforeCommitAsync"/>
/// swaps to Marten's connection and calls SaveChangesAsync.
/// </summary>
internal class EfCoreProjectionStorage<TDoc, TId, TDbContext> : IProjectionStorage<TDoc, TId>
/// <remarks>
/// AOT: TDoc carries the full DAM flag set required by
/// <c>DbContext.Find&lt;TEntity&gt;</c> / <c>FindAsync&lt;TEntity&gt;</c> and
/// <c>IModel.FindEntityType(Type)</c> on the EF Core API surface. The trim
/// requirement propagates to consumers — callers that close TDoc with a
/// concrete entity type satisfy it implicitly.
/// </remarks>
internal class EfCoreProjectionStorage<
[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicConstructors
| DynamicallyAccessedMemberTypes.NonPublicConstructors
| DynamicallyAccessedMemberTypes.PublicFields
| DynamicallyAccessedMemberTypes.NonPublicFields
| DynamicallyAccessedMemberTypes.PublicProperties
| DynamicallyAccessedMemberTypes.NonPublicProperties
| DynamicallyAccessedMemberTypes.Interfaces)]
TDoc, TId, TDbContext> : IProjectionStorage<TDoc, TId>
where TDoc : class where TId : notnull where TDbContext : DbContext
{
public TDbContext DbContext { get; }
Expand Down
19 changes: 18 additions & 1 deletion src/Marten.EntityFrameworkCore/EfCoreSingleStreamProjection.cs
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
using System;
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using System.Threading;
using System.Threading.Tasks;
using JasperFx.Core;
Expand All @@ -23,7 +24,23 @@ namespace Marten.EntityFrameworkCore;
/// <typeparam name="TDoc">The aggregate document type persisted by EF Core</typeparam>
/// <typeparam name="TId">The stream identity type</typeparam>
/// <typeparam name="TDbContext">The EF Core DbContext type to use</typeparam>
public abstract class EfCoreSingleStreamProjection<TDoc, TId, TDbContext>
/// <remarks>
/// AOT: TDoc carries the DAM flag set required by the wrapped
/// <see cref="EfCoreProjectionStorage{TDoc, TId, TDbContext}"/> — propagating
/// the EF Core <c>FindEntityType</c> / <c>Find&lt;TEntity&gt;</c> trim contract
/// up to consumers.
/// </remarks>
public abstract class EfCoreSingleStreamProjection<
[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicConstructors
| DynamicallyAccessedMemberTypes.NonPublicConstructors
| DynamicallyAccessedMemberTypes.PublicFields
| DynamicallyAccessedMemberTypes.NonPublicFields
| DynamicallyAccessedMemberTypes.PublicProperties
| DynamicallyAccessedMemberTypes.NonPublicProperties
| DynamicallyAccessedMemberTypes.Interfaces)]
TDoc, TId,
[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicConstructors)]
TDbContext>
: SingleStreamProjection<TDoc, TId>, IValidatedProjection<StoreOptions>
where TDoc : class where TId : notnull where TDbContext : DbContext
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
<GenerateAssemblyInformationalVersionAttribute>true</GenerateAssemblyInformationalVersionAttribute>
<GenerateDocumentationFile>true</GenerateDocumentationFile>
<Nullable>enable</Nullable>
<IsAotCompatible>true</IsAotCompatible>
</PropertyGroup>

<ItemGroup>
Expand Down
Loading
Loading