DEV Community

Mihail Brinza
Mihail Brinza

Posted on

Stop using magic strings in your Firestore .NET queries

If you use Firestore with .NET, you've probably written code like this:

query.WhereEqualTo("Location.home_country", "Portugal");
Enter fullscreen mode Exit fullscreen mode

That "Location.home_country" is a string. Nobody checks it at compile time. If you typo it, rename a property, or forget that your C# property Country maps to home_country in Firestore, you won't know until runtime.

I kept running into this in my own projects, so I wrote a typed wrapper around Google's official Firestore client that uses lambda expressions instead of strings.

What it looks like

// Before — string-based, no compile-time checking
query.WhereEqualTo("Locaiton.home_country", "Portugal"); // typo, compiles fine

// After — lambda-based, compiler catches errors
query.WhereEqualTo(u => u.Locaiton.Country, "Portugal");
// CS1061: 'User' does not contain a definition for 'Locaiton'
Enter fullscreen mode Exit fullscreen mode

The library reads [FirestoreProperty] attributes automatically, so you always use the C# property name and it resolves the Firestore storage name for you.

Updates are type-safe too

With the official client, you can pass any object as a field value. With the typed client, the value type is inferred from the property:

// won't compile — Age is int
await doc.UpdateAsync(u => u.Age, "eighteen");

// works
await doc.UpdateAsync(u => u.Age, 18);
Enter fullscreen mode Exit fullscreen mode

Multi-field updates also work:

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

await document.UpdateAsync(update);
Enter fullscreen mode Exit fullscreen mode

Compare that to the official client:

var updates = new Dictionary<FieldPath, object>
{
    { new FieldPath("Age"), 18 },
    { new FieldPath("Location.home_country"), "Spain" }
};
await document.UpdateAsync(updates);
Enter fullscreen mode Exit fullscreen mode

How it works

The library uses a MemberExpression visitor to walk the lambda expression tree, check each property for [FirestoreProperty] attributes, and build the correct Firestore field path. Simple fields resolve in about 450ns, nested fields in about 1μs. In practice, this is invisible next to the network call to Firestore.

Everything else (transactions, listeners, batched writes, subcollections) is delegated directly to the official Google.Cloud.Firestore client. You're not giving up any functionality.

Getting started

dotnet add package Firestore.Typed.Client
Enter fullscreen mode Exit fullscreen mode
FirestoreDb db = await FirestoreDb.CreateAsync("your-project-id");
TypedCollectionReference<User> collection = db.TypedCollection<User>("users");

TypedQuery<User> query = collection
    .WhereGreaterThanOrEqualTo(u => u.Age, 18)
    .WhereEqualTo(u => u.Location.Country, "Portugal")
    .OrderBy(u => u.Age);

TypedQuerySnapshot<User> results = await query.GetSnapshotAsync();
foreach (TypedDocumentSnapshot<User> doc in results.Documents)
{
    User user = doc.Object;
}
Enter fullscreen mode Exit fullscreen mode

Targets .NET Standard 2.0, so it works with .NET Framework 4.6.1+ through .NET 10.

Source: https://github.com/mihail-brinza/firestore-dotnet-typed-client

Top comments (0)