Firestore #2 Getting started with Firestore using C# and .NET Core

19. May 2023 Firestore 0

Overview

In the previous post, I’ve talked about the Firestore fundamentals. In post we’ll learn how to set up a simple .net project and try to read and write from/to Firestore.

I will use VS Code on mac using .net core 7. You can still follow up if you are using Rider or Visual Studio on Windows.

Case study

In this example we are going to create a simple blog application where we have Users, BlogPosts, and Reviews.

Setting up the project

Open a terminal and,

Add a new solution called BlogPosts:

dotnet new sln --name BlogPosts

Add a new console application called BlogPosts

dotnet new console -n BlogPosts -o BlogPosts

Add the newly created project to your solution:

dotnet sln add ./BlogPosts/BlogPosts.csproj

Your solution should look like this:

Adding data models

Create a new folder under BlogPosts called Models.

Create 3 models:

  • BlogPost
  • User
  • Review

BlogPost.cs

namespace BlogPosts.Models;

public class BlogPost
{
    public User Author { get; set; }
    public string Title { get; set; }
    public string Body { get; set; }
    public DateTime DateAdded { get; set; }
}

Review.cs

namespace BlogPosts.Models;

public class Review
{
    public User User { get; set; }
    public string Comment { get; set; }
    public DateTime DateAdded { get; set; }
}

User.cs

namespace BlogPosts.Models;

public class User
{
    public string Name { get; set; }
}

Adding Google.Cloud.Firestore SDK to project

In order to communicate with Firstore you need to add Google.Cloud.Firestore SDK to the project.

In a real multi-layer application this should be added to your repository layer. But in our case we will just add it to our console application.

dotnet add ./BlogPosts/BlogPosts.csproj package Google.Cloud.Firestore

Decorate models with Firestore attributes

In order to write models in Firestore and be able to deserialise the results back to our models we need to decorate them with Firestore attributes:

  • FirestoreDate to decorate the root document
  • FirestoreProperty to decorate document fields

Modify the models to include these together with relations:

BlogPost.cs

using Google.Cloud.Firestore;

namespace BlogPosts.Models;

[FirestoreData]
public class BlogPost
{
    [FirestoreProperty]
    public User Author { get; set; }
    [FirestoreProperty]
    public string Title { get; set; }
    [FirestoreProperty]
    public string Body { get; set; }
    [FirestoreProperty]
    public string DateAdded { get; set; }

    public ICollection<Review> Reviews { get; set; }
}

Review.cs

using Google.Cloud.Firestore;

namespace BlogPosts.Models;

[FirestoreData]
public class Review
{
    [FirestoreProperty]
    public User User { get; set; }
    [FirestoreProperty]
    public string Comment { get; set; }
    [FirestoreProperty]
    public string DateAdded { get; set; }
}

User.cs

using Google.Cloud.Firestore;

namespace BlogPosts.Models;

[FirestoreData]
public class User
{
    [FirestoreProperty]
    public string Name { get; set; }

    public ICollection<BlogPost> BlogPosts { get; set; }
    public ICollection<Review> Reviews { get; set; }
}

Note that on the linked fields we are not adding any attributes. This is because when you query a document in Firestore, it performs a shallow copy of that document. That means, it only returns the document fields, but not its collections. You need to query the nested collections separately.

Interact with Firestore

Authentication

The application needs to be authenticated against GCP with a service account JSON file when running anywhere but the GCP itself. We need to download the service account JSON file and set the GOOGLE_APPLICATION_CREDENTIALS environment variable to the file’s location so Firestore SDK can use it to authenticate.

In order to download the service account Json file, navigate to IAM (Identity and Access Management) in GCP Console.

On the left bat, select Service Accounts.

Find the App Engine default service account and click the three dots next to it.

Click Add Key and then choose Create new key.

In the modal click Create.

A JSON key will be generated and downloaded.

Copy the key into your solution and rename it to iam-key.json

Add the following line to Program.cs:

Environment.SetEnvironmentVariable("GOOGLE_APPLICATION_CREDENTIALS", "../iam-key.json");

