// ========================================================================== // Squidex Headless CMS // ========================================================================== // Copyright (c) Squidex UG (haftungsbeschraenkt) // All rights reserved. Licensed under the MIT license. // ========================================================================== using System.Globalization; using System.Linq.Expressions; using System.Runtime.CompilerServices; using MongoDB.Bson; using MongoDB.Driver; using Squidex.Infrastructure.States; namespace Squidex.Infrastructure.MongoDb { public static class MongoExtensions { private static readonly ReplaceOptions UpsertReplace = new ReplaceOptions { IsUpsert = true }; public static async Task CollectionExistsAsync(this IMongoDatabase database, string collectionName, CancellationToken ct = default) { var options = new ListCollectionNamesOptions { Filter = new BsonDocument("name", collectionName) }; var collections = await database.ListCollectionNamesAsync(options, ct); return await collections.AnyAsync(ct); } public static Task AnyAsync(this IMongoCollection collection, CancellationToken ct = default) { var find = collection.Find(new BsonDocument()).Limit(1); return find.AnyAsync(ct); } public static async Task InsertOneIfNotExistsAsync(this IMongoCollection collection, T document, CancellationToken ct = default) { try { await collection.InsertOneAsync(document, null, ct); } catch (MongoWriteException ex) when (ex.WriteError?.Category == ServerErrorCategory.DuplicateKey) { return false; } return true; } public static async IAsyncEnumerable ToAsyncEnumerable(this IFindFluent find, [EnumeratorCancellation] CancellationToken ct = default) { var cursor = await find.ToCursorAsync(ct); while (await cursor.MoveNextAsync(ct)) { foreach (var item in cursor.Current) { ct.ThrowIfCancellationRequested(); yield return item; } } } public static IFindFluent Only(this IFindFluent find, Expression> include) { return find.Project(Builders.Projection.Include(include)); } public static IFindFluent Only(this IFindFluent find, Expression> include1, Expression> include2) { return find.Project(Builders.Projection.Include(include1).Include(include2)); } public static IFindFluent Only(this IFindFluent find, Expression> include1, Expression> include2, Expression> include3) { return find.Project(Builders.Projection.Include(include1).Include(include2).Include(include3)); } public static IFindFluent Not(this IFindFluent find, Expression> exclude) { return find.Project(Builders.Projection.Exclude(exclude)); } public static IFindFluent Not(this IFindFluent find, Expression> exclude1, Expression> exclude2) { return find.Project(Builders.Projection.Exclude(exclude1).Exclude(exclude2)); } public static long ToLong(this BsonValue value) { switch (value.BsonType) { case BsonType.Int32: return value.AsInt32; case BsonType.Int64: return value.AsInt64; case BsonType.Double: return (long)value.AsDouble; default: throw new InvalidCastException($"Cannot cast from {value.BsonType} to long."); } } public static async Task UpsertVersionedAsync(this IMongoCollection collection, TKey key, long oldVersion, long newVersion, T document, CancellationToken ct = default) where T : IVersionedEntity where TKey : notnull { try { document.DocumentId = key; document.Version = newVersion; Expression> filter = oldVersion > EtagVersion.Any ? x => x.DocumentId.Equals(key) && x.Version == oldVersion : x => x.DocumentId.Equals(key); var result = await collection.ReplaceOneAsync(filter, document, UpsertReplace, ct); return result.IsAcknowledged && result.ModifiedCount == 1; } catch (MongoWriteException ex) when (ex.WriteError?.Category == ServerErrorCategory.DuplicateKey) { var existingVersion = await collection.Find(x => x.DocumentId.Equals(key)).Only(x => x.DocumentId, x => x.Version) .FirstOrDefaultAsync(ct); if (existingVersion != null) { var field = Field.Of(x => nameof(x.Version)); throw new InconsistentStateException(existingVersion[field].AsInt64, oldVersion); } else { throw new InconsistentStateException(EtagVersion.Any, oldVersion); } } } public static async Task GetMajorVersionAsync(this IMongoDatabase database, CancellationToken ct = default) { var command = new BsonDocumentCommand(new BsonDocument { { "buildInfo", 1 } }); var document = await database.RunCommandAsync(command, cancellationToken: ct); var versionString = document["version"].AsString; var versionMajor = versionString.Split('.')[0]; int.TryParse(versionMajor, NumberStyles.Integer, CultureInfo.InvariantCulture, out int result); return result; } } }