Cosmos Db with EF Core
1. Intro
EF Core 3 makes it easy to build applications with Cosmos Db. We will use the new Cosmos Db provider to create a NoSQL-powered RESTful web API.
The finished version of the code can be found in this repository
2. Prerequisites
I recommend installing and running the Cosmos Db Emulator on your machine. This will allow us to work with cosmos DB without an Azure subscription.
We will also need Visual Studio 2019 and the .NET Core 3.1 SDK installed on our development machine in order to use EF Core with Cosmos Db.
3. Project Setup
Our first step is to create a .NET Core 3.1 Web API by using the Visual Studio template for a .NET Core web application.
On our Web API Project, we’ll also need to install the following Nuget package:
Install-Package Microsoft.EntityFrameworkCore.Cosmos -Version 3.1.2
4. Data Modelling
We will create an API which allows users to manage daily to-do lists. The first step is to add the following domain models:
//ToDoList.cs public class ToDoList { public string Id { get; set; } public string ProfileId { get; set; } public int Day { get; set; } public int Month { get; set; } public int Year { get; set; } public ICollection<ToDoAction> ToDoActions { get; set; } }
//ToDoAction.cs public class ToDoAction { public string Action { get; set; } public bool Completed { get; set; } }
We are now ready to create our DbContext class:
//ToDoForDayDbContext.cs public class ToDoForDayDbContext : DbContext { public ToDoForDayDbContext(DbContextOptions<ToDoForDayDbContext> options) : base(options) { } public DbSet<ToDoList> ToDoLists { get; set; } protected override void OnModelCreating(ModelBuilder modelBuilder) { base.OnModelCreating(modelBuilder); //ProfileId will be our partition key for ToDoLists modelBuilder.Entity<ToDoList>() .HasPartitionKey(l => l.ProfileId); //ToDoList owns many ToDoActions modelBuilder.Entity<ToDoList>() .OwnsMany(l => l.ToDoActions); } }
Most of the above code should look familiar to an EF Core developer. However, a few details within OnModelCreating method may require explanation.
First, we have defined a partition key for the ToDoList entity. When building an application with Cosmos Db, the choice of partition key is critical.
Queries perform much better when seeking data within a single partition. Furthermore, Cosmos Db transactions are only supported when modifying data inside a single partition.
ProfileId is a reasonable choice for a partition key here, because it is likely that any individual query by a user will only be seeking the data within their own profile. It would also open up the possibility of performing transactions upon a user’s to-do lists if we wanted to move tasks between different to-do lists.
Second, we have specified that each ToDoList owns a collection of ToDoActions. Owned Types are not unique to the CosmosDb EF Core provider, however, they are particularly useful when working with CosmosDb.
The owned ToDoActions will be stored in the same JSON object as the ToDoList. When using document-style databases it is preferable to store related information in a single document. Unlike SQL databases, NoSQL databases are not optimized for joining different records.
5. Configuration
We will have to configure our DI container so that we can easily inject our DbContext class where it is needed. We can do this by modifying the ConfigureServices method of the Startup.cs file:
public void ConfigureServices(IServiceCollection services) { var cosmosServiceUri = "https://localhost:8081"; var cosmosAuthKey = "C2y6yDjf5/R+ob0N8A7Cgv30VRDJIWEHLM+4QDU5DE2nQ9nDuVTqobD4b8mGGyPMbIZnqyMsEcaGQy67XIw/Jw=="; var cosmosDatabaseId = "ToDoForDay"; services.AddDbContext<ToDoForDayDbContext>(o => { o.UseCosmos(cosmosServiceUri, cosmosAuthKey, cosmosDatabaseId); }); services.AddControllers(); }
We have now told our application how to access the Cosmos Db emulator on our machine. The cosmosServiceUri and the cosmosAuthKey are hard-coded here to match the defaults for the Cosmos Db emulator.
6. Building The API
Let’s add a controller called ToDoListsController and inject our DbContext into it:
[Route("api/[controller]")] [ApiController] public class ToDoListsController : ControllerBase { private readonly ToDoForDayDbContext toDoForDayDbContext; public ToDoListsController(ToDoForDayDbContext toDoForDayDbContext) { this.toDoForDayDbContext = toDoForDayDbContext; } }
Let’s implement an endpoint for creating a to-do list. Note that we’ve taken a shortcut for simplicity by calling the EnsureCreatedAsync method here. Ideally, this would be done when the application starts.
[HttpPost] public async Task<ToDoList> Post(ToDoList toDoList) { await toDoForDayDbContext.Database.EnsureCreatedAsync(); toDoList.Id = Guid.NewGuid().ToString(); toDoForDayDbContext.ToDoLists.Add(toDoList); await toDoForDayDbContext.SaveChangesAsync(); return toDoList; }
We can now test our API directly by using Postman or another tool to call the endpoint: localhost:XXXX/api/todolists. Here is an example JSON payload for testing:
{ "ProfileId": "076ef659-8811-4717-9c48-c14fd4759b93", "Day": 4, "Month": 4, "Year": 2020, "toDoActions": [ { "action": "Install Visual Studio", "completed": false } ] }
We can also verify that the to-do list was created by checking the Cosmos Db emulator directly.
It is now fairly simple to implement endpoints for the remaining HTTP Verbs:
[HttpGet] public async Task<List<ToDoList>> Get() { return await toDoForDayDbContext.ToDoLists.ToListAsync(); } [HttpGet("{id}")] public async Task<ToDoList> Get(string id) { return await toDoForDayDbContext.ToDoLists.SingleOrDefaultAsync(t => t.Id == id); } [HttpPut("{id}")] public async Task<ToDoList> Put(string id, ToDoList toDoList) { var toDoListToUpdate = await toDoForDayDbContext.ToDoLists.SingleOrDefaultAsync(t => t.Id == id); toDoListToUpdate.Day = toDoList.Day; toDoListToUpdate.Month = toDoList.Month; toDoListToUpdate.Year = toDoList.Year; toDoListToUpdate.ToDoActions = toDoList.ToDoActions; await toDoForDayDbContext.SaveChangesAsync(); return toDoListToUpdate; } [HttpDelete("{id}")] public async Task Delete(string id) { var todoList = await toDoForDayDbContext.ToDoLists.SingleOrDefaultAsync(t => t.Id == id); toDoForDayDbContext.Remove(todoList); await toDoForDayDbContext.SaveChangesAsync(); }