Using RavenDB Unit of Work and .NET Core MVC
We were asked about best practices for managing the RavenDB session (unit of work) in a .NET Core MVC application. I thought it is interesting enough to warrant its own post.
RavenDB’s client API is divided into the Document Store, which holds the overall configuration required to access a RavenDB and the Document Session, which is a short lived object implementing Unit of Work pattern and typically only used for a single request.
We’ll start by adding the RavenDB configuration to the appsettings.json file, like so:
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters. Learn more about bidirectional Unicode characters
{ "Database": { "Urls": [ "https://a.rock-n.roll.ravendb.cloud", "https://b.rock-n.roll.ravendb.cloud", "https://c.rock-n.roll.ravendb.cloud", ], "DatabaseName": "Jazz", "CertPath": "/mnt/config/jazz.pfx", "CertPass": null } }
We bind it to the following strongly typed configuration class:
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters. Learn more about bidirectional Unicode characters
public class Settings { public DatabaseSettings Database { get; set; } public class DatabaseSettings { public string[] Urls { get; set; } public string DatabaseName { get; set; } public string CertPath { get;set; } public string CertPass { get;set; } } }
The last thing to do it to register this with the container for dependency injection purposes:
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters. Learn more about bidirectional Unicode characters
public void ConfigureServices(IServiceCollection services) { var settings = new Settings(); Configuration.Bind(settings); var store = new DocumentStore { Urls = settings.Urls, Database = settings.DatabaseName, Certificate = new X509Certificate2( settings.CertPath, settings.CertPass) }; store.Initialize(); services.AddSingleton<IDocumentStore>(store); services.AddScoped<IAsyncDocumentSession>(serviceProvider => { return serviceProvider .GetService<IDocumentStore>() .OpenAsyncSession(); }); }
We register both the Document Store and the Document Session in the container, but note that the session is registered on a scoped mode, so each connection will get a new session.
Finally, let’s make actual use of the session in a controller:
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters. Learn more about bidirectional Unicode characters
public class UsersController : Controller { private readonly IAsyncDocumentSession _session; public UsersController(IAsyncDocumentSession session) { _session = session; } public async Task<IActionResult> Index() { var users = await session.Query<User>().ToListAsync(); return View(users); } }
Note that we used to recommend having SaveChangesAsync run for you automatically, but at this time, I think it is probably better to do this explicitly.
Comments
I created a filter for doing Save automatically (https://github.com/RemiBou/Toss.Blazor/blob/master/Toss.Server/Data/RavenDBSaveAsyncActionFilter.cs) is that a bad practice for you ?
Related: you may be interested in RavenDB.DependencyInjection.
It lets you do what Ayende says here in the post, only with less code. Here's how it looks:
And you're done. There's also some optional extensions that let you custom load a certificate (e.g. from the cert store), as well as some overloads for configuring the doc store before initialization. It's available on NuGet as RavenDB.DependencyInjection.
@Remi, that's good: an action filter for Razor/Blazor pages to call .SaveChangesAsync(). In fact, I do exactly that in RavenDB.Identity Razor sample project. For MVC/API projects, I recommend a RavenController base class.
Remi, It depends on the team and the project. We found that some people didn't like this automatic behavior, and some adored it.
Oren - you might want to take a look at the constructor in the UserController class.
Randy, thanks, fixed
The patterns you posted are great and match the recommended approach from Microsoft. What I don't like is the template code in the controller.
When every controller uses session this code has to basically be copy-pasted in each one. Which I don't like that much.
Instead I made a BaseController
And RavenDBMiddleware
Which I invoke before app.UseMvc() in the Startup.cs Configure method.
This approach is quite similar to the old recommended approach with ASP.NET MVC and .NET 4.7
Comment preview