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 asListenerTask
.
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.