/// <summary> /// Applies a document modification to the document tree. Returns the DocumentChange event for /// successful modifications, or null if the old and new documents have the same update timestamp. /// </summary> private DocumentChange ModifyDocument(DocumentSnapshot newDocument) { var docRef = newDocument.Reference; if (!_documentSet.TryGetDocument(docRef, out var oldDocument)) { // TODO: Is this appropriate? Java throws an NPE here... throw new InvalidOperationException("Attempt to create a document modification, but document wasn't in set."); } if (oldDocument.UpdateTime == newDocument.UpdateTime) { return(null); } int oldIndex = _documentSet.IndexOf(docRef); _documentSet = _documentSet.WithDocumentRemoved(docRef); _documentSet = _documentSet.WithDocumentAdded(newDocument); int newIndex = _documentSet.IndexOf(docRef); return(new DocumentChange(newDocument, DocumentChange.Type.Modified, oldIndex, newIndex)); }
internal async Task <QuerySnapshot> SnapshotAsync(ByteString transactionId, CancellationToken cancellationToken) { var responses = StreamResponsesAsync(transactionId, cancellationToken); Timestamp?readTime = null; List <DocumentSnapshot> snapshots = new List <DocumentSnapshot>(); await responses.ForEachAsync(response => { if (response.Document != null) { snapshots.Add(DocumentSnapshot.ForDocument(Database, response.Document, Timestamp.FromProto(response.ReadTime))); } if (readTime == null && response.ReadTime != null) { readTime = Timestamp.FromProto(response.ReadTime); } }, cancellationToken).ConfigureAwait(false); GaxPreconditions.CheckState(readTime != null, "The stream returned from RunQuery did not provide a read timestamp."); return(new QuerySnapshot(this, snapshots.AsReadOnly(), readTime.Value)); }
private ChangeSet ExtractChanges(Timestamp readTime) { ChangeSet changeSet = new ChangeSet(); foreach (var entry in _changeMap) { if (entry.Value == null) { if (_documentSet.TryGetDocument(entry.Key, out var document)) { changeSet.Deletes.Add(document); } } else { DocumentSnapshot snapshot = DocumentSnapshot.ForDocument(_query.Database, entry.Value, readTime); var list = _documentSet.Contains(entry.Key) ? changeSet.Updates : changeSet.Adds; list.Add(snapshot); } } return(changeSet); }
/// <summary> /// Watch this document for changes. /// </summary> /// <param name="callback">The callback to invoke each time the document changes. Must not be null.</param> /// <param name="cancellationToken">Optional cancellation token which may be used to cancel the listening operation.</param> /// <returns>A <see cref="FirestoreChangeListener"/> which may be used to monitor the listening operation and stop it gracefully.</returns> public FirestoreChangeListener Listen(Func <DocumentSnapshot, CancellationToken, Task> callback, CancellationToken cancellationToken = default) { GaxPreconditions.CheckNotNull(callback, nameof(callback)); var target = WatchStream.CreateTarget(this); Func <QuerySnapshot, CancellationToken, Task> queryCallback = async(querySnapshot, localCancellationToken) => { foreach (var doc in querySnapshot) { if (doc.Reference.Equals(this)) { await callback(doc, localCancellationToken).ConfigureAwait(false); return; } } // TODO: This will mean parsing the path back to a DocumentReference. Maybe we should accept "this". var missingDoc = DocumentSnapshot.ForMissingDocument(Database, Path, querySnapshot.ReadTime); await callback(missingDoc, cancellationToken).ConfigureAwait(false); }; var stream = new WatchStream(new WatchState(Parent, queryCallback), target, Database, cancellationToken); return(FirestoreChangeListener.Start(stream)); }
/// <summary> /// Fetches document snapshots from the server, based on an optional transaction ID. /// </summary> /// <param name="documents">The document references to fetch. Must not be null, or contain null references.</param> /// <param name="transactionId">A transaction ID, or null to not include any transaction ID.</param> /// <param name="fieldMask">The field mask to use to restrict which fields are retrieved. May be null, in which /// case no field mask is applied, and the complete documents are retrieved.</param> /// <param name="cancellationToken">A cancellation token for the operation.</param> /// <returns>The document snapshots, in the order they are provided in the response. (This may not be the order of <paramref name="documents"/>.)</returns> internal async Task <IList <DocumentSnapshot> > GetDocumentSnapshotsAsync(IEnumerable <DocumentReference> documents, ByteString transactionId, FieldMask fieldMask, CancellationToken cancellationToken) { GaxPreconditions.CheckNotNull(documents, nameof(documents)); var request = new BatchGetDocumentsRequest { Database = RootPath, Documents = { documents.Select(ExtractPath) }, Mask = fieldMask?.ToProto() }; if (transactionId != null) { request.Transaction = transactionId; } var clock = Client.Settings.Clock ?? SystemClock.Instance; var scheduler = Client.Settings.Scheduler ?? SystemScheduler.Instance; var callSettings = _batchGetCallSettings.WithCancellationToken(cancellationToken); // This is the function that we'll retry. We can't use the built-in retry functionality, because it's not a unary gRPC call. // (We could potentially simulate a unary call, but it would be a little odd to do so.) // Note that we perform a "whole request" retry. In theory we could collect some documents, then see an error, and only // request the remaining documents. Given how rarely we retry anyway in practice, that's probably not worth doing. Func <BatchGetDocumentsRequest, CallSettings, Task <List <DocumentSnapshot> > > function = async(req, settings) => { var stream = Client.BatchGetDocuments(req, settings); using (var responseStream = stream.ResponseStream) { List <DocumentSnapshot> snapshots = new List <DocumentSnapshot>(); // Note: no need to worry about passing the cancellation token in here, as we've passed it into the overall call. // If the token is cancelled, the call will be aborted. while (await responseStream.MoveNext().ConfigureAwait(false)) { var response = responseStream.Current; var readTime = Timestamp.FromProto(response.ReadTime); switch (response.ResultCase) { case BatchGetDocumentsResponse.ResultOneofCase.Found: snapshots.Add(DocumentSnapshot.ForDocument(this, response.Found, readTime)); break; case BatchGetDocumentsResponse.ResultOneofCase.Missing: snapshots.Add(DocumentSnapshot.ForMissingDocument(this, response.Missing, readTime)); break; default: throw new InvalidOperationException($"Unknown response type: {response.ResultCase}"); } } return(snapshots); } }; var retryingTask = RetryHelper.Retry(function, request, callSettings, clock, scheduler); return(await retryingTask.ConfigureAwait(false)); string ExtractPath(DocumentReference documentReference) { GaxPreconditions.CheckArgument(documentReference != null, nameof(documents), "DocumentReference sequence must not contain null elements."); return(documentReference.Path); } }
private Cursor CreateCursorFromSnapshot(DocumentSnapshot snapshot, bool before, out IReadOnlyList <InternalOrdering> newOrderings) { GaxPreconditions.CheckArgument(Equals(snapshot.Reference.Parent, Collection), nameof(snapshot), "Snapshot was from incorrect collection"); GaxPreconditions.CheckNotNull(snapshot, nameof(snapshot)); var cursor = new Cursor { Before = before }; bool hasDocumentId = false; // We may or may not need to add some orderings; this is communicated through the out parameter. newOrderings = _orderings; // Only used when we need to add orderings; set newOrderings to this at the same time. List <InternalOrdering> modifiedOrderings = null; if (_orderings.Count == 0 && _filters != null) { // If no explicit ordering is specified, use the first inequality to define an implicit order. foreach (var filter in _filters) { if (!filter.IsEqualityFilter()) { modifiedOrderings = new List <InternalOrdering>(newOrderings) { new InternalOrdering(filter.Field, Direction.Ascending) }; newOrderings = modifiedOrderings; } } } else { hasDocumentId = _orderings.Any(order => Equals(order.Field, FieldPath.DocumentId)); } if (!hasDocumentId) { // Add implicit sorting by name, using the last specified direction. Direction lastDirection = _orderings.Count == 0 ? Direction.Ascending : _orderings.Last().Direction; // Clone iff this is the first new ordering. if (modifiedOrderings == null) { modifiedOrderings = new List <InternalOrdering>(newOrderings); newOrderings = modifiedOrderings; } modifiedOrderings.Add(new InternalOrdering(FieldPath.DocumentId, lastDirection)); } foreach (var ordering in newOrderings) { var field = ordering.Field; var value = Equals(field, FieldPath.DocumentId) ? ValueSerializer.Serialize(snapshot.Reference) : snapshot.ExtractValue(field); if (value == null) { throw new ArgumentException($"Snapshot does not contain field {field}", nameof(snapshot)); } cursor.Values.Add(ValueSerializer.Serialize(value)); } return(cursor); }
internal Query EndAtSnapshot(DocumentSnapshot snapshot, bool before) { var cursor = CreateCursorFromSnapshot(snapshot, before, out var newOrderings); return(new Query(Collection, _offset, _limit, newOrderings, _filters, _projections, _startAt, cursor)); }
internal IAsyncEnumerable <DocumentSnapshot> StreamAsync(ByteString transactionId, CancellationToken cancellationToken) => StreamResponsesAsync(transactionId, cancellationToken) .Where(resp => resp.Document != null) .Select(resp => DocumentSnapshot.ForDocument(Database, resp.Document, Timestamp.FromProto(resp.ReadTime)));
/// <summary> /// Creates and returns a new query that ends at the document snapshot provided fields relative to the order of the /// query. /// </summary> /// <remarks> /// This call replaces any previously specified end position in the query. /// </remarks> /// <param name="snapshot">The snapshot of the document to end at.</param> /// <returns>A new query based on the current one, but with the specified end position.</returns> public Query EndAt(DocumentSnapshot snapshot) => EndAtSnapshot(snapshot, false);
/// <summary> /// Creates and returns a new query that ends before the document snapshot provided fields relative to the order of the /// query. /// </summary> /// <remarks> /// This call replaces any previously specified end position in the query. /// </remarks> /// <param name="snapshot">The snapshot of the document to end before. Must not be null.</param> /// <returns>A new query based on the current one, but with the specified end position.</returns> public Query EndBefore(DocumentSnapshot snapshot) => EndAtSnapshot(snapshot, true);
/// <summary> /// Creates and returns a new query that starts after the document snapshot provided fields relative to the order of the /// query. /// </summary> /// <remarks> /// This call replaces any previously specified start position in the query. /// </remarks> /// <param name="snapshot">The snapshot of the document to start after. Must not be null.</param> /// <returns>A new query based on the current one, but with the specified start position.</returns> public Query StartAfter(DocumentSnapshot snapshot) => StartAtSnapshot(snapshot, false);
/// <summary> /// Creates and returns a new query that starts at the document snapshot provided fields relative to the order of the /// query. /// </summary> /// <remarks> /// This call replaces any previously specified start position in the query. /// </remarks> /// <param name="snapshot">The snapshot of the document to start at. Must not be null.</param> /// <returns>A new query based on the current one, but with the specified start position.</returns> public Query StartAt(DocumentSnapshot snapshot) => StartAtSnapshot(snapshot, true);
/// <summary> /// Constructs a new context. /// </summary> /// <param name="snapshot">The document snapshot being deserialized. Must not be null.</param> internal DeserializationContext(DocumentSnapshot snapshot) { Snapshot = GaxPreconditions.CheckNotNull(snapshot, nameof(snapshot)); }