The Fastest Possible HTTP Queries with Marten

I’ve been piddling this weekend with testing out JasperFx Software‘s soon to be officially curated AI Skills. To test and refine those new skills, I’ve been using my buddies Chris Woodruff and Joseph Guadagno‘s MoreSpeakers application as a sample application to port to Wolverine and Marten (and a half dozen others too so far).

I’m sure that you’ll be positively shocked to know that it’s taken quite a bit of minor corrections and “oh, yeah” enhancements to the guidance in the skills to get to exactly where I’d want the translated code to get to. It’s not exactly this bad, but what it’s most reminded me of was my experience coaching youth basketball teams of very young kids when I constantly kick myself after the first game for all the very basic basketball rules and strategies I’d forgotten to tell them about.

Anyway, on to the Marten and Wolverine part of this. Consider this HTTP endpoint in 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);
}

Pretty common request to run a query against the database, then stream the results down to the HTTP response. I’ll write a follow up post later to discuss the greater set of changes, but let’s take that endpoint code above and make it a whole lot more efficient by utilizing Marten.AspNetCore‘s ability to just stream JSON write out of the database like this:

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);
}

The version above is 100% functionally equivalent to the first version, but it’s a lot more efficient at runtime because what it’s doing is writing the JSON directly from the database (Marten is already storing state using PostgreSQL’s JSONB type) right to the HTTP response byte by byte.

And just to be silly and be even more serious about the optimization, let’s introduce Marten’s compiled query feature that completely eliminates the runtime work of having to interpret the LINQ expression into an executable plan for executing the query:

// 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);
}

That’s a little bit uglier code that we had to go out of our way to write compared to the simpler, original mechanism, but that’s basically how performance optimization generally goes!

At no point is it ever trying to deserialize the actual ExpertiseCategory objects in memory. There are of course some limitations or gotchas:

  • There’s no anti-corruption layer of any kind, and this can only send down exactly what is persisted in the Marten database. I’ll tackle this in more detail in a follow up post about the conversion, but I’m going to say I don’t really think this is a big deal at all, and we can introduce some kind of mapping later if we want to change what’s actually stored or how the JSON is served up to the client.
  • You may have to be careful to make Marten’s JSON storage configuration match what HTTP clients want — which is probably just using camel casing and maybe opting into Enum values being serialized as strings.

But now, let’s compare the code above to what the original version using EF pCore had to do. Let’s say it’s about a wash in how long it takes Marten and EF Core to translate the

  1. EF Core has to parse the LINQ expression and turn that into both SQL and some internal execution plan about how to turn the raw results into C# objects
  2. EF Core executes the SQL statement, and if this happens to be a .NET type that has nested objects or collections, this could easily be an ugly SQL statement with multiple JOINs — which Marten doesn’t have to do all
  3. EF Core has to loop around the database results and create .NET objects that map to the raw database results
  4. The original version used AutoMapper in some places to map the internal entities to the DTO types that were going to be delivered over HTTP. That’s a very common .NET architecture, but that’s more runtime overhead and Garbage Collection thrashing than the Marten version
  5. My buddies used an idiomatic Clean/Onion Architecture approach, so there’s a couple extra layers of indirection in their endpoints that require a DI container to build more objects on each request, so there’s even more GC thrasing. It’s not obvious at all, but in the Wolverine versions of the endpoint, there’s absolutely zero usage of the DI container at runtime (that’s not true for every possible endpoint of course).
  6. ASP.Net Core feeds those newly created objects into a JSON serializer and writes the results down to the HTTP response. The AspNetCore team has optimized the heck out of that process, but it’s still 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 with EF Core and Clean Architecture approaches.

I’ll come back later this week with a bigger post on the differences in structure between the original version and the Critter Stack result. It’s actually turning out to be a great exercise for me because the problem domain and domain model mapping of MoreSpeakers actually lends itself to a good example of using DCB to model Event Sourcing. Stay tuned later for that one!

Leave a comment