a teal trianglea yellow circle
Published on

Rapid Backend Development with Marten DB

Authors

Introduction

Building a personal project can be exciting, but it can also be a lot of work. In this post, I will go over how to use Marten DB to quickly build a backend for your project.

The Problem

When building a project, you will often need to build a backend for it. This can be a lot of work, and it's easy to get distracted by the infrastructure and forget about the project. In this post, I will go over how to use Marten DB to quickly build a backend for your project.

The Solution

Marten DB is a fantastic document store that uses PostgreSQL as the database engine. It is easy to use, and allows you to quickly build your backend.

Marten's Background

Before Marten came around, the de facto Document DB to use was Raven DB. Raven DB was a fantastic Document DB, but it was not free.

The developers behind Marten were working on a 'very large web application' that was suffering with performance and stability issues - that was using RavenDB as the document store. They decided to build their own document store, and Marten was born.

But why the name?

Straight from the Marten DB website:

The project name Marten came from a quick Google search one day for "what are the natural predators of ravens?" -- which led to us to use the marten as our project codename and avatar.

A bit of a superhero origin story, but it's a great name!

Setting up Marten DB

First things first lets create a new ASP.NET Core Web API, and add MediatR to it. While we are at it, let's also add Marten.

dotnet new webapi -o MartenTest
cd MartenTest
dotnet add package Marten
dotnet add package MediatR

Now we need to add a connection string to our appsettings.json file. I am using a local PostgreSQL database, but you can use any version Marten supports, which as of right now is: PostgreSQL 9.6 or above database with PLV8

appsettings.Development.json

{
  "ConnectionStrings": {
    "MartenTest": "Host=localhost;Database=MartenTest;Username=postgres;Password=postgres"
  }
}

Next lets setup the pipeline in Program.cs

builder.Services.AddMarten(options =>
{
    options.Connection(builder.Configuration.GetConnectionString("Marten"));

    options.AutoCreateSchemaObjects = builder.Environment.IsDevelopment()
        ? AutoCreate.All
        : AutoCreate.CreateOrUpdate;
});

// For the sake of this article we'll just put everything into one project
builder.Services.AddMediatR(typeof(Program).Assembly);

As a homage to the Blazor template using WeatherForecasts as the model of choice, lets build a few commands and queries to perform CRUD operations on.

MartenTest/Domain/WeatherForecast.cs

public record WeatherForecast(DateTime Date, int TemperatureC, string Summary);

Now, if you've used Document Stores to any degree before - we need to have a field that acts as the identity or primary key. Marten DB by default looks for the Id property, so we need to manually configure it to use Date instead.

MartenTest/Program.cs

builder.Services.AddMarten(options =>
{
    ...
    
    options.Schema.For<WeatherForecast>()
        .Identity(x => x.Date);
});

CREATE

MartenTest/Application/CreateWeatherForecastCommand.cs

public record CreateWeatherForecastCommand(WeatherForecast WeatherForecast) : IRequest<WeatherForecast>;

public class CreateWeatherForecastCommandHandler : IRequestHandler<CreateWeatherForecastCommand, WeatherForecast>
{
    private readonly IDocumentSession _documentSession;

    public CreateWeatherForecastCommandHandler(IDocumentSession documentSession)
    {
        _documentSession = documentSession;
    }

    public async Task<WeatherForecast> Handle(CreateWeatherForecastCommand request, CancellationToken cancellationToken)
    {
        await _documentSession.Insert(request.WeatherForecast);
        await _documentSession.SaveChangesAsync(cancellationToken);

        return request.WeatherForecast;
    }
}

UPDATE

MartenTest/Application/UpdateWeatherForecastCommand.cs

public record UpdateWeatherForecastCommand(WeatherForecast WeatherForecast) : IRequest<WeatherForecast>;

