[DotNet] MartenDB Introduction 01
Lately I read some material about logging. Along the way I ran into MartenDB, a library that bends Postgres into a document database. It looked fun enough to write about. MartenDB wants to push the JSONB support in Postgres as far as it goes. The official documentation has two parts. The first part shows how to use Postgres as a MongoDB replacement through MartenDB. The second part shows how to build Event Sourcing on top of the same framework. I will probably stop before I get to the second part, haha.
Document databases and PostgreSQL
In system development, most teams treat a relational database as the default place to store data. That habit makes most developers start from the design of the table schema. The whole system design then has to fit inside the limits of the relational database. A document database brings real advantages. One of them is a data model that no table schema constrains.
The problem is sharpest early in a project. At that point you cannot pin down the business logic, so a database schema is hard to design. You often discover later that the plan does not match the real requirements. By then you cannot change the table schema easily, so you live with the mistake.
For that reason many startups now build on a document database such as MongoDB. As this demand grew, the RDBMS camp added support for the JSON format as well. PostgreSQL, for example, offers the JSONB type, and it later improved its query support for JSON data.
Introduction to MartenDB
MartenDB is a framework that improves on PostgreSQL. It wants to make full use of the JSONB support in PostgreSQL. Most developers and DBAs may not be very familiar with JSONB operations. Marten wants those operations to be simple and easy to use. If you are curious, watch this video. Even if you do not choose MartenDB, I still recommend that you read the official documentation. It contains small tricks for JSONB in PostgreSQL, such as how to index a key. All of them are very useful.
Basic operations in MartenDB
Basic configuration
First install the Marten NuGet package. If you use the dependency injection that Microsoft provides, add this configuration.C#services.AddMarten(opts => { // Put the Postgres connection string here opts.Connection(connectionString); // You can supply a custom logger opts.Logger(); }) // This setting behaves differently per environment // In development it creates the required tables automatically // In production it never creates anything .OptimizeArtifactWorkflow() // In most cases you can turn this setting on .UseLightweightSessions();If you do not use dependency injection, one short line is enough.
C#var store = DocumentStore.For(connectionString);DocumentStore
Marten has aDocumentStoreobject that controls the database connection, much likeDbContextin EF Core.IDocumentStore:
Its lifetime isSingleton. It manages the connection, the operational settings, data migration and so on.IDocumentSession:
Its lifetime isScoped. You use it to query and to write. By default it uses an identity map that caches the data of every query. UseLightweightSessions, which I mentioned above, turns that behavior off. Marten offers that switch because a single request rarely repeats the same query. The cache also disappears together with the object when the request ends.IQuerySession:
I recommend this one for the read-only case.
Other settings
Marten offers a number of other settings. I list only a few here.- It serializes with JSON.NET by default. You can switch to System.Text or write your own.
- You can configure a retry policy.
- You can configure the columns, but you can customize the alias of a table only in limited ways. Every table must carry the mt_ prefix ( judging by this issue, the author has no plan at all to open it up).
- Connections to several databases (this probably pairs with the Event Source feature).
An example
I only adjusted the example from the documentation a little and moved it into a WebAPI project.C#[ApiController] [Route("[controller]")] public class UserController : ControllerBase { [HttpPost] public async Task<Guid> Post( [FromBody] CreateUserRequest request, // This action writes, so it uses IDocumentSession [FromServices] IDocumentSession session) { var user = new User { FirstName = request.FirstName, LastName = request.LastName, Internal = request.Internal }; session.Insert(user); await session.SaveChangesAsync(); // Note that an Id is mandatory // You can model the data as a hierarchy of objects // Turn everything except the Id into a value object with a record return user.Id; } [HttpGet("/{id:guid}")] public async Task<User?> Get( [FromRoute] Guid id, // This action is read-only, so IQuerySession is enough [FromServices] IQuerySession session) => await session.LoadAsync<User>(id); // Linq queries work too // await session.LoadAsync<User>().FirstOrDefaultAsync(x => x.Id == id); }There are further techniques, such as upsert and batch operations, which I do not show here. Next, look at the table that Marten generated.
table_name: mt_doc_user
id data mt_last_modified mt_version mt_dotnet_type 0188d468-8526-4c4e-ac0a-d142363918ab 2023-06-19 16:06:39.785208 +00:00 0188d468-8528-4889-8a47-bdc600e5e039 User The generated table is called
mt_doc_user. Marten serialized the whole User into data, and it treats the Id as the id of the row. The documentation also explains that you can pull a field out into its own column when you need to search it easily. It covers index tricks as well. The whole thing is fairly simple to use.
Wrap-up
On the whole MartenDB is easy enough to use, and it works only with PostgreSQL. It lets you define the binding to the database yourself, a little like code-first in EF Core. It also has settings for database concurrency. After all, what I mainly want from it is a low-cost event store. I hope the next post does not take too long.