I've been testing JasperFx Software's forthcoming AI Skills by porting the MoreSpeakers application to Wolverine and Marten. Like coaching youth basketball, I've discovered fundamental gaps in the guidance that needed correction.
Consider this HTTP endpoint from the translated system:
public static class GetExpertiseCategoriesEndpoint{
[WolverineGet("/api/expertise")]
public static Task<IReadOnlyList<ExpertiseCategory>> Get(IQuerySession session, CancellationToken ct)
=> session.Query<ExpertiseCategory>()
.Where(c => c.IsActive)
.OrderBy(c => c.Sector)
.ThenBy(c => c.Name)
.ToListAsync(ct);
}This runs a database query and streams results to the HTTP response. We can make it far more efficient using Marten.AspNetCore's JSON streaming capability:
public static class GetExpertiseCategoriesEndpoint{
[WolverineGet("/api/expertise")]
// It's an imperfect world. I've never been able to come up with a syntax
// option that would eliminate the need for this attribute that isn't as ugly
// as using the attribute, so ¯\_(ツ)_/¯
[ProducesResponseType<ExpertiseCategory[]>(200, "application/json")]
public static Task Get(IQuerySession session, HttpContext context)
=> session.Query<ExpertiseCategory>()
.Where(c => c.IsActive)
.OrderBy(c => c.Sector)
.ThenBy(c => c.Name)
.WriteArray(context);
}This version is functionally identical but significantly more efficient. It's writing the JSON directly from the database without deserializing objects, since Marten uses PostgreSQL's JSONB type.
For even more optimization, we can introduce Marten's compiled query feature:
// Compiled query — Marten pre-compiles the SQL and query plan once,
// then reuses it for every execution. Combined with WriteArray(),
// the result streams raw JSON from PostgreSQL with zero C# allocation.
public class ActiveExpertiseCategoriesQuery : ICompiledListQuery<ExpertiseCategory>{
public Expression<Func<IMartenQueryable<ExpertiseCategory>, IEnumerable<ExpertiseCategory>>> QueryIs()
=> q => q.Where(c => c.IsActive)
.OrderBy(c => c.Sector)
.ThenBy(c => c.Name);
}
public static class GetExpertiseCategoriesEndpoint{
[WolverineGet("/api/expertise")]
[ProducesResponseType<ExpertiseCategory[]>(200, "application/json")]
public static Task Get(IQuerySession session, HttpContext context)
=> session.WriteArray(new ActiveExpertiseCategoriesQuery(), context);
}This approach is slightly more verbose but represents how performance optimization typically works — that's basically how performance optimization generally goes!
At no point does it deserialize actual objects in memory. However, there are some limitations:
- There's no anti-corruption layer; this sends exactly what's persisted in Marten. This can be addressed later with mapping if needed.
- You must ensure Marten's JSON storage configuration matches HTTP client expectations — typically camel casing and string-serialized enums.
Comparing this to the original EF Core version, the Marten approach eliminates several inefficiencies:
- EF Core parses LINQ expressions into SQL and execution plans
- EF Core executes SQL (potentially with multiple JOINs for nested objects)
- EF Core loops through results creating .NET objects
- The original used AutoMapper for entity-to-DTO mapping
- The Clean/Onion Architecture approach required extra DI container usage per request
- ASP.Net Core serializes objects to JSON
The Marten version bypasses most of this overhead. The whole point of that exhaustive list is just to illustrate how much more efficient the Marten version potentially is than the typical .NET approach.
A follow-up post will explore structural differences between the original and Critter Stack implementations, including how the MoreSpeakers domain model lends itself to Event Sourcing using DCB.