Connecting to database

You can use FirestoreDbBuilder to create a firestore client in order to connect to database. Note that you need to change the project id to your project id:


using Google.Cloud.Firestore;

Environment.SetEnvironmentVariable("GOOGLE_APPLICATION_CREDENTIALS", "../iam-key.json");

static FirestoreDb GetFirestoreClient() =>
    new FirestoreDbBuilder { ProjectId = "dispatchertimer-demo-firestore" }.Build();

Insert Data

static async Task InsertBlog()
{
    var user = new User
    {
        Name = "Behnam"
    };

    var blog = new BlogPost
    {
        Author = user,
        Title = "Firestore demo",
        Body = "Our first firestore document",
        DateAdded = DateTime.UtcNow.ToShortDateString()
    };

    var firestoreDb = GetFirestoreClient();
    // firestoreDb.Collection("blog-posts").AddAsync(blog);
    await firestoreDb.Collection("blog-posts")
                     .Document(Guid.NewGuid().ToString())
                     .SetAsync(blog);
}

Note that we haven’t yet created blog-posts collection. Firestore will be automatically creating that when first called.

There are two ways adding a document:

  • using AddAsync on the collection object; it will generate document Id automatically.
  • using SetAsync on the document object; it needs the document Id.

Note that Firestore doesn’t any data type to represent UUIDs, we have to convert it to string.

Run the application and go to Firestore dashboard in GCP Console. You will see the blog-posts collection has been created and a BlogPost has been added to it.

Insert nested collections

Now that we’ve added a blog post we can add reviews to it which is a nested collection in a blog post.

static async Task<string> AddReview(string documentId, string text)
{
    var user = new User
    {
        Name = "Daisy"
    };

    var review = new Review
    {
        User = user,
        Comment = text,
        DateAdded = DateTime.UtcNow.ToShortDateString()
    };

    string id = Guid.NewGuid().ToString();
    var firestoreDb = GetFirestoreClient();
    await firestoreDb.Collection("blog-posts")
                     .Document(documentId)
                     .Collection("reviews")
                     .Document(id)
                     .SetAsync(review);

    return id;
}

Note how we are referencing nested collections. This can go as deep as many levels.

Note that a Firestore document can only be 1MB in size. That is exclusive of its nested collections.

By passing the document id of the Blog Post you can add few reviews:

await AddReview(id, "This is a review");
await AddReview(id, "Yet another review");

View the document on the Firestore dashboard:

Note that now reviews collection is inside a blog post document. Each blog post will have its own reviews collection.

Querying Firestore

Now that we’ve added blogs and reviews let’s query them.

static async Task ShowAllBlogs()
{
    var firestoreDb = GetFirestoreClient();
    var documentReferences = await firestoreDb.Collection("blog-posts").GetSnapshotAsync();

    Console.WriteLine($"Total {documentReferences.Count} blogs");
    foreach (var document in documentReferences)
    {
        var blog = document.ConvertTo<BlogPost>();
        Console.WriteLine($"Post {document.Id} - title {blog.Title}");
    }
}

In this block we get a snapshot of all the blog posts form Firestore. Note that this won’t actually query all the documents. It just gets the reference to the documents.

In the foreach we loop through the references and convert them to our BlogPost model. Note that, at this point, Firestore will actually retrieve the document from Firestore.

Result:

Query nested documents

static async Task ShowBlogsWithReviews()
{
    var firestoreDb = GetFirestoreClient();
    var blogReferences = await firestoreDb.Collection(BLOGS_COLLECTION).GetSnapshotAsync();

    Console.WriteLine($"Total {blogReferences.Count} blogs");
    foreach (var blogDocument in blogReferences)
    {
        var blogPost = blogDocument.ConvertTo<BlogPost>();
        Console.WriteLine($"Post {blogDocument.Id} - title {blogPost.Title}");

        var reviewReferences = await firestoreDb.Collection("blog-posts")
                                                .Document(blogDocument.Id)
                                                .Collection("reviews")
                                                .GetSnapshotAsync();

        Console.WriteLine($"{Environment.NewLine}Blog {blogDocument.Id} has {reviewReferences.Count} reviews");
        foreach (var reviewDocument in reviewReferences)
        {
            var review = reviewDocument.ConvertTo<Review>();
            Console.WriteLine($"Review {reviewDocument.Id} - [{review.DateAdded}] title {review.Comment}");
        }
    }
}

