Skip to content

Use this LLM Friendly Docs as an MCP server for Marten.

The search box in the website knows all the secrets—try it!

For any queries, join our Discord Channel to reach us faster.

JasperFx Logo

JasperFx provides formal support for Marten and other JasperFx libraries. Please check our Support Plans for more details.

Marten.AspNetCore

TIP

For a little more context, see the blog post Efficient Web Services with Marten V4.

Marten has a small addon that adds helpers for ASP.Net Core development, expressly the ability to very efficiently stream the raw JSON of persisted documents straight to an HTTP response without every having to waste time with deserialization/serialization or even reading the data into a JSON string in memory.

First, to get started, Marten provides Marten.AspNetCore plugin.

Install it through the Nuget package.

powershell
PM> Install-Package Marten.AspNetCore

Single Document

If you need to write a single Marten document to the HTTP response by its id, the most efficient way is this syntax shown in a small sample MVC Core controller method:

cs
[HttpGet("/issue/{issueId}")]
public Task Get(Guid issueId, [FromServices] IQuerySession session, [FromQuery] string? sc = null)
{
    // This "streams" the raw JSON to the HttpResponse
    // w/o ever having to read the full JSON string or
    // deserialize/serialize within the HTTP request
    return sc is null
        ? session.Json
            .WriteById<Issue>(issueId, HttpContext)
        : session.Json
            .WriteById<Issue>(issueId, HttpContext, onFoundStatus: int.Parse(sc));

}

snippet source | anchor

That syntax will write the HTTP content-type and content-length response headers as you'd expect, and copy the raw JSON for the document to the HttpResponse.Body stream if the document is found. The status code will be 200 if the document is found, and 404 if it is not.

Likewise, if you need to write a single document from a Linq query, you have this syntax:

cs
[HttpGet("/issue2/{issueId}")]
public Task Get2(Guid issueId, [FromServices] IQuerySession session, [FromQuery] string? sc = null)
{
    return sc is null
        ? session.Query<Issue>().Where(x => x.Id == issueId)
            .WriteSingle(HttpContext)
        : session.Query<Issue>().Where(x => x.Id == issueId)
            .WriteSingle(HttpContext, onFoundStatus: int.Parse(sc));
}

snippet source | anchor

Multiple Documents

The WriteArray() extension method will allow you to write an array of documents in a Linq query to the outgoing HTTP response like this:

cs
[HttpGet("/issue/open")]
public Task OpenIssues([FromServices] IQuerySession session, [FromQuery] string? sc = null)
{
    // This "streams" the raw JSON to the HttpResponse
    // w/o ever having to read the full JSON string or
    // deserialize/serialize within the HTTP request
    return sc is null
        ? session.Query<Issue>().Where(x => x.Open)
            .WriteArray(HttpContext)
        : session.Query<Issue>().Where(x => x.Open)
            .WriteArray(HttpContext, onFoundStatus: int.Parse(sc));
}

snippet source | anchor

Compiled Query Support

The absolute fastest way to invoke querying in Marten is by using compiled queries that allow you to use Linq queries without the runtime overhead of continuously parsing Linq expressions every time.

Back to the sample endpoint above where we write an array of all the open issues. We can express the same query in a simple compiled query like this:

cs
public class OpenIssues: ICompiledListQuery<Issue>
{
    public Expression<Func<IMartenQueryable<Issue>, IEnumerable<Issue>>> QueryIs()
    {
        return q => q.Where(x => x.Open);
    }
}

snippet source | anchor

And use that in an MVC Controller method like this:

cs
[HttpGet("/issue2/open")]
public Task OpenIssues2([FromServices] IQuerySession session, [FromQuery] string? sc = null)
{
    return sc is null
        ? session.WriteArray(new OpenIssues(), HttpContext)
        : session.WriteArray(new OpenIssues(), HttpContext, onFoundStatus: int.Parse(sc));
}

snippet source | anchor

Likewise, you could use a compiled query to write a single document. As a contrived sample, here's an example compiled query that reads a single Issue document by its id:

cs
public class IssueById: ICompiledQuery<Issue, Issue>
{
    public Expression<Func<IMartenQueryable<Issue>, Issue>> QueryIs()
    {
        return q => q.FirstOrDefault(x => x.Id == Id);
    }

    public Guid Id { get; set; }
}

snippet source | anchor

And the usage of that to write JSON directly to the HttpContext in a controller method:

cs
[HttpGet("/issue3/{issueId}")]
public Task Get3(Guid issueId, [FromServices] IQuerySession session, [FromQuery] string? sc = null)
{
    return sc is null
        ? session.Query<Issue>().Where(x => x.Id == issueId)
            .WriteSingle(HttpContext)
        : session.Query<Issue>().Where(x => x.Id == issueId)
            .WriteSingle(HttpContext, onFoundStatus: int.Parse(sc));
}

snippet source | anchor

Writing Event Sourcing Aggregates

If you are using Marten's event sourcing and single stream projections, the WriteLatest<T>() extension method on IEventStoreOperations lets you stream the projected aggregate's JSON directly to an HTTP response. This is the event sourcing equivalent of WriteById<T>() for documents.

The key advantage is performance: for Inline projections, the aggregate already exists as raw JSONB in PostgreSQL and is streamed directly to the HTTP response with zero deserialization or serialization. For Async projections that are caught up, the same optimization applies. Only when the async daemon is behind does Marten fall back to rebuilding the aggregate in memory.

This is built on top of FetchLatest<T>() — see Reading Aggregates for details on how each projection lifecycle is handled.

Usage with a Guid-identified stream:

