// Copyright Epic Games, Inc. All Rights Reserved. using System.Linq.Expressions; using System.Runtime.CompilerServices; using System.Text.Json; using MongoDB.Bson; using MongoDB.Bson.Serialization; using MongoDB.Driver; namespace HordeServer.Utilities { /// /// Extension methods for MongoDB /// public static class MongoExtensions { /// /// Maps a constructor for a type into a classmap /// public static void MapConstructor(this BsonClassMap classMap, Expression> generator, params string[] argumentNames) { NewExpression newExpr = (NewExpression)generator.Body; classMap.MapConstructor(newExpr.Constructor, argumentNames); } /// /// Rounds a time value to its BSON equivalent (ie. milliseconds since Unix Epoch). /// /// /// public static DateTime RoundToBsonDateTime(DateTime time) { return BsonUtils.ToDateTimeFromMillisecondsSinceEpoch(BsonUtils.ToMillisecondsSinceEpoch(time)); } /// /// Renders a filter definition to a document using the default serializer registry /// public static BsonDocument Render(this FilterDefinition filter) { IBsonSerializerRegistry serializerRegistry = BsonSerializer.SerializerRegistry; IBsonSerializer documentSerializer = serializerRegistry.GetSerializer(); return filter.Render(documentSerializer, serializerRegistry, MongoDB.Driver.Linq.LinqProvider.V2); } /// /// Renders a filter definition to a document using the default serializer registry /// public static BsonValue Render(this UpdateDefinition update) { IBsonSerializerRegistry serializerRegistry = BsonSerializer.SerializerRegistry; IBsonSerializer documentSerializer = serializerRegistry.GetSerializer(); return update.Render(documentSerializer, serializerRegistry, MongoDB.Driver.Linq.LinqProvider.V2); } /// /// Executes a query with a particular index hint /// /// /// /// /// /// /// /// public static async Task FindWithHintAsync(this IMongoCollection collection, FilterDefinition filter, string? indexHint, Func, Task> processAsync) { FindOptions? findOptions = null; if (indexHint != null) { findOptions = new FindOptions { Hint = new BsonString(indexHint) }; } try { return await processAsync(collection.Find(filter, findOptions)); } catch (MongoCommandException ex) when (indexHint != null && ex.Code == 2) { return await processAsync(collection.Find(filter)); } } /// /// Filters the documents returned from a search /// /// The query to filter /// Index of the first document to return /// Number of documents to return /// New query public static IFindFluent Range(this IFindFluent query, int? index, int? count) { if (index != null && index.Value != 0) { query = query.Skip(index.Value); } if (count != null) { query = query.Limit(count.Value); } return query; } /// /// Filters the documents returned from a search /// /// The query to filter /// Index of the first document to return /// Number of documents to return /// New query public static IAggregateFluent Range(this IAggregateFluent query, int? index, int? count) { if (index != null && index.Value != 0) { query = query.Skip(index.Value); } if (count != null) { query = query.Limit(count.Value); } return query; } /// /// Attempts to insert a document into a collection, handling the error case that a document with the given key already exists /// /// /// Collection to insert into /// The document to insert /// Cancellation token for the operation /// True if the document was inserted, false if it already exists public static async Task InsertOneIgnoreDuplicatesAsync(this IMongoCollection collection, TDocument newDocument, CancellationToken cancellationToken = default) { try { await collection.InsertOneAsync(newDocument, cancellationToken: cancellationToken); return true; } catch (MongoWriteException ex) { if (ex.WriteError.Category == ServerErrorCategory.DuplicateKey) { return false; } else { throw; } } } /// /// Attempts to insert a document into a collection, handling the error case that a document with the given key already exists /// /// /// Collection to insert into /// The document to insert /// Cancellation token for the operation public static async Task InsertManyIgnoreDuplicatesAsync(this IMongoCollection collection, List newDocuments, CancellationToken cancellationToken = default) { try { if (newDocuments.Count > 0) { await collection.InsertManyAsync(newDocuments, new InsertManyOptions { IsOrdered = false }, cancellationToken); } } catch (MongoWriteException ex) { if (ex.WriteError.Category != ServerErrorCategory.DuplicateKey) { throw; } } catch (MongoBulkWriteException ex) { if (ex.WriteErrors.Any(x => x.Category != ServerErrorCategory.DuplicateKey)) { throw; } } } /// /// Sets a field to a value, or unsets it if the value is null /// /// The document type /// Type of the field to set /// Update builder /// Expression for the field to set /// New value to set /// Update defintiion public static UpdateDefinition SetOrUnsetNull(this UpdateDefinitionBuilder updateBuilder, Expression> field, TField? value) where TField : struct { if (value.HasValue) { return updateBuilder.Set(field, value.Value); } else { return updateBuilder.Unset(new ExpressionFieldDefinition(field)); } } /// /// Sets a field to a value, or unsets it if the value is null /// /// The document type /// Type of the field to set /// Update builder /// Expression for the field to set /// New value to set /// Update defintiion public static UpdateDefinition SetOrUnsetNull(this UpdateDefinition update, Expression> field, TField? value) where TField : struct { if (value.HasValue) { return update.Set(field, value.Value); } else { return update.Unset(new ExpressionFieldDefinition(field)); } } /// /// Sets a field to a value, or unsets it if the value is null /// /// The document type /// Update builder /// Expression for the field to set /// New value to set /// Update defintiion public static UpdateDefinition SetOrUnsetBool(this UpdateDefinition update, Expression> field, bool value) { if (value) { return update.Set(field, value); } else { return update.Unset(new ExpressionFieldDefinition(field)); } } /// /// Sets a field to a value, or unsets it if the value is null /// /// The document type /// Update builder /// Expression for the field to set /// New value to set /// Update defintiion public static UpdateDefinition SetOrUnsetBool(this UpdateDefinitionBuilder updateBuilder, Expression> field, bool value) { if (value) { return updateBuilder.Set(field, value); } else { return updateBuilder.Unset(new ExpressionFieldDefinition(field)); } } /// /// Sets a field to a value, or unsets it if the value is null /// /// The document type /// Type of the field to set /// Update builder /// Expression for the field to set /// New value to set /// Update defintiion public static UpdateDefinition SetOrUnsetNullRef(this UpdateDefinitionBuilder updateBuilder, Expression> field, TField? value) where TField : class { if (value != null) { return updateBuilder.Set(field, value); } else { return updateBuilder.Unset(new ExpressionFieldDefinition(field)); } } /// /// Creates a filter definition from a linq expression. This is not generally explicitly castable, so expose it as a FilterDefinitionBuilder method. /// /// The document type /// The filter builder /// Expression to parse /// New filter definition #pragma warning disable IDE0060 public static FilterDefinition Expr(this FilterDefinitionBuilder filter, Expression> expression) { return expression; } #pragma warning restore IDE0060 /// /// Creates an async enumerator from the given query /// /// Document type /// Query source /// Cancellation token for the operation /// #pragma warning disable IDE1006 // Naming Styles #pragma warning disable VSTHRD200 // Use Async suffix public static async IAsyncEnumerable ToAsyncEnumerable(this IAsyncCursorSource source, [EnumeratorCancellation] CancellationToken cancellationToken) #pragma warning restore VSTHRD200 // Use Async suffix #pragma warning restore IDE1006 // Naming Styles { using (IAsyncCursor cursor = await source.ToCursorAsync(cancellationToken)) { while (await cursor.MoveNextAsync(cancellationToken)) { foreach (T value in cursor.Current) { yield return value; cancellationToken.ThrowIfCancellationRequested(); } } } } /// /// Converts a JsonElement to a BsonValue /// /// The element to convert /// Bson value public static BsonValue ToBsonValue(this JsonElement element) { switch (element.ValueKind) { case JsonValueKind.True: return true; case JsonValueKind.False: return false; case JsonValueKind.String: return element.GetString(); case JsonValueKind.Array: return new BsonArray(element.EnumerateArray().Select(x => x.ToBsonValue())); case JsonValueKind.Object: return new BsonDocument(element.EnumerateObject().Select(x => new BsonElement(x.Name, x.Value.ToBsonValue()))); default: return BsonNull.Value; } } } }