Note that we need to get the blog-posts documents before we can get to their reviews.

Result:

Code


using BlogPosts.Models;
using Google.Cloud.Firestore;

Environment.SetEnvironmentVariable("GOOGLE_APPLICATION_CREDENTIALS", "../iam-key.json");
const string BLOGS_COLLECTION = "blog-posts";
const string REVIEWS_COLLECTION = "reviews";

static FirestoreDb GetFirestoreClient() =>
    new FirestoreDbBuilder { ProjectId = "dispatchertimer-demo-firestore" }.Build();

var id = await AddNewBlog("Firestore demo");
await AddReview(id, "This is a review");
await AddReview(id, "Yet another review");

await ShowAllBlogs();
await ShowBlogsWithReviews();

static async Task<string> AddNewBlog(string title)
{
    var user = new User
    {
        Name = "Behnam"
    };

    var blog = new BlogPost
    {
        Author = user,
        Title = title,
        Body = "Our first firestore document",
        DateAdded = DateTime.UtcNow.ToShortDateString()
    };

    string id = Guid.NewGuid().ToString();

    var firestoreDb = GetFirestoreClient();
    await firestoreDb.Collection(BLOGS_COLLECTION)
                     .Document(id)
                     .SetAsync(blog);

    return id;
}

static async Task<string> AddReview(string documentId, string text)
{
    var user = new User
    {
        Name = "Daisy"
    };

    var review = new Review
    {
        User = user,
        Comment = text,
        DateAdded = DateTime.UtcNow.ToShortDateString()
    };

    string id = Guid.NewGuid().ToString();
    var firestoreDb = GetFirestoreClient();
    await firestoreDb.Collection(BLOGS_COLLECTION)
                     .Document(documentId)
                     .Collection(REVIEWS_COLLECTION)
                     .Document(id)
                     .SetAsync(review);

    return id;
}

static async Task ShowAllBlogs()
{
    var firestoreDb = GetFirestoreClient();
    var documentReferences = await firestoreDb.Collection(BLOGS_COLLECTION).GetSnapshotAsync();

    Console.WriteLine($"Total {documentReferences.Count} blogs");
    foreach (var document in documentReferences)
    {
        var blog = document.ConvertTo<BlogPost>();
        Console.WriteLine($"Post {document.Id} - title {blog.Title}");
    }
}

static async Task ShowBlogsWithReviews()
{
    var firestoreDb = GetFirestoreClient();
    var blogReferences = await firestoreDb.Collection(BLOGS_COLLECTION).GetSnapshotAsync();

    Console.WriteLine($"Total {blogReferences.Count} blogs");
    foreach (var blogDocument in blogReferences)
    {
        var blogPost = blogDocument.ConvertTo<BlogPost>();
        Console.WriteLine($"Post {blogDocument.Id} - title {blogPost.Title}");

        var reviewReferences = await firestoreDb.Collection(BLOGS_COLLECTION)
                                                .Document(blogDocument.Id)
                                                .Collection(REVIEWS_COLLECTION)
                                                .GetSnapshotAsync();

        Console.WriteLine($"{Environment.NewLine}Blog {blogDocument.Id} has {reviewReferences.Count} reviews");
        foreach (var reviewDocument in reviewReferences)
        {
            var review = reviewDocument.ConvertTo<Review>();
            Console.WriteLine($"Review {reviewDocument.Id} - [{review.DateAdded}] title {review.Comment}");
        }
        Console.WriteLine($"{Environment.NewLine}{Environment.NewLine}");
    }
}

Leave a Reply

Your email address will not be published. Required fields are marked *

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