cs
[HttpGet("/order/{orderId:guid}")]
public Task GetOrder(Guid orderId, [FromServices] IDocumentSession session)
{
    // Streams the raw JSON of the projected aggregate to the HTTP response
    // without deserialization/serialization when the projection is stored inline
    return session.Events.WriteLatest<Order>(orderId, HttpContext);
}

snippet source | anchor

Usage with a string-identified stream:

cs
[HttpGet("/named-order/{orderId}")]
public Task GetNamedOrder(string orderId, [FromServices] IDocumentSession session)
{
    return session.Events.WriteLatest<NamedOrder>(orderId, HttpContext);
}

snippet source | anchor

Like WriteById<T>(), WriteLatest<T>() returns a 200 status with the JSON body if the aggregate is found, or a 404 with no body if not found. You can customize the content type and success status code:

csharp
// Use a custom status code and content type
await session.Events.WriteLatest<Order>(orderId, HttpContext,
    contentType: "application/json; charset=utf-8",
    onFoundStatus: 201);

WARNING

WriteLatest<T>() requires IDocumentSession (not IQuerySession) because FetchLatest<T>() is only available on IDocumentSession.

There is also a lower-level StreamLatestJson<T>() method on IEventStoreOperations that writes the raw JSON to any Stream, which you can use to build your own response handling:

csharp
var stream = new MemoryStream();
bool found = await session.Events.StreamLatestJson<Order>(orderId, stream);

Typed Streaming Result Types 8.x

For Minimal API endpoints (and for frameworks like Wolverine.Http that dispatch any IResult return value), Marten.AspNetCore ships three typed result wrappers that carry the streaming behavior above as endpoint return values while also contributing correct OpenAPI metadata:

TypeSourceResponse shape404 on miss?
StreamOne<T>IQueryable<T> — regular Marten document querySingle Tyes
StreamMany<T>IQueryable<T> — regular Marten document queryJSON array T[]no (empty array = 200)
StreamAggregate<T>IDocumentSession + stream id — event-sourcedSingle Tyes

Each type implements both IResult (so ASP.NET Minimal API dispatches it via ExecuteAsync) and IEndpointMetadataProvider (so Swashbuckle, NSwag, and the built-in OpenAPI generator see the right response shape), while delegating the actual body write to WriteSingle/WriteArray/WriteLatest. Returning one from an endpoint is a concise, typed alternative to writing the HTTP handshake manually.

StreamOne<T> — single document with 404 on miss

csharp
app.MapGet("/issues/{id:guid}",
    (Guid id, IQuerySession session) =>
        new StreamOne<Issue>(session.Query<Issue>().Where(x => x.Id == id)));

Returns 200 application/json with the document JSON on a hit, 404 on a miss. Content-Length and Content-Type are set automatically, matching the behavior of WriteSingle<T>.

StreamMany<T> — JSON array

csharp
app.MapGet("/issues/open",
    (IQuerySession session) =>
        new StreamMany<Issue>(session.Query<Issue>().Where(x => x.Open)));

Returns 200 application/json with a JSON array body. An empty result set yields [], not a 404 — matching the behavior of WriteArray<T>.

StreamAggregate<T> — event-sourced aggregate (latest)

csharp
app.MapGet("/orders/{id:guid}",
    (Guid id, IDocumentSession session) =>
        new StreamAggregate<Order>(session, id));

Returns 200 application/json with the JSON of the latest projected aggregate state, or 404 if no stream exists. A constructor overload accepts string ids for stores configured with string-keyed streams.

StreamOne vs StreamAggregate

  • StreamOne<T> is for regular Marten documents — plain objects persisted via session.Store() and queried with session.Query<T>(). The query hits the document table directly.
  • StreamAggregate<T> is for event-sourced aggregates. Marten rebuilds the latest aggregate state by folding events from the event store (or reads a projected snapshot if one is configured). Use this when T is an event-sourced aggregate, not a stored document.

Customizing status code and content type

All three types expose init-only properties:

csharp
app.MapPost("/issues",
    (CreateIssue cmd, IQuerySession session) =>
        new StreamOne<Issue>(session.Query<Issue>().Where(x => x.Id == cmd.IssueId))
        {
            OnFoundStatus = StatusCodes.Status201Created,
            ContentType = "application/vnd.myapi.issue+json"
        });

Compiled query overloads

StreamOne and StreamMany also accept Marten compiled queries. These overloads take an extra generic argument for the query result type and the IQuerySession alongside the compiled query:

csharp
public class IssueById : ICompiledQuery<Issue, Issue>
{
    public Guid Id { get; set; }
    public Expression<Func<IMartenQueryable<Issue>, Issue>> QueryIs()
        => q => q.FirstOrDefault(x => x.Id == Id);
}

public class OpenIssues : ICompiledListQuery<Issue>
{
    public Expression<Func<IMartenQueryable<Issue>, IEnumerable<Issue>>> QueryIs()
        => q => q.Where(x => x.Open);
}

app.MapGet("/issues/{id:guid}",
    (Guid id, IQuerySession session) =>
        new StreamOne<Issue, Issue>(session, new IssueById { Id = id }));

app.MapGet("/issues/open",
    (IQuerySession session) =>
        new StreamMany<Issue, IEnumerable<Issue>>(session, new OpenIssues()));

These use WriteOne / WriteArray for compiled queries under the hood. OpenAPI metadata advertises 200: TOut (and 404 for StreamOne), where TOut is the compiled query's declared return type. Prefer compiled queries when the endpoint is on a hot path — Marten caches the compiled SQL and bypasses LINQ parsing on subsequent calls.

Released under the MIT License.