diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 00000000..c545a099 --- /dev/null +++ b/.editorconfig @@ -0,0 +1,28 @@ +# Remove the line below if you want to inherit .editorconfig settings from higher directories +root = true + +# C# files +[*.cs] + +#### Suppress warnings #### + +# CA1034: Nested types should not be visible +dotnet_diagnostic.CA1034.severity = none + +# CA1063: Implement IDisposable Correctly +dotnet_diagnostic.CA1063.severity = none + +# CA1303: Do not pass literals as localized parameters +dotnet_diagnostic.CA1303.severity = none # Don't need translated exceptions + +# CA1707: Identifiers should not contain underscores +dotnet_diagnostic.CA1707.severity = none # I want to use underscore in constants + +# CA1812: Avoid uninstantiated internal classes +dotnet_diagnostic.CA1812.severity = none # Doing extensive use of dependency injection + +# CA1816: Dispose methods should call SuppressFinalize +dotnet_diagnostic.CA1816.severity = none + +# CA2225: Operator overloads have named alternates +dotnet_diagnostic.CA2225.severity = none diff --git a/.github/workflows/myget-feature-deploy.yml b/.github/workflows/myget-feature-deploy.yml deleted file mode 100644 index d0b1879c..00000000 --- a/.github/workflows/myget-feature-deploy.yml +++ /dev/null @@ -1,29 +0,0 @@ -name: Build and package feature branch. Deploy to MyGet - -on: - push: - branches: - - 'feature/**' - -jobs: - build-test-package: - runs-on: ubuntu-latest - - steps: - - uses: actions/checkout@master - with: - fetch-depth: 0 - - - name: Setup .NET Core SDK - uses: actions/setup-dotnet@master - with: - dotnet-version: '3.1.x' - - - name: Build with dotnet - run: dotnet build --configuration Release - - - name: Generate nuget package - run: dotnet pack --configuration Release -o nupkg - - - name: Push packages - run: dotnet nuget push './nupkg/*.nupkg' --api-key ${{secrets.MYGET_APIKEY}} --source https://www.myget.org/F/etherna/api/v3/index.json diff --git a/.github/workflows/myget-stable-deploy.yml b/.github/workflows/myget-unstable-deploy.yml similarity index 87% rename from .github/workflows/myget-stable-deploy.yml rename to .github/workflows/myget-unstable-deploy.yml index a8346b09..fe2b5bde 100644 --- a/.github/workflows/myget-stable-deploy.yml +++ b/.github/workflows/myget-unstable-deploy.yml @@ -1,8 +1,10 @@ -name: Build test and package dev and master branch. Deploy to MyGet +name: Unstable release deploy to MyGet on: push: - branches: [ master, dev ] + branches: + - dev + - 'release/**' jobs: build-test-package: diff --git a/.github/workflows/nuget-release-deploy.yml b/.github/workflows/nuget-stable-deploy.yml similarity index 89% rename from .github/workflows/nuget-release-deploy.yml rename to .github/workflows/nuget-stable-deploy.yml index c5fd3b0f..ef2dc40f 100644 --- a/.github/workflows/nuget-release-deploy.yml +++ b/.github/workflows/nuget-stable-deploy.yml @@ -1,8 +1,9 @@ -name: Build test and package master branch. Deploy to NuGet +name: Stable release deploy to NuGet on: push: - branches: master + branches: + - master jobs: build-test-package: diff --git a/.gitignore b/.gitignore index c395abfd..28f83ffa 100644 --- a/.gitignore +++ b/.gitignore @@ -160,6 +160,7 @@ publish/ # NuGet Packages *.nupkg +*.snupkg # The packages folder can be ignored because of Package Restore **/packages/* # except build/, which is used as an MSBuild target. diff --git a/MongODM.sln b/MongODM.sln index e3a97b2e..cfd11552 100644 --- a/MongODM.sln +++ b/MongODM.sln @@ -21,16 +21,17 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "MongODM.Core.Tests", "test\ EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{03C64D98-FF9F-4760-AE82-203953FF4940}" ProjectSection(SolutionItems) = preProject + .editorconfig = .editorconfig .gitignore = .gitignore LICENSE.txt = LICENSE.txt MongODM.sln.licenseheader = MongODM.sln.licenseheader + README.md = README.md EndProjectSection EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "workflows", "workflows", "{D4BB5972-5F7B-43ED-81C1-69ECF0E62D1E}" ProjectSection(SolutionItems) = preProject - .github\workflows\myget-feature-deploy.yml = .github\workflows\myget-feature-deploy.yml - .github\workflows\myget-stable-deploy.yml = .github\workflows\myget-stable-deploy.yml - .github\workflows\nuget-release-deploy.yml = .github\workflows\nuget-release-deploy.yml + .github\workflows\myget-unstable-deploy.yml = .github\workflows\myget-unstable-deploy.yml + .github\workflows\nuget-stable-deploy.yml = .github\workflows\nuget-stable-deploy.yml EndProjectSection EndProject Global diff --git a/README.md b/README.md index 5a29733c..5e9eed82 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,4 @@ -MongODM +MongODM ========= ## Overview diff --git a/src/ExecutionContext/ExecutionContext.csproj b/src/ExecutionContext/ExecutionContext.csproj index 98695310..d327b750 100644 --- a/src/ExecutionContext/ExecutionContext.csproj +++ b/src/ExecutionContext/ExecutionContext.csproj @@ -8,7 +8,12 @@ Execution context provider 8.0 enable + https://github.com/Etherna/mongodm + git + true + true + snupkg LICENSE.txt @@ -21,6 +26,10 @@ all runtime; build; native; contentfiles; analyzers; buildtransitive + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + diff --git a/src/MongODM.AspNetCore/MongODM.AspNetCore.csproj b/src/MongODM.AspNetCore/MongODM.AspNetCore.csproj index 5fccab9a..516bd6e8 100644 --- a/src/MongODM.AspNetCore/MongODM.AspNetCore.csproj +++ b/src/MongODM.AspNetCore/MongODM.AspNetCore.csproj @@ -8,7 +8,12 @@ Asp.Net Core adapter for MongODM 8.0 enable + https://github.com/Etherna/mongodm + git + true + true + snupkg LICENSE.txt @@ -31,6 +36,10 @@ all runtime; build; native; contentfiles; analyzers; buildtransitive + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + diff --git a/src/MongODM.AspNetCore/ServiceCollectionExtensions.cs b/src/MongODM.AspNetCore/ServiceCollectionExtensions.cs index 2ae9f8e4..595dfb61 100644 --- a/src/MongODM.AspNetCore/ServiceCollectionExtensions.cs +++ b/src/MongODM.AspNetCore/ServiceCollectionExtensions.cs @@ -16,6 +16,7 @@ using Etherna.ExecContext.AsyncLocal; using Etherna.MongODM; using Etherna.MongODM.AspNetCore; +using Etherna.MongODM.Models; using Etherna.MongODM.ProxyModels; using Etherna.MongODM.Repositories; using Etherna.MongODM.Serialization; @@ -32,17 +33,21 @@ namespace Microsoft.Extensions.DependencyInjection { public static class ServiceCollectionExtensions { - public static MongODMConfiguration UseMongODM( + public static MongODMConfiguration UseMongODM( this IServiceCollection services, IEnumerable? executionContexts = null) - where TTaskRunner : class, ITaskRunner => - UseMongODM(services, executionContexts); + where TTaskRunner : class, ITaskRunner + where TModelBase : class, IModel => //needed because of this https://jira.mongodb.org/browse/CSHARP-3154 + UseMongODM( + services, + executionContexts); - public static MongODMConfiguration UseMongODM( + public static MongODMConfiguration UseMongODM( this IServiceCollection services, IEnumerable? executionContexts = null) - where TProxyGenerator: class, IProxyGenerator + where TProxyGenerator : class, IProxyGenerator where TTaskRunner: class, ITaskRunner + where TModelBase: class, IModel //needed because of this https://jira.mongodb.org/browse/CSHARP-3154 { services.TryAddSingleton(); @@ -70,18 +75,23 @@ public static MongODMConfiguration UseMongODM( * the same dbContext different components could have different instances of the same component. */ services.TryAddTransient(); - services.TryAddTransient(); + services.TryAddTransient(); services.TryAddTransient(); + services.TryAddTransient(); services.TryAddTransient(); services.TryAddTransient(); services.TryAddSingleton(); //tasks + services.TryAddTransient(); services.TryAddTransient(); - //castle proxy generator. + //castle proxy generator services.TryAddSingleton(new Castle.DynamicProxy.ProxyGenerator()); + //static configurations + services.TryAddSingleton>(); + return new MongODMConfiguration(services); } diff --git a/src/MongODM.AspNetCore/StaticConfigurationBuilder.cs b/src/MongODM.AspNetCore/StaticConfigurationBuilder.cs new file mode 100644 index 00000000..39531480 --- /dev/null +++ b/src/MongODM.AspNetCore/StaticConfigurationBuilder.cs @@ -0,0 +1,41 @@ +// Copyright 2020-present Etherna Sagl +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +using Etherna.MongODM.Conventions; +using Etherna.MongODM.Models.Internal; +using Etherna.MongODM.ProxyModels; +using Etherna.MongODM.Utility; +using MongoDB.Bson; +using MongoDB.Bson.Serialization; +using MongoDB.Bson.Serialization.Conventions; + +namespace Etherna.MongODM.AspNetCore +{ + public class StaticConfigurationBuilder : IStaticConfigurationBuilder + { + public StaticConfigurationBuilder(IProxyGenerator proxyGenerator) + { + // Register conventions. + ConventionRegistry.Register("Enum string", new ConventionPack + { + new EnumRepresentationConvention(BsonType.String) + }, c => true); + + BsonSerializer.RegisterDiscriminatorConvention(typeof(TModelBase), + new HierarchicalProxyTolerantDiscriminatorConvention("_t", proxyGenerator)); + BsonSerializer.RegisterDiscriminatorConvention(typeof(EntityModelBase), + new HierarchicalProxyTolerantDiscriminatorConvention("_t", proxyGenerator)); + } + } +} diff --git a/src/MongODM.Core/Conventions/HierarchicalProxyTolerantDiscriminatorConvention.cs b/src/MongODM.Core/Conventions/HierarchicalProxyTolerantDiscriminatorConvention.cs new file mode 100644 index 00000000..9fb64f3b --- /dev/null +++ b/src/MongODM.Core/Conventions/HierarchicalProxyTolerantDiscriminatorConvention.cs @@ -0,0 +1,40 @@ +// Copyright 2020-present Etherna Sagl +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +using Etherna.MongODM.ProxyModels; +using MongoDB.Bson; +using MongoDB.Bson.Serialization.Conventions; +using System; + +namespace Etherna.MongODM.Conventions +{ + public class HierarchicalProxyTolerantDiscriminatorConvention : HierarchicalDiscriminatorConvention + { + // Fields. + private readonly IProxyGenerator proxyGenerator; + + // Constructors. + public HierarchicalProxyTolerantDiscriminatorConvention( + string elementName, + IProxyGenerator proxyGenerator) + : base(elementName) + { + this.proxyGenerator = proxyGenerator ?? throw new ArgumentNullException(nameof(proxyGenerator)); + } + + // Methods. + public override BsonValue GetDiscriminator(Type nominalType, Type actualType) => + base.GetDiscriminator(nominalType, proxyGenerator.PurgeProxyType(actualType)); + } +} diff --git a/src/MongODM.Core/DbContext.cs b/src/MongODM.Core/DbContext.cs index a3ff89c1..7161a21a 100644 --- a/src/MongODM.Core/DbContext.cs +++ b/src/MongODM.Core/DbContext.cs @@ -14,17 +14,19 @@ using Etherna.MongODM.Migration; using Etherna.MongODM.Models; +using Etherna.MongODM.Models.Internal; +using Etherna.MongODM.Models.Internal.ModelMaps; using Etherna.MongODM.ProxyModels; using Etherna.MongODM.Repositories; using Etherna.MongODM.Serialization; using Etherna.MongODM.Serialization.Modifiers; using Etherna.MongODM.Utility; -using MongoDB.Bson; -using MongoDB.Bson.Serialization.Conventions; using MongoDB.Driver; +using MongoDB.Driver.Linq; using System; using System.Collections.Generic; using System.Linq; +using System.Reflection; using System.Threading; using System.Threading.Tasks; @@ -37,80 +39,84 @@ public abstract class DbContext : IDbContext // Constructors and initialization. public DbContext( - IDbContextDependencies dependencies, + IDbDependencies dependencies, DbContextOptions options) { - DBCache = dependencies.DbCache; - DBMaintainer = dependencies.DbMaintainer; + if (dependencies is null) + throw new ArgumentNullException(nameof(dependencies)); + if (options is null) + throw new ArgumentNullException(nameof(options)); + + ApplicationVersion = options.ApplicationVersion; + DbCache = dependencies.DbCache; + DbMaintainer = dependencies.DbMaintainer; + DbMigrationManager = dependencies.DbMigrationManager; + DbOperations = new CollectionRepository(options.DbOperationsCollectionName); DocumentSchemaRegister = dependencies.DocumentSchemaRegister; - DocumentVersion = options.DocumentVersion; + Identifier = options.Identifier ?? GetType().Name; + LibraryVersion = GetType() + .GetTypeInfo() + .Assembly + .GetCustomAttribute() + ?.InformationalVersion + ?.Split('+')[0] ?? "1.0.0"; ProxyGenerator = dependencies.ProxyGenerator; RepositoryRegister = dependencies.RepositoryRegister; SerializerModifierAccessor = dependencies.SerializerModifierAccessor; // Initialize MongoDB driver. Client = new MongoClient(options.ConnectionString); - Database = Client.GetDatabase(options.DBName); + Database = Client.GetDatabase(options.DbName); // Initialize internal dependencies. + DbMaintainer.Initialize(this); + DbMigrationManager.Initialize(this); DocumentSchemaRegister.Initialize(this); - DBMaintainer.Initialize(this); RepositoryRegister.Initialize(this); // Initialize repositories. foreach (var repository in RepositoryRegister.ModelRepositoryMap.Values) repository.Initialize(this); - // Customize conventions. - ConventionRegistry.Register("Enum string", new ConventionPack - { - new EnumRepresentationConvention(BsonType.String) - }, c => true); + // Register model maps. + //internal maps + new DbMigrationOperationMap().Register(this); + new ModelBaseMap().Register(this); + new OperationBaseMap().Register(this); + new SeedOperationMap().Register(this); - // Register serializers. - foreach (var serializerCollector in SerializerCollectors) - serializerCollector.Register(this); + //application maps + foreach (var maps in ModelMapsCollectors) + maps.Register(this); // Build and freeze document schema register. DocumentSchemaRegister.Freeze(); } // Public properties. + public SemanticVersion ApplicationVersion { get; } public IReadOnlyCollection ChangedModelsList => - DBCache.LoadedModels.Values + DbCache.LoadedModels.Values .Where(model => (model as IAuditable)?.IsChanged == true) .ToList(); public IMongoClient Client { get; } public IMongoDatabase Database { get; } - public IDbCache DBCache { get; } - public IDbMaintainer DBMaintainer { get; } + public IDbCache DbCache { get; } + public IDbMaintainer DbMaintainer { get; } + public IDbMigrationManager DbMigrationManager { get; } + public ICollectionRepository DbOperations { get; } + public virtual IEnumerable DocumentMigrationList { get; } = Array.Empty(); public IDocumentSchemaRegister DocumentSchemaRegister { get; } - public DocumentVersion DocumentVersion { get; } - public bool IsMigrating { get; private set; } + public string Identifier { get; } + public SemanticVersion LibraryVersion { get; } public IProxyGenerator ProxyGenerator { get; } public IRepositoryRegister RepositoryRegister { get; } public ISerializerModifierAccessor SerializerModifierAccessor { get; } // Protected properties. - protected virtual IEnumerable MigrationTaskList { get; } = Array.Empty(); - protected abstract IEnumerable SerializerCollectors { get; } + protected abstract IEnumerable ModelMapsCollectors { get; } // Methods. - public async Task MigrateRepositoriesAsync(CancellationToken cancellationToken = default) - { - IsMigrating = true; - - // Migrate collections. - foreach (var migration in MigrationTaskList) - await migration.MigrateAsync(cancellationToken); - - // Build indexes. - foreach (var repository in RepositoryRegister.ModelCollectionRepositoryMap.Values) - await repository.BuildIndexesAsync(DocumentSchemaRegister, cancellationToken); - - IsMigrating = false; - } - public virtual async Task SaveChangesAsync(CancellationToken cancellationToken = default) { /* @@ -146,16 +152,46 @@ public virtual async Task SaveChangesAsync(CancellationToken cancellationToken = // Commit updated models replacement. foreach (var model in ChangedModelsList) { - var modelType = model.GetType().BaseType; - if (RepositoryRegister.ModelCollectionRepositoryMap.ContainsKey(modelType)) //can't replace if is a file + var modelType = ProxyGenerator.PurgeProxyType(model.GetType()); + while (modelType != typeof(object)) //try to find right collection. Can't replace model if it is stored on gridfs { - var repository = RepositoryRegister.ModelCollectionRepositoryMap[modelType]; - await repository.ReplaceAsync(model); + if (RepositoryRegister.ModelCollectionRepositoryMap.ContainsKey(modelType)) + { + var repository = RepositoryRegister.ModelCollectionRepositoryMap[modelType]; + await repository.ReplaceAsync(model).ConfigureAwait(false); + break; + } + else + { + modelType = modelType.BaseType; + } } } } + public async Task SeedIfNeededAsync() + { + // Check if already seeded. + if (await DbOperations.QueryElementsAsync(elements => + elements.OfType() + .AnyAsync(sop => sop.DbContextName == Identifier)).ConfigureAwait(false)) + return false; + + // Seed. + await SeedAsync().ConfigureAwait(false); + + // Report operation. + var seedOperation = new SeedOperation(this); + await DbOperations.CreateAsync(seedOperation).ConfigureAwait(false); + + return true; + } + public Task StartSessionAsync(CancellationToken cancellationToken = default) => Client.StartSessionAsync(cancellationToken: cancellationToken); + + // Protected methods. + protected virtual Task SeedAsync() => + Task.CompletedTask; } } \ No newline at end of file diff --git a/src/MongODM.Core/DbContextOptions.cs b/src/MongODM.Core/DbContextOptions.cs index f1d4375a..91aaf9be 100644 --- a/src/MongODM.Core/DbContextOptions.cs +++ b/src/MongODM.Core/DbContextOptions.cs @@ -19,10 +19,12 @@ namespace Etherna.MongODM { public class DbContextOptions { + public SemanticVersion ApplicationVersion { get; set; } = "1.0.0"; public string ConnectionString { get; set; } = "mongodb://localhost/localDb"; - public string DBName => ConnectionString.Split('?')[0] + public string DbName => ConnectionString.Split('?')[0] .Split('/').Last(); - public DocumentVersion DocumentVersion { get; set; } = "1.0.0"; + public string DbOperationsCollectionName { get; set; } = "_db_ops"; + public string? Identifier { get; set; } } public class DbContextOptions : DbContextOptions diff --git a/src/MongODM.Core/Exceptions/InvalidEntityTypeException.cs b/src/MongODM.Core/Exceptions/InvalidEntityTypeException.cs index f57a0dce..94eea405 100644 --- a/src/MongODM.Core/Exceptions/InvalidEntityTypeException.cs +++ b/src/MongODM.Core/Exceptions/InvalidEntityTypeException.cs @@ -23,5 +23,8 @@ public InvalidEntityTypeException() public InvalidEntityTypeException(string message) : base(message) { } + + public InvalidEntityTypeException(string message, Exception innerException) : base(message, innerException) + { } } } diff --git a/src/MongODM.Core/Extensions/ClassMapExtensions.cs b/src/MongODM.Core/Extensions/ClassMapExtensions.cs index ab18f7a8..2d1501ac 100644 --- a/src/MongODM.Core/Extensions/ClassMapExtensions.cs +++ b/src/MongODM.Core/Extensions/ClassMapExtensions.cs @@ -28,6 +28,9 @@ public static BsonMemberMap SetMemberSerializer( IBsonSerializer serializer) where TMember : class { + if (classMap is null) + throw new ArgumentNullException(nameof(classMap)); + var member = classMap.GetMemberMap(memberLambda); if (member == null) member = classMap.MapMember(memberLambda); @@ -41,6 +44,9 @@ public static BsonMemberMap SetMemberSerializer { + if (serializer is null) + throw new ArgumentNullException(nameof(serializer)); + if (typeof(TMember) == typeof(TSerializer)) return classMap.SetMemberSerializer(memberLambda, (IBsonSerializer)serializer); else diff --git a/src/MongODM.Core/IDbContext.cs b/src/MongODM.Core/IDbContext.cs index f37de024..e0fce431 100644 --- a/src/MongODM.Core/IDbContext.cs +++ b/src/MongODM.Core/IDbContext.cs @@ -12,12 +12,15 @@ // See the License for the specific language governing permissions and // limitations under the License. +using Etherna.MongODM.Migration; +using Etherna.MongODM.Models.Internal; using Etherna.MongODM.ProxyModels; using Etherna.MongODM.Repositories; using Etherna.MongODM.Serialization; using Etherna.MongODM.Serialization.Modifiers; using Etherna.MongODM.Utility; using MongoDB.Driver; +using System.Collections.Generic; using System.Threading; using System.Threading.Tasks; @@ -28,6 +31,11 @@ namespace Etherna.MongODM /// public interface IDbContext { + /// + /// Current application version. + /// + SemanticVersion ApplicationVersion { get; } + // Properties. /// /// Current MongoDB client. @@ -42,28 +50,43 @@ public interface IDbContext /// /// Database cache container. /// - IDbCache DBCache { get; } + IDbCache DbCache { get; } /// /// Database operator interested into maintenance tasks. /// - IDbMaintainer DBMaintainer { get; } - + IDbMaintainer DbMaintainer { get; } + + /// + /// Manage migrations over database context + /// + IDbMigrationManager DbMigrationManager { get; } + + /// + /// Internal collection for keep db operations execution log + /// + ICollectionRepository DbOperations { get; } + + /// + /// List of registered migration tasks + /// + IEnumerable DocumentMigrationList { get; } + /// /// Container for model serialization and document schema information. /// IDocumentSchemaRegister DocumentSchemaRegister { get; } - + /// - /// Current operating document version. + /// DbContext unique identifier. /// - DocumentVersion DocumentVersion { get; } - + string Identifier { get; } + /// - /// Flag reporting eventual current migration operation. + /// Current MongODM library version /// - bool IsMigrating { get; } - + SemanticVersion LibraryVersion { get; } + /// /// Current model proxy generator. /// @@ -81,16 +104,16 @@ public interface IDbContext // Methods. /// - /// Start a database migration process. + /// Save current model changes on db. /// /// Cancellation token - Task MigrateRepositoriesAsync(CancellationToken cancellationToken = default); + Task SaveChangesAsync(CancellationToken cancellationToken = default); /// - /// Save current model changes on db. + /// Seed database context if still not seeded /// - /// Cancellation token - Task SaveChangesAsync(CancellationToken cancellationToken = default); + /// True if seed has been executed. False otherwise + Task SeedIfNeededAsync(); /// /// Start a new database transaction session. diff --git a/src/MongODM.Core/Migration/MigrationResult.cs b/src/MongODM.Core/Migration/MigrationResult.cs new file mode 100644 index 00000000..58192c0f --- /dev/null +++ b/src/MongODM.Core/Migration/MigrationResult.cs @@ -0,0 +1,40 @@ +// Copyright 2020-present Etherna Sagl +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +namespace Etherna.MongODM.Migration +{ + public class MigrationResult + { + // Constructors. + private MigrationResult() { } + + // Properties. + public bool Succeded { get; private set; } + public long MigratedDocuments { get; private set; } + + // Methods. + public static MigrationResult Failed() => + new MigrationResult + { + Succeded = false + }; + + public static MigrationResult Succeeded(long migratedDocuments) => + new MigrationResult + { + Succeded = true, + MigratedDocuments = migratedDocuments + }; + } +} \ No newline at end of file diff --git a/src/MongODM.Core/Migration/MongoCollectionMigration.cs b/src/MongODM.Core/Migration/MongoCollectionMigration.cs index 9d550826..a081d53c 100644 --- a/src/MongODM.Core/Migration/MongoCollectionMigration.cs +++ b/src/MongODM.Core/Migration/MongoCollectionMigration.cs @@ -32,29 +32,61 @@ public class MongoCollectionMigration where TModelDest : class, IEntityModel { + // Fields. private readonly Func converter; private readonly Func discriminator; private readonly IMongoCollection destinationCollection; - private readonly IMongoCollection sourceCollection; + private readonly ICollectionRepository _sourceCollection; + // Constructor. public MongoCollectionMigration( ICollectionRepository sourceCollection, ICollectionRepository destinationCollection, Func converter, - Func discriminator) + Func discriminator, + string id) + : base(id) { - this.sourceCollection = sourceCollection.Collection; + if (sourceCollection is null) + throw new ArgumentNullException(nameof(sourceCollection)); + if (destinationCollection is null) + throw new ArgumentNullException(nameof(destinationCollection)); + + _sourceCollection = sourceCollection; this.destinationCollection = destinationCollection.Collection; this.converter = converter; this.discriminator = discriminator; } - public override Task MigrateAsync(CancellationToken cancellationToken = default) => - sourceCollection.Find(Builders.Filter.Empty, new FindOptions { NoCursorTimeout = true }) - .ForEachAsync(obj => + // Properties. + public override ICollectionRepository SourceCollection => _sourceCollection; + + // Methods. + public override async Task MigrateAsync( + int callbackEveryDocuments = 0, + Func? callbackAsync = null, + CancellationToken cancellationToken = default) + { + if (callbackEveryDocuments < 0) + throw new ArgumentOutOfRangeException(nameof(callbackEveryDocuments), "Value can't be negative"); + + // Migrate documents. + var totMigratedDocuments = 0L; + await _sourceCollection.Collection.Find(Builders.Filter.Empty, new FindOptions { NoCursorTimeout = true }) + .ForEachAsync(async model => { - if (discriminator(obj)) - destinationCollection.InsertOneAsync(converter(obj)); - }, cancellationToken); + if (callbackEveryDocuments > 0 && + totMigratedDocuments % callbackEveryDocuments == 0 && + callbackAsync != null) + await callbackAsync.Invoke(totMigratedDocuments).ConfigureAwait(false); + + if (discriminator(model)) + await destinationCollection.InsertOneAsync(converter(model)).ConfigureAwait(false); + + totMigratedDocuments++; + }, cancellationToken).ConfigureAwait(false); + + return MigrationResult.Succeeded(totMigratedDocuments); + } } } diff --git a/src/MongODM.Core/Migration/MongoDocumentMigration.cs b/src/MongODM.Core/Migration/MongoDocumentMigration.cs index 9b3d854a..6a79adc8 100644 --- a/src/MongODM.Core/Migration/MongoDocumentMigration.cs +++ b/src/MongODM.Core/Migration/MongoDocumentMigration.cs @@ -17,6 +17,7 @@ using Etherna.MongODM.Serialization; using MongoDB.Bson; using MongoDB.Driver; +using System; using System.Threading; using System.Threading.Tasks; @@ -30,22 +31,37 @@ namespace Etherna.MongODM.Migration public class MongoDocumentMigration : MongoMigrationBase where TModel : class, IEntityModel { - private readonly DocumentVersion minimumDocumentVersion; - private readonly IMongoCollection sourceCollection; + // Fields. + private readonly SemanticVersion minimumDocumentVersion; + private readonly ICollectionRepository _sourceCollection; + // Constructors. public MongoDocumentMigration( ICollectionRepository sourceCollection, - DocumentVersion minimumDocumentVersion) + SemanticVersion minimumDocumentVersion, + string id) + : base(id) { - this.sourceCollection = sourceCollection.Collection; + if (sourceCollection is null) + throw new ArgumentNullException(nameof(sourceCollection)); + + _sourceCollection = sourceCollection; this.minimumDocumentVersion = minimumDocumentVersion; } - /// - /// Fix all documents prev of MinimumDocumentVersion - /// - public override async Task MigrateAsync(CancellationToken cancellationToken = default) + // Properties. + public override ICollectionRepository SourceCollection => _sourceCollection; + + // Methods. + public override async Task MigrateAsync( + int callbackEveryDocuments = 0, + Func? callbackAsync = null, + CancellationToken cancellationToken = default) { + if (callbackEveryDocuments < 0) + throw new ArgumentOutOfRangeException(nameof(callbackEveryDocuments), "Value can't be negative"); + + // Building filter. var filterBuilder = Builders.Filter; var filter = filterBuilder.Or( // No version in document (very old). @@ -70,9 +86,22 @@ public override async Task MigrateAsync(CancellationToken cancellationToken = de filterBuilder.Eq($"{DbContext.DocumentVersionElementName}.1", minimumDocumentVersion.MinorRelease), filterBuilder.Lt($"{DbContext.DocumentVersionElementName}.2", minimumDocumentVersion.PatchRelease))); - // Replace documents. - await sourceCollection.Find(filter, new FindOptions { NoCursorTimeout = true }) - .ForEachAsync(obj => sourceCollection.ReplaceOneAsync(Builders.Filter.Eq(m => m.Id, obj.Id), obj), cancellationToken); + // Migrate documents. + var totMigratedDocuments = 0L; + await _sourceCollection.Collection.Find(filter, new FindOptions { NoCursorTimeout = true }) + .ForEachAsync(async model => + { + if (callbackEveryDocuments > 0 && + totMigratedDocuments % callbackEveryDocuments == 0 && + callbackAsync != null) + await callbackAsync.Invoke(totMigratedDocuments).ConfigureAwait(false); + + await _sourceCollection.Collection.ReplaceOneAsync(Builders.Filter.Eq(m => m.Id, model.Id), model).ConfigureAwait(false); + + totMigratedDocuments++; + }, cancellationToken).ConfigureAwait(false); + + return MigrationResult.Succeeded(totMigratedDocuments); } } } diff --git a/src/MongODM.Core/Migration/MongoMigrationBase.cs b/src/MongODM.Core/Migration/MongoMigrationBase.cs index 30d84690..2a54bdcd 100644 --- a/src/MongODM.Core/Migration/MongoMigrationBase.cs +++ b/src/MongODM.Core/Migration/MongoMigrationBase.cs @@ -12,6 +12,8 @@ // See the License for the specific language governing permissions and // limitations under the License. +using Etherna.MongODM.Repositories; +using System; using System.Threading; using System.Threading.Tasks; @@ -19,6 +21,23 @@ namespace Etherna.MongODM.Migration { public abstract class MongoMigrationBase { - public abstract Task MigrateAsync(CancellationToken cancellationToken = default); + // Constructors. + public MongoMigrationBase(string id) + { + Id = id ?? throw new ArgumentNullException(nameof(id)); + } + + // Properties. + public string Id { get; } + public abstract ICollectionRepository SourceCollection { get; } + + // Methods. + /// + /// Perform migration with optional updating callback + /// + /// Interval of processed documents between callback invokations. 0 if ignore callback + /// The async callback function. Parameter is number of processed documents + /// The migration result + public abstract Task MigrateAsync(int callbackEveryDocuments = 0, Func? callbackAsync = null, CancellationToken cancellationToken = default); } } diff --git a/src/MongODM.Core/Models/Internal/DbMigrationOpAgg/DocumentMigrationLog.cs b/src/MongODM.Core/Models/Internal/DbMigrationOpAgg/DocumentMigrationLog.cs new file mode 100644 index 00000000..5f18d842 --- /dev/null +++ b/src/MongODM.Core/Models/Internal/DbMigrationOpAgg/DocumentMigrationLog.cs @@ -0,0 +1,38 @@ +// Copyright 2020-present Etherna Sagl +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +namespace Etherna.MongODM.Models.Internal.DbMigrationOpAgg +{ + public class DocumentMigrationLog : MigrationLogBase + { + // Constructors. + public DocumentMigrationLog( + string collectionName, + string documentMigrationId, + ExecutionState state, + long totMigratedDocs) + : base(state) + { + CollectionName = collectionName; + DocumentMigrationId = documentMigrationId; + TotMigratedDocs = totMigratedDocs; + } + protected DocumentMigrationLog() { } + + // Properties. + public virtual string CollectionName { get; protected set; } = default!; + public virtual string DocumentMigrationId { get; protected set; } = default!; + public virtual long TotMigratedDocs { get; protected set; } + } +} diff --git a/src/MongODM.Core/Models/Internal/DbMigrationOpAgg/IndexMigrationLog.cs b/src/MongODM.Core/Models/Internal/DbMigrationOpAgg/IndexMigrationLog.cs new file mode 100644 index 00000000..b6cea61d --- /dev/null +++ b/src/MongODM.Core/Models/Internal/DbMigrationOpAgg/IndexMigrationLog.cs @@ -0,0 +1,32 @@ +// Copyright 2020-present Etherna Sagl +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +namespace Etherna.MongODM.Models.Internal.DbMigrationOpAgg +{ + public class IndexMigrationLog : MigrationLogBase + { + // Constructors. + public IndexMigrationLog( + string repository, + ExecutionState state) + : base(state) + { + Repository = repository; + } + protected IndexMigrationLog() { } + + // Properties. + public virtual string Repository { get; protected set; } = default!; + } +} diff --git a/src/MongODM.Core/Models/Internal/DbMigrationOpAgg/MigrationLogBase.cs b/src/MongODM.Core/Models/Internal/DbMigrationOpAgg/MigrationLogBase.cs new file mode 100644 index 00000000..05390bd9 --- /dev/null +++ b/src/MongODM.Core/Models/Internal/DbMigrationOpAgg/MigrationLogBase.cs @@ -0,0 +1,42 @@ +// Copyright 2020-present Etherna Sagl +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +using System; + +namespace Etherna.MongODM.Models.Internal.DbMigrationOpAgg +{ + public abstract class MigrationLogBase : ModelBase + { + // Enums. + public enum ExecutionState + { + Executing, + Succeded, + Skipped, + Failed + } + + // Constructors. + public MigrationLogBase(ExecutionState state) + { + CreationDateTime = DateTime.Now; + State = state; + } + protected MigrationLogBase() { } + + // Properties. + public virtual ExecutionState State { get; protected set; } + public virtual DateTime CreationDateTime { get; protected set; } + } +} diff --git a/src/MongODM.Core/Models/Internal/DbMigrationOperation.cs b/src/MongODM.Core/Models/Internal/DbMigrationOperation.cs new file mode 100644 index 00000000..8e01b456 --- /dev/null +++ b/src/MongODM.Core/Models/Internal/DbMigrationOperation.cs @@ -0,0 +1,100 @@ +// Copyright 2020-present Etherna Sagl +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +using Etherna.MongODM.Attributes; +using Etherna.MongODM.Models.Internal.DbMigrationOpAgg; +using System; +using System.Collections.Generic; + +namespace Etherna.MongODM.Models.Internal +{ + public class DbMigrationOperation : OperationBase + { + // Enums. + public enum Status + { + New, + Running, + Completed, + Cancelled + } + + // Fields. + private List _logs = new List(); + + // Constructors. + public DbMigrationOperation(IDbContext dbContext, string? author) + : base(dbContext) + { + Author = author; + CurrentStatus = Status.New; + } + protected DbMigrationOperation() { } + + // Properties. + public virtual string? Author { get; protected set; } + public virtual DateTime? CompletedDateTime { get; protected set; } + public virtual Status CurrentStatus { get; protected set; } + public virtual IEnumerable Logs + { + get => _logs; + protected set => _logs = new List(value ?? Array.Empty()); + } + public virtual string? TaskId { get; protected set; } + + // Methods. + [PropertyAlterer(nameof(Logs))] + public virtual void AddLog(MigrationLogBase log) + { + if (log is null) + throw new ArgumentNullException(nameof(log)); + + _logs.Add(log); + } + + [PropertyAlterer(nameof(CurrentStatus))] + public virtual void TaskCancelled() + { + if (CurrentStatus == Status.Completed) + throw new InvalidOperationException(); + + CurrentStatus = Status.Cancelled; + } + + [PropertyAlterer(nameof(CompletedDateTime))] + [PropertyAlterer(nameof(CurrentStatus))] + public virtual void TaskCompleted() + { + if (CurrentStatus != Status.Running) + throw new InvalidOperationException(); + + CompletedDateTime = DateTime.Now; + CurrentStatus = Status.Completed; + } + + [PropertyAlterer(nameof(CurrentStatus))] + [PropertyAlterer(nameof(TaskId))] + public virtual void TaskStarted(string taskId) + { + if (taskId is null) + throw new ArgumentNullException(nameof(taskId)); + + if (CurrentStatus != Status.New) + throw new InvalidOperationException(); + + CurrentStatus = Status.Running; + TaskId = taskId; + } + } +} diff --git a/src/MongODM.Core/Models/Internal/EntityModelBase.cs b/src/MongODM.Core/Models/Internal/EntityModelBase.cs new file mode 100644 index 00000000..27a76f67 --- /dev/null +++ b/src/MongODM.Core/Models/Internal/EntityModelBase.cs @@ -0,0 +1,60 @@ +// Copyright 2020-present Etherna Sagl +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +using System; +using System.Collections.Generic; + +namespace Etherna.MongODM.Models.Internal +{ + public abstract class EntityModelBase : ModelBase, IEntityModel + { + private DateTime _creationDateTime; + + // Constructors and dispose. + protected EntityModelBase() + { + _creationDateTime = DateTime.Now; + } + + public virtual void DisposeForDelete() { } + + // Properties. + public virtual DateTime CreationDateTime { get => _creationDateTime; protected set => _creationDateTime = value; } + } + + public abstract class EntityModelBase : EntityModelBase, IEntityModel + { + // Properties. + public virtual TKey Id { get; protected set; } = default!; + + // Methods. + public override bool Equals(object? obj) + { + if (ReferenceEquals(this, obj)) return true; + if (obj is null) return false; + if (EqualityComparer.Default.Equals(Id, default!) || + !(obj is IEntityModel) || + EqualityComparer.Default.Equals((obj as IEntityModel)!.Id, default!)) return false; + return GetType() == obj.GetType() && + EqualityComparer.Default.Equals(Id, (obj as IEntityModel)!.Id); + } + + public override int GetHashCode() + { + if (EqualityComparer.Default.Equals(Id, default!)) + return -1; + return Id!.GetHashCode(); + } + } +} diff --git a/src/MongODM.Core/Models/Internal/ModelBase.cs b/src/MongODM.Core/Models/Internal/ModelBase.cs new file mode 100644 index 00000000..e733815c --- /dev/null +++ b/src/MongODM.Core/Models/Internal/ModelBase.cs @@ -0,0 +1,25 @@ +// Copyright 2020-present Etherna Sagl +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; + +namespace Etherna.MongODM.Models.Internal +{ + public abstract class ModelBase : IModel + { + [SuppressMessage("Usage", "CA2227:Collection properties should be read only", Justification = "Setter needed for deserialization scope")] + public virtual IDictionary? ExtraElements { get; protected set; } + } +} diff --git a/src/MongODM.Core/Models/Internal/ModelMaps/DbMigrationOperationMap.cs b/src/MongODM.Core/Models/Internal/ModelMaps/DbMigrationOperationMap.cs new file mode 100644 index 00000000..049318ee --- /dev/null +++ b/src/MongODM.Core/Models/Internal/ModelMaps/DbMigrationOperationMap.cs @@ -0,0 +1,37 @@ +// Copyright 2020-present Etherna Sagl +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +using Etherna.MongODM.Models.Internal.DbMigrationOpAgg; +using Etherna.MongODM.Serialization; + +namespace Etherna.MongODM.Models.Internal.ModelMaps +{ + class DbMigrationOperationMap : IModelMapsCollector + { + public void Register(IDbContext dbContext) + { + dbContext.DocumentSchemaRegister.RegisterModelSchema( + "0.20.0"); + + dbContext.DocumentSchemaRegister.RegisterModelSchema( + "0.20.0"); + + dbContext.DocumentSchemaRegister.RegisterModelSchema( + "0.20.0"); + + dbContext.DocumentSchemaRegister.RegisterModelSchema( + "0.20.0"); + } + } +} diff --git a/src/MongODM.Core/Models/Internal/ModelMaps/ModelBaseMap.cs b/src/MongODM.Core/Models/Internal/ModelMaps/ModelBaseMap.cs new file mode 100644 index 00000000..a946473f --- /dev/null +++ b/src/MongODM.Core/Models/Internal/ModelMaps/ModelBaseMap.cs @@ -0,0 +1,40 @@ +// Copyright 2020-present Etherna Sagl +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +using Etherna.MongODM.Serialization; +using MongoDB.Bson; +using MongoDB.Bson.Serialization.IdGenerators; +using MongoDB.Bson.Serialization.Serializers; + +namespace Etherna.MongODM.Models.Internal.ModelMaps +{ + class ModelBaseMap : IModelMapsCollector + { + public void Register(IDbContext dbContext) + { + // register class maps. + dbContext.DocumentSchemaRegister.RegisterModelSchema("0.20.0"); + + dbContext.DocumentSchemaRegister.RegisterModelSchema>("0.20.0", + cm => + { + cm.AutoMap(); + + // Set Id representation. + cm.IdMemberMap.SetSerializer(new StringSerializer(BsonType.ObjectId)) + .SetIdGenerator(new StringObjectIdGenerator()); + }); + } + } +} diff --git a/src/MongODM.Core/Models/Internal/ModelMaps/OperationBaseMap.cs b/src/MongODM.Core/Models/Internal/ModelMaps/OperationBaseMap.cs new file mode 100644 index 00000000..8c41bb47 --- /dev/null +++ b/src/MongODM.Core/Models/Internal/ModelMaps/OperationBaseMap.cs @@ -0,0 +1,35 @@ +// Copyright 2020-present Etherna Sagl +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +using Etherna.MongODM.Serialization; +using Etherna.MongODM.Serialization.Serializers; + +namespace Etherna.MongODM.Models.Internal.ModelMaps +{ + class OperationBaseMap : IModelMapsCollector + { + public void Register(IDbContext dbContext) + { + dbContext.DocumentSchemaRegister.RegisterModelSchema( + "0.20.0", //mongodm library's version + cm => cm.AutoMap(), + initCustomSerializer: () => + new ExtendedClassMapSerializer( + dbContext.DbCache, + dbContext.LibraryVersion, + dbContext.SerializerModifierAccessor) + { AddVersion = true }); + } + } +} diff --git a/src/MongODM.Core/Models/Internal/ModelMaps/SeedOperationMap.cs b/src/MongODM.Core/Models/Internal/ModelMaps/SeedOperationMap.cs new file mode 100644 index 00000000..18359a85 --- /dev/null +++ b/src/MongODM.Core/Models/Internal/ModelMaps/SeedOperationMap.cs @@ -0,0 +1,27 @@ +// Copyright 2020-present Etherna Sagl +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +using Etherna.MongODM.Serialization; + +namespace Etherna.MongODM.Models.Internal.ModelMaps +{ + class SeedOperationMap : IModelMapsCollector + { + public void Register(IDbContext dbContext) + { + dbContext.DocumentSchemaRegister.RegisterModelSchema( + "0.20.0"); + } + } +} diff --git a/src/MongODM.Core/Models/Internal/OperationBase.cs b/src/MongODM.Core/Models/Internal/OperationBase.cs new file mode 100644 index 00000000..ff541b3b --- /dev/null +++ b/src/MongODM.Core/Models/Internal/OperationBase.cs @@ -0,0 +1,35 @@ +// Copyright 2020-present Etherna Sagl +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +using System; + +namespace Etherna.MongODM.Models.Internal +{ + public abstract class OperationBase : EntityModelBase + { + // Constructors and dispose. + public OperationBase(IDbContext dbContext) + { + if (dbContext is null) + throw new ArgumentNullException(nameof(dbContext)); + + CreationDateTime = DateTime.Now; + DbContextName = dbContext.Identifier; + } + protected OperationBase() { } + + // Properties. + public virtual string DbContextName { get; protected set; } = default!; + } +} diff --git a/src/MongODM.Core/Models/Internal/SeedOperation.cs b/src/MongODM.Core/Models/Internal/SeedOperation.cs new file mode 100644 index 00000000..3a401134 --- /dev/null +++ b/src/MongODM.Core/Models/Internal/SeedOperation.cs @@ -0,0 +1,25 @@ +// Copyright 2020-present Etherna Sagl +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +namespace Etherna.MongODM.Models.Internal +{ + public class SeedOperation : OperationBase + { + // Constructors. + public SeedOperation(IDbContext owner) + : base(owner) + { } + protected SeedOperation() { } + } +} diff --git a/src/MongODM.Core/MongODM.Core.csproj b/src/MongODM.Core/MongODM.Core.csproj index 3fc6c30c..9c33e289 100644 --- a/src/MongODM.Core/MongODM.Core.csproj +++ b/src/MongODM.Core/MongODM.Core.csproj @@ -8,7 +8,12 @@ ODM framework for MongoDB 8.0 enable + https://github.com/Etherna/mongodm + git + true + true + snupkg LICENSE.txt @@ -18,6 +23,14 @@ all runtime; build; native; contentfiles; analyzers; buildtransitive + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + diff --git a/src/MongODM.Core/ProxyModels/AuditableInterceptor.cs b/src/MongODM.Core/ProxyModels/AuditableInterceptor.cs index c505d01b..b7e088b6 100644 --- a/src/MongODM.Core/ProxyModels/AuditableInterceptor.cs +++ b/src/MongODM.Core/ProxyModels/AuditableInterceptor.cs @@ -35,6 +35,9 @@ public AuditableInterceptor(IEnumerable additionalInterfaces) // Protected methods. protected override bool InterceptInterface(IInvocation invocation) { + if (invocation is null) + throw new ArgumentNullException(nameof(invocation)); + // Intercept ISummarizable invocations if (invocation.Method.DeclaringType == typeof(IAuditable)) { @@ -61,10 +64,13 @@ protected override bool InterceptInterface(IInvocation invocation) protected override void InterceptModel(IInvocation invocation) { + if (invocation is null) + throw new ArgumentNullException(nameof(invocation)); + // Filter sets. if (isAuditingEnabled) { - if (invocation.Method.Name.StartsWith("set_")) + if (invocation.Method.Name.StartsWith("set_", StringComparison.InvariantCulture)) { var propertyName = invocation.Method.Name.Substring(4); var propertyInfo = typeof(TModel).GetMember(propertyName).Single(); @@ -72,7 +78,7 @@ protected override void InterceptModel(IInvocation invocation) // Add property to edited set. changedMembers.Add(propertyInfo); } - else if (invocation.Method.Name.StartsWith("get_")) + else if (invocation.Method.Name.StartsWith("get_", StringComparison.InvariantCulture)) { //ignore get } diff --git a/src/MongODM.Core/ProxyModels/IProxyGenerator.cs b/src/MongODM.Core/ProxyModels/IProxyGenerator.cs index f6b208fd..e7c39c45 100644 --- a/src/MongODM.Core/ProxyModels/IProxyGenerator.cs +++ b/src/MongODM.Core/ProxyModels/IProxyGenerator.cs @@ -21,5 +21,6 @@ public interface IProxyGenerator object CreateInstance(IDbContext dbContext, Type type, params object[] constructorArguments); TModel CreateInstance(IDbContext dbContext, params object[] constructorArguments); bool IsProxyType(Type type); + Type PurgeProxyType(Type type); } } \ No newline at end of file diff --git a/src/MongODM.Core/ProxyModels/ModelInterceptorBase.cs b/src/MongODM.Core/ProxyModels/ModelInterceptorBase.cs index dca32894..20d4365b 100644 --- a/src/MongODM.Core/ProxyModels/ModelInterceptorBase.cs +++ b/src/MongODM.Core/ProxyModels/ModelInterceptorBase.cs @@ -30,6 +30,9 @@ public ModelInterceptorBase(IEnumerable additionalInterfaces) public void Intercept(IInvocation invocation) { + if (invocation is null) + throw new ArgumentNullException(nameof(invocation)); + if (additionalInterfaces.Contains(invocation.Method.DeclaringType)) { var handled = InterceptInterface(invocation); @@ -66,6 +69,9 @@ protected virtual bool InterceptInterface(IInvocation invocation) /// Current invocation protected virtual void InterceptModel(IInvocation invocation) { + if (invocation is null) + throw new ArgumentNullException(nameof(invocation)); + invocation.Proceed(); } } diff --git a/src/MongODM.Core/ProxyModels/ProxyGenerator.cs b/src/MongODM.Core/ProxyModels/ProxyGenerator.cs index dca8690c..f7ae3110 100644 --- a/src/MongODM.Core/ProxyModels/ProxyGenerator.cs +++ b/src/MongODM.Core/ProxyModels/ProxyGenerator.cs @@ -21,7 +21,7 @@ namespace Etherna.MongODM.ProxyModels { - public class ProxyGenerator : IProxyGenerator + public class ProxyGenerator : IProxyGenerator, IDisposable { // Fields. private readonly Castle.DynamicProxy.IProxyGenerator proxyGeneratorCore; @@ -47,6 +47,11 @@ public object CreateInstance( Type type, params object[] constructorArguments) { + if (dbContext is null) + throw new ArgumentNullException(nameof(dbContext)); + if (type is null) + throw new ArgumentNullException(nameof(type)); + // Get configuration. (Type[] AdditionalInterfaces, Func InterceptorInstancerSelector) configuration = (null!, null!); modelConfigurationDictionaryLock.EnterReadLock(); @@ -128,6 +133,12 @@ public object CreateInstance( public TModel CreateInstance(IDbContext dbContext, params object[] constructorArguments) => (TModel)CreateInstance(dbContext, typeof(TModel), constructorArguments); + public void Dispose() + { + modelConfigurationDictionaryLock.Dispose(); + proxyTypeDictionaryLock.Dispose(); + } + public bool IsProxyType(Type type) { proxyTypeDictionaryLock.EnterReadLock(); @@ -141,6 +152,16 @@ public bool IsProxyType(Type type) } } + public Type PurgeProxyType(Type type) + { + if (type is null) + throw new ArgumentNullException(nameof(type)); + + return IsProxyType(type) ? + type.BaseType : + type; + } + // Protected virtual methods. protected virtual IEnumerable GetCustomAdditionalInterfaces(Type modelType) => Array.Empty(); diff --git a/src/MongODM.Core/ProxyModels/ReferenceableInterceptor.cs b/src/MongODM.Core/ProxyModels/ReferenceableInterceptor.cs index 1c343485..cfa7079b 100644 --- a/src/MongODM.Core/ProxyModels/ReferenceableInterceptor.cs +++ b/src/MongODM.Core/ProxyModels/ReferenceableInterceptor.cs @@ -30,7 +30,7 @@ public class ReferenceableInterceptor : ModelInterceptorBase settedMemberNames = new Dictionary(); // - private readonly IRepository repository; + private readonly IRepository repository; // Constructors. public ReferenceableInterceptor( @@ -38,12 +38,26 @@ public ReferenceableInterceptor( IDbContext dbContext) : base(additionalInterfaces) { - repository = (IRepository)dbContext.RepositoryRegister.ModelRepositoryMap[typeof(TModel)]; + if (dbContext is null) + throw new ArgumentNullException(nameof(dbContext)); + + var repositoryModelType = typeof(TModel); + while (!dbContext.RepositoryRegister.ModelRepositoryMap.ContainsKey(repositoryModelType)) + { + if (repositoryModelType == typeof(object)) + throw new InvalidOperationException($"Cant find valid repository for model type {typeof(TModel)}"); + repositoryModelType = repositoryModelType.BaseType; + } + + repository = dbContext.RepositoryRegister.ModelRepositoryMap[repositoryModelType]; } // Protected methods. protected override bool InterceptInterface(IInvocation invocation) { + if (invocation is null) + throw new ArgumentNullException(nameof(invocation)); + // Intercept ISummarizable invocations if (invocation.Method.DeclaringType == typeof(IReferenceable)) { @@ -83,8 +97,11 @@ protected override bool InterceptInterface(IInvocation invocation) protected override void InterceptModel(IInvocation invocation) { + if (invocation is null) + throw new ArgumentNullException(nameof(invocation)); + // Filter gets. - if (invocation.Method.Name.StartsWith("get_") && isSummary) + if (invocation.Method.Name.StartsWith("get_", StringComparison.InvariantCulture) && isSummary) { var propertyName = invocation.Method.Name.Substring(4); @@ -97,7 +114,7 @@ protected override void InterceptModel(IInvocation invocation) } // Filter sets. - else if (invocation.Method.Name.StartsWith("set_")) + else if (invocation.Method.Name.StartsWith("set_", StringComparison.InvariantCulture)) { var propertyName = invocation.Method.Name.Substring(4); @@ -108,7 +125,7 @@ protected override void InterceptModel(IInvocation invocation) // Filter normal methods. else { - var attributes = invocation.Method.GetCustomAttributes(true) ?? new PropertyAltererAttribute[0]; + var attributes = invocation.Method.GetCustomAttributes(true) ?? Array.Empty(); foreach (var propertyName in from attribute in attributes select attribute.PropertyName) { @@ -137,10 +154,13 @@ protected override void InterceptModel(IInvocation invocation) // Helpers. private async Task FullLoadAsync(TModel model) { + if (model.Id is null) + throw new InvalidOperationException("model or id can't be null"); + if (isSummary) { // Merge full object to current. - var fullModel = await repository.TryFindOneAsync(model.Id); + var fullModel = (await repository.TryFindOneAsync(model.Id).ConfigureAwait(false)) as TModel; MergeFullModel(model, fullModel); } } diff --git a/src/MongODM.Core/ReflectionHelper.cs b/src/MongODM.Core/ReflectionHelper.cs index f6b38b3c..091ef24f 100644 --- a/src/MongODM.Core/ReflectionHelper.cs +++ b/src/MongODM.Core/ReflectionHelper.cs @@ -26,58 +26,20 @@ public static class ReflectionHelper private static readonly Dictionary> propertyRegister = new Dictionary>(); private static readonly ReaderWriterLockSlim propertyRegisterLock = new ReaderWriterLockSlim(); - public static TClass CloneModel(TClass srcObj, params Expression>[] memberLambdas) - where TClass : new() - { - var destObj = new TClass(); - CloneModel(srcObj, destObj, memberLambdas); - return destObj; - } - - public static void CloneModel(TClass srcObj, TClass destObj, params Expression>[] memberLambdas) - { - if (srcObj is null) - throw new ArgumentNullException(nameof(srcObj)); - if (destObj is null) - throw new ArgumentNullException(nameof(destObj)); - - IEnumerable membersToClone; - - if (memberLambdas.Any()) - { - membersToClone = memberLambdas.Select(l => GetMemberInfoFromLambda(l, typeof(TClass))); - } - else // clone full object - { - membersToClone = GetWritableInstanceProperties(typeof(TClass)); - } - - foreach (var member in membersToClone) - { - SetValue(destObj, member, GetValue(srcObj, member)); - } - } - - public static void CloneModel(object srcObj, object destObj, Type actualType) => - CloneModel(srcObj, destObj, GetWritableInstanceProperties(actualType)); - - public static void CloneModel(object srcObj, object destObj, IEnumerable members) - { - foreach (var member in members) - { - SetValue(destObj, member, GetValue(srcObj, member)); - } - } - public static MemberInfo FindProperty(LambdaExpression lambdaExpression) { + if (lambdaExpression is null) + throw new ArgumentNullException(nameof(lambdaExpression)); + Expression expressionToCheck = lambdaExpression; bool done = false; while (!done) { +#pragma warning disable CA1062 // Validate arguments of public methods. Suppressing for an issue in Microsoft.CodeAnalysis.FxCopAnalyzers v3.0.0 switch (expressionToCheck.NodeType) +#pragma warning restore CA1062 // Validate arguments of public methods { case ExpressionType.Convert: expressionToCheck = ((UnaryExpression)expressionToCheck).Operand; @@ -86,7 +48,7 @@ public static MemberInfo FindProperty(LambdaExpression lambdaExpression) expressionToCheck = ((LambdaExpression)expressionToCheck).Body; break; case ExpressionType.MemberAccess: - var memberExpression = ((MemberExpression)expressionToCheck); + var memberExpression = (MemberExpression)expressionToCheck; if (memberExpression.Expression.NodeType != ExpressionType.Parameter && memberExpression.Expression.NodeType != ExpressionType.Convert) @@ -110,6 +72,11 @@ public static MemberInfo FindProperty(LambdaExpression lambdaExpression) public static PropertyInfo FindPropertyImplementation(PropertyInfo interfacePropertyInfo, Type actualType) { + if (interfacePropertyInfo is null) + throw new ArgumentNullException(nameof(interfacePropertyInfo)); + if (actualType is null) + throw new ArgumentNullException(nameof(actualType)); + var interfaceType = interfacePropertyInfo.DeclaringType; // An interface map must be used because because there is no @@ -137,19 +104,13 @@ public static PropertyInfo FindPropertyImplementation(PropertyInfo interfaceProp }); } - public static object? GetDefaultValue(Type type) - { - if (type.IsValueType) - { - return Activator.CreateInstance(type); - } - return null; - } - public static MemberInfo GetMemberInfoFromLambda( Expression> memberLambda, Type? actualType = null) { + if (memberLambda is null) + throw new ArgumentNullException(nameof(memberLambda)); + var body = memberLambda.Body; MemberExpression memberExpression; switch (body.NodeType) @@ -214,6 +175,9 @@ public static TMember GetValueFromLambda(TModel source, Express /// The list of properties public static IEnumerable GetWritableInstanceProperties(Type objectType) { + if (objectType is null) + throw new ArgumentNullException(nameof(objectType)); + propertyRegisterLock.EnterReadLock(); try { diff --git a/src/MongODM.Core/Repositories/CollectionRepository.cs b/src/MongODM.Core/Repositories/CollectionRepository.cs index 9c0909c2..3031dd4d 100644 --- a/src/MongODM.Core/Repositories/CollectionRepository.cs +++ b/src/MongODM.Core/Repositories/CollectionRepository.cs @@ -49,6 +49,7 @@ public CollectionRepository(CollectionRepositoryOptions options) // Properties. public IMongoCollection Collection => _collection ??= DbContext.Database.GetCollection(options.Name); + public override string Name => options.Name; // Public methods. public override async Task BuildIndexesAsync(IDocumentSchemaRegister schemaRegister, CancellationToken cancellationToken = default) @@ -95,7 +96,7 @@ public override async Task BuildIndexesAsync(IDocumentSchemaRegister schemaRegis // Get current indexes. var currentIndexes = new List(); - using (var indexList = await Collection.Indexes.ListAsync(cancellationToken)) + using (var indexList = await Collection.Indexes.ListAsync(cancellationToken).ConfigureAwait(false)) while (indexList.MoveNext()) currentIndexes.AddRange(indexList.Current); @@ -106,11 +107,11 @@ public override async Task BuildIndexesAsync(IDocumentSchemaRegister schemaRegis where !newIndexes.Any(newIndex => newIndex.name == indexName) select index) { - await Collection.Indexes.DropOneAsync(oldIndex.GetElement("name").Value.ToString(), cancellationToken); + await Collection.Indexes.DropOneAsync(oldIndex.GetElement("name").Value.ToString(), cancellationToken).ConfigureAwait(false); } // Build new indexes. - await Collection.Indexes.CreateManyAsync(newIndexes.Select(i => i.createIndex), cancellationToken); + await Collection.Indexes.CreateManyAsync(newIndexes.Select(i => i.createIndex), cancellationToken).ConfigureAwait(false); } public virtual Task> FindAsync( @@ -126,8 +127,13 @@ public Task FindOneAsync( public virtual Task QueryElementsAsync( Func, Task> query, - AggregateOptions? aggregateOptions = null) => - query(Collection.AsQueryable(aggregateOptions)); + AggregateOptions? aggregateOptions = null) + { + if (query is null) + throw new ArgumentNullException(nameof(query)); + + return query(Collection.AsQueryable(aggregateOptions)); + } public virtual Task ReplaceAsync( object model, @@ -162,7 +168,7 @@ public virtual Task ReplaceAsync( try { - return await FindOneAsync(predicate, cancellationToken); + return await FindOneAsync(predicate, cancellationToken).ConfigureAwait(false); } catch (EntityNotFoundException) { @@ -177,10 +183,15 @@ protected override Task CreateOnDBAsync(IEnumerable models, Cancellation protected override Task CreateOnDBAsync(TModel model, CancellationToken cancellationToken) => Collection.InsertOneAsync(model, null, cancellationToken); - protected override Task DeleteOnDBAsync(TModel model, CancellationToken cancellationToken) => - Collection.DeleteOneAsync( + protected override Task DeleteOnDBAsync(TModel model, CancellationToken cancellationToken) + { + if (model is null) + throw new ArgumentNullException(nameof(model)); + + return Collection.DeleteOneAsync( Builders.Filter.Eq(m => m.Id, model.Id), cancellationToken); + } protected override async Task FindOneOnDBAsync(TKey id, CancellationToken cancellationToken = default) { @@ -189,7 +200,7 @@ protected override async Task FindOneOnDBAsync(TKey id, CancellationToke try { - return await FindOneOnDBAsync(m => m.Id!.Equals(id), cancellationToken: cancellationToken); + return await FindOneOnDBAsync(m => m.Id!.Equals(id), cancellationToken: cancellationToken).ConfigureAwait(false); } catch (EntityNotFoundException) { @@ -207,7 +218,7 @@ private async Task FindOneOnDBAsync( var element = await Collection.AsQueryable() .Where(predicate) - .SingleOrDefaultAsync(cancellationToken); + .SingleOrDefaultAsync(cancellationToken).ConfigureAwait(false); if (element == default(TModel)) throw new EntityNotFoundException("Can't find element"); @@ -229,7 +240,7 @@ private async Task ReplaceHelperAsync( await Collection.ReplaceOneAsync( Builders.Filter.Eq(m => m.Id, model.Id), model, - cancellationToken: cancellationToken); + cancellationToken: cancellationToken).ConfigureAwait(false); } else { @@ -237,12 +248,12 @@ await Collection.ReplaceOneAsync( session, Builders.Filter.Eq(m => m.Id, model.Id), model, - cancellationToken: cancellationToken); + cancellationToken: cancellationToken).ConfigureAwait(false); } // Update dependent documents. if (updateDependentDocuments) - DbContext.DBMaintainer.OnUpdatedModel((IAuditable)model, model.Id); + DbContext.DbMaintainer.OnUpdatedModel((IAuditable)model, model.Id); // Reset changed members. (model as IAuditable)?.ResetChangedMembers(); diff --git a/src/MongODM.Core/Repositories/GridFSRepository.cs b/src/MongODM.Core/Repositories/GridFSRepository.cs index 3aa65a33..f45e2d04 100644 --- a/src/MongODM.Core/Repositories/GridFSRepository.cs +++ b/src/MongODM.Core/Repositories/GridFSRepository.cs @@ -48,6 +48,7 @@ public GridFSRepository(GridFSRepositoryOptions options) // Properties. public IGridFSBucket GridFSBucket => _gridFSBucket ??= new GridFSBucket(DbContext.Database, new GridFSBucketOptions { BucketName = options.Name }); + public override string Name => options.Name; // Methods. public override Task BuildIndexesAsync(IDocumentSchemaRegister schemaRegister, CancellationToken cancellationToken = default) => Task.CompletedTask; @@ -56,13 +57,16 @@ public virtual Task DownloadAsBytesAsync(string id, CancellationToken ca GridFSBucket.DownloadAsBytesAsync(ObjectId.Parse(id), null, cancellationToken); public virtual async Task DownloadAsStreamAsync(string id, CancellationToken cancellationToken = default) => - await GridFSBucket.OpenDownloadStreamAsync(ObjectId.Parse(id), null, cancellationToken); + await GridFSBucket.OpenDownloadStreamAsync(ObjectId.Parse(id), null, cancellationToken).ConfigureAwait(false); // Protected methods. protected override async Task CreateOnDBAsync(IEnumerable models, CancellationToken cancellationToken) { + if (models is null) + throw new ArgumentNullException(nameof(models)); + foreach (var model in models) - await CreateOnDBAsync(model, cancellationToken); + await CreateOnDBAsync(model, cancellationToken).ConfigureAwait(false); } protected override async Task CreateOnDBAsync(TModel model, CancellationToken cancellationToken) @@ -75,12 +79,17 @@ protected override async Task CreateOnDBAsync(TModel model, CancellationToken ca var id = await GridFSBucket.UploadFromStreamAsync(model.Name, model.Stream, new GridFSUploadOptions { Metadata = options.MetadataSerializer?.Invoke(model) - }); + }).ConfigureAwait(false); ReflectionHelper.SetValue(model, m => m.Id, id.ToString()); } - protected override Task DeleteOnDBAsync(TModel model, CancellationToken cancellationToken) => - GridFSBucket.DeleteAsync(ObjectId.Parse(model.Id), cancellationToken); + protected override Task DeleteOnDBAsync(TModel model, CancellationToken cancellationToken) + { + if (model is null) + throw new ArgumentNullException(nameof(model)); + + return GridFSBucket.DeleteAsync(ObjectId.Parse(model.Id), cancellationToken); + } protected override async Task FindOneOnDBAsync(string id, CancellationToken cancellationToken = default) { @@ -88,7 +97,7 @@ protected override async Task FindOneOnDBAsync(string id, CancellationTo throw new ArgumentNullException(nameof(id)); var filter = Builders.Filter.Eq("_id", ObjectId.Parse(id)); - var mongoFile = await GridFSBucket.Find(filter).SingleOrDefaultAsync(cancellationToken); + var mongoFile = await GridFSBucket.Find(filter).SingleOrDefaultAsync(cancellationToken).ConfigureAwait(false); if (mongoFile == null) throw new EntityNotFoundException($"Can't find key {id}"); diff --git a/src/MongODM.Core/Repositories/IRepository.cs b/src/MongODM.Core/Repositories/IRepository.cs index 7937552c..f389b9d4 100644 --- a/src/MongODM.Core/Repositories/IRepository.cs +++ b/src/MongODM.Core/Repositories/IRepository.cs @@ -26,6 +26,7 @@ public interface IRepository : IDbContextInitializable IDbContext DbContext { get; } Type GetKeyType { get; } Type GetModelType { get; } + string Name { get; } Task BuildIndexesAsync( IDocumentSchemaRegister schemaRegister, @@ -34,6 +35,20 @@ Task BuildIndexesAsync( Task DeleteAsync( IEntityModel model, CancellationToken cancellationToken = default); + + Task FindOneAsync( + object id, + CancellationToken cancellationToken = default); + + /// + /// Try to find a model and don't throw exception if it is not found + /// + /// Model's Id + /// The cancellation token + /// The model, null if it doesn't exist + Task TryFindOneAsync( + object id, + CancellationToken cancellationToken = default); } public interface IRepository : IRepository diff --git a/src/MongODM.Core/Repositories/RepositoryBase.cs b/src/MongODM.Core/Repositories/RepositoryBase.cs index 36a412f2..8fff9da2 100644 --- a/src/MongODM.Core/Repositories/RepositoryBase.cs +++ b/src/MongODM.Core/Repositories/RepositoryBase.cs @@ -45,30 +45,34 @@ public virtual void Initialize(IDbContext dbContext) public Type GetKeyType => typeof(TKey); public Type GetModelType => typeof(TModel); public bool IsInitialized { get; private set; } + public abstract string Name { get; } // Methods. public abstract Task BuildIndexesAsync(IDocumentSchemaRegister schemaRegister, CancellationToken cancellationToken = default); public virtual async Task CreateAsync(IEnumerable models, CancellationToken cancellationToken = default) { - await CreateOnDBAsync(models, cancellationToken); - await DbContext.SaveChangesAsync(); + await CreateOnDBAsync(models, cancellationToken).ConfigureAwait(false); + await DbContext.SaveChangesAsync().ConfigureAwait(false); } public virtual async Task CreateAsync(TModel model, CancellationToken cancellationToken = default) { - await CreateOnDBAsync(model, cancellationToken); - await DbContext.SaveChangesAsync(); + await CreateOnDBAsync(model, cancellationToken).ConfigureAwait(false); + await DbContext.SaveChangesAsync().ConfigureAwait(false); } public async Task DeleteAsync(TKey id, CancellationToken cancellationToken = default) { - var model = await FindOneAsync(id, cancellationToken: cancellationToken); - await DeleteAsync(model, cancellationToken); + var model = await FindOneAsync(id, cancellationToken: cancellationToken).ConfigureAwait(false); + await DeleteAsync(model, cancellationToken).ConfigureAwait(false); } public virtual async Task DeleteAsync(TModel model, CancellationToken cancellationToken = default) { + if (model is null) + throw new ArgumentNullException(nameof(model)); + // Process cascade delete. var referencesIdsPaths = DbContext.DocumentSchemaRegister.GetModelEntityReferencesIds(typeof(TModel)) .Where(d => d.UseCascadeDelete == true) @@ -77,41 +81,47 @@ public virtual async Task DeleteAsync(TModel model, CancellationToken cancellati .Select(d => d.MemberPath); foreach (var idPath in referencesIdsPaths) - await CascadeDeleteMembersAsync(model, idPath); + await CascadeDeleteMembersAsync(model, idPath).ConfigureAwait(false); // Unlink dependent models. model.DisposeForDelete(); - await DbContext.SaveChangesAsync(); + await DbContext.SaveChangesAsync().ConfigureAwait(false); // Delete model. - await DeleteOnDBAsync(model, cancellationToken); + await DeleteOnDBAsync(model, cancellationToken).ConfigureAwait(false); // Remove from cache. - if (DbContext.DBCache.LoadedModels.ContainsKey(model.Id!)) - DbContext.DBCache.RemoveModel(model.Id!); + if (DbContext.DbCache.LoadedModels.ContainsKey(model.Id!)) + DbContext.DbCache.RemoveModel(model.Id!); } public async Task DeleteAsync(IEntityModel model, CancellationToken cancellationToken = default) { if (!(model is TModel castedModel)) throw new InvalidEntityTypeException("Invalid model type"); - await DeleteAsync(castedModel, cancellationToken); + await DeleteAsync(castedModel, cancellationToken).ConfigureAwait(false); } + public async Task FindOneAsync(object id, CancellationToken cancellationToken = default) => + await FindOneAsync((TKey)id, cancellationToken).ConfigureAwait(false); + public virtual async Task FindOneAsync( TKey id, CancellationToken cancellationToken = default) { - if (DbContext.DBCache.LoadedModels.ContainsKey(id!)) + if (DbContext.DbCache.LoadedModels.ContainsKey(id!)) { - var cachedModel = DbContext.DBCache.LoadedModels[id!] as TModel; + var cachedModel = DbContext.DbCache.LoadedModels[id!] as TModel; if ((cachedModel as IReferenceable)?.IsSummary == false) return cachedModel!; } - return await FindOneOnDBAsync(id, cancellationToken); + return await FindOneOnDBAsync(id, cancellationToken).ConfigureAwait(false); } + public async Task TryFindOneAsync(object id, CancellationToken cancellationToken = default) => + await TryFindOneAsync((TKey)id, cancellationToken).ConfigureAwait(false); + public async Task TryFindOneAsync( TKey id, CancellationToken cancellationToken = default) @@ -123,7 +133,7 @@ public virtual async Task FindOneAsync( try { - return await FindOneAsync(id, cancellationToken); + return await FindOneAsync(id, cancellationToken).ConfigureAwait(false); } catch (EntityNotFoundException) { @@ -153,8 +163,10 @@ private async Task CascadeDeleteMembersAsync(object currentModel, IEnumerable().ToArray()) - await CascadeDeleteMembersAsync(itemValue, memberTail); + await CascadeDeleteMembersAsync(itemValue, memberTail).ConfigureAwait(false); } else { - await CascadeDeleteMembersAsync(memberValue, memberTail); + await CascadeDeleteMembersAsync(memberValue, memberTail).ConfigureAwait(false); } } } diff --git a/src/MongODM.Core/Repositories/RepositoryRegister.cs b/src/MongODM.Core/Repositories/RepositoryRegister.cs index 156b6a2e..b96d4dd7 100644 --- a/src/MongODM.Core/Repositories/RepositoryRegister.cs +++ b/src/MongODM.Core/Repositories/RepositoryRegister.cs @@ -48,8 +48,8 @@ public IReadOnlyDictionary ModelCollectionRepositor { var dbContextType = dbContext.GetType(); - //select ICollectionRepository<,> implementing properties - var repos = dbContextType.GetProperties(BindingFlags.Public | BindingFlags.Instance) + // Select ICollectionRepository<,> from dbcontext properties. + var repos = dbContextType.GetProperties(BindingFlags.Public | BindingFlags.Instance | BindingFlags.FlattenHierarchy) .Where(prop => { var propType = prop.PropertyType; @@ -67,7 +67,7 @@ public IReadOnlyDictionary ModelCollectionRepositor return false; }); - //construct register + // Initialize register. _modelCollectionRepositoryMap = repos.ToDictionary( prop => ((ICollectionRepository)prop.GetValue(dbContext)).GetModelType, prop => (ICollectionRepository)prop.GetValue(dbContext)); diff --git a/src/MongODM.Core/Serialization/DocumentSchema.cs b/src/MongODM.Core/Serialization/DocumentSchema.cs index c3ba3089..34242d56 100644 --- a/src/MongODM.Core/Serialization/DocumentSchema.cs +++ b/src/MongODM.Core/Serialization/DocumentSchema.cs @@ -20,10 +20,16 @@ namespace Etherna.MongODM.Serialization public class DocumentSchema { // Constructors. - public DocumentSchema(BsonClassMap classMap, Type modelType, IBsonSerializer? serializer, DocumentVersion version) + public DocumentSchema( + BsonClassMap classMap, + Type modelType, + BsonClassMap? proxyClassMap, + IBsonSerializer? serializer, + SemanticVersion version) { ClassMap = classMap; ModelType = modelType; + ProxyClassMap = proxyClassMap; Serializer = serializer; Version = version; } @@ -31,7 +37,8 @@ public DocumentSchema(BsonClassMap classMap, Type modelType, IBsonSerializer? se // Properties. public BsonClassMap ClassMap { get; } public Type ModelType { get; } + public BsonClassMap? ProxyClassMap { get; } public IBsonSerializer? Serializer { get; } - public DocumentVersion Version { get; } + public SemanticVersion Version { get; } } } diff --git a/src/MongODM.Core/Serialization/DocumentSchemaMemberMap.cs b/src/MongODM.Core/Serialization/DocumentSchemaMemberMap.cs index 028f795a..93fa73cb 100644 --- a/src/MongODM.Core/Serialization/DocumentSchemaMemberMap.cs +++ b/src/MongODM.Core/Serialization/DocumentSchemaMemberMap.cs @@ -26,7 +26,7 @@ public class DocumentSchemaMemberMap public DocumentSchemaMemberMap( Type rootModelType, IEnumerable memberPath, - DocumentVersion version, + SemanticVersion version, bool? useCascadeDelete) { MemberPath = memberPath; @@ -73,7 +73,7 @@ public IEnumerable MemberPathToId } public Type RootModelType { get; } public bool? UseCascadeDelete { get; } - public DocumentVersion Version { get; } + public SemanticVersion Version { get; } // Methods. public string MemberPathToString() => diff --git a/src/MongODM.Core/Serialization/DocumentSchemaRegister.cs b/src/MongODM.Core/Serialization/DocumentSchemaRegister.cs index ce687f32..e1ddd5b1 100644 --- a/src/MongODM.Core/Serialization/DocumentSchemaRegister.cs +++ b/src/MongODM.Core/Serialization/DocumentSchemaRegister.cs @@ -27,7 +27,7 @@ namespace Etherna.MongODM.Serialization { - public class DocumentSchemaRegister : IDocumentSchemaRegister + public class DocumentSchemaRegister : IDocumentSchemaRegister, IDisposable { // Fields. private readonly ReaderWriterLockSlim configLock = new ReaderWriterLockSlim(LockRecursionPolicy.SupportsRecursion); @@ -64,6 +64,11 @@ public void Initialize(IDbContext dbContext) public IEnumerable Schemas => schemas; // Methods. + public void Dispose() + { + configLock.Dispose(); + } + public void Freeze() { configLock.EnterReadLock(); @@ -85,12 +90,18 @@ public void Freeze() foreach (var schemaGroup in schemas.GroupBy(s => s.ModelType) .Select(group => group.OrderByDescending(s => s.Version).First())) { - // Register class map. + // Register regular model. + //register class map BsonClassMap.RegisterClassMap(schemaGroup.ClassMap); - // Register serializer. + //register serializer if (schemaGroup.Serializer != null) BsonSerializer.RegisterSerializer(schemaGroup.ModelType, schemaGroup.Serializer); + + // Register proxy model. + //register proxy class map + if (schemaGroup.ProxyClassMap != null) + BsonClassMap.RegisterClassMap(schemaGroup.ProxyClassMap); } // Compile dependency registers. @@ -100,7 +111,7 @@ public void Freeze() CompileDependencyRegisters( schema.ModelType, - new EntityMember[0], + Array.Empty(), schema.ClassMap, schema.Version); } @@ -121,7 +132,7 @@ public IEnumerable GetMemberDependencies(MemberInfo mem if (memberDependenciesMap.TryGetValue(memberInfo, out List dependencies)) return dependencies; - return new DocumentSchemaMemberMap[0]; + return Array.Empty(); } public IEnumerable GetModelDependencies(Type modelType) @@ -130,7 +141,7 @@ public IEnumerable GetModelDependencies(Type modelType) if (modelDependenciesMap.TryGetValue(modelType, out List dependencies)) return dependencies; - return new DocumentSchemaMemberMap[0]; + return Array.Empty(); } public IEnumerable GetModelEntityReferencesIds(Type modelType) @@ -139,13 +150,13 @@ public IEnumerable GetModelEntityReferencesIds(Type mod if (modelEntityReferencesIdsMap.TryGetValue(modelType, out List dependencies)) return dependencies; - return new DocumentSchemaMemberMap[0]; + return Array.Empty(); } public void RegisterModelSchema( - DocumentVersion fromVersion, + SemanticVersion fromVersion, Func>? initCustomSerializer = null, - Func>? modelMigrationAsync = null) + Func>? modelMigrationAsync = null) where TModel : class => RegisterModelSchema( fromVersion, @@ -154,10 +165,10 @@ public void RegisterModelSchema( modelMigrationAsync); public void RegisterModelSchema( - DocumentVersion fromVersion, + SemanticVersion fromVersion, Action> classMapInitializer, Func>? initCustomSerializer = null, - Func>? modelMigrationAsync = null) + Func>? modelMigrationAsync = null) where TModel : class => RegisterModelSchema( fromVersion, @@ -166,18 +177,33 @@ public void RegisterModelSchema( modelMigrationAsync); public void RegisterModelSchema( - DocumentVersion fromVersion, + SemanticVersion fromVersion, BsonClassMap classMap, Func>? initCustomSerializer = null, - Func>? modelMigrationAsync = null) + Func>? modelMigrationAsync = null) where TModel : class { + if (classMap is null) + throw new ArgumentNullException(nameof(classMap)); + configLock.EnterWriteLock(); try { if (IsFrozen) throw new InvalidOperationException("Register is frozen"); + // If not abstract, adjustments for use proxygenerator. + BsonClassMap? proxyClassMap = null; + if (!typeof(TModel).IsAbstract) + { + //set creator + classMap.SetCreator(() => dbContext.ProxyGenerator.CreateInstance(dbContext)); + + //generate proxy classmap + proxyClassMap = new BsonClassMap( + dbContext.ProxyGenerator.CreateInstance(dbContext).GetType()); + } + // Generate model serializer. IBsonSerializer? serializer = null; @@ -187,14 +213,14 @@ public void RegisterModelSchema( else if (!typeof(TModel).IsAbstract) //else if can deserialize, set default serializer serializer = new ExtendedClassMapSerializer( - dbContext.DBCache, - dbContext.DocumentVersion, + dbContext.DbCache, + dbContext.ApplicationVersion, serializerModifierAccessor, (m, v) => modelMigrationAsync?.Invoke(m, v) ?? Task.FromResult(m)) { AddVersion = typeof(IEntityModel).IsAssignableFrom(typeof(TModel)) }; //true only for entity models // Register schema. - schemas.Add(new DocumentSchema(classMap, typeof(TModel), serializer, fromVersion)); + schemas.Add(new DocumentSchema(classMap, typeof(TModel), proxyClassMap, serializer, fromVersion)); } finally { @@ -207,7 +233,7 @@ private void CompileDependencyRegisters( Type modelType, IEnumerable memberPath, BsonClassMap currentClassMap, - DocumentVersion version, + SemanticVersion version, bool? useCascadeDeleteSetting = null) { // Ignore class maps of abstract types. (child classes will map all their members) diff --git a/src/MongODM.Core/Serialization/ExtendedBsonDocumentReader.cs b/src/MongODM.Core/Serialization/ExtendedBsonDocumentReader.cs index 50ca3ebe..3847983c 100644 --- a/src/MongODM.Core/Serialization/ExtendedBsonDocumentReader.cs +++ b/src/MongODM.Core/Serialization/ExtendedBsonDocumentReader.cs @@ -23,6 +23,6 @@ public ExtendedBsonDocumentReader(BsonDocument document) : base(document) { } - public DocumentVersion? DocumentVersion { get; set; } + public SemanticVersion? DocumentVersion { get; set; } } } diff --git a/src/MongODM.Core/Serialization/IDocumentSchemaRegister.cs b/src/MongODM.Core/Serialization/IDocumentSchemaRegister.cs index 76c7264b..4136ef8b 100644 --- a/src/MongODM.Core/Serialization/IDocumentSchemaRegister.cs +++ b/src/MongODM.Core/Serialization/IDocumentSchemaRegister.cs @@ -48,9 +48,9 @@ public interface IDocumentSchemaRegister : IDbContextInitializable /// Custom serializer initializer /// Model migration method void RegisterModelSchema( - DocumentVersion fromVersion, + SemanticVersion fromVersion, Func>? initCustomSerializer = null, - Func>? modelMigrationAsync = null) + Func>? modelMigrationAsync = null) where TModel : class; /// @@ -62,10 +62,10 @@ void RegisterModelSchema( /// Custom serializer initializer /// Model migration method void RegisterModelSchema( - DocumentVersion fromVersion, + SemanticVersion fromVersion, Action> classMapInitializer, Func>? initCustomSerializer = null, - Func>? modelMigrationAsync = null) + Func>? modelMigrationAsync = null) where TModel : class; /// @@ -77,10 +77,10 @@ void RegisterModelSchema( /// Custom serializer initializer /// Model migration method void RegisterModelSchema( - DocumentVersion fromVersion, + SemanticVersion fromVersion, BsonClassMap classMap, Func>? initCustomSerializer = null, - Func>? modelMigrationAsync = null) + Func>? modelMigrationAsync = null) where TModel : class; } } \ No newline at end of file diff --git a/src/MongODM.Core/Serialization/IModelSerializerCollector.cs b/src/MongODM.Core/Serialization/IModelMapsCollector.cs similarity index 94% rename from src/MongODM.Core/Serialization/IModelSerializerCollector.cs rename to src/MongODM.Core/Serialization/IModelMapsCollector.cs index 910ccbca..fc46ec60 100644 --- a/src/MongODM.Core/Serialization/IModelSerializerCollector.cs +++ b/src/MongODM.Core/Serialization/IModelMapsCollector.cs @@ -14,7 +14,7 @@ namespace Etherna.MongODM.Serialization { - public interface IModelSerializerCollector + public interface IModelMapsCollector { // Methods. void Register(IDbContext dbContext); diff --git a/src/MongODM.Core/Serialization/DocumentVersion.cs b/src/MongODM.Core/Serialization/SemanticVersion.cs similarity index 77% rename from src/MongODM.Core/Serialization/DocumentVersion.cs rename to src/MongODM.Core/Serialization/SemanticVersion.cs index f5247cf3..0ce49bf1 100644 --- a/src/MongODM.Core/Serialization/DocumentVersion.cs +++ b/src/MongODM.Core/Serialization/SemanticVersion.cs @@ -13,19 +13,20 @@ // limitations under the License. using System; +using System.Globalization; using System.Text; using System.Text.RegularExpressions; namespace Etherna.MongODM.Serialization { - public class DocumentVersion : IComparable + public class SemanticVersion : IComparable { // Constructors. /// /// Construct from string version /// /// The version as string (ex. 3.1.4-alpha1) - public DocumentVersion(string version) + public SemanticVersion(string version) { // Accepted formats for version: // * 3 @@ -42,11 +43,11 @@ public DocumentVersion(string version) var patchGroup = match.Groups["patch"]; var labelGroup = match.Groups["label"]; - MajorRelease = int.Parse(majorGroup.Value); + MajorRelease = int.Parse(majorGroup.Value, CultureInfo.InvariantCulture); if (minorGroup.Success) - MinorRelease = int.Parse(minorGroup.Value); + MinorRelease = int.Parse(minorGroup.Value, CultureInfo.InvariantCulture); if (patchGroup.Success) - PatchRelease = int.Parse(patchGroup.Value); + PatchRelease = int.Parse(patchGroup.Value, CultureInfo.InvariantCulture); if (labelGroup.Success) LabelRelease = labelGroup.Value; } @@ -58,7 +59,7 @@ public DocumentVersion(string version) /// Minor version /// Patch version /// Additional label - public DocumentVersion( + public SemanticVersion( int major, int minor, int patch, @@ -77,17 +78,17 @@ public DocumentVersion( public string? LabelRelease { get; private set; } // Overrides. - public int CompareTo(DocumentVersion? other) + public int CompareTo(SemanticVersion? other) { // If other is not a valid object reference, this instance is greater. if (other is null) return 1; - if (this > other) return 1; + if (this < other) return -1; if (this == other) return 0; - else return -1; + else return 1; } - public override bool Equals(object obj) => this == (obj as DocumentVersion); + public override bool Equals(object obj) => this == (obj as SemanticVersion); public override int GetHashCode() { @@ -112,12 +113,12 @@ public override string ToString() } // Operators. - public static bool operator < (DocumentVersion? x, DocumentVersion? y) + public static bool operator < (SemanticVersion? x, SemanticVersion? y) { // Check if null. - if (y == null) + if (y is null) return false; - else if (x == null) //y != null + else if (x is null) //y != null return true; // Check major release. @@ -135,9 +136,9 @@ public override string ToString() return false; } - public static bool operator > (DocumentVersion? x, DocumentVersion? y) => y < x; + public static bool operator > (SemanticVersion? x, SemanticVersion? y) => y < x; - public static bool operator == (DocumentVersion? x, DocumentVersion? y) + public static bool operator == (SemanticVersion? x, SemanticVersion? y) { if (ReferenceEquals(x, y)) return true; if (x is null || y is null) return false; @@ -148,8 +149,12 @@ public override string ToString() x.LabelRelease == y.LabelRelease; } - public static bool operator != (DocumentVersion x, DocumentVersion y) => !(x == y); + public static bool operator != (SemanticVersion x, SemanticVersion y) => !(x == y); - public static implicit operator DocumentVersion(string version) => new DocumentVersion(version); + public static bool operator <= (SemanticVersion x, SemanticVersion y) => x < y || x == y; + + public static bool operator >=(SemanticVersion x, SemanticVersion y) => y <= x; + + public static implicit operator SemanticVersion(string version) => new SemanticVersion(version); } } diff --git a/src/MongODM.Core/Serialization/Serializers/DictionarySerializer.cs b/src/MongODM.Core/Serialization/Serializers/DictionarySerializer.cs index e6c21e28..4ead2513 100644 --- a/src/MongODM.Core/Serialization/Serializers/DictionarySerializer.cs +++ b/src/MongODM.Core/Serialization/Serializers/DictionarySerializer.cs @@ -15,6 +15,7 @@ using MongoDB.Bson.Serialization; using MongoDB.Bson.Serialization.Options; using MongoDB.Bson.Serialization.Serializers; +using System; using System.Collections.Generic; namespace Etherna.MongODM.Serialization.Serializers @@ -42,9 +43,11 @@ public DictionarySerializer(DictionaryRepresentation dictionaryRepresentation, I { } // Properties. + public IBsonSerializer ChildSerializer => ValueSerializer; + public IEnumerable ContainedClassMaps => ValueSerializer is IClassMapContainerSerializer classMapContainer ? - classMapContainer.ContainedClassMaps : new BsonClassMap[0]; + classMapContainer.ContainedClassMaps : Array.Empty(); public bool? UseCascadeDelete => (ValueSerializer as IReferenceContainerSerializer)?.UseCascadeDelete; @@ -52,12 +55,18 @@ ValueSerializer is IClassMapContainerSerializer classMapContainer ? // Public methods. public override void Serialize(BsonSerializationContext context, BsonSerializationArgs args, IDictionary value) { + if (value is null) + throw new ArgumentNullException(nameof(value)); + // Force to exclude enumerable actual type from serialization. args = new BsonSerializationArgs(value.GetType(), true, args.SerializeIdFirst); base.Serialize(context, args, value); } + public IBsonSerializer WithChildSerializer(IBsonSerializer childSerializer) => + WithValueSerializer((IBsonSerializer)childSerializer); + /// /// Returns a serializer that has been reconfigured with the specified dictionary representation. /// @@ -108,11 +117,6 @@ protected override ICollection> CreateAccumulator() = new Dictionary(); // Explicit interface implementations. - IBsonSerializer IChildSerializerConfigurable.ChildSerializer => ValueSerializer; - - IBsonSerializer IChildSerializerConfigurable.WithChildSerializer(IBsonSerializer childSerializer) => - WithValueSerializer((IBsonSerializer)childSerializer); - IBsonSerializer IDictionaryRepresentationConfigurable.WithDictionaryRepresentation(DictionaryRepresentation dictionaryRepresentation) => WithDictionaryRepresentation(dictionaryRepresentation); } diff --git a/src/MongODM.Core/Serialization/Serializers/EnumerableSerializer.cs b/src/MongODM.Core/Serialization/Serializers/EnumerableSerializer.cs index 9e980173..59d69ce9 100644 --- a/src/MongODM.Core/Serialization/Serializers/EnumerableSerializer.cs +++ b/src/MongODM.Core/Serialization/Serializers/EnumerableSerializer.cs @@ -14,6 +14,7 @@ using MongoDB.Bson.Serialization; using MongoDB.Bson.Serialization.Serializers; +using System; using System.Collections.Generic; namespace Etherna.MongODM.Serialization.Serializers @@ -47,9 +48,11 @@ public EnumerableSerializer(IBsonSerializerRegistry serializerRegistry) { } // Properties. + public IBsonSerializer ChildSerializer => ItemSerializer; + public IEnumerable ContainedClassMaps => ItemSerializer is IClassMapContainerSerializer classMapContainer ? - classMapContainer.ContainedClassMaps : new BsonClassMap[0]; + classMapContainer.ContainedClassMaps : Array.Empty(); public bool? UseCascadeDelete => (ItemSerializer as IReferenceContainerSerializer)?.UseCascadeDelete; @@ -57,12 +60,18 @@ ItemSerializer is IClassMapContainerSerializer classMapContainer ? // Public methods. public override void Serialize(BsonSerializationContext context, BsonSerializationArgs args, IEnumerable value) { + if (value is null) + throw new ArgumentNullException(nameof(value)); + // Force to exclude enumerable actual type from serialization. args = new BsonSerializationArgs(value.GetType(), true, args.SerializeIdFirst); base.Serialize(context, args, value); } + public IBsonSerializer WithChildSerializer(IBsonSerializer childSerializer) => + WithItemSerializer((IBsonSerializer)childSerializer); + /// /// Returns a serializer that has been reconfigured with the specified item serializer. /// @@ -74,8 +83,13 @@ public EnumerableSerializer WithItemSerializer(IBsonSerializer ite } // Protected methods. - protected override void AddItem(object accumulator, TItem item) => + protected override void AddItem(object accumulator, TItem item) + { + if (accumulator is null) + throw new ArgumentNullException(nameof(accumulator)); + ((List)accumulator).Add(item); + } protected override object CreateAccumulator() => new List(); @@ -85,11 +99,5 @@ protected override IEnumerable EnumerateItemsInSerializationOrder(IEnumer protected override IEnumerable FinalizeResult(object accumulator) => (IEnumerable)accumulator; - - // Explicit interface implementations. - IBsonSerializer IChildSerializerConfigurable.ChildSerializer => ItemSerializer; - - IBsonSerializer IChildSerializerConfigurable.WithChildSerializer(IBsonSerializer childSerializer) => - WithItemSerializer((IBsonSerializer)childSerializer); } } diff --git a/src/MongODM.Core/Serialization/Serializers/ExtendedClassMapSerializer.cs b/src/MongODM.Core/Serialization/Serializers/ExtendedClassMapSerializer.cs index a2c0d59c..7b09ba1c 100644 --- a/src/MongODM.Core/Serialization/Serializers/ExtendedClassMapSerializer.cs +++ b/src/MongODM.Core/Serialization/Serializers/ExtendedClassMapSerializer.cs @@ -45,16 +45,19 @@ private struct ExtraElementCondition private readonly IDbCache dbCache; private readonly ISerializerModifierAccessor serializerModifierAccessor; private readonly ICollection extraElements; - private readonly Func> fixDeserializedModelAsync; + private readonly Func> fixDeserializedModelAsync; private BsonClassMapSerializer _serializer = default!; // Constructor. public ExtendedClassMapSerializer( IDbCache dbCache, - DocumentVersion documentVersion, + SemanticVersion documentVersion, ISerializerModifierAccessor serializerModifierAccessor, - Func>? fixDeserializedModelAsync = null) + Func>? fixDeserializedModelAsync = null) { + if (documentVersion is null) + throw new ArgumentNullException(nameof(documentVersion)); + this.dbCache = dbCache; this.serializerModifierAccessor = serializerModifierAccessor; extraElements = new List(); @@ -95,6 +98,9 @@ public ExtendedClassMapSerializer AddExtraElement( public override TModel Deserialize(BsonDeserializationContext context, BsonDeserializationArgs args) { + if (context is null) + throw new ArgumentNullException(nameof(context)); + // Check if null. if (context.Reader.CurrentBsonType == BsonType.Null) { @@ -106,12 +112,12 @@ public override TModel Deserialize(BsonDeserializationContext context, BsonDeser var bsonDocument = BsonDocumentSerializer.Instance.Deserialize(context, args); // Get version. - DocumentVersion? documentVersion = null; + SemanticVersion? documentVersion = null; if (bsonDocument.TryGetElement(DbContext.DocumentVersionElementName, out BsonElement versionElement)) documentVersion = BsonValueToDocumentVersion(versionElement.Value); // Initialize localContext and bsonReader - var bsonReader = new ExtendedBsonDocumentReader(bsonDocument) + using var bsonReader = new ExtendedBsonDocumentReader(bsonDocument) { DocumentVersion = documentVersion }; @@ -159,6 +165,9 @@ public bool GetDocumentId(object document, out object id, out Type idNominalType public override void Serialize(BsonSerializationContext context, BsonSerializationArgs args, TModel value) { + if (context is null) + throw new ArgumentNullException(nameof(context)); + // Serialize null object. if (value == null) { @@ -168,7 +177,7 @@ public override void Serialize(BsonSerializationContext context, BsonSerializati // Initialize localContext, bsonDocument and bsonWriter. var bsonDocument = new BsonDocument(); - var bsonWriter = new ExtendedBsonDocumentWriter(bsonDocument) + using var bsonWriter = new ExtendedBsonDocumentWriter(bsonDocument) { IsRootDocument = !(context.Writer is ExtendedBsonDocumentWriter) }; @@ -176,16 +185,6 @@ public override void Serialize(BsonSerializationContext context, BsonSerializati bsonWriter, builder => builder.IsDynamicType = context.IsDynamicType); - // Purify model from proxy class. - if (value.GetType() != typeof(TModel)) - { - var constructor = typeof(TModel).GetConstructor(BindingFlags.Public | BindingFlags.Instance, null, new Type[0], null) ?? - typeof(TModel).GetConstructor(BindingFlags.NonPublic | BindingFlags.Instance, null, new Type[0], null); - var newModel = (TModel)constructor.Invoke(new object[0]); - ReflectionHelper.CloneModel(value, newModel); - value = newModel; - } - // Serialize. Serializer.Serialize(localContext, args, value); @@ -218,12 +217,12 @@ public bool TryGetMemberSerializationInfo(string memberName, out BsonSerializati Serializer.TryGetMemberSerializationInfo(memberName, out serializationInfo); // Helpers. - private static DocumentVersion? BsonValueToDocumentVersion(BsonValue bsonValue) => + private static SemanticVersion? BsonValueToDocumentVersion(BsonValue bsonValue) => bsonValue switch { BsonNull _ => null, - BsonString bsonString => new DocumentVersion(bsonString.AsString), - BsonArray bsonArray => new DocumentVersion( + BsonString bsonString => new SemanticVersion(bsonString.AsString), + BsonArray bsonArray => new SemanticVersion( bsonArray[0].AsInt32, bsonArray[1].AsInt32, bsonArray[2].AsInt32, @@ -231,7 +230,7 @@ public bool TryGetMemberSerializationInfo(string memberName, out BsonSerializati _ => throw new NotSupportedException(), }; - private static BsonArray DocumentVersionToBsonArray(DocumentVersion documentVersion) + private static BsonArray DocumentVersionToBsonArray(SemanticVersion documentVersion) { var bsonArray = new BsonArray(new[] { diff --git a/src/MongODM.Core/Serialization/Serializers/ExtraElementsSerializer.cs b/src/MongODM.Core/Serialization/Serializers/ExtraElementsSerializer.cs index f8cd935d..cba47513 100644 --- a/src/MongODM.Core/Serialization/Serializers/ExtraElementsSerializer.cs +++ b/src/MongODM.Core/Serialization/Serializers/ExtraElementsSerializer.cs @@ -16,6 +16,7 @@ using MongoDB.Bson.IO; using MongoDB.Bson.Serialization; using MongoDB.Bson.Serialization.Serializers; +using System; using System.Collections.Generic; namespace Etherna.MongODM.Serialization.Serializers @@ -26,6 +27,9 @@ public class ExtraElementsSerializer : SerializerBase public override void Serialize(BsonSerializationContext context, BsonSerializationArgs args, object value) { + if (context is null) + throw new ArgumentNullException(nameof(context)); + if (value is IDictionary dictionary) { context.Writer.WriteStartDocument(); @@ -62,7 +66,8 @@ public static TValue DeserializeValue( * can't be serialized on root of documents. */ var document = new BsonDocument(); - var serializationContext = BsonSerializationContext.CreateRoot(new BsonDocumentWriter(document)); + using var documentWriter = new BsonDocumentWriter(document); + var serializationContext = BsonSerializationContext.CreateRoot(documentWriter); serializationContext.Writer.WriteStartDocument(); serializationContext.Writer.WriteName("container"); @@ -76,7 +81,8 @@ public static TValue DeserializeValue( } // Deserialize. - var deserializationContext = BsonDeserializationContext.CreateRoot(new BsonDocumentReader(document)); + using var documentReader = new BsonDocumentReader(document); + var deserializationContext = BsonDeserializationContext.CreateRoot(documentReader); deserializationContext.Reader.ReadStartDocument(); deserializationContext.Reader.ReadName(); diff --git a/src/MongODM.Core/Serialization/Serializers/GeoPointSerializer.cs b/src/MongODM.Core/Serialization/Serializers/GeoPointSerializer.cs index 0cf522a6..50785c63 100644 --- a/src/MongODM.Core/Serialization/Serializers/GeoPointSerializer.cs +++ b/src/MongODM.Core/Serialization/Serializers/GeoPointSerializer.cs @@ -42,7 +42,7 @@ public GeoPointSerializer( longitudeMemberInfo = ReflectionHelper.GetMemberInfoFromLambda(longitudeMember); latitudeMemberInfo = ReflectionHelper.GetMemberInfoFromLambda(latitudeMember); pointSerializer = new GeoJsonPointSerializer(); - this.dbContext = dbContext; + this.dbContext = dbContext ?? throw new ArgumentNullException(nameof(dbContext)); proxyGenerator = dbContext.ProxyGenerator; } diff --git a/src/MongODM.Core/Serialization/Serializers/HexToBinaryDataSerializer.cs b/src/MongODM.Core/Serialization/Serializers/HexToBinaryDataSerializer.cs index 43a36124..be7291c3 100644 --- a/src/MongODM.Core/Serialization/Serializers/HexToBinaryDataSerializer.cs +++ b/src/MongODM.Core/Serialization/Serializers/HexToBinaryDataSerializer.cs @@ -23,6 +23,9 @@ public class HexToBinaryDataSerializer : SerializerBase { public override string Deserialize(BsonDeserializationContext context, BsonDeserializationArgs args) { + if (context is null) + throw new ArgumentNullException(nameof(context)); + var bsonReader = context.Reader; var bsonType = bsonReader.GetCurrentBsonType(); switch (bsonType) @@ -43,6 +46,9 @@ public override string Deserialize(BsonDeserializationContext context, BsonDeser public override void Serialize(BsonSerializationContext context, BsonSerializationArgs args, string value) { + if (context is null) + throw new ArgumentNullException(nameof(context)); + if (value == null) { context.Writer.WriteNull(); diff --git a/src/MongODM.Core/Serialization/Serializers/ReadOnlyDictionarySerializer.cs b/src/MongODM.Core/Serialization/Serializers/ReadOnlyDictionarySerializer.cs index a5bac97e..48e5d61e 100644 --- a/src/MongODM.Core/Serialization/Serializers/ReadOnlyDictionarySerializer.cs +++ b/src/MongODM.Core/Serialization/Serializers/ReadOnlyDictionarySerializer.cs @@ -15,6 +15,7 @@ using MongoDB.Bson.Serialization; using MongoDB.Bson.Serialization.Options; using MongoDB.Bson.Serialization.Serializers; +using System; using System.Collections.Generic; namespace Etherna.MongODM.Serialization.Serializers @@ -42,9 +43,11 @@ public ReadOnlyDictionarySerializer(DictionaryRepresentation dictionaryRepresent { } // Properties. + public IBsonSerializer ChildSerializer => ValueSerializer; + public IEnumerable ContainedClassMaps => ValueSerializer is IClassMapContainerSerializer classMapContainer ? - classMapContainer.ContainedClassMaps : new BsonClassMap[0]; + classMapContainer.ContainedClassMaps : Array.Empty(); public bool? UseCascadeDelete => (ValueSerializer as IReferenceContainerSerializer)?.UseCascadeDelete; @@ -52,12 +55,18 @@ ValueSerializer is IClassMapContainerSerializer classMapContainer ? // Public methods. public override void Serialize(BsonSerializationContext context, BsonSerializationArgs args, IReadOnlyDictionary value) { + if (value is null) + throw new ArgumentNullException(nameof(value)); + // Force to exclude enumerable actual type from serialization. args = new BsonSerializationArgs(value.GetType(), true, args.SerializeIdFirst); base.Serialize(context, args, value); } + public IBsonSerializer WithChildSerializer(IBsonSerializer childSerializer) => + WithValueSerializer((IBsonSerializer)childSerializer); + /// /// Returns a serializer that has been reconfigured with the specified dictionary representation. /// @@ -108,11 +117,6 @@ protected override ICollection> CreateAccumulator() = new Dictionary(); // Explicit interface implementations. - IBsonSerializer IChildSerializerConfigurable.ChildSerializer => ValueSerializer; - - IBsonSerializer IChildSerializerConfigurable.WithChildSerializer(IBsonSerializer childSerializer) => - WithValueSerializer((IBsonSerializer)childSerializer); - IBsonSerializer IDictionaryRepresentationConfigurable.WithDictionaryRepresentation(DictionaryRepresentation dictionaryRepresentation) => WithDictionaryRepresentation(dictionaryRepresentation); } diff --git a/src/MongODM.Core/Serialization/Serializers/ReferenceSerializer.cs b/src/MongODM.Core/Serialization/Serializers/ReferenceSerializer.cs index 15084c20..1e4ef06d 100644 --- a/src/MongODM.Core/Serialization/Serializers/ReferenceSerializer.cs +++ b/src/MongODM.Core/Serialization/Serializers/ReferenceSerializer.cs @@ -14,8 +14,6 @@ using Etherna.MongODM.Models; using Etherna.MongODM.ProxyModels; -using Etherna.MongODM.Serialization.Modifiers; -using Etherna.MongODM.Utility; using MongoDB.Bson; using MongoDB.Bson.Serialization; using MongoDB.Bson.Serialization.Conventions; @@ -29,7 +27,7 @@ namespace Etherna.MongODM.Serialization.Serializers { public class ReferenceSerializer : - SerializerBase, IBsonSerializer, IBsonDocumentSerializer, IBsonIdProvider, IReferenceContainerSerializer + SerializerBase, IBsonSerializer, IBsonDocumentSerializer, IBsonIdProvider, IReferenceContainerSerializer, IDisposable where TModelBase : class, IEntityModel { // Fields. @@ -38,10 +36,7 @@ public class ReferenceSerializer : private readonly ReaderWriterLockSlim configLockAdapters = new ReaderWriterLockSlim(LockRecursionPolicy.SupportsRecursion); private readonly ReaderWriterLockSlim configLockClassMaps = new ReaderWriterLockSlim(LockRecursionPolicy.SupportsRecursion); private readonly ReaderWriterLockSlim configLockSerializers = new ReaderWriterLockSlim(LockRecursionPolicy.SupportsRecursion); - private readonly IDbCache dbCache; private readonly IDbContext dbContext; - private readonly IProxyGenerator proxyGenerator; - private readonly ISerializerModifierAccessor serializerModifierAccessor; private readonly IDictionary registeredAdapters = new Dictionary(); private readonly IDictionary registeredClassMaps = new Dictionary(); @@ -52,10 +47,7 @@ public ReferenceSerializer( IDbContext dbContext, bool useCascadeDelete) { - this.dbCache = dbContext.DBCache; - this.proxyGenerator = dbContext.ProxyGenerator; - this.serializerModifierAccessor = dbContext.SerializerModifierAccessor; - this.dbContext = dbContext; + this.dbContext = dbContext ?? throw new ArgumentNullException(nameof(dbContext)); UseCascadeDelete = useCascadeDelete; } @@ -75,6 +67,9 @@ public IDiscriminatorConvention DiscriminatorConvention // Methods. public override TModelBase Deserialize(BsonDeserializationContext context, BsonDeserializationArgs args) { + if (context is null) + throw new ArgumentNullException(nameof(context)); + // Check bson type. var bsonReader = context.Reader; var bsonType = bsonReader.GetCurrentBsonType(); @@ -105,10 +100,10 @@ public override TModelBase Deserialize(BsonDeserializationContext context, BsonD return null!; // Check if model as been loaded in cache. - if (dbCache.LoadedModels.ContainsKey(id) && - !serializerModifierAccessor.IsNoCacheEnabled) + if (dbContext.DbCache.LoadedModels.ContainsKey(id) && + !dbContext.SerializerModifierAccessor.IsNoCacheEnabled) { - var cachedModel = (TModelBase)dbCache.LoadedModels[id]; + var cachedModel = (TModelBase)dbContext.DbCache.LoadedModels[id]; if (((IReferenceable)cachedModel).IsSummary) { @@ -139,7 +134,7 @@ public override TModelBase Deserialize(BsonDeserializationContext context, BsonD else { // Set model as summarizable. - if (serializerModifierAccessor.IsReadOnlyReferencedIdEnabled) + if (dbContext.SerializerModifierAccessor.IsReadOnlyReferencedIdEnabled) { ((IReferenceable)model).ClearSettedMembers(); ((IReferenceable)model).SetAsSummary(new[] { nameof(model.Id) }); @@ -153,14 +148,21 @@ public override TModelBase Deserialize(BsonDeserializationContext context, BsonD ((IAuditable)model).EnableAuditing(); // Add in cache. - if (!serializerModifierAccessor.IsNoCacheEnabled) - dbCache.AddModel(model.Id!, model); + if (!dbContext.SerializerModifierAccessor.IsNoCacheEnabled) + dbContext.DbCache.AddModel(model.Id!, model); } } return model!; } + public void Dispose() + { + configLockAdapters.Dispose(); + configLockClassMaps.Dispose(); + configLockSerializers.Dispose(); + } + public IBsonSerializer GetAdapter() where TModel : class, TModelBase { @@ -194,11 +196,39 @@ public IBsonSerializer GetAdapter() public bool GetDocumentId(object document, out object id, out Type idNominalType, out IIdGenerator idGenerator) { - IsProxyClassType(document, out Type documentType); + if (document is null) + throw new ArgumentNullException(nameof(document)); + + var documentType = dbContext.ProxyGenerator.PurgeProxyType(document.GetType()); var serializer = (IBsonIdProvider)GetSerializer(documentType); return serializer.GetDocumentId(document, out id, out idNominalType, out idGenerator); } - + + public ReferenceSerializer RegisterProxyType() + { + var proxyType = dbContext.ProxyGenerator.CreateInstance(dbContext)!.GetType(); + + // Initialize class map. + var createBsonClassMapInfo = GetType().GetMethod(nameof(CreateBsonClassMap), BindingFlags.Instance | BindingFlags.NonPublic); + var createBsonClassMap = createBsonClassMapInfo.MakeGenericMethod(proxyType); + + var classMap = (BsonClassMap)createBsonClassMap.Invoke(this, new object[] { null! }); + + // Add info to dictionary of registered types. + configLockClassMaps.EnterWriteLock(); + try + { + registeredClassMaps.Add(proxyType, classMap); + } + finally + { + configLockClassMaps.ExitWriteLock(); + } + + // Return this for cascade use. + return this; + } + public ReferenceSerializer RegisterType(Action>? classMapInitializer = null) where TModel : class { @@ -207,7 +237,7 @@ public ReferenceSerializer RegisterType(Action proxyGenerator.CreateInstance(dbContext)); + classMap.SetCreator(() => dbContext.ProxyGenerator.CreateInstance(dbContext)); // Add info to dictionary of registered types. configLockClassMaps.EnterWriteLock(); @@ -226,6 +256,9 @@ public ReferenceSerializer RegisterType(Action CreateBsonClassMap(Action> classMapInitializer) + /// + /// Create a new BsonClassMap for type TModel, and link its baseClassMap if already registered + /// + /// The destination model type of class map + /// The class map inizializer. Empty initilization if null + /// The new created class map + private BsonClassMap CreateBsonClassMap(Action>? classMapInitializer = null) { + classMapInitializer ??= cm => { }; + BsonClassMap classMap = new BsonClassMap(classMapInitializer); var baseType = typeof(TModel).BaseType; configLockClassMaps.EnterReadLock(); @@ -366,16 +380,5 @@ private IBsonSerializer GetSerializer(Type actualType) configLockSerializers.ExitWriteLock(); } } - - private bool IsProxyClassType(TModel value, out Type modelType) - { - modelType = value!.GetType(); - if (proxyGenerator.IsProxyType(modelType)) - { - modelType = modelType.BaseType; - return true; - } - return false; - } } } diff --git a/src/MongODM.Core/Serialization/Serializers/ReferenceSerializerSwitch.cs b/src/MongODM.Core/Serialization/Serializers/ReferenceSerializerSwitch.cs index 6e48f049..9de88074 100644 --- a/src/MongODM.Core/Serialization/Serializers/ReferenceSerializerSwitch.cs +++ b/src/MongODM.Core/Serialization/Serializers/ReferenceSerializerSwitch.cs @@ -27,7 +27,7 @@ public class ReferenceSerializerSwitch : // Nested classes. public class CaseContext { - public DocumentVersion? DocumentVersion { get; set; } + public SemanticVersion? DocumentVersion { get; set; } } // Fields. @@ -42,7 +42,7 @@ public ReferenceSerializerSwitch( Func deserializer)[] caseDeserializers) { this.caseDeserializers = caseDeserializers ?? - new(Func, Func)[0]; + Array.Empty<(Func, Func)>(); this.defaultSerializer = defaultSerializer ?? throw new ArgumentNullException(nameof(defaultSerializer)); } @@ -53,6 +53,9 @@ public ReferenceSerializerSwitch( // Methods. public override TModel Deserialize(BsonDeserializationContext context, BsonDeserializationArgs args) { + if (context is null) + throw new ArgumentNullException(nameof(context)); + var extendedReader = context.Reader as ExtendedBsonDocumentReader; var switchContext = new CaseContext { DocumentVersion = extendedReader?.DocumentVersion }; diff --git a/test/MongODM.Core.Tests/Models/FakeModelProxy.cs b/src/MongODM.Core/Tasks/IMigrateDbContextTask.cs similarity index 73% rename from test/MongODM.Core.Tests/Models/FakeModelProxy.cs rename to src/MongODM.Core/Tasks/IMigrateDbContextTask.cs index 109a1d09..386a837a 100644 --- a/test/MongODM.Core.Tests/Models/FakeModelProxy.cs +++ b/src/MongODM.Core/Tasks/IMigrateDbContextTask.cs @@ -12,9 +12,13 @@ // See the License for the specific language governing permissions and // limitations under the License. -namespace Etherna.MongODM.Models +using System.Threading.Tasks; + +namespace Etherna.MongODM.Tasks { - public class FakeModelProxy : FakeModel + public interface IMigrateDbContextTask { + Task RunAsync(string dbMigrationOpId, string taskId) + where TDbContext : class, IDbContext; } -} +} \ No newline at end of file diff --git a/src/MongODM.Core/Tasks/ITaskRunner.cs b/src/MongODM.Core/Tasks/ITaskRunner.cs index aa65ffa7..6b000ecb 100644 --- a/src/MongODM.Core/Tasks/ITaskRunner.cs +++ b/src/MongODM.Core/Tasks/ITaskRunner.cs @@ -19,6 +19,7 @@ namespace Etherna.MongODM.Tasks { public interface ITaskRunner { + void RunMigrateDbTask(Type dbContextType, string dbMigrationOpId); void RunUpdateDocDependenciesTask(Type dbContextType, Type modelType, Type keyType, IEnumerable idPaths, object modelId); } } diff --git a/src/MongODM.Core/Tasks/MigrateDbContextTask.cs b/src/MongODM.Core/Tasks/MigrateDbContextTask.cs new file mode 100644 index 00000000..00c154f3 --- /dev/null +++ b/src/MongODM.Core/Tasks/MigrateDbContextTask.cs @@ -0,0 +1,94 @@ +// Copyright 2020-present Etherna Sagl +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +using Etherna.MongODM.Models.Internal; +using Etherna.MongODM.Models.Internal.DbMigrationOpAgg; +using System; +using System.Threading.Tasks; + +namespace Etherna.MongODM.Tasks +{ + public class MigrateDbContextTask : IMigrateDbContextTask + { + // Fields. + private readonly IServiceProvider serviceProvider; + + // Constructors. + public MigrateDbContextTask( + IServiceProvider serviceProvider) + { + this.serviceProvider = serviceProvider; + } + + // Methods. + public async Task RunAsync(string dbMigrationOpId, string taskId) + where TDbContext : class, IDbContext + { + var dbContext = (TDbContext)serviceProvider.GetService(typeof(TDbContext)); + var dbMigrationOp = (DbMigrationOperation)await dbContext.DbOperations.FindOneAsync(dbMigrationOpId).ConfigureAwait(false); + + // Start migrate operation. + dbMigrationOp.TaskStarted(taskId); + await dbContext.SaveChangesAsync().ConfigureAwait(false); + + // Migrate documents. + foreach (var docMigration in dbContext.DocumentMigrationList) + { + //running document migration + var result = await docMigration.MigrateAsync(500, + async procDocs => + { + dbMigrationOp.AddLog(new DocumentMigrationLog( + docMigration.SourceCollection.Name, + docMigration.Id, + MigrationLogBase.ExecutionState.Executing, + procDocs)); + + await dbContext.SaveChangesAsync().ConfigureAwait(false); + }).ConfigureAwait(false); + + //ended document migration log + dbMigrationOp.AddLog(new DocumentMigrationLog( + docMigration.SourceCollection.Name, + docMigration.Id, + result.Succeded ? + MigrationLogBase.ExecutionState.Succeded : + MigrationLogBase.ExecutionState.Failed, + result.MigratedDocuments)); + + await dbContext.SaveChangesAsync().ConfigureAwait(false); + } + + // Build indexes. + foreach (var repository in dbContext.RepositoryRegister.ModelCollectionRepositoryMap.Values) + { + dbMigrationOp.AddLog(new IndexMigrationLog( + repository.Name, + MigrationLogBase.ExecutionState.Executing)); + await dbContext.SaveChangesAsync().ConfigureAwait(false); + + await repository.BuildIndexesAsync(dbContext.DocumentSchemaRegister).ConfigureAwait(false); + + dbMigrationOp.AddLog(new IndexMigrationLog( + repository.Name, + MigrationLogBase.ExecutionState.Succeded)); + await dbContext.SaveChangesAsync().ConfigureAwait(false); + } + + // Complete task. + dbMigrationOp.TaskCompleted(); + await dbContext.SaveChangesAsync().ConfigureAwait(false); + } + } +} diff --git a/src/MongODM.Core/Tasks/Queues.cs b/src/MongODM.Core/Tasks/Queues.cs index 437c2fea..7ba6cc09 100644 --- a/src/MongODM.Core/Tasks/Queues.cs +++ b/src/MongODM.Core/Tasks/Queues.cs @@ -14,7 +14,7 @@ namespace Etherna.MongODM.Tasks { - public class Queues + public static class Queues { public const string DB_MAINTENANCE = "db_maintenance"; } diff --git a/src/MongODM.Core/Tasks/UpdateDocDependenciesTask.cs b/src/MongODM.Core/Tasks/UpdateDocDependenciesTask.cs index 0bc36292..5121d76d 100644 --- a/src/MongODM.Core/Tasks/UpdateDocDependenciesTask.cs +++ b/src/MongODM.Core/Tasks/UpdateDocDependenciesTask.cs @@ -44,6 +44,9 @@ public async Task RunAsync( where TModel : class, IEntityModel where TDbContext : class, IDbContext { + if (idPaths is null) + throw new ArgumentNullException(nameof(idPaths)); + var dbContext = (TDbContext)serviceProvider.GetService(typeof(TDbContext)); // Get repository. @@ -59,10 +62,10 @@ public async Task RunAsync( { using var cursor = await repository.FindAsync( Builders.Filter.Eq(idPath, modelId), - new FindOptions { NoCursorTimeout = true }); + new FindOptions { NoCursorTimeout = true }).ConfigureAwait(false); // Load and replace. - while (await cursor.MoveNextAsync()) + while (await cursor.MoveNextAsync().ConfigureAwait(false)) { foreach (var model in cursor.Current) { @@ -71,9 +74,11 @@ public async Task RunAsync( try { // Replace on db. - await repository.ReplaceAsync(model, false); + await repository.ReplaceAsync(model, false).ConfigureAwait(false); } +#pragma warning disable CA1031 // Do not catch general exception types. Internal exceptions thrown by MongoDB drivers catch { } +#pragma warning restore CA1031 // Do not catch general exception types // Add id to upgraded list. upgradedDocumentsId.Add(model.Id); diff --git a/src/MongODM.Core/Utility/DBCache.cs b/src/MongODM.Core/Utility/DbCache.cs similarity index 100% rename from src/MongODM.Core/Utility/DBCache.cs rename to src/MongODM.Core/Utility/DbCache.cs diff --git a/src/MongODM.Core/Utility/DbContextDependencies.cs b/src/MongODM.Core/Utility/DbDependencies.cs similarity index 74% rename from src/MongODM.Core/Utility/DbContextDependencies.cs rename to src/MongODM.Core/Utility/DbDependencies.cs index 3fb8c1c5..2aad8a66 100644 --- a/src/MongODM.Core/Utility/DbContextDependencies.cs +++ b/src/MongODM.Core/Utility/DbDependencies.cs @@ -19,18 +19,25 @@ namespace Etherna.MongODM.Utility { - public class DbContextDependencies : IDbContextDependencies + public class DbDependencies : IDbDependencies { - public DbContextDependencies( + public DbDependencies( IDbCache dbCache, IDbMaintainer dbMaintainer, + IDbMigrationManager dbContextMigrationManager, IDocumentSchemaRegister documentSchemaRegister, IProxyGenerator proxyGenerator, IRepositoryRegister repositoryRegister, - ISerializerModifierAccessor serializerModifierAccessor) + ISerializerModifierAccessor serializerModifierAccessor, +#pragma warning disable IDE0060 // Remove unused parameter. It's needed for run static configurations +#pragma warning disable CA1801 // Review unused parameters. Same of above + IStaticConfigurationBuilder staticConfigurationBuilder) +#pragma warning restore CA1801 // Review unused parameters +#pragma warning restore IDE0060 // Remove unused parameter { DbCache = dbCache; DbMaintainer = dbMaintainer; + DbMigrationManager = dbContextMigrationManager; DocumentSchemaRegister = documentSchemaRegister; ProxyGenerator = proxyGenerator; RepositoryRegister = repositoryRegister; @@ -39,6 +46,7 @@ public DbContextDependencies( public IDbCache DbCache { get; } public IDbMaintainer DbMaintainer { get; } + public IDbMigrationManager DbMigrationManager { get; } public IDocumentSchemaRegister DocumentSchemaRegister { get; } public IProxyGenerator ProxyGenerator { get; } public IRepositoryRegister RepositoryRegister { get; } diff --git a/src/MongODM.Core/Utility/DBMaintainer.cs b/src/MongODM.Core/Utility/DbMaintainer.cs similarity index 100% rename from src/MongODM.Core/Utility/DBMaintainer.cs rename to src/MongODM.Core/Utility/DbMaintainer.cs diff --git a/src/MongODM.Core/Utility/DbMigrationManager.cs b/src/MongODM.Core/Utility/DbMigrationManager.cs new file mode 100644 index 00000000..e10f3d04 --- /dev/null +++ b/src/MongODM.Core/Utility/DbMigrationManager.cs @@ -0,0 +1,89 @@ +// Copyright 2020-present Etherna Sagl +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +using Etherna.MongODM.Extensions; +using Etherna.MongODM.Models.Internal; +using Etherna.MongODM.Tasks; +using MongoDB.Driver; +using MongoDB.Driver.Linq; +using System; +using System.Collections.Generic; +using System.Threading.Tasks; + +namespace Etherna.MongODM.Utility +{ + public class DbMigrationManager : IDbMigrationManager + { + // Fields. + private IDbContext dbContext = default!; + private readonly ITaskRunner taskRunner; + + // Constructor and initialization. + public DbMigrationManager(ITaskRunner taskRunner) + { + this.taskRunner = taskRunner; + } + public void Initialize(IDbContext dbContext) + { + if (IsInitialized) + throw new InvalidOperationException("Instance already initialized"); + + this.dbContext = dbContext; + + IsInitialized = true; + } + + // Properties. + public bool IsInitialized { get; private set; } + + // Methods. + public async Task> GetLastMigrationsAsync(int page, int take) => + await dbContext.DbOperations.QueryElementsAsync(elements => + elements.OfType() + .PaginateDescending(r => r.CreationDateTime, page, take) + .ToListAsync()).ConfigureAwait(false); + + public async Task GetMigrationAsync(string migrateOperationId) + { + if (migrateOperationId is null) + throw new ArgumentNullException(nameof(migrateOperationId)); + + var migrateOp = await dbContext.DbOperations.QueryElementsAsync(elements => + elements.OfType() + .Where(op => op.Id == migrateOperationId) + .FirstAsync()).ConfigureAwait(false); + + return migrateOp; + } + + public async Task IsMigrationRunningAsync() + { + var migrateOp = await dbContext.DbOperations.QueryElementsAsync(elements => + elements.OfType() + .Where(op => op.DbContextName == dbContext.Identifier) + .Where(op => op.CurrentStatus == DbMigrationOperation.Status.Running) + .FirstOrDefaultAsync()).ConfigureAwait(false); + + return migrateOp; + } + + public async Task StartDbContextMigrationAsync(string authorId) + { + var migrateOp = new DbMigrationOperation(dbContext, authorId); + await dbContext.DbOperations.CreateAsync(migrateOp).ConfigureAwait(false); + + taskRunner.RunMigrateDbTask(dbContext.GetType(), migrateOp.Id); + } + } +} diff --git a/src/MongODM.Core/Utility/IDBCache.cs b/src/MongODM.Core/Utility/IDbCache.cs similarity index 100% rename from src/MongODM.Core/Utility/IDBCache.cs rename to src/MongODM.Core/Utility/IDbCache.cs diff --git a/src/MongODM.Core/Utility/IDbContextDependencies.cs b/src/MongODM.Core/Utility/IDbDependencies.cs similarity index 92% rename from src/MongODM.Core/Utility/IDbContextDependencies.cs rename to src/MongODM.Core/Utility/IDbDependencies.cs index 0abd0819..20890318 100644 --- a/src/MongODM.Core/Utility/IDbContextDependencies.cs +++ b/src/MongODM.Core/Utility/IDbDependencies.cs @@ -19,10 +19,11 @@ namespace Etherna.MongODM.Utility { - public interface IDbContextDependencies + public interface IDbDependencies { IDbCache DbCache { get; } IDbMaintainer DbMaintainer { get; } + IDbMigrationManager DbMigrationManager { get; } IDocumentSchemaRegister DocumentSchemaRegister { get; } IProxyGenerator ProxyGenerator { get; } IRepositoryRegister RepositoryRegister { get; } diff --git a/src/MongODM.Core/Utility/IDBMaintainer.cs b/src/MongODM.Core/Utility/IDbMaintainer.cs similarity index 100% rename from src/MongODM.Core/Utility/IDBMaintainer.cs rename to src/MongODM.Core/Utility/IDbMaintainer.cs diff --git a/src/MongODM.Core/Utility/IDbMigrationManager.cs b/src/MongODM.Core/Utility/IDbMigrationManager.cs new file mode 100644 index 00000000..0959b970 --- /dev/null +++ b/src/MongODM.Core/Utility/IDbMigrationManager.cs @@ -0,0 +1,35 @@ +// Copyright 2020-present Etherna Sagl +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +using Etherna.MongODM.Models.Internal; +using System.Collections.Generic; +using System.Threading.Tasks; + +namespace Etherna.MongODM.Utility +{ + public interface IDbMigrationManager : IDbContextInitializable + { + Task IsMigrationRunningAsync(); + + Task> GetLastMigrationsAsync(int page, int take); + + Task GetMigrationAsync(string migrateOperationId); + + /// + /// Start a db context migration process. + /// + /// Id of user requiring the migration + Task StartDbContextMigrationAsync(string authorId); + } +} \ No newline at end of file diff --git a/src/MongODM.Core/Utility/IStaticConfigurationBuilder.cs b/src/MongODM.Core/Utility/IStaticConfigurationBuilder.cs new file mode 100644 index 00000000..da171a3e --- /dev/null +++ b/src/MongODM.Core/Utility/IStaticConfigurationBuilder.cs @@ -0,0 +1,28 @@ +// Copyright 2020-present Etherna Sagl +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +namespace Etherna.MongODM.Utility +{ + /// + /// This interface has the scope to inizialize only one time static configurations, when IoC system + /// has been configured, dependencies can be resolved, and before that any dbcontext starts to operate. + /// For a proper use, implements it in a class where configuration is invoked by constructor. + /// So configure it as a singleton on IoC system, and injectit as a dependency for DbContext. + /// +#pragma warning disable CA1040 // Avoid empty interfaces + public interface IStaticConfigurationBuilder + { + } +#pragma warning restore CA1040 // Avoid empty interfaces +} \ No newline at end of file diff --git a/src/MongODM.Hangfire/MongODM.Hangfire.csproj b/src/MongODM.Hangfire/MongODM.Hangfire.csproj index 8425c79a..e4aa78c1 100644 --- a/src/MongODM.Hangfire/MongODM.Hangfire.csproj +++ b/src/MongODM.Hangfire/MongODM.Hangfire.csproj @@ -8,7 +8,12 @@ Linker for use MongoDB with Hangfire 8.0 enable + https://github.com/Etherna/mongodm + git + true + true + snupkg LICENSE.txt @@ -22,6 +27,10 @@ all runtime; build; native; contentfiles; analyzers; buildtransitive + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + diff --git a/src/MongODM.Hangfire/Tasks/HangfireTaskRunner.cs b/src/MongODM.Hangfire/Tasks/HangfireTaskRunner.cs index 65863f95..7424f2ec 100644 --- a/src/MongODM.Hangfire/Tasks/HangfireTaskRunner.cs +++ b/src/MongODM.Hangfire/Tasks/HangfireTaskRunner.cs @@ -38,6 +38,10 @@ public HangfireTaskRunner( } // Methods. + public void RunMigrateDbTask(Type dbContextType, string dbMigrationOpId) => + backgroundJobClient.Enqueue( + task => task.RunAsync(dbContextType, dbMigrationOpId, null!)); + public void RunUpdateDocDependenciesTask(Type dbContextType, Type modelType, Type keyType, IEnumerable idPaths, object modelId) => backgroundJobClient.Enqueue( task => task.RunAsync(dbContextType, modelType, keyType, idPaths, modelId)); diff --git a/src/MongODM.Hangfire/Tasks/MigrateDbContextTaskFacade.cs b/src/MongODM.Hangfire/Tasks/MigrateDbContextTaskFacade.cs new file mode 100644 index 00000000..ca9c4da2 --- /dev/null +++ b/src/MongODM.Hangfire/Tasks/MigrateDbContextTaskFacade.cs @@ -0,0 +1,46 @@ +// Copyright 2020-present Etherna Sagl +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +using Etherna.MongODM.Tasks; +using Hangfire; +using Hangfire.Server; +using System; +using System.Reflection; +using System.Threading.Tasks; + +namespace Etherna.MongODM.HF.Tasks +{ + class MigrateDbContextTaskFacade + { + // Fields. + private readonly IMigrateDbContextTask task; + + // Constructors. + public MigrateDbContextTaskFacade(IMigrateDbContextTask task) + { + this.task = task; + } + + // Methods. + [Queue(Queues.DB_MAINTENANCE)] + public Task RunAsync(Type dbContextType, string dbMigrationOpId, PerformContext context) + { + var method = typeof(MigrateDbContextTask).GetMethod( + nameof(MigrateDbContextTask.RunAsync), BindingFlags.Public | BindingFlags.Instance) + .MakeGenericMethod(dbContextType); + + return (Task)method.Invoke(task, new object[] { dbMigrationOpId, context.BackgroundJob.Id }); + } + } +} diff --git a/src/MongODM.Hangfire/Tasks/UpdateDocDependenciesTaskFacade.cs b/src/MongODM.Hangfire/Tasks/UpdateDocDependenciesTaskFacade.cs index 362af942..73e48ebc 100644 --- a/src/MongODM.Hangfire/Tasks/UpdateDocDependenciesTaskFacade.cs +++ b/src/MongODM.Hangfire/Tasks/UpdateDocDependenciesTaskFacade.cs @@ -16,13 +16,11 @@ using Hangfire; using System; using System.Collections.Generic; -using System.Diagnostics.CodeAnalysis; using System.Reflection; using System.Threading.Tasks; namespace Etherna.MongODM.HF.Tasks { - [SuppressMessage("Performance", "CA1812:Avoid uninstantiated internal classes", Justification = "It is instantiated by Hangfire")] class UpdateDocDependenciesTaskFacade { // Fields. diff --git a/test/MongODM.Core.Tests/ExtendedClassMapSerializerTest.cs b/test/MongODM.Core.Tests/ExtendedClassMapSerializerTest.cs index 98ca63e3..1771af26 100644 --- a/test/MongODM.Core.Tests/ExtendedClassMapSerializerTest.cs +++ b/test/MongODM.Core.Tests/ExtendedClassMapSerializerTest.cs @@ -80,7 +80,7 @@ public SerializationTestElement( // Fields. private readonly Mock dbCacheMock; - private readonly DocumentVersion documentVersion = new DocumentVersion("1.0.0"); + private readonly SemanticVersion documentVersion = new SemanticVersion("1.0.0"); private readonly Mock serializerModifierAccessorMock; // Constructor. @@ -320,34 +320,6 @@ public static IEnumerable SerializationTests new BsonElement("StringProp", BsonNull.Value) } as IEnumerable), condition: _ => false), - - // With a proxy class. - new SerializationTestElement( - new FakeModelProxy() - { - Id = "idVal", - IntegerProp = 42, - ObjectProp = new FakeModel(), - StringProp = "yes" - }, - new BsonDocument(new BsonElement[] - { - new BsonElement("_id", new BsonString("idVal")), - new BsonElement("CreationDateTime", new BsonDateTime(new DateTime())), - new BsonElement("EnumerableProp", BsonNull.Value), - new BsonElement("IntegerProp", new BsonInt32(42)), - new BsonElement("ObjectProp", new BsonDocument(new BsonElement[] - { - new BsonElement("_id", BsonNull.Value), - new BsonElement("CreationDateTime", new BsonDateTime(new DateTime())), - new BsonElement("EnumerableProp", BsonNull.Value), - new BsonElement("IntegerProp", new BsonInt32(0)), - new BsonElement("ObjectProp", BsonNull.Value), - new BsonElement("StringProp", BsonNull.Value) - } as IEnumerable)), - new BsonElement("StringProp", new BsonString("yes")) - } as IEnumerable), - condition: _ => false) }; return tests.Select(t => new object[] { t }); diff --git a/test/MongODM.Core.Tests/ReferenceableInterceptorTest.cs b/test/MongODM.Core.Tests/ReferenceableInterceptorTest.cs index 32c77e43..b16e7808 100644 --- a/test/MongODM.Core.Tests/ReferenceableInterceptorTest.cs +++ b/test/MongODM.Core.Tests/ReferenceableInterceptorTest.cs @@ -17,6 +17,7 @@ using Etherna.MongODM.ProxyModels; using Etherna.MongODM.Repositories; using Moq; +using System; using System.Collections.Generic; using System.Threading; using System.Threading.Tasks; @@ -38,8 +39,11 @@ public ReferenceableInterceptorTest() repositoryMock = new Mock>(); dbContextMock = new Mock(); - dbContextMock.Setup(c => c.RepositoryRegister.ModelRepositoryMap[typeof(FakeModel)]) - .Returns(() => repositoryMock.Object); + dbContextMock.Setup(c => c.RepositoryRegister.ModelRepositoryMap) + .Returns(() => new Dictionary + { + [typeof(FakeModel)] = repositoryMock.Object + }); interceptor = new ReferenceableInterceptor( new[] { typeof(IReferenceable) }, @@ -136,8 +140,8 @@ public void GetNotLoadedMember() IntegerProp = 42 }; - repositoryMock.Setup(r => r.TryFindOneAsync(modelId, It.IsAny())) - .Returns(Task.FromResult(new FakeModel + repositoryMock.Setup(r => r.TryFindOneAsync((object)modelId, It.IsAny())) + .Returns(Task.FromResult(new FakeModel { Id = modelId, IntegerProp = 7, @@ -162,7 +166,7 @@ public void GetNotLoadedMember() // Assert. getPropertyInvocationMock.Verify(i => i.Proceed(), Times.Once()); - repositoryMock.Verify(r => r.TryFindOneAsync(modelId, It.IsAny()), Times.Once); + repositoryMock.Verify(r => r.TryFindOneAsync((object)modelId, It.IsAny()), Times.Once); interceptor.Intercept(getIsSummaryInvocationMock.Object); interceptor.Intercept(getLoadedMembersInvocationMock.Object);