Relations with Azure Cosmos DB and EF Core

After some first experiments using the EF Core provider with Azure Cosmos DB, with the second article on EF Core with Azure Cosmos DB has a focus on associations. With relational databases, EF Core can be used to define relations between different tables in an OO-model. Using a NoSQL database like Azure Cosmos DB, there are different needs. With NoSQL, relations defined in the OO model should often result in hierarchical information stored within one entity. This article describes mapping for such scenarios with EF Core.

Unicorn in the Cosmos

Azure Cosmos DB is a NoSQL database from Microsoft. It is globally distributed and offers multiple models. You can store data with key/values pairs, records with multiple columns (table storage), documents, and data linked within graphs. If you need massive amounts of data, reads, and writes with fast responses and high throughput, Azure Cosmos DB gives you great options. EF Core can be used with the documents version to store data. Here, JSON is used storing documents, and the content of the documents can be queried.

Mapping to Azure Cosmos DB with EF Core

In the previous article about using EF Core with Azure Cosmos DB, I’ve demonstrated first steps including using shadow state with EF Core and Cosmos DB.

Defining Relations

The sample application defines Book, Chapter, and Author types. The Book type defines the LeadAuthor property of type Author. This is a one-to-one relation. The property named Chapters is of type IEnumerable to define a one-to-many relation between Book and Chapter.

public class Book
{
  public Book(string title, string publisher)
    : this(title, publisher, Author.Empty)
  { }

  public Book(string title, string publisher, Author leadAuthor, params Chapter[] chapters)
  {
    BookId = Guid.NewGuid();
    Title = title;
    Publisher = publisher;
    LeadAuthor = leadAuthor;
    _chapters.AddRange(chapters);
  }

  public Guid BookId { get; set; }
  public string Title { get; set; }
  public string? Publisher { get; set; }

  private List<Chapter> _chapters = new List<Chapter>();
  public IEnumerable<Chapter> Chapters => _chapters:

  public Author LeadAuthor { get; set; }
}
public class Chapter
{
  public Chapter(int number, string title, int pages) =>
    (Number, Title, Pages) = (number, title, pages);

  public Guid ChapterId { get; set; } = Guid.NewGuid();
  public int Number { get; set; }
  public string Title { get; set; }
  public int Pages { get; set; }
}
public class Author
{
  public Author(string firstName, string lastName) =>
    (FirstName, LastName) = (firstName, lastName);

  public Guid AuthorId { get; set; } = Guid.NewGuid();
  public string FirstName { get; set; }
  public string LastName { get; set; }

  public override string ToString() => $"{FirstName} {LastName}";

  public static Author Empty => new Author(string.Empty, string.Empty);
}

Define Owned Entities

The relations are defined in the DbContext derived type BooksContext. In the OnModelCreating method, first the name of the EF Core container is defined. By default, the name of the container is the name of the context. Here, the container name is specified invoking the method HasDefaultContainerName. To define different entity types in different containers, the EntityTypeBuilder extension method ToContainer can be used.

With Azure Cosmos DB, container names should not be mapped to the types of the entities. You can store multiple entities of different types into a single container. With the first pricing models for Azure Cosmos DB, payment was by container. With a new variant for the pricing, you can use shared RUs for multiple containers of the same database – but this option needs to be configured on creation of the database.

The relation between Book and the Author types is defined with the OwnsOne method. With relational databases, defining this relation, the columns of the Author type are stored in the table of the Books table. With Cosmos DB, the content of the associated lead author is stored in the book entity. The Book type defines a association to a list of Chapter objects. This relation is mapped with the OwnsMany extension method to define an owned collection. This method is new since EF Core 2.2. With relational databases, owned collections are mapped to separate tables, such as normal one-to-many relations. With relational databases, using this extension method can still result in a different behavior to automatically load the relation on querying the owner type. With document-oriented databases such as Cosmos DB, the entity collection is stored within the owned entity.

public class BooksContext : DbContext
{
  public BooksContext(DbContextOptions<BooksContext> options)
    : base(options) { }

  public DbSet<Book>? Books { get; set; }

  protected override void OnModelCreating(ModelBuilder modelBuilder)
  {
    modelBuilder.HasDefaultContainerName("BooksContainer");
    modelBuilder.Entity<Book>().OwnsOne(b => b.LeadAuthor);
    modelBuilder.Entity<Book>().OwnsMany(b => b.Chapters);
  }
}

Besides using OwnsOne or OwnsMany with the owner entity, you can also define the Author and Chapter types to be owned, either by using the fluent model API Owned on the modelBuilder, or specify the Owned attribute on these types.

