User guide

Accessing documents and collections

From FirestoreDb, you can create a DocumentReference or CollectionReference directly by their path from the database root. From a DocumentReference you can create a CollectionReference for a child collection, and likewise from a CollectionReference you can create a DocumentReference for a child document.

FirestoreDb db = FirestoreDb.Create(projectId);

// You can create references directly from FirestoreDb:
CollectionReference citiesFromDb = db.Collection("cities");
DocumentReference londonFromDb = db.Document("cities/london");
CollectionReference londonRestaurantsFromDb = db.Collection("cities/london/restaurants");

// Or from other references:
DocumentReference londonFromCities = citiesFromDb.Document("london");
CollectionReference londonRestaurantFromLondon = londonFromDb.Collection("restaurants");

Writing documents

See the data model page for the different representations available. For the rest of this section, we will use the following attributed class:

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

    [FirestoreProperty]
    public string State { get; set; }

    [FirestoreProperty]
    public string Country { get; set; }

    [FirestoreProperty("Capital")]
    public bool IsCapital { get; set; }

    [FirestoreProperty]
    public long Population { get; set; }
}

Creating a document

A specific document can be created using DocumentReference.CreateAsync; alternatively, CollectionReference.AddAsync will generate a random document ID within the associated collection.

FirestoreDb db = FirestoreDb.Create(projectId);

// Create a document with a random ID in the "cities" collection.
CollectionReference collection = db.Collection("cities");
City city = new City
{
    Name = "Los Angeles",
    Country = "USA",
    State = "CA",
    IsCapital = false,
    Population = 3900000L
};

// Alternatively, collection.Document("los-angeles").Create(city);
DocumentReference document = await collection.AddAsync(city);

Once created, a document can be modified in multiple ways.

Updating specific fields

Specific fields in the document can be modified using DocumentReference.UpdateAsync. Single fields can be updated by passing in the field name and the new value; to update multiple fields, pass in a dictionary mapping each field name to its new value.

A precondition may be specified for the update.

Dictionary<FieldPath, object> updates = new Dictionary<FieldPath, object>
{
    { new FieldPath("Population"), 3900005L },
    { new FieldPath("Country"), "United States of America" }
};
await document.UpdateAsync(updates);

Setting document data with optional merging

The SetAsync operation is versatile. By default it replaces all data in the document. For example, this code:

City newCity = new City
{
    Name = "Los Angeles",
    Country = "United States of America",
    Population = 3900005L
};
await document.SetAsync(newCity);