public class UpdateWeatherForecastCommandHandler : IRequestHandler<UpdateWeatherForecastCommand, WeatherForecast>
{
    private readonly IDocumentSession _documentSession;

    public UpdateWeatherForecastCommandHandler(IDocumentSession documentSession)
    {
        _documentSession = documentSession;
    }

    public async Task<WeatherForecast> Handle(UpdateWeatherForecastCommand request, CancellationToken cancellationToken)
    {
        _documentSession.Store(request.WeatherForecast);
        await _documentSession.SaveChangesAsync(cancellationToken);

        return request.WeatherForecast;
    }
}

An important thing to note is the nomenclature. You would use Insert to create a new document, and Store to update or create an existing document.

DELETE

MartenTest/Application/DeleteWeatherForecastCommand.cs

public record DeleteWeatherForecastCommand(DateTime Date) : IRequest;

public class DeleteWeatherForecastCommandHandler : IRequestHandler<DeleteWeatherForecastCommand>
{
    private readonly IDocumentSession _documentSession;

    public DeleteWeatherForecastCommandHandler(IDocumentSession documentSession)
    {
        _documentSession = documentSession;
    }

    public async Task<Unit> Handle(DeleteWeatherForecastCommand request, CancellationToken cancellationToken)
    {
        _documentSession.Delete<WeatherForecast>(new WeatherForecast(request.Date, 0, ""));
        await _documentSession.SaveChangesAsync(cancellationToken);

        return Unit.Value;
    }
}

READ

MartenTest/Application/GetWeatherForecastQuery.cs

public record GetWeatherForecastQuery(DateTime Date) : IRequest<WeatherForecast>;

public class GetWeatherForecastQueryHandler : IRequestHandler<GetWeatherForecastQuery, WeatherForecast>
{
    private readonly IDocumentSession _documentSession;

    public GetWeatherForecastQueryHandler(IDocumentSession documentSession)
    {
        _documentSession = documentSession;
    }

    public Task<WeatherForecast> Handle(GetWeatherForecastQuery request, CancellationToken cancellationToken)
    {
        return _documentSession.Query<WeatherForecast>()
            .FirstOrDefaultAsync(x => x.Date == request.Date, cancellationToken);
    }
}

Just like that we have a fully functional infrastructure. As a bonus - lets use minimal APIs to expose our CRUD operations.

Minimal APIs are a new feature in .NET 6 that allows you to build web APIs without having to use controllers. You can read more about them here.

MediatR is discussing adding support for minimal APIs here.

MartenTest/WebApi/Endpoints.cs

public static class Endpoints
{
    public static void MapEndpoints(this WebApplication app)
    {
        app.MapGet("/weatherforecast/{date}", (DateTime date, IMediator mediator, CancellationToken ct) =>
            mediator.Send(new GetWeatherForecastQuery(date), ct)
        );
        app.MapPost("/weatherforecast", ([FromBody] model, IMediator mediator, CancellationToken ct) =>
            mediator.Send(new CreateWeatherForecastCommand(model), ct)
        );
        app.MapPut("/weatherforecast", ([FromBody] model, IMediator mediator, CancellationToken ct) =>
            mediator.Send(new UpdateWeatherForecastCommand(model), ct)
        );
        app.MapDelete("/weatherforecast/{date}", (DateTime date, IMediator mediator, CancellationToken ct) =>
            mediator.Send(new DeleteWeatherForecastCommand(date), ct)
        );
    }
}

MartenTest/Program.cs

var app = builder.Build();

...

app.MapEndpoints();

app.Run();

And that's it! We have a fully functional CRUD API that uses Marten DB as the data store.

Conclusion

Marten DB is a great tool for building Document Stores in .NET. It's easy to use, and has a lot of great features. I hope this article has helped you get started with Marten DB. If you have any questions or comments, feel free to reach out to me on Twitter @LukeParkerDev.

Please let me know what you think of Marten DB, or this article. I'd love to hear your feedback in the comments below!