Adding Records

The sample application defines the BooksService class. Here, the database is created in the method CreateTheDatabaseAsync. The method WriteBooksAsync creates a new book with multiple chapters and a lead author. The code is not different when using a relational database:

public async Task CreateTheDatabaseAsync()
{
  var created = await _booksContext.Database.EnsureCreatedAsync();
  string message = created ? "created" : "already exists";
  Console.WriteLine($"database {message}");
}

public async Task WriteBooksAsync()
{
  var author = new Author("Christian", "Nagel");
  var c1 = new Chapter(1, ".NET Applications and Tools", 38);
  var c2 = new Chapter(2, "Core C#", 38);
  var c3 = new Chapter(3, "Objects and Types", 34);

  var book1 = new Book("Professional C# 7 and .NET Core 2.0", "Wrox Press", author, c1, c2, c3);
  _booksContext.Books?.Add(book1);
  int changed = await _booksContext.SaveChangesAsync();
  Console.WriteLine($"created {changed} record(s)");
}

Running the application to create the database and to write a book, using the Data Explorer in the Microsoft Azure Portal, the document can be retrieved:

{
"BookId": "bda886b2-76f7-4b4e-acbb-4a213067a0e9",
"Discriminator": "Book",
"Publisher": "Wrox Press",
"Title": "Professional C# 7 and .NET Core 2.0",
"id": "Book|bda886b2-76f7-4b4e-acbb-4a213067a0e9",
"Chapters": [
{
"BookId": "bda886b2-76f7-4b4e-acbb-4a213067a0e9",
"ChapterId": "3bebe9b4-55da-40ac-9953-d5dfaa02e441",
"Discriminator": "Chapter",
"Number": 1,
"Pages": 38,
"Title": ".NET Applications and Tools"
},
{
"BookId": "bda886b2-76f7-4b4e-acbb-4a213067a0e9",
"ChapterId": "e41104e0-a8cf-4680-8e09-88b5bb798920",
"Discriminator": "Chapter",
"Number": 2,
"Pages": 38,
"Title": "Core C#"
},
{
"BookId": "bda886b2-76f7-4b4e-acbb-4a213067a0e9",
"ChapterId": "48e4afcc-bc8a-4765-a995-a30384931954",
"Discriminator": "Chapter",
"Number": 3,
"Pages": 34,
"Title": "Objects and Types"
}
],
"LeadAuthor": {
"BookId": "bda886b2-76f7-4b4e-acbb-4a213067a0e9",
"AuthorId": "b59e4e0b-e257-4d98-8ca6-dd693e56c6b8",
"Discriminator": "Author",
"FirstName": "Christian",
"LastName": "Nagel"
},
"__partitionKey": "0",
"_rid": "6fk5AN7uXiwBAAAAAAAAAA==",
"_self": "dbs/6fk5AA==/colls/6fk5AN7uXiw=/docs/6fk5AN7uXiwBAAAAAAAAAA==/",
"_etag": "\"08005bb4-0000-0d00-0000-5cb3304b0000\"",
"_attachments": "attachments/",
"_ts": 1555247179
}

Query

A query to Cosmos DB can now done in the same way like with a relational database. With owned entities, and owned collections, results are retrieved:

public void ReadBooks()
{
  var books = _booksContext.Books.Where(b => b.Publisher == "Wrox Press");
  foreach (var book in books)
  {
    Console.WriteLine($"{book.Title} {book.Publisher} {book.BookId}");
    foreach (var chapter in book.Chapters)
    {
      Console.WriteLine($"chapter: {chapter.Title}");
    }
    Console.WriteLine($"author: {book.LeadAuthor}");
    Console.WriteLine();
  }
}

Summary

The same API used with Microsoft SQL Server – Entity Framework Core (EF Core) – can not only be used with relational databases, but also with NoSQL. Contrary to the older version Entity Framework, EF Core was designed not only for relational databases, but also for NoSQL. With .NET Core 3.0, a provider is planned for Azure Cosmos DB. Concepts such as owned entities and owned collections map nicely to NoSQL.

If you’ve read this far, consider buying me a coffee which helps me staying up longer and writing more articles.

Buy Me A Coffee

Interesting Links for this article:

First experiments using EF Core with Azure Cosmos DB

Mapping to Getter-only Properties with EF Core

More information on EF Core and writing data-driven applications is in my book Professional C# 7 and .NET Core 2.0, and in my Data Programming workshops.

Enjoy learning and programming!

Christian

2 thoughts on “Relations with Azure Cosmos DB and EF Core

Leave a comment

This site uses Akismet to reduce spam. Learn how your comment data is processed.