... would end up wiping out setting the State field in the document to a null value. (Capital would still be false, as that's the default value of the bool type.)

To use our class model but specify which fields we want to merge, we can specify an appropriate SetOptions.

City newCity = new City
{
    Name = "Los Angeles",
    Country = "United States of America",
    Population = 3900005L
};
await document.SetAsync(newCity, SetOptions.MergeFields("Country", "Population"));

A simpler option in many cases - particularly when your document doesn't deal with nested data - is to use an anonymous type to specify the fields you want to modify, and specify SetOptions.MergeAll so that all the fields you've specified in the anonymous type are merged, but no others.

await document.SetAsync(
    new { Country = "United States of America", Population = 3900005L },
    SetOptions.MergeAll);

Deleting a document

Deleting a document is simple via DocumentReference.DeleteAsync. A precondition can be specified (for example, to only delete the document if its last-update timestamp matches one you know); otherwise the delete operation is unconditional.

await document.DeleteAsync();
// With no precondition, the delete call succeeds even if the document
// doesn't exist. With a precondition of "document must exist" the call
// will fail if the document doesn't exist.
await document.DeleteAsync();

All of these operations can also be performed in batches via WriteBatch, or within transactions.

Reading documents

Once your data is in Firestore, you probably want to read it at some point.

Fetching a document snapshot

DocumentReference allows you to fetch a snapshot of a document:

DocumentSnapshot snapshot = await document.GetSnapshotAsync();
// Even if there's no document in the server, we still get a snapshot
// back - but it knows the document doesn't exist.
Console.WriteLine(snapshot.Exists);

// Individual fields can be checked and fetched
Console.WriteLine(snapshot.ContainsField("Planet")); // False
Console.WriteLine(snapshot.GetValue<string>("Name")); // Los Angeles

// Or you can deserialize to a dictionary or a model
City fetchedCity = snapshot.ConvertTo<City>();
Console.WriteLine(fetchedCity.Name); // Los Angeles

Querying

You can also query collections, either directly to retrieve all the data from all the documents in the collection, or with filtering, projections, ordering etc.

FirestoreDb db = FirestoreDb.Create(projectId);
CollectionReference collection = db.Collection("cities");

// A CollectionReference is a Query, so we can just fetch everything
QuerySnapshot allCities = await collection.GetSnapshotAsync();
foreach (DocumentSnapshot document in allCities.Documents)
{
    // Do anything you'd normally do with a DocumentSnapshot
    City city = document.ConvertTo<City>();
    Console.WriteLine(city.Name);
}

// But we can apply filters, perform ordering etc too.
Query bigCitiesQuery = collection
    .WhereGreaterThan("Population", 3000000)
    .OrderByDescending("Population");

QuerySnapshot bigCities = await bigCitiesQuery.GetSnapshotAsync();
foreach (DocumentSnapshot document in bigCities.Documents)
{
    // Do anything you'd normally do with a DocumentSnapshot
    City city = document.ConvertTo<City>();
    Console.WriteLine($"{city.Name}: {city.Population}");
}

Transactions

Transactions accept a callback of user code, which is then passed a Transaction object to work with.

The callback can return optionally a value.

The callback will be executed multiple times if the transaction needs to be retried due to conflicting modifications.

In this section, we'll deal with a simple document that just has a single counter. We want to keep an up-to-date counter, and periodically (once per day, for example) we'll update another counter to match the current value. For more details of counters, see the main Firestore user guide.

Updating the daily counter from the current one

Once a day, we want to atomically fetch the current counter, and update the daily one.

FirestoreDb db = FirestoreDb.Create(projectId);
CollectionReference collection = db.Collection("counters");
DocumentReference currentCounter = collection.Document("current");
DocumentReference dailyCounter = collection.Document("daily");

await db.RunTransactionAsync(async transaction =>
{
    DocumentSnapshot currentSnapshot = await transaction.GetSnapshotAsync(currentCounter);
    long counter = currentSnapshot.GetValue<long>("Counter");
    transaction.Set(dailyCounter, new { Counter = counter });
});

Updating the current counter

When we update the current counter, we may well want to know the current value afterwards. That's easily done by returning it from the callback:

FirestoreDb db = FirestoreDb.Create(projectId);
CollectionReference collection = db.Collection("counters");
DocumentReference currentCounter = collection.Document("current");

long newValue = await db.RunTransactionAsync(async transaction =>
{
    DocumentSnapshot currentSnapshot = await transaction.GetSnapshotAsync(currentCounter);
    long counter = currentSnapshot.GetValue<long>("Counter") + 1;
    transaction.Set(currentCounter, new { Counter = counter });
    return counter;
});
// Use the value we've just written in application code
Console.WriteLine(newValue);

Listening for changes

Firestore allows you to listen for changes to either a single document or the results of a query. You provide a callback which is executed each time a change occurs.

First we'll see an example of each, then go into details.

Listening for changes on a document

This example starts listening for changes on a document that doesn't exist yet, then creates the document, updates it, deletes it, and recreates it. Each of these changes is logged by the callback. After stopping the listening operation, the document is updated one final time - which doesn't produce any output.

FirestoreDb db = FirestoreDb.Create(projectId);
// Create a random document ID. The document doesn't exist yet.
DocumentReference doc = db.Collection(collectionId).Document();            

FirestoreChangeListener listener = doc.Listen(snapshot =>
{
    Console.WriteLine($"Callback received document snapshot");
    Console.WriteLine($"Document exists? {snapshot.Exists}");
    if (snapshot.Exists)
    {
        Console.WriteLine($"Value of 'value' field: {snapshot.GetValue<int?>("value")}");
    }
    Console.WriteLine();
});

Console.WriteLine("Creating document");
await doc.CreateAsync(new { value = 10 });
await Task.Delay(1000);

Console.WriteLine($"Updating document");
await doc.SetAsync(new { value = 20 });
await Task.Delay(1000);

Console.WriteLine($"Deleting document");
await doc.DeleteAsync();
await Task.Delay(1000);

Console.WriteLine("Creating document again");
await doc.CreateAsync(new { value = 30 });
await Task.Delay(1000);

Console.WriteLine("Stopping the listener");
await listener.StopAsync();

Console.WriteLine($"Updating document (no output expected)");
await doc.SetAsync(new { value = 40 });
await Task.Delay(1000);

Listening for changes in a query

This example listens for changes in a query of "documents with a score greater than 5, in descending score order". Each document has two fields: "Name" and "Score".

When the listener is set up, the test makes the following data changes:

  • Add a document for Sophie, with score 7
  • Add a document for James, with score 10
  • Update the score for Sophie to 11 (changing its order within the query)
  • Update the score for Sophie to 12 (no change in order, but the document is updated)
  • Update the score for James to 4 (no longer matches the query)
  • Delete the document for Sophie
FirestoreDb db = FirestoreDb.Create(projectId);
CollectionReference collection = db.Collection(collectionId);
Query query = collection.WhereGreaterThan("Score", 5).OrderByDescending("Score");

FirestoreChangeListener listener = query.Listen(snapshot =>
{
    Console.WriteLine($"Callback received query snapshot");
    Console.WriteLine($"Count: {snapshot.Count}");
    Console.WriteLine("Changes:");
    foreach (DocumentChange change in snapshot.Changes)
    {
        DocumentSnapshot document = change.Document;
        Console.WriteLine($"{document.Reference.Id}: ChangeType={change.ChangeType}; OldIndex={change.OldIndex}; NewIndex={change.NewIndex})");
        if (document.Exists)
        {
            string name = document.GetValue<string>("Name");
            int score = document.GetValue<int>("Score");
            Console.WriteLine($"  Document data: Name={name}; Score={score}");
        }
    }
    Console.WriteLine();
});

Console.WriteLine("Creating document for Sophie (Score = 7)");
DocumentReference doc1Ref = await collection.AddAsync(new { Name = "Sophie", Score = 7 });
Console.WriteLine($"Sophie document ID: {doc1Ref.Id}");
await Task.Delay(1000);

Console.WriteLine("Creating document for James (Score = 10)");
DocumentReference doc2Ref = await collection.AddAsync(new { Name = "James", Score = 10 });
Console.WriteLine($"James document ID: {doc2Ref.Id}");
await Task.Delay(1000);

Console.WriteLine("Modifying document for Sophie (set Score = 11, higher than score for James)");
await doc1Ref.UpdateAsync("Score", 11);
await Task.Delay(1000);

Console.WriteLine("Modifying document for Sophie (set Score = 12, no change in position)");
await doc1Ref.UpdateAsync("Score", 12);
await Task.Delay(1000);

Console.WriteLine("Modifying document for James (set Score = 4, below threshold for query)");
await doc2Ref.UpdateAsync("Score", 4);
await Task.Delay(1000);

Console.WriteLine("Deleting document for Sophie");
await doc1Ref.DeleteAsync();
await Task.Delay(1000);

Console.WriteLine("Stopping listener");
await listener.StopAsync();

Listener threading

Each listener you start runs independently. It effectively loops through three steps:

  • Wait for the server to provide changes
  • Update its internal model
  • Report any changes via the callback

The listener waits for the callback to complete before continuing. The callbacks can be provided as either a synchronous action or an asynchronous function that returns a task. For asynchronous callbacks, the listener waits for the returned task to complete. This allows you to use async/await within a callback without worrying about the callback executing multiple times concurrently for a single listener.

Each listener acts independently without any synchronization, so if you use the same callback for multiple listeners, the callback could be called multiple times concurrently, one for each listener.

Although each listener "logically" operates in a single-threaded fashion, it uses asynchronous operations and so the actual thread used can change between callbacks. We strongly recommend against using thread-local storage; ideally, make no assumptions about the thread that will run your callback.

Stopping listeners

When you start listening for changes, the method returns a FirestoreChangeListener. This has two important members:

  • The ListenerTask property returns a task representing the ongoing listen operation.
  • The StopAsync method allows you to stop the listener. For convenience, this returns the same task as ListenerTask.

The StopAsync method will allow any currently executing callback to complete before the listener shuts down. However, both the original Listen calls and the StopAsync methods accept an optional CancellationToken. If this cancellation token is cancelled, not only will the listener stop, but the cancellation token passed to any asynchronous callback will also be cancelled. This allows you to perform asynchronous operations within the callback but still be able to cancel the whole listener quickly.

In the "graceful" shutdown case, nothing is cancelled: you call StopAsync, any current callback completes, and then the listener task completes. If this is all you need, you never need to provide a cancellation token to the methods to start or stop listening. The cancellation token functionality has been provided for two specific scenarios:

  • The listen operation is being executed as part of another cancellable operation. In this case you'd provide the cancellation token when you start listening.

  • The listen operation is long-running, but you need to shut it down as part of shutting down some larger system (such as a web server). Typically this shutdown procedure provides a cancellation token: if the graceful shutdown doesn't complete within a certain time, the cancellation token is cancelled. The signature of StopAsync allows you to integrate Firestore into this pattern easily.

The final status of the listener task will be:

  • RanToCompletion if shutdown completed gracefully.
  • Faulted if either the listener encountered a permanent error, or the callback threw an exception.
  • Canceled if shutdown was either caused by the "start" cancellation token being canceled, or if the "stop" cancellation token was canceled.