Pre-Building Generated Types
Marten uses runtime code generation backed by Roslyn runtime compilation for dynamic code. This is both much more powerful than source generators in what it allows us to actually do, but can have significant memory usage and “cold start” problems (seems to depend on exact configurations, so it’s not a given that you’ll have these issues). Fear not though, Marten introduced a facility to “generate ahead” the code to greatly optimize the "cold start" and memory usage in production scenarios.
The code generation for document storage, event handling, event projections, and additional document stores can be done with one of three modes as shown below:
using var store = DocumentStore.For(opts =>
{
opts.Connection("some connection string");
// This is the default. Marten will always generate
// code dynamically at runtime
opts.GeneratedCodeMode = TypeLoadMode.Dynamic;
// Marten will only use types that are compiled into
// the application assembly ahead of time. This is the
// "pre-built" model
opts.GeneratedCodeMode = TypeLoadMode.Static;
// Explained Below :)
opts.GeneratedCodeMode = TypeLoadMode.Auto;
});
The Auto mode was added alleviate usability issues for folks who did not find the command line options or pre-registration of document types to be practical. Using the Marten.Testing.Documents.User
document from the Marten testing suite as an example, let's start a new document store with the Auto
mode:
using var store = DocumentStore.For(opts =>
{
// ConnectionSource is a little helper in the Marten
// test suite
opts.Connection(ConnectionSource.ConnectionString);
opts.GeneratedCodeMode = TypeLoadMode.Auto;
});
First note that I didn't do anything to tell Marten about the User
document. When this code below is executed for the very first time:
await using var session = store.LightweightSession();
var user = new User { UserName = "admin" };
session.Store(user);
await session.SaveChangesAsync();
Marten encounters the User
document type for the first time, and determines that it needs a type called UserProvider1415907724
(the numeric suffix is a repeatable hash of the generated type's full type name) that is a Marten-generated type that "knows" how to do every possible storage or loading of the User
document type. Marten will do one of two things next:
- If the
UserProvider1415907724
type can be found in the main application assembly, Marten will create a new instance of that class and use that from here on out for allUser
operations - If the
UserProvider1415907724
type cannot be found, Marten will generate the necessary C# code at runtime, write that code to a new file calledUserProvider1415907724.cs
at/Internal/Generated/DocumentStorage
from the file root of your .Net project directory so that the code can be compiled into the application assembly on the next compilation. Finally, Marten will compile the generated code at runtime and use that dynamic assembly to build the actual object for theUser
document type.
The hope is that if a development team uses this approach during its internal testing and debugging, the generated code will just be checked into source control and compiled into the actually deployed binaries for the system in production deployments. Of course, if the Marten configuration changes, you will need to delete the generated code.
TIP
Just like ASP.NET Core, Marten uses the IHostEnvironment.ApplicationName
property to determine the main application assembly. If that value is missing, Marten falls back to the Assembly.GetEntryAssembly()
value.
In some cases you may need to help Marten and .Net itself out to "know" what the application assembly and the correct project root directory for the generated code to be written to. In test harnesses or serverless runtimes like AWS Lambda / Azure Functions you can override the application assembly and project path with this new Marten helper:
using var host = Host.CreateDefaultBuilder()
.ConfigureServices(services =>
{
services.AddMarten(opts =>
{
opts.Connection("some connection string");
opts.SetApplicationProject(typeof(User).Assembly);
});
})
.StartAsync();
Generating all Types Upfront
TIP
Also see the blog post Dynamic Code Generation in Marten V4.
To use the Marten command line tooling to generate all the dynamic code upfront:
To enable the optimized cold start, there are a couple steps:
- Use the Marten command line extensions for your application
- Register all document types, compiled query types, and event store projections upfront in your
DocumentStore
configuration - In your deployment process, you'll need to generate the Marten code with
dotnet run -- codegen write
before actually compiling the build products that will be deployed to production
TIP
In the near future, Marten will probably be extended with better auto-discovery features for document types, compiled queries, and event projections to make this feature easier to use.
As an example, here is the Marten configuration from the project we used to test the pre-generated source code model:
public static class Program
{
public static Task<int> Main(string[] args)
{
return CreateHostBuilder(args).RunOaktonCommands(args);
}
public static IHostBuilder CreateHostBuilder(string[] args)
{
return Host.CreateDefaultBuilder(args)
.ConfigureServices((hostContext, services) =>
{
services.AddMartenStore<IOtherStore>(opts =>
{
opts.Connection(ConnectionSource.ConnectionString);
opts.RegisterDocumentType<Target>();
opts.GeneratedCodeMode = TypeLoadMode.Auto;
});
services.AddMarten(opts =>
{
opts.AutoCreateSchemaObjects = AutoCreate.All;
opts.DatabaseSchemaName = "cli";
opts.DisableNpgsqlLogging = true;
opts.Events.UseOptimizedProjectionRebuilds = true;
opts.MultiTenantedWithSingleServer(
ConnectionSource.ConnectionString,
t => t.WithTenants("tenant1", "tenant2", "tenant3")
);
// This is important, setting this option tells Marten to
// *try* to use pre-generated code at runtime
opts.GeneratedCodeMode = TypeLoadMode.Auto;
//opts.Schema.For<Activity>().AddSubClass<DaemonTests.TestingSupport.Trip>();
// You have to register all persisted document types ahead of time
// RegisterDocumentType<T>() is the equivalent of saying Schema.For<T>()
// just to let Marten know that document type exists
opts.RegisterDocumentType<Target>();
opts.RegisterDocumentType<User>();
// If you use compiled queries, you will need to register the
// compiled query types with Marten ahead of time
opts.RegisterCompiledQueryType(typeof(FindUserByAllTheThings));
// Register all event store projections ahead of time
opts.Projections
.Add(new TripProjectionWithCustomName(), ProjectionLifecycle.Async);
opts.Projections
.Add(new DayProjection(), ProjectionLifecycle.Async);
opts.Projections
.Add(new DistanceProjection(), ProjectionLifecycle.Async);
opts.Projections
.Add(new SimpleProjection(), ProjectionLifecycle.Inline);
// This is actually important to register "live" aggregations too for the code generation
//opts.Projections.LiveStreamAggregation<Trip>();
}).AddAsyncDaemon(DaemonMode.Solo);
});
}
}
Okay, after all that, there should be a new command line option called codegen
for your project. Assuming that you have Oakton wired up as your command line parser, you can preview all the code that Marten would generate for the known document types, compiled queries, and the event store support with this command:
dotnet run -- codegen preview
TIP
Because the generated code can easily get out of sync with the Marten configuration at development time, the Marten team recommends ignoring the generated code files in your source control so that stale generated code is never accidentally migrated to production.
To write the generated code to your project directory, use:
dotnet run -- codegen write
This will build all the dynamic code and write it to the /Internal/Generated/
folder of your project. The code will be in just two files, Events.cs
for the event store support and DocumentStorage.cs
for everything related to document storage. If you like, you can reformat that code and split the types to different files if you want to browse that code -- but remember that it's generated code and that pretty well always means that it's pretty ugly code.
To clean out the generated code, use:
dotnet run -- codegen delete
To just prove out that the code generation is valid, use this command:
dotnet run -- codegen test