Firestore.Typed.Client 2.0.1

dotnet add package Firestore.Typed.Client --version 2.0.1
                    
NuGet\Install-Package Firestore.Typed.Client -Version 2.0.1
                    
This command is intended to be used within the Package Manager Console in Visual Studio, as it uses the NuGet module's version of Install-Package.
<PackageReference Include="Firestore.Typed.Client" Version="2.0.1" />
                    
For projects that support PackageReference, copy this XML node into the project file to reference the package.
<PackageVersion Include="Firestore.Typed.Client" Version="2.0.1" />
                    
Directory.Packages.props
<PackageReference Include="Firestore.Typed.Client" />
                    
Project file
For projects that support Central Package Management (CPM), copy this XML node into the solution Directory.Packages.props file to version the package.
paket add Firestore.Typed.Client --version 2.0.1
                    
#r "nuget: Firestore.Typed.Client, 2.0.1"
                    
#r directive can be used in F# Interactive and Polyglot Notebooks. Copy this into the interactive tool or source code of the script to reference the package.
#:package Firestore.Typed.Client@2.0.1
                    
#:package directive can be used in C# file-based apps starting in .NET 10 preview 4. Copy this into a .cs file before any lines of code to reference the package.
#addin nuget:?package=Firestore.Typed.Client&version=2.0.1
                    
Install as a Cake Addin
#tool nuget:?package=Firestore.Typed.Client&version=2.0.1
                    
Install as a Cake Tool

Firestore .NET Typed Client

GitHub Workflow Status (with event) coverage alternate text is missing from this package README image alternate text is missing from this package README image alternate text is missing from this package README image

A light-weight, strongly-typed Firestore client that catches query errors at compile time instead of in production.

The Problem

The official Google Firestore client references fields by name using plain strings. This means typos, renamed properties, and wrong field names all compile without errors and only fail at runtime.

// Official client — compiles fine, breaks at runtime
query.WhereEqualTo("Locaiton.home_country", "Portugal");

// Typed client — compiler catches the typo immediately
query.WhereEqualTo(u => u.Locaiton.Country, "Portugal");
// CS1061: 'User' does not contain a definition for 'Locaiton'

Firestore also lets you define custom storage names via [FirestoreProperty("home_country")]. With the official client you have to remember to use "home_country" instead of "Country" in every query. The typed client resolves this automatically:

// Official client — you need to know the Firestore storage name
query.WhereEqualTo("Location.home_country", "Portugal");

// Typed client — uses the C# property, resolves the storage name for you
query.WhereEqualTo(u => u.Location.Country, "Portugal");

Why use the Typed Client?

Official Client Typed Client
Field references Magic strings Lambda expressions
Custom field names Manual lookup Automatic
Wrong field name Silent runtime failure Compile error
Type mismatch on update Runtime exception Compile error
API compatibility Full (wraps the official client)

This library wraps the official Google.Cloud.Firestore client. Everything the official client supports (transactions, listeners, batched writes) works the same way.

Installation

dotnet add package Firestore.Typed.Client

Compatibility

This library targets .NET Standard 2.0, which means it works with:

  • .NET Framework 4.6.1+
  • .NET Core 2.0+
  • .NET 5, 6, 7, 8, 9, 10+

Quick Start

The examples below use the following data model:

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

    [FirestoreProperty("second_name")]
    public string SecondName { get; set; }

    [FirestoreProperty]
    public int Age { get; set; }

    [FirestoreProperty]
    public Location Location { get; set; }
}

[FirestoreData]
public class Location
{
    [FirestoreProperty]
    public string City { get; set; }

    [FirestoreProperty("home_country")]
    public string Country { get; set; }
}

For more information about data modeling, see the Official Documentation.

FirestoreDb db = await FirestoreDb.CreateAsync("your-project-id");

// Create a document with a random ID in the "users" collection.
TypedCollectionReference<User> collection = db.TypedCollection<User>("users");

User newUser = new User
{
    FirstName  = "John",
    SecondName = "Doe",
    Age        = 10,
    Location = new Location
    {
        City    = "Lisbon",
        Country = "Portugal"
    }
};

TypedDocumentReference<User> document = await collection.AddAsync(newUser);

// A TypedDocumentReference<User> doesn't contain the data - it's just a path.
// Let's fetch the current document.
TypedDocumentSnapshot<User> snapshot = await document.GetSnapshotAsync();

// We can access individual fields by selecting them using a lambda expression
string firstName = snapshot.GetValue(user => user.FirstName);
int age = snapshot.GetValue(user => user.Age);
string country = snapshot.GetValue(user => user.Location.Country);

// Or get the deserialized object
User? createdUser = snapshot.Object;

// Query the collection for all documents
// where doc.Age < 35 && doc.Location.home_country == Portugal.
TypedQuery<User> query = collection
    .WhereLessThan(user => user.Age, 35)
    .WhereEqualTo(user => user.Location.Country, "Portugal");
TypedQuerySnapshot<User> querySnapshot = await query.GetSnapshotAsync();

foreach (TypedDocumentSnapshot<User> queryResult in querySnapshot.Documents)
{
    User? userResult = queryResult.Object;
}

API

Access Collection

TypedCollectionReference<User> collection = db.TypedCollection<User>("users");

Creating a Document

AddAsync only accepts the document type TDocument, so passing the wrong type won't compile.

TypedDocumentReference<User> document = await collection.AddAsync(user);

Reading Documents

TypedDocumentSnapshot<User> snapshot = await document.GetSnapshotAsync();
bool documentExists = snapshot.Exists;

// Individual fields can be checked and fetched
bool hasAge = snapshot.ContainsField(user => user.Age);
string city = snapshot.GetValue(user => user.Location.City);

// Or get the deserialized object
User user = snapshot.Object;

Updating Specific Fields

The typed client enforces the correct value type for each field. Passing the wrong type won't compile.

Typed Client

Multi field update:

UpdateDefinition<User> update = new UpdateDefinition<User>()
    .Set(user => user.Age, 18)
    .Set(user => user.Location.Country, "Spain");

WriteResult result = await document.UpdateAsync(update);

Single field update:

WriteResult result = await document.UpdateAsync(user => user.Age, 18);
Official Client

Multi field update:

// Note that here we need to refer to the custom field name "home_country",
// while with the typed client the reference to the custom name is automatic
Dictionary<FieldPath, object> updates = new Dictionary<FieldPath, object>
{
    { new FieldPath("Age"), 18 },
    { new FieldPath("Location.home_country"), "Spain" }
};
WriteResult result = await document.UpdateAsync(updates);

Single field update:

WriteResult result = await document.UpdateAsync("Age", 18);

Replace Document with Optional Merge

// Replaces document, with non specified fields having the default value
await document.SetAsync(newUser);

// Sets Age and FirstName, keeping everything else as it was
await document.SetAsync(newUser, TypedSetOptions<User>.MergeFields(u => u.Age, u => u.FirstName));

// The untyped way of merging using anonymous classes is also supported
await document.SetAsync(new { Age = 38, FirstName = "John" }, SetOptions.MergeAll);

Querying

Typed Client
TypedCollectionReference<User> collection = db.TypedCollection<User>("users");

// A TypedCollectionReference<User> is a TypedQuery<User>, so we can fetch everything
TypedQuerySnapshot<User> allUsers = await collection.GetSnapshotAsync();
foreach (TypedDocumentSnapshot<User> document in allUsers.Documents)
{
    User user = document.Object;
}

// Filters, ordering, and pagination are supported
TypedQuery<User> adultsFromPortugalQuery = collection
    .OrderBy(user => user.Age)
    .WhereGreaterThanOrEqualTo(user => user.Age, 18)
    .WhereEqualTo(user => user.Location.Country, "Portugal")
    .OrderByDescending(user => user.Age);

TypedQuerySnapshot<User> results = await adultsFromPortugalQuery.GetSnapshotAsync();
foreach (TypedDocumentSnapshot<User> document in results.Documents)
{
    User user = document.Object;
    Console.WriteLine($"{user.FirstName}: {user.SecondName}");
}
Official Client
CollectionReference collection = db.Collection("users");

QuerySnapshot allUsers = await collection.GetSnapshotAsync();
foreach (DocumentSnapshot document in allUsers.Documents)
{
    User user = document.ConvertTo<User>();
}

Query adultsFromPortugalQuery = collection
    .OrderBy("Age")
    .WhereGreaterThanOrEqualTo("Age", 18)
    .WhereEqualTo("Location.home_country", "Portugal")
    .OrderByDescending("Age");

QuerySnapshot results = await adultsFromPortugalQuery.GetSnapshotAsync();
foreach (DocumentSnapshot document in results.Documents)
{
    User user = document.ConvertTo<User>();
    Console.WriteLine($"{user.FirstName}: {user.SecondName}");
}

Deleting Documents

Works the same as the official client. See the Official Documentation.

await document.DeleteAsync();

Everything else (transactions, listeners, batched writes) works exactly as the official client. For more details, see the Official Documentation.

Benchmarks

This section compares the Official Client performance against the Typed client. The benchmarks were performed using the Firestore Emulator with the following setup:

Windows 11, AMD Ryzen 5 7600X 4.70GHz, 6 cores
.NET SDK 10.0.101, .NET 10.0.1 (x64 RyuJIT x86-64-v4)
BenchmarkDotNet v0.15.8

1. Lambda Field Translator Benchmark

The only real overhead of the Typed Client is translating lambda expressions into Firestore field names. Below are the results of translating a SimpleField u => u.FirstName and a NestedField u => u.Location.Country.

| Method      | Mean       | Error   | StdDev  | Gen0   | Allocated |
|------------ |-----------:|--------:|--------:|-------:|----------:|
| SimpleField |   445.7 ns | 5.11 ns | 4.53 ns | 0.0401 |     672 B |
| NestedField | 1,017.5 ns | 6.40 ns | 5.00 ns | 0.0725 |    1232 B |

2. Single Entity Benchmark

This benchmark consists of creating a user, getting it by id, and querying by country. Each operation is run 50 times with interleaved typed/official calls for fair comparison.

| Method         |     Mean |
|--------------- |---------:|
| TypedClient    | 5.69 ms  |
| OfficialClient | 6.07 ms  |

3. Multiple Entities Benchmark

This benchmark consists of inserting users in batch, querying by Age and Country, fetching all documents, and cleaning up. Each size is run 10 times with interleaved calls.

| Users | TypedClient |  OfficialClient |  Diff |
|------:|------------:|----------------:|------:|
|     1 |     7.22 ms |         6.44 ms | +12%  |
|     5 |     8.02 ms |         7.60 ms |  +6%  |
|    10 |     7.41 ms |         7.35 ms |  +1%  |
|    50 |    75.25 ms |        83.21 ms | -10%  |
|   100 |   101.01 ms |        97.14 ms |  +4%  |

The differences between the TypedClient and the Official one fluctuate in both directions and fall within normal variance for I/O-bound emulator operations. The TypedClient uses the OfficialClient underneath, so the performance overhead is negligible.

Product Compatible and additional computed target framework versions.
.NET net5.0 was computed.  net5.0-windows was computed.  net6.0 was computed.  net6.0-android was computed.  net6.0-ios was computed.  net6.0-maccatalyst was computed.  net6.0-macos was computed.  net6.0-tvos was computed.  net6.0-windows was computed.  net7.0 was computed.  net7.0-android was computed.  net7.0-ios was computed.  net7.0-maccatalyst was computed.  net7.0-macos was computed.  net7.0-tvos was computed.  net7.0-windows was computed.  net8.0 was computed.  net8.0-android was computed.  net8.0-browser was computed.  net8.0-ios was computed.  net8.0-maccatalyst was computed.  net8.0-macos was computed.  net8.0-tvos was computed.  net8.0-windows was computed.  net9.0 was computed.  net9.0-android was computed.  net9.0-browser was computed.  net9.0-ios was computed.  net9.0-maccatalyst was computed.  net9.0-macos was computed.  net9.0-tvos was computed.  net9.0-windows was computed.  net10.0 was computed.  net10.0-android was computed.  net10.0-browser was computed.  net10.0-ios was computed.  net10.0-maccatalyst was computed.  net10.0-macos was computed.  net10.0-tvos was computed.  net10.0-windows was computed. 
.NET Core netcoreapp2.0 was computed.  netcoreapp2.1 was computed.  netcoreapp2.2 was computed.  netcoreapp3.0 was computed.  netcoreapp3.1 was computed. 
.NET Standard netstandard2.0 is compatible.  netstandard2.1 was computed. 
.NET Framework net461 was computed.  net462 was computed.  net463 was computed.  net47 was computed.  net471 was computed.  net472 was computed.  net48 was computed.  net481 was computed. 
MonoAndroid monoandroid was computed. 
MonoMac monomac was computed. 
MonoTouch monotouch was computed. 
Tizen tizen40 was computed.  tizen60 was computed. 
Xamarin.iOS xamarinios was computed. 
Xamarin.Mac xamarinmac was computed. 
Xamarin.TVOS xamarintvos was computed. 
Xamarin.WatchOS xamarinwatchos was computed. 
Compatible target framework(s)
Included target framework(s) (in package)
Learn more about Target Frameworks and .NET Standard.

NuGet packages

This package is not used by any NuGet packages.

GitHub repositories

This package is not used by any popular GitHub repositories.

Version Downloads Last Updated
2.0.1 98 3/8/2026
2.0.0 93 3/8/2026
1.1.0 400 11/24/2023
1.0.6 678 8/31/2022
1.0.5 516 8/31/2022
1.0.4 511 8/31/2022
1.0.3 535 8/30/2022
1.0.2 546 8/30/2022
1.0.1 568 8/29/2022
1.0.0 532 8/8/2022