Firestore #2 Getting started with Firestore using C# and .NET Core
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 documentFirestoreProperty
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.
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}");
}
}