diff --git a/src/Squidex.Domain.Apps.Entities.MongoDb/Contents/MongoContentDraftCollection.cs b/src/Squidex.Domain.Apps.Entities.MongoDb/Contents/MongoContentDraftCollection.cs index 30c0edda0..653e35172 100644 --- a/src/Squidex.Domain.Apps.Entities.MongoDb/Contents/MongoContentDraftCollection.cs +++ b/src/Squidex.Domain.Apps.Entities.MongoDb/Contents/MongoContentDraftCollection.cs @@ -10,6 +10,7 @@ using System.Collections.Generic; using System.Linq; using System.Threading; using System.Threading.Tasks; +using Microsoft.Extensions.Options; using MongoDB.Driver; using NodaTime; using Squidex.Domain.Apps.Core.ConvertContent; @@ -29,28 +30,33 @@ namespace Squidex.Domain.Apps.Entities.MongoDb.Contents { internal sealed class MongoContentDraftCollection : MongoContentCollection { - public MongoContentDraftCollection(IMongoDatabase database, IJsonSerializer serializer) + private readonly MongoDbOptions options; + + public MongoContentDraftCollection(IMongoDatabase database, IJsonSerializer serializer, IOptions options) : base(database, serializer, "State_Content_Draft") { + this.options = options.Value; } protected override async Task SetupCollectionAsync(IMongoCollection collection, CancellationToken ct = default) { - await collection.Indexes.CreateManyAsync( - new[] - { - new CreateIndexModel( - Index - .Ascending(x => x.IndexedSchemaId) - .Ascending(x => x.Id) - .Ascending(x => x.IsDeleted)), + await collection.Indexes.CreateOneAsync( + new CreateIndexModel( + Index + .Ascending(x => x.IndexedSchemaId) + .Ascending(x => x.Id) + .Ascending(x => x.IsDeleted)), null, ct); + + if (!options.IsCosmosDb) + { + await collection.Indexes.CreateOneAsync( new CreateIndexModel( Index .Text(x => x.DataText) .Ascending(x => x.IndexedSchemaId) .Ascending(x => x.IsDeleted) - .Ascending(x => x.Status)) - }, ct); + .Ascending(x => x.Status)), null, ct); + } await base.SetupCollectionAsync(collection, ct); } diff --git a/src/Squidex.Domain.Apps.Entities.MongoDb/Contents/MongoContentPublishedCollection.cs b/src/Squidex.Domain.Apps.Entities.MongoDb/Contents/MongoContentPublishedCollection.cs index 0184b87e1..08d819402 100644 --- a/src/Squidex.Domain.Apps.Entities.MongoDb/Contents/MongoContentPublishedCollection.cs +++ b/src/Squidex.Domain.Apps.Entities.MongoDb/Contents/MongoContentPublishedCollection.cs @@ -8,6 +8,7 @@ using System; using System.Threading; using System.Threading.Tasks; +using Microsoft.Extensions.Options; using MongoDB.Driver; using Squidex.Domain.Apps.Core.ConvertContent; using Squidex.Domain.Apps.Entities.Apps; @@ -20,19 +21,24 @@ namespace Squidex.Domain.Apps.Entities.MongoDb.Contents { internal sealed class MongoContentPublishedCollection : MongoContentCollection { - public MongoContentPublishedCollection(IMongoDatabase database, IJsonSerializer serializer) + private readonly MongoDbOptions options; + + public MongoContentPublishedCollection(IMongoDatabase database, IJsonSerializer serializer, IOptions options) : base(database, serializer, "State_Content_Published") { + this.options = options.Value; } protected override async Task SetupCollectionAsync(IMongoCollection collection, CancellationToken ct = default) { - await collection.Indexes.CreateManyAsync( - new[] - { - new CreateIndexModel(Index.Text(x => x.DataText).Ascending(x => x.IndexedSchemaId)), - new CreateIndexModel(Index.Ascending(x => x.IndexedSchemaId).Ascending(x => x.Id)) - }, ct); + await collection.Indexes.CreateOneAsync( + new CreateIndexModel(Index.Ascending(x => x.IndexedSchemaId).Ascending(x => x.Id)), null, ct); + + if (!options.IsCosmosDb) + { + await collection.Indexes.CreateOneAsync( + new CreateIndexModel(Index.Text(x => x.DataText).Ascending(x => x.IndexedSchemaId)), null, ct); + } await base.SetupCollectionAsync(collection, ct); } diff --git a/src/Squidex.Domain.Apps.Entities.MongoDb/Contents/MongoContentRepository.cs b/src/Squidex.Domain.Apps.Entities.MongoDb/Contents/MongoContentRepository.cs index 3c663d642..f24c6be1d 100644 --- a/src/Squidex.Domain.Apps.Entities.MongoDb/Contents/MongoContentRepository.cs +++ b/src/Squidex.Domain.Apps.Entities.MongoDb/Contents/MongoContentRepository.cs @@ -9,6 +9,7 @@ using System; using System.Collections.Generic; using System.Threading; using System.Threading.Tasks; +using Microsoft.Extensions.Options; using MongoDB.Driver; using NodaTime; using Squidex.Domain.Apps.Core.Contents; @@ -19,6 +20,7 @@ using Squidex.Domain.Apps.Entities.Schemas; using Squidex.Infrastructure; using Squidex.Infrastructure.Json; using Squidex.Infrastructure.Log; +using Squidex.Infrastructure.MongoDb; using Squidex.Infrastructure.Queries; namespace Squidex.Domain.Apps.Entities.MongoDb.Contents @@ -31,17 +33,18 @@ namespace Squidex.Domain.Apps.Entities.MongoDb.Contents private readonly MongoContentDraftCollection contentsDraft; private readonly MongoContentPublishedCollection contentsPublished; - public MongoContentRepository(IMongoDatabase database, IAppProvider appProvider, IJsonSerializer serializer) + public MongoContentRepository(IMongoDatabase database, IAppProvider appProvider, IJsonSerializer serializer, IOptions options) { Guard.NotNull(appProvider, nameof(appProvider)); Guard.NotNull(serializer, nameof(serializer)); + Guard.NotNull(options, nameof(options)); this.appProvider = appProvider; this.serializer = serializer; - contentsDraft = new MongoContentDraftCollection(database, serializer); - contentsPublished = new MongoContentPublishedCollection(database, serializer); + contentsDraft = new MongoContentDraftCollection(database, serializer, options); + contentsPublished = new MongoContentPublishedCollection(database, serializer, options); this.database = database; } diff --git a/src/Squidex.Domain.Apps.Entities.MongoDb/History/MongoHistoryEventRepository.cs b/src/Squidex.Domain.Apps.Entities.MongoDb/History/MongoHistoryEventRepository.cs index 6b5e6cd77..5a39bb706 100644 --- a/src/Squidex.Domain.Apps.Entities.MongoDb/History/MongoHistoryEventRepository.cs +++ b/src/Squidex.Domain.Apps.Entities.MongoDb/History/MongoHistoryEventRepository.cs @@ -9,6 +9,8 @@ using System; using System.Collections.Generic; using System.Threading; using System.Threading.Tasks; +using Microsoft.Extensions.Options; +using MongoDB.Bson.Serialization; using MongoDB.Driver; using Squidex.Domain.Apps.Entities.History; using Squidex.Domain.Apps.Entities.History.Repositories; @@ -18,9 +20,17 @@ namespace Squidex.Domain.Apps.Entities.MongoDb.History { public class MongoHistoryEventRepository : MongoRepositoryBase, IHistoryEventRepository { - public MongoHistoryEventRepository(IMongoDatabase database) + public MongoHistoryEventRepository(IMongoDatabase database, IOptions options) : base(database) { + if (options.Value.IsCosmosDb) + { + var classMap = BsonClassMap.RegisterClassMap(); + + classMap.MapProperty(x => x.Created) + .SetElementName("_ts"); + classMap.AutoMap(); + } } protected override string CollectionName() diff --git a/src/Squidex.Infrastructure.Azure/EventSourcing/CosmosDbEventStore.cs b/src/Squidex.Infrastructure.Azure/EventSourcing/CosmosDbEventStore.cs index 227289fe3..a07bf13ec 100644 --- a/src/Squidex.Infrastructure.Azure/EventSourcing/CosmosDbEventStore.cs +++ b/src/Squidex.Infrastructure.Azure/EventSourcing/CosmosDbEventStore.cs @@ -84,6 +84,21 @@ namespace Squidex.Infrastructure.EventSourcing await documentClient.CreateDocumentCollectionIfNotExistsAsync(databaseUri, new DocumentCollection { + IndexingPolicy = new IndexingPolicy + { + IncludedPaths = new Collection + { + new IncludedPath + { + Path = "/*", + Indexes = new Collection + { + Index.Range(DataType.Number), + Index.Range(DataType.String), + } + } + } + }, UniqueKeyPolicy = new UniqueKeyPolicy { UniqueKeys = new Collection diff --git a/src/Squidex.Infrastructure.Azure/EventSourcing/CosmosDbEventStore_Writer.cs b/src/Squidex.Infrastructure.Azure/EventSourcing/CosmosDbEventStore_Writer.cs index 793e934e5..ac0024573 100644 --- a/src/Squidex.Infrastructure.Azure/EventSourcing/CosmosDbEventStore_Writer.cs +++ b/src/Squidex.Infrastructure.Azure/EventSourcing/CosmosDbEventStore_Writer.cs @@ -59,7 +59,7 @@ namespace Squidex.Infrastructure.EventSourcing return; } - var currentVersion = GetEventStreamOffset(streamName); + var currentVersion = await GetEventStreamOffsetAsync(streamName); if (expectedVersion != EtagVersion.Any && expectedVersion != currentVersion) { @@ -80,7 +80,7 @@ namespace Squidex.Infrastructure.EventSourcing { if (ex.StatusCode == HttpStatusCode.Conflict) { - currentVersion = GetEventStreamOffset(streamName); + currentVersion = await GetEventStreamOffsetAsync(streamName); if (expectedVersion != EtagVersion.Any) { @@ -105,13 +105,13 @@ namespace Squidex.Infrastructure.EventSourcing } } - private long GetEventStreamOffset(string streamName) + private async Task GetEventStreamOffsetAsync(string streamName) { var query = documentClient.CreateDocumentQuery(collectionUri, FilterBuilder.LastPosition(streamName)); - var document = query.ToList().FirstOrDefault(); + var document = await query.FirstOrDefaultAsync(); if (document != null) { diff --git a/src/Squidex.Infrastructure.Azure/EventSourcing/FilterBuilder.cs b/src/Squidex.Infrastructure.Azure/EventSourcing/FilterBuilder.cs index 7180ad59c..b6bd7686c 100644 --- a/src/Squidex.Infrastructure.Azure/EventSourcing/FilterBuilder.cs +++ b/src/Squidex.Infrastructure.Azure/EventSourcing/FilterBuilder.cs @@ -5,41 +5,14 @@ // All rights reserved. Licensed under the MIT license. // ========================================================================== -using System; using System.Collections.Generic; -using System.Threading; -using System.Threading.Tasks; using Microsoft.Azure.Documents; -using Microsoft.Azure.Documents.Client; -using Microsoft.Azure.Documents.Linq; using Squidex.Infrastructure.Json.Objects; namespace Squidex.Infrastructure.EventSourcing { internal static class FilterBuilder { - public static async Task QueryAsync(this DocumentClient documentClient, Uri collectionUri, SqlQuerySpec querySpec, Func handler, CancellationToken ct = default) - { - var query = - documentClient.CreateDocumentQuery(collectionUri, querySpec) - .AsDocumentQuery(); - - using (query) - { - var result = new List(); - - while (query.HasMoreResults && !ct.IsCancellationRequested) - { - var commits = await query.ExecuteNextAsync(ct); - - foreach (var commit in commits) - { - await handler(commit); - } - } - } - } - public static SqlQuerySpec AllIds(string streamName) { var query = diff --git a/src/Squidex.Infrastructure.Azure/EventSourcing/FilterExtensions.cs b/src/Squidex.Infrastructure.Azure/EventSourcing/FilterExtensions.cs new file mode 100644 index 000000000..c24e93ff1 --- /dev/null +++ b/src/Squidex.Infrastructure.Azure/EventSourcing/FilterExtensions.cs @@ -0,0 +1,62 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschraenkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.Azure.Documents; +using Microsoft.Azure.Documents.Client; +using Microsoft.Azure.Documents.Linq; + +namespace Squidex.Infrastructure.EventSourcing +{ + internal static class FilterExtensions + { + public static async Task FirstOrDefaultAsync(this IQueryable queryable, CancellationToken ct = default) + { + var documentQuery = queryable.AsDocumentQuery(); + + using (documentQuery) + { + if (documentQuery.HasMoreResults) + { + var results = await documentQuery.ExecuteNextAsync(ct); + + return results.FirstOrDefault(); + } + } + + return default; + } + + public static Task QueryAsync(this DocumentClient documentClient, Uri collectionUri, SqlQuerySpec querySpec, Func handler, CancellationToken ct = default) + { + var query = documentClient.CreateDocumentQuery(collectionUri, querySpec); + + return query.QueryAsync(handler, ct); + } + + public static async Task QueryAsync(this IQueryable queryable, Func handler, CancellationToken ct = default) + { + var documentQuery = queryable.AsDocumentQuery(); + + using (documentQuery) + { + while (documentQuery.HasMoreResults && !ct.IsCancellationRequested) + { + var items = await documentQuery.ExecuteNextAsync(ct); + + foreach (var item in items) + { + await handler(item); + } + } + } + } + } +} diff --git a/src/Squidex.Infrastructure.MongoDb/MongoDb/MongoDbOptions.cs b/src/Squidex.Infrastructure.MongoDb/MongoDb/MongoDbOptions.cs new file mode 100644 index 000000000..65462dc6c --- /dev/null +++ b/src/Squidex.Infrastructure.MongoDb/MongoDb/MongoDbOptions.cs @@ -0,0 +1,14 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschraenkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +namespace Squidex.Infrastructure.MongoDb +{ + public sealed class MongoDbOptions + { + public bool IsCosmosDb { get; set; } + } +} diff --git a/src/Squidex.Infrastructure/Configuration/Options.cs b/src/Squidex.Infrastructure/Configuration/Alternatives.cs similarity index 85% rename from src/Squidex.Infrastructure/Configuration/Options.cs rename to src/Squidex.Infrastructure/Configuration/Alternatives.cs index 89cb9f596..77d70602f 100644 --- a/src/Squidex.Infrastructure/Configuration/Options.cs +++ b/src/Squidex.Infrastructure/Configuration/Alternatives.cs @@ -10,9 +10,9 @@ using System.Collections.Generic; namespace Microsoft.Extensions.Configuration { - public sealed class Options : Dictionary + public sealed class Alternatives : Dictionary { - public Options() + public Alternatives() : base(StringComparer.OrdinalIgnoreCase) { } diff --git a/src/Squidex.Infrastructure/Configuration/ConfigurationExtensions.cs b/src/Squidex.Infrastructure/Configuration/ConfigurationExtensions.cs index fafb0ee84..4c86b80a0 100644 --- a/src/Squidex.Infrastructure/Configuration/ConfigurationExtensions.cs +++ b/src/Squidex.Infrastructure/Configuration/ConfigurationExtensions.cs @@ -46,7 +46,7 @@ namespace Microsoft.Extensions.Configuration return value; } - public static string ConfigureByOption(this IConfiguration config, string path, Options options) + public static string ConfigureByOption(this IConfiguration config, string path, Alternatives options) { var value = config.GetRequiredValue(path); diff --git a/src/Squidex/Config/Domain/AssetServices.cs b/src/Squidex/Config/Domain/AssetServices.cs index 5b8e36706..669cd29b8 100644 --- a/src/Squidex/Config/Domain/AssetServices.cs +++ b/src/Squidex/Config/Domain/AssetServices.cs @@ -20,7 +20,7 @@ namespace Squidex.Config.Domain { public static void AddMyAssetServices(this IServiceCollection services, IConfiguration config) { - config.ConfigureByOption("assetStore:type", new Options + config.ConfigureByOption("assetStore:type", new Alternatives { ["Default"] = () => { diff --git a/src/Squidex/Config/Domain/EventStoreServices.cs b/src/Squidex/Config/Domain/EventStoreServices.cs index 69ebbbab5..ee90b1d47 100644 --- a/src/Squidex/Config/Domain/EventStoreServices.cs +++ b/src/Squidex/Config/Domain/EventStoreServices.cs @@ -26,7 +26,7 @@ namespace Squidex.Config.Domain { public static void AddMyEventStoreServices(this IServiceCollection services, IConfiguration config) { - config.ConfigureByOption("eventStore:type", new Options + config.ConfigureByOption("eventStore:type", new Alternatives { ["MongoDb"] = () => { diff --git a/src/Squidex/Config/Domain/StoreServices.cs b/src/Squidex/Config/Domain/StoreServices.cs index 63559f3d4..b95d436ba 100644 --- a/src/Squidex/Config/Domain/StoreServices.cs +++ b/src/Squidex/Config/Domain/StoreServices.cs @@ -10,6 +10,7 @@ using IdentityServer4.Stores; using Microsoft.AspNetCore.Identity; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Options; using Migrate_01.Migrations; using MongoDB.Driver; using Squidex.Domain.Apps.Entities; @@ -31,6 +32,7 @@ using Squidex.Infrastructure.Diagnostics; using Squidex.Infrastructure.EventSourcing; using Squidex.Infrastructure.Json; using Squidex.Infrastructure.Migrations; +using Squidex.Infrastructure.MongoDb; using Squidex.Infrastructure.States; using Squidex.Infrastructure.UsageTracking; @@ -40,7 +42,7 @@ namespace Squidex.Config.Domain { public static void AddMyStoreServices(this IServiceCollection services, IConfiguration config) { - config.ConfigureByOption("store:type", new Options + config.ConfigureByOption("store:type", new Alternatives { ["MongoDB"] = () => { @@ -48,6 +50,10 @@ namespace Squidex.Config.Domain var mongoDatabaseName = config.GetRequiredValue("store:mongoDb:database"); var mongoContentDatabaseName = config.GetOptionalValue("store:mongoDb:contentDatabase", mongoDatabaseName); + var isCosmosDb = config.GetOptionalValue("store:mongoDB:isCosmosDB"); + + services.Configure(config.GetSection("store:mongoDB")); + services.AddSingleton(typeof(ISnapshotStore<,>), typeof(MongoSnapshotStore<,>)); services.AddSingletonAs(_ => Singletons.GetOrAdd(mongoConfiguration, s => new MongoClient(s))) @@ -97,7 +103,8 @@ namespace Squidex.Config.Domain services.AddSingletonAs(c => new MongoContentRepository( c.GetRequiredService().GetDatabase(mongoContentDatabaseName), c.GetRequiredService(), - c.GetRequiredService())) + c.GetRequiredService(), + c.GetRequiredService>())) .AsOptional() .AsOptional>() .AsOptional(); diff --git a/src/Squidex/Config/Orleans/OrleansServices.cs b/src/Squidex/Config/Orleans/OrleansServices.cs index aed630ded..36c3cdc71 100644 --- a/src/Squidex/Config/Orleans/OrleansServices.cs +++ b/src/Squidex/Config/Orleans/OrleansServices.cs @@ -58,7 +58,7 @@ namespace Squidex.Config.Orleans var siloPort = config.GetOptionalValue("orleans:siloPort", 11111); - config.ConfigureByOption("orleans:clustering", new Options + config.ConfigureByOption("orleans:clustering", new Alternatives { ["MongoDB"] = () => { @@ -81,7 +81,7 @@ namespace Squidex.Config.Orleans } }); - config.ConfigureByOption("store:type", new Options + config.ConfigureByOption("store:type", new Alternatives { ["MongoDB"] = () => { diff --git a/tools/Migrate_01/Migrations/ConvertOldSnapshotStores.cs b/tools/Migrate_01/Migrations/ConvertOldSnapshotStores.cs index 4528f8e5b..58c3f8643 100644 --- a/tools/Migrate_01/Migrations/ConvertOldSnapshotStores.cs +++ b/tools/Migrate_01/Migrations/ConvertOldSnapshotStores.cs @@ -7,23 +7,33 @@ using System.Linq; using System.Threading.Tasks; +using Microsoft.Extensions.Options; using MongoDB.Bson; using MongoDB.Driver; using Squidex.Infrastructure.Migrations; +using Squidex.Infrastructure.MongoDb; +using Squidex.Infrastructure.Tasks; namespace Migrate_01.Migrations { public sealed class ConvertOldSnapshotStores : IMigration { private readonly IMongoDatabase database; + private readonly MongoDbOptions options; - public ConvertOldSnapshotStores(IMongoDatabase database) + public ConvertOldSnapshotStores(IMongoDatabase database, IOptions options) { this.database = database; + this.options = options.Value; } public Task UpdateAsync() { + if (options.IsCosmosDb) + { + return TaskHelper.Done; + } + var collections = new[] { "States_Apps",