diff --git a/.editorconfig b/.editorconfig index 1b9ef798..436132e9 100644 --- a/.editorconfig +++ b/.editorconfig @@ -24,3 +24,6 @@ dotnet_diagnostic.CA1040.severity = none # Sometime they are needed # CA1812: Avoid uninstantiated internal classes dotnet_diagnostic.CA1812.severity = none # Doing extensive use of dependency injection + +# CS1591: Missing XML comment for publicly visible type or member +dotnet_diagnostic.CS1591.severity = none # There are currently too much missing descriptions diff --git a/.github/workflows/myget-unstable-deploy.yml b/.github/workflows/myget-unstable-deploy.yml index 7e3aef43..7ad35bca 100644 --- a/.github/workflows/myget-unstable-deploy.yml +++ b/.github/workflows/myget-unstable-deploy.yml @@ -15,20 +15,12 @@ jobs: with: fetch-depth: 0 - - name: Setup .NET Core SDK 2.1.x - uses: actions/setup-dotnet@v1 + - name: Setup dotnet + uses: actions/setup-dotnet@v2 with: - dotnet-version: '2.1.x' - - - name: Setup .NET Core SDK 3.1.x - uses: actions/setup-dotnet@v1 - with: - dotnet-version: '3.1.x' - - - name: Setup .NET SDK 5.0.x - uses: actions/setup-dotnet@v1 - with: - dotnet-version: '5.0.x' + dotnet-version: | + 3.1.x + 6.0.x - name: Build with dotnet run: dotnet build --configuration Release diff --git a/.github/workflows/nuget-stable-deploy.yml b/.github/workflows/nuget-stable-deploy.yml index 14e6990a..aaa8b75a 100644 --- a/.github/workflows/nuget-stable-deploy.yml +++ b/.github/workflows/nuget-stable-deploy.yml @@ -13,21 +13,13 @@ jobs: - uses: actions/checkout@master with: fetch-depth: 0 - - - name: Setup .NET Core SDK 2.1.x - uses: actions/setup-dotnet@v1 - with: - dotnet-version: '2.1.x' - - - name: Setup .NET Core SDK 3.1.x - uses: actions/setup-dotnet@v1 - with: - dotnet-version: '3.1.x' - - name: Setup .NET SDK 5.0.x - uses: actions/setup-dotnet@v1 + - name: Setup dotnet + uses: actions/setup-dotnet@v2 with: - dotnet-version: '5.0.x' + dotnet-version: | + 3.1.x + 6.0.x - name: Build with dotnet run: dotnet build --configuration Release diff --git a/MongODM.sln b/MongODM.sln index c2956af9..9549975c 100644 --- a/MongODM.sln +++ b/MongODM.sln @@ -9,10 +9,6 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "test", "test", "{CF1ABDEA-7 EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "MongODM.Hangfire", "src\MongODM.Hangfire\MongODM.Hangfire.csproj", "{10897D0D-4898-4A4D-8D1E-B2435E93D9A1}" EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ExecutionContext", "src\ExecutionContext\ExecutionContext.csproj", "{DB6C020D-1C93-4456-8FB5-EF7CF505DF7B}" -EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ExecutionContext.Tests", "test\ExecutionContext.Tests\ExecutionContext.Tests.csproj", "{BF4F963A-DBCE-4C53-A209-502F4CAF12C5}" -EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "MongODM.AspNetCore", "src\MongODM.AspNetCore\MongODM.AspNetCore.csproj", "{6374F645-5D17-494E-9529-7F83426900B3}" EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "MongODM.Core", "src\MongODM.Core\MongODM.Core.csproj", "{4F2498A9-D60D-4D49-95B9-BC78EE2917B5}" @@ -52,14 +48,6 @@ Global {10897D0D-4898-4A4D-8D1E-B2435E93D9A1}.Debug|Any CPU.Build.0 = Debug|Any CPU {10897D0D-4898-4A4D-8D1E-B2435E93D9A1}.Release|Any CPU.ActiveCfg = Release|Any CPU {10897D0D-4898-4A4D-8D1E-B2435E93D9A1}.Release|Any CPU.Build.0 = Release|Any CPU - {DB6C020D-1C93-4456-8FB5-EF7CF505DF7B}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {DB6C020D-1C93-4456-8FB5-EF7CF505DF7B}.Debug|Any CPU.Build.0 = Debug|Any CPU - {DB6C020D-1C93-4456-8FB5-EF7CF505DF7B}.Release|Any CPU.ActiveCfg = Release|Any CPU - {DB6C020D-1C93-4456-8FB5-EF7CF505DF7B}.Release|Any CPU.Build.0 = Release|Any CPU - {BF4F963A-DBCE-4C53-A209-502F4CAF12C5}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {BF4F963A-DBCE-4C53-A209-502F4CAF12C5}.Debug|Any CPU.Build.0 = Debug|Any CPU - {BF4F963A-DBCE-4C53-A209-502F4CAF12C5}.Release|Any CPU.ActiveCfg = Release|Any CPU - {BF4F963A-DBCE-4C53-A209-502F4CAF12C5}.Release|Any CPU.Build.0 = Release|Any CPU {6374F645-5D17-494E-9529-7F83426900B3}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {6374F645-5D17-494E-9529-7F83426900B3}.Debug|Any CPU.Build.0 = Debug|Any CPU {6374F645-5D17-494E-9529-7F83426900B3}.Release|Any CPU.ActiveCfg = Release|Any CPU @@ -90,8 +78,6 @@ Global EndGlobalSection GlobalSection(NestedProjects) = preSolution {10897D0D-4898-4A4D-8D1E-B2435E93D9A1} = {490DAED7-DAD8-459A-A20E-F57F2F6F619E} - {DB6C020D-1C93-4456-8FB5-EF7CF505DF7B} = {490DAED7-DAD8-459A-A20E-F57F2F6F619E} - {BF4F963A-DBCE-4C53-A209-502F4CAF12C5} = {CF1ABDEA-794F-4474-858D-BCB61F367D72} {6374F645-5D17-494E-9529-7F83426900B3} = {490DAED7-DAD8-459A-A20E-F57F2F6F619E} {4F2498A9-D60D-4D49-95B9-BC78EE2917B5} = {490DAED7-DAD8-459A-A20E-F57F2F6F619E} {50D6BEE5-54B5-43F3-BA92-6107AB6E311E} = {CF1ABDEA-794F-4474-858D-BCB61F367D72} diff --git a/samples/AspNetCoreSample/AspNetCoreSample.csproj b/samples/AspNetCoreSample/AspNetCoreSample.csproj index bd638c65..a190c614 100644 --- a/samples/AspNetCoreSample/AspNetCoreSample.csproj +++ b/samples/AspNetCoreSample/AspNetCoreSample.csproj @@ -1,7 +1,7 @@ - net5.0 + net6.0 Etherna.MongODM.AspNetCoreSample Etherna Sagl diff --git a/samples/AspNetCoreSample/Models/ModelMaps/CatMap.cs b/samples/AspNetCoreSample/Models/ModelMaps/CatMap.cs index 9f38fcbd..9af473be 100644 --- a/samples/AspNetCoreSample/Models/ModelMaps/CatMap.cs +++ b/samples/AspNetCoreSample/Models/ModelMaps/CatMap.cs @@ -21,7 +21,7 @@ class CatMap : IModelMapsCollector { public void Register(IDbContext dbContext) { - dbContext.SchemaRegister.AddModelMapsSchema("cd37bafa-a36d-4b1f-815a-deb50c49d030"); + dbContext.SchemaRegistry.AddModelMapsSchema("cd37bafa-a36d-4b1f-815a-deb50c49d030"); } } } diff --git a/samples/AspNetCoreSample/Models/ModelMaps/ModelBaseMap.cs b/samples/AspNetCoreSample/Models/ModelMaps/ModelBaseMap.cs index 44ab0a22..231f738e 100644 --- a/samples/AspNetCoreSample/Models/ModelMaps/ModelBaseMap.cs +++ b/samples/AspNetCoreSample/Models/ModelMaps/ModelBaseMap.cs @@ -12,11 +12,11 @@ // See the License for the specific language governing permissions and // limitations under the License. +using Etherna.MongoDB.Bson; +using Etherna.MongoDB.Bson.Serialization.IdGenerators; +using Etherna.MongoDB.Bson.Serialization.Serializers; using Etherna.MongODM.Core; using Etherna.MongODM.Core.Serialization; -using MongoDB.Bson; -using MongoDB.Bson.Serialization.IdGenerators; -using MongoDB.Bson.Serialization.Serializers; namespace Etherna.MongODM.AspNetCoreSample.Models.ModelMaps { @@ -24,9 +24,9 @@ class ModelBaseMap : IModelMapsCollector { public void Register(IDbContext dbContext) { - dbContext.SchemaRegister.AddModelMapsSchema("1252861f-82d9-4c72-975e-3571d5e1b6e6"); + dbContext.SchemaRegistry.AddModelMapsSchema("1252861f-82d9-4c72-975e-3571d5e1b6e6"); - dbContext.SchemaRegister.AddModelMapsSchema>("81dd8b35-a0af-44d9-80b4-ab7ae9844eb5", modelMap => + dbContext.SchemaRegistry.AddModelMapsSchema>("81dd8b35-a0af-44d9-80b4-ab7ae9844eb5", modelMap => { modelMap.AutoMap(); diff --git a/samples/AspNetCoreSample/Pages/Index.cshtml.cs b/samples/AspNetCoreSample/Pages/Index.cshtml.cs index 8668856c..ee9191fa 100644 --- a/samples/AspNetCoreSample/Pages/Index.cshtml.cs +++ b/samples/AspNetCoreSample/Pages/Index.cshtml.cs @@ -12,11 +12,11 @@ // See the License for the specific language governing permissions and // limitations under the License. +using Etherna.MongoDB.Driver; using Etherna.MongODM.AspNetCoreSample.Models; using Etherna.MongODM.AspNetCoreSample.Persistence; using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc.RazorPages; -using MongoDB.Driver; using System; using System.Collections.Generic; using System.ComponentModel.DataAnnotations; diff --git a/samples/AspNetCoreSample/Persistence/SampleDbContext.cs b/samples/AspNetCoreSample/Persistence/SampleDbContext.cs index 00b47524..36fa463b 100644 --- a/samples/AspNetCoreSample/Persistence/SampleDbContext.cs +++ b/samples/AspNetCoreSample/Persistence/SampleDbContext.cs @@ -15,21 +15,15 @@ using Etherna.MongODM.AspNetCoreSample.Models; using Etherna.MongODM.AspNetCoreSample.Models.ModelMaps; using Etherna.MongODM.Core; -using Etherna.MongODM.Core.Options; using Etherna.MongODM.Core.Repositories; using Etherna.MongODM.Core.Serialization; using System.Collections.Generic; +using System.Threading.Tasks; namespace Etherna.MongODM.AspNetCoreSample.Persistence { public class SampleDbContext : DbContext, ISampleDbContext { - public SampleDbContext( - IDbDependencies dependencies, - DbContextOptions options) - : base(dependencies, options) - { } - public ICollectionRepository Cats { get; } = new CollectionRepository("cats"); protected override IEnumerable ModelMapsCollectors => @@ -38,5 +32,12 @@ public SampleDbContext( new ModelBaseMap(), new CatMap() }; + + protected override Task SeedAsync() + { + // Seed here. + + return base.SeedAsync(); + } } } diff --git a/samples/AspNetCoreSample/Properties/launchSettings.json b/samples/AspNetCoreSample/Properties/launchSettings.json index e79b8d8e..34683463 100644 --- a/samples/AspNetCoreSample/Properties/launchSettings.json +++ b/samples/AspNetCoreSample/Properties/launchSettings.json @@ -3,7 +3,7 @@ "windowsAuthentication": false, "anonymousAuthentication": true, "iisExpress": { - "applicationUrl": "http://localhost:57677", + "applicationUrl": "http://localhost:47677", "sslPort": 44384 } }, diff --git a/samples/AspNetCoreSample/Startup.cs b/samples/AspNetCoreSample/Startup.cs index 8a34c1cd..54282c0f 100644 --- a/samples/AspNetCoreSample/Startup.cs +++ b/samples/AspNetCoreSample/Startup.cs @@ -12,7 +12,7 @@ // See the License for the specific language governing permissions and // limitations under the License. -using Etherna.MongODM.AspNetCoreSample.Models; +using Etherna.MongODM.AspNetCore.UI; using Etherna.MongODM.AspNetCoreSample.Persistence; using Hangfire; using Microsoft.AspNetCore.Builder; @@ -35,7 +35,9 @@ public void ConfigureServices(IServiceCollection services) { services.AddRazorPages(); - services.AddMongODMWithHangfire() + services.AddHangfireServer(); + + services.AddMongODMWithHangfire() .AddDbContext(); services.AddMongODMAdminDashboard(); @@ -52,13 +54,14 @@ public void Configure(IApplicationBuilder app) app.UseAuthorization(); - app.UseHangfireServer(); app.UseHangfireDashboard(); app.UseEndpoints(endpoints => { endpoints.MapRazorPages(); }); + + app.SeedDbContexts(); } } } diff --git a/src/ExecutionContext/AsyncLocal/AsyncLocalContext.cs b/src/ExecutionContext/AsyncLocal/AsyncLocalContext.cs deleted file mode 100644 index 38b59568..00000000 --- a/src/ExecutionContext/AsyncLocal/AsyncLocalContext.cs +++ /dev/null @@ -1,56 +0,0 @@ -// 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; -using System.Threading; - -namespace Etherna.ExecContext.AsyncLocal -{ - /// - /// Async local context implementation. This can be used as singleton or with multiple instances. - /// The container permits to have an Item instance inside this - /// method calling tree. - /// - /// - /// Before try to use an async local context, call the method - /// for initialize the container, and receive a context handler. - /// After have used, dispose the handler for destroy the current context dictionary. - /// - public class AsyncLocalContext : IAsyncLocalContext, IHandledAsyncLocalContext - { - // Fields. - private static readonly AsyncLocal?> asyncLocalContext = new(); - - // Properties. - public IDictionary? Items => asyncLocalContext.Value; - - // Static properties. - public static IAsyncLocalContext Instance { get; } = new AsyncLocalContext(); - - // Methods. - public IAsyncLocalContextHandler InitAsyncLocalContext() - { - if (asyncLocalContext.Value != null) - throw new InvalidOperationException("Only one context at time is supported"); - - asyncLocalContext.Value = new Dictionary(); - - return new AsyncLocalContextHandler(this); - } - - public void OnDisposed(IAsyncLocalContextHandler context) => - asyncLocalContext.Value = null; - } -} diff --git a/src/ExecutionContext/AsyncLocal/AsyncLocalContextHandler.cs b/src/ExecutionContext/AsyncLocal/AsyncLocalContextHandler.cs deleted file mode 100644 index 4f9325e8..00000000 --- a/src/ExecutionContext/AsyncLocal/AsyncLocalContextHandler.cs +++ /dev/null @@ -1,35 +0,0 @@ -// 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.ExecContext.AsyncLocal -{ - /// - /// The handler for an initialization. - /// Dispose this for release the context. - /// - public sealed class AsyncLocalContextHandler : IAsyncLocalContextHandler - { - // Constructors. - internal AsyncLocalContextHandler(IHandledAsyncLocalContext handledContext) - { - HandledContext = handledContext; - } - - // Properties. - internal IHandledAsyncLocalContext HandledContext { get; } - - // Methods. - public void Dispose() => HandledContext.OnDisposed(this); - } -} diff --git a/src/ExecutionContext/AsyncLocal/IAsyncLocalContext.cs b/src/ExecutionContext/AsyncLocal/IAsyncLocalContext.cs deleted file mode 100644 index 1695d3ec..00000000 --- a/src/ExecutionContext/AsyncLocal/IAsyncLocalContext.cs +++ /dev/null @@ -1,32 +0,0 @@ -// 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.ExecContext.AsyncLocal -{ - /// - /// The interface. - /// Permits to create an async local context living with the method calling tree. - /// - public interface IAsyncLocalContext : IExecutionContext - { - /// - /// Initialize a new async local context - /// - /// The new context handler - /// Throw when another local context is found - IAsyncLocalContextHandler InitAsyncLocalContext(); - } -} \ No newline at end of file diff --git a/src/ExecutionContext/ExecutionContext.csproj b/src/ExecutionContext/ExecutionContext.csproj deleted file mode 100644 index 930dd300..00000000 --- a/src/ExecutionContext/ExecutionContext.csproj +++ /dev/null @@ -1,42 +0,0 @@ - - - - netstandard2.0 - true - Etherna.ExecContext - Etherna Sagl - Execution context provider - 9.0 - enable - true - AllEnabledByDefault - - https://github.com/Etherna/mongodm - git - true - true - snupkg - LICENSE - true - true - - - - - all - runtime; build; native; contentfiles; analyzers; buildtransitive - - - all - runtime; build; native; contentfiles; analyzers; buildtransitive - - - - - - True - - - - - diff --git a/src/ExecutionContext/ExecutionContextSelector.cs b/src/ExecutionContext/ExecutionContextSelector.cs deleted file mode 100644 index a055b954..00000000 --- a/src/ExecutionContext/ExecutionContextSelector.cs +++ /dev/null @@ -1,54 +0,0 @@ -// 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.ExecContext -{ - /// - /// A multi context selector that take different contexts, and select the first available. - /// - /// - /// This class is intended to have the same lifetime of it's consumer. For example, in case - /// of using with a DbContext, the same DbContext instance will use the same ContextSelector - /// instance. This mean that if a DbContext is running over different execution contexts, - /// every invoke on same context needs to return the same dictionary. - /// The simplest way to perform this, is to return the first not null available dictionary - /// on subscribed contexts. - /// - public class ExecutionContextSelector : IExecutionContext - { - // Fields. - private readonly IEnumerable contexts; - - // Constructors. - public ExecutionContextSelector(IEnumerable contexts) - { - this.contexts = contexts ?? throw new ArgumentNullException(nameof(contexts)); - } - - // Proeprties. - public IDictionary? Items - { - get - { - foreach (var context in contexts) - if (context.Items != null) - return context.Items; - return null; - } - } - } -} diff --git a/src/ExecutionContext/GlobalSuppressions.cs b/src/ExecutionContext/GlobalSuppressions.cs deleted file mode 100644 index 0e5488c9..00000000 --- a/src/ExecutionContext/GlobalSuppressions.cs +++ /dev/null @@ -1,22 +0,0 @@ -// 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. - -// This file is used by Code Analysis to maintain SuppressMessage -// attributes that are applied to this project. -// Project-level suppressions either have no target or are given -// a specific target and scoped to a namespace, type, member, etc. - -using System.Diagnostics.CodeAnalysis; - -[assembly: SuppressMessage("Globalization", "CA1303:Do not pass literals as localized parameters", Justification = "This library will be not localized", Scope = "NamespaceAndDescendants", Target = "~N:Etherna.ExecContext")] diff --git a/src/MongODM.AspNetCore.UI/Areas/MongODM/Pages/Index.cshtml.cs b/src/MongODM.AspNetCore.UI/Areas/MongODM/Pages/Index.cshtml.cs index e3c93c18..e6d9aa8b 100644 --- a/src/MongODM.AspNetCore.UI/Areas/MongODM/Pages/Index.cshtml.cs +++ b/src/MongODM.AspNetCore.UI/Areas/MongODM/Pages/Index.cshtml.cs @@ -13,8 +13,10 @@ // limitations under the License. using Etherna.MongODM.Core; +using Etherna.MongODM.Core.Options; using Microsoft.AspNetCore.Mvc.RazorPages; using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Options; using System; using System.Collections.Generic; using System.Linq; @@ -25,16 +27,19 @@ namespace Etherna.MongODM.AspNetCore.UI.Areas.MongODM.Pages public class IndexModel : PageModel { // Fields. - private readonly IMongODMConfiguration configuration; + private readonly MongODMOptions options; private readonly IServiceProvider serviceProvider; // Constructor. public IndexModel( - IMongODMConfiguration configuration, + IOptions options, IServiceProvider serviceProvider) { - this.configuration = configuration ?? throw new ArgumentNullException(nameof(configuration)); - this.serviceProvider = serviceProvider ?? throw new ArgumentNullException(nameof(serviceProvider)); + if (options is null) + throw new ArgumentNullException(nameof(options)); + + this.options = options.Value; + this.serviceProvider = serviceProvider; } // Properties. @@ -61,7 +66,7 @@ public async Task OnPostAsync(string identifier) private void InitializePage() { // Get dbcontext instances. - var dbContextTypes = configuration.DbContextTypes; + var dbContextTypes = options.DbContextTypes; DbContexts = dbContextTypes.Select(type => (IDbContext)serviceProvider.GetRequiredService(type)); } } diff --git a/src/MongODM.AspNetCore.UI/MongODM.AspNetCore.UI.csproj b/src/MongODM.AspNetCore.UI/MongODM.AspNetCore.UI.csproj index 2289e5f8..c5b3929b 100644 --- a/src/MongODM.AspNetCore.UI/MongODM.AspNetCore.UI.csproj +++ b/src/MongODM.AspNetCore.UI/MongODM.AspNetCore.UI.csproj @@ -1,8 +1,7 @@  - netcoreapp2.1;netcoreapp3.1 - + netcoreapp3.1;net6.0 true true Etherna.MongODM.AspNetCore.UI @@ -21,6 +20,7 @@ LICENSE true true + True @@ -32,20 +32,16 @@ - + all runtime; build; native; contentfiles; analyzers; buildtransitive - + all runtime; build; native; contentfiles; analyzers; buildtransitive - - - - True @@ -53,4 +49,8 @@ + + + + diff --git a/src/MongODM.AspNetCore.UI/Properties/AssemblyInfo.cs b/src/MongODM.AspNetCore.UI/Properties/AssemblyInfo.cs index 83cc064e..dd2b8d00 100644 --- a/src/MongODM.AspNetCore.UI/Properties/AssemblyInfo.cs +++ b/src/MongODM.AspNetCore.UI/Properties/AssemblyInfo.cs @@ -1,3 +1,17 @@ -using System; +// 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; [assembly: CLSCompliant(false)] \ No newline at end of file diff --git a/src/MongODM.AspNetCore.UI/ServiceCollectionExtensions.cs b/src/MongODM.AspNetCore.UI/ServiceCollectionExtensions.cs index ff954a57..74545bfe 100644 --- a/src/MongODM.AspNetCore.UI/ServiceCollectionExtensions.cs +++ b/src/MongODM.AspNetCore.UI/ServiceCollectionExtensions.cs @@ -12,14 +12,14 @@ // See the License for the specific language governing permissions and // limitations under the License. -using Etherna.MongODM.AspNetCore.UI; using Etherna.MongODM.AspNetCore.UI.Auth.Handlers; using Etherna.MongODM.AspNetCore.UI.Auth.Requirements; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc.RazorPages; +using Microsoft.Extensions.DependencyInjection; using System; -namespace Microsoft.Extensions.DependencyInjection +namespace Etherna.MongODM.AspNetCore.UI { public static class ServiceCollectionExtensions { @@ -44,15 +44,14 @@ public static IServiceCollection AddMongODMAdminDashboard( routeModel => { foreach (var selector in routeModel.Selectors) - { - var attributeRouteModel = selector.AttributeRouteModel; + if (selector.AttributeRouteModel?.Template is not null) + { + var segments = selector.AttributeRouteModel.Template.Split('/'); + if (segments[0] == AreaName) + segments[0] = dashboardOptions.BasePath; - var segments = selector.AttributeRouteModel.Template.Split('/'); - if (segments[0] == AreaName) - segments[0] = dashboardOptions.BasePath; - - selector.AttributeRouteModel.Template = string.Join("/", segments); - } + selector.AttributeRouteModel.Template = string.Join("/", segments); + } }); }); diff --git a/src/MongODM.AspNetCore/AspNetCoreMongODMConfiguration.cs b/src/MongODM.AspNetCore/AspNetCoreMongODMConfiguration.cs deleted file mode 100644 index f4618de9..00000000 --- a/src/MongODM.AspNetCore/AspNetCoreMongODMConfiguration.cs +++ /dev/null @@ -1,45 +0,0 @@ -// 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.Core; -using Microsoft.Extensions.DependencyInjection; -using System; - -namespace Etherna.MongODM.AspNetCore -{ - public class AspNetCoreMongODMConfiguration : MongODMConfiguration - { - // Fields. - private readonly IServiceCollection services; - - // Constructor. - public AspNetCoreMongODMConfiguration(IServiceCollection services) - { - this.services = services; - } - - // Protected methods. - protected override void RegisterSingleton() => - services.AddSingleton(); - - protected override void RegisterSingleton(TService instance) => - services.AddSingleton(instance); - - protected override void RegisterSingleton() => - services.AddSingleton(); - - protected override void RegisterSingleton(Func implementationFactory) => - services.AddSingleton(implementationFactory); - } -} diff --git a/src/MongODM.Core/Utility/DbDependencies.cs b/src/MongODM.AspNetCore/DbDependencies.cs similarity index 55% rename from src/MongODM.Core/Utility/DbDependencies.cs rename to src/MongODM.AspNetCore/DbDependencies.cs index ff8e3af5..fc416ea7 100644 --- a/src/MongODM.Core/Utility/DbDependencies.cs +++ b/src/MongODM.AspNetCore/DbDependencies.cs @@ -12,44 +12,60 @@ // See the License for the specific language governing permissions and // limitations under the License. +using Etherna.ExecContext; +using Etherna.MongoDB.Bson.Serialization; +using Etherna.MongODM.Core; +using Etherna.MongODM.Core.Options; using Etherna.MongODM.Core.ProxyModels; using Etherna.MongODM.Core.Repositories; using Etherna.MongODM.Core.Serialization.Mapping; using Etherna.MongODM.Core.Serialization.Modifiers; +using Etherna.MongODM.Core.Utility; +using Microsoft.Extensions.Options; +using System; -namespace Etherna.MongODM.Core.Utility +namespace Etherna.MongODM.AspNetCore { public class DbDependencies : IDbDependencies { public DbDependencies( + IBsonSerializerRegistry bsonSerializerRegistry, IDbCache dbCache, IDbMaintainer dbMaintainer, IDbMigrationManager dbContextMigrationManager, + IDiscriminatorRegistry discriminatorRegistry, + IExecutionContext executionContext, + IOptions mongODMOptions, IProxyGenerator proxyGenerator, - IRepositoryRegister repositoryRegister, - ISchemaRegister schemaRegister, - ISerializerModifierAccessor serializerModifierAccessor, -#pragma warning disable IDE0060 // Remove unused parameter. It's needed for run static configurations -#pragma warning disable CA1801 // Review unused parameters - IStaticConfigurationBuilder staticConfigurationBuilder) -#pragma warning restore CA1801 // Review unused parameters -#pragma warning restore IDE0060 // Remove unused parameter + IRepositoryRegistry repositoryRegistry, + ISchemaRegistry schemaRegistry, + ISerializerModifierAccessor serializerModifierAccessor) { + if (mongODMOptions is null) + throw new ArgumentNullException(nameof(mongODMOptions)); + BsonSerializerRegistry = bsonSerializerRegistry; DbCache = dbCache; DbMaintainer = dbMaintainer; DbMigrationManager = dbContextMigrationManager; - SchemaRegister = schemaRegister; + DiscriminatorRegistry = discriminatorRegistry; + ExecutionContext = executionContext; + MongODMOptions = mongODMOptions.Value; + SchemaRegistry = schemaRegistry; ProxyGenerator = proxyGenerator; - RepositoryRegister = repositoryRegister; + RepositoryRegistry = repositoryRegistry; SerializerModifierAccessor = serializerModifierAccessor; } + public IBsonSerializerRegistry BsonSerializerRegistry { get; } public IDbCache DbCache { get; } public IDbMaintainer DbMaintainer { get; } public IDbMigrationManager DbMigrationManager { get; } + public IDiscriminatorRegistry DiscriminatorRegistry { get; } + public IExecutionContext ExecutionContext { get; } + public MongODMOptions MongODMOptions { get; } public IProxyGenerator ProxyGenerator { get; } - public IRepositoryRegister RepositoryRegister { get; } - public ISchemaRegister SchemaRegister { get; } + public IRepositoryRegistry RepositoryRegistry { get; } + public ISchemaRegistry SchemaRegistry { get; } public ISerializerModifierAccessor SerializerModifierAccessor { get; } } } diff --git a/src/MongODM.AspNetCore/Extensions/ApplicationBuilderExtensions.cs b/src/MongODM.AspNetCore/Extensions/ApplicationBuilderExtensions.cs new file mode 100644 index 00000000..1eb2214f --- /dev/null +++ b/src/MongODM.AspNetCore/Extensions/ApplicationBuilderExtensions.cs @@ -0,0 +1,57 @@ +// 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.ExecContext.AsyncLocal; +using Etherna.MongODM.Core; +using Etherna.MongODM.Core.Options; +using Microsoft.AspNetCore.Builder; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Options; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; + +namespace Etherna.MongODM +{ + public static class ApplicationBuilderExtensions + { + public static IApplicationBuilder SeedDbContexts( + this IApplicationBuilder builder) + { + if (builder is null) + throw new ArgumentNullException(nameof(builder)); + + var serviceProvider = builder.ApplicationServices; + var mongODMOptions = serviceProvider.GetRequiredService>(); + + // Get dbcontext instances. + var dbContextTypes = mongODMOptions.Value.DbContextTypes; + var dbContexts = dbContextTypes.Select(type => (IDbContext)serviceProvider.GetRequiredService(type)); + + // Create an execution context. + using var execContext = AsyncLocalContext.Instance.InitAsyncLocalContext(); + + // Seed all dbcontexts. + var tasks = new List(); + foreach (var dbContext in dbContexts) + if (!dbContext.IsSeeded) + tasks.Add(dbContext.SeedIfNeededAsync()); + + Task.WaitAll(tasks.ToArray()); + + return builder; + } + } +} diff --git a/src/MongODM.AspNetCore/Extensions/ServiceCollectionExtensions.cs b/src/MongODM.AspNetCore/Extensions/ServiceCollectionExtensions.cs new file mode 100644 index 00000000..cc42f8cf --- /dev/null +++ b/src/MongODM.AspNetCore/Extensions/ServiceCollectionExtensions.cs @@ -0,0 +1,119 @@ +// 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.ExecContext; +using Etherna.ExecContext.AspNetCore; +using Etherna.MongoDB.Bson; +using Etherna.MongoDB.Bson.Serialization; +using Etherna.MongoDB.Bson.Serialization.Conventions; +using Etherna.MongODM.AspNetCore; +using Etherna.MongODM.Core; +using Etherna.MongODM.Core.Conventions; +using Etherna.MongODM.Core.Options; +using Etherna.MongODM.Core.ProxyModels; +using Etherna.MongODM.Core.Repositories; +using Etherna.MongODM.Core.Serialization.Mapping; +using Etherna.MongODM.Core.Serialization.Modifiers; +using Etherna.MongODM.Core.Tasks; +using Etherna.MongODM.Core.Utility; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.DependencyInjection.Extensions; +using System; + +namespace Etherna.MongODM +{ + public static class ServiceCollectionExtensions + { + public static IMongODMConfiguration AddMongODM( + this IServiceCollection services, + Action? configureOptions = null) + where TTaskRunner : class, ITaskRunner, ITaskRunnerBuilder => + AddMongODM(services, configureOptions); + + public static IMongODMConfiguration AddMongODM( + this IServiceCollection services, + Action? configureOptions = null) + where TProxyGenerator : class, IProxyGenerator + where TTaskRunner : class, ITaskRunner, ITaskRunnerBuilder + { + // MongODM generic configuration. + var configuration = new MongODMConfiguration(services); + + services.AddOptions() + .Configure(configureOptions ?? (_ => { })) + .PostConfigure( + (options, proxyGenerator, taskRunnerBuilder) => + { + // Register global conventions. + ConventionRegistry.Register("Enum string", new ConventionPack + { + new EnumRepresentationConvention(BsonType.String) + }, c => true); + + // Freeze configuration into mongodm options. + configuration.Freeze(options); + + // Link options to services. + taskRunnerBuilder.SetMongODMOptions(options); + }); + + services.AddExecutionContext(); + + services.TryAddSingleton(); + services.TryAddSingleton(); + services.TryAddSingleton(sp => (TTaskRunner)sp.GetRequiredService()); + + /* Register discriminator convention on typeof(object) because we need a method to handle + * default returned instance from static calls to BsonSerializer.LookupDiscriminatorConvention(Type). + * Several points internal to drivers invoke this method, and we can't avoid it. We need to set the default. + */ + var sp = services.BuildServiceProvider(); + var execContext = sp.GetRequiredService(); + BsonSerializer.RegisterDiscriminatorConvention(typeof(object), + new HierarchicalProxyTolerantDiscriminatorConvention("_t", execContext)); + + /* For same motive of handle static calls to BsonSerializer.LookupSerializer(Type), + * we need a way to inject a current context accessor. This is a modification on official drivers, + * waiting an official implementation of serialization contexts. + */ + BsonSerializer.SetSerializationContextAccessor(new SerializationContextAccessor(execContext)); + + // DbContext internal. + //dependencies + /***** + * Transient dependencies have to be injected only into DbContext instance, + * and passed to other with Initialize() method. This because otherwise inside + * 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.TryAddTransient(); + services.TryAddSingleton(); + + //tasks + services.TryAddTransient(); + services.TryAddTransient(); + + //castle proxy generator + services.TryAddSingleton(new Castle.DynamicProxy.ProxyGenerator()); + + return configuration; + } + } +} diff --git a/src/MongODM.AspNetCore/IMongODMConfiguration.cs b/src/MongODM.AspNetCore/IMongODMConfiguration.cs new file mode 100644 index 00000000..888951be --- /dev/null +++ b/src/MongODM.AspNetCore/IMongODMConfiguration.cs @@ -0,0 +1,62 @@ +// 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.Core; +using Etherna.MongODM.Core.Options; +using System; + +namespace Etherna.MongODM.AspNetCore +{ + public interface IMongODMConfiguration + { + bool IsFrozen { get; } + + // Methods. + IMongODMConfiguration AddDbContext( + Action? dbContextOptionsConfig = null) + where TDbContext : DbContext, new(); + + IMongODMConfiguration AddDbContext( + TDbContext dbContext, + Action? dbContextOptionsConfig = null) + where TDbContext : DbContext; + + IMongODMConfiguration AddDbContext( + Func dbContextCreator, + Action? dbContextOptionsConfig = null) + where TDbContext : DbContext; + + IMongODMConfiguration AddDbContext( + Action? dbContextOptionsConfig = null) + where TDbContext : class, IDbContext + where TDbContextImpl : DbContext, TDbContext, new(); + + IMongODMConfiguration AddDbContext( + TDbContextImpl dbContext, + Action? dbContextOptionsConfig = null) + where TDbContext : class, IDbContext + where TDbContextImpl : DbContext, TDbContext; + + IMongODMConfiguration AddDbContext( + Func dbContextCreator, + Action? dbContextOptionsConfig = null) + where TDbContext : class, IDbContext + where TDbContextImpl : DbContext, TDbContext; + + /// + /// Freeze configuration. + /// + void Freeze(IMongODMOptionsBuilder mongODMOptionsBuilder); + } +} diff --git a/src/MongODM.AspNetCore/MongODM.AspNetCore.csproj b/src/MongODM.AspNetCore/MongODM.AspNetCore.csproj index 217d643f..4c93967a 100644 --- a/src/MongODM.AspNetCore/MongODM.AspNetCore.csproj +++ b/src/MongODM.AspNetCore/MongODM.AspNetCore.csproj @@ -1,7 +1,7 @@  - netcoreapp2.1;netcoreapp3.1;net5.0 + netcoreapp3.1;net6.0 true Etherna.MongODM.AspNetCore Etherna Sagl @@ -19,6 +19,7 @@ LICENSE true true + True @@ -30,11 +31,12 @@ - + + all runtime; build; native; contentfiles; analyzers; buildtransitive - + all runtime; build; native; contentfiles; analyzers; buildtransitive diff --git a/src/MongODM.AspNetCore/MongODMConfiguration.cs b/src/MongODM.AspNetCore/MongODMConfiguration.cs new file mode 100644 index 00000000..8abfd072 --- /dev/null +++ b/src/MongODM.AspNetCore/MongODMConfiguration.cs @@ -0,0 +1,170 @@ +// 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.MongoDB.Driver; +using Etherna.MongODM.Core; +using Etherna.MongODM.Core.Options; +using Microsoft.Extensions.DependencyInjection; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading; + +namespace Etherna.MongODM.AspNetCore +{ + public class MongODMConfiguration : IMongODMConfiguration, IDisposable + { + // Fields. + private readonly ReaderWriterLockSlim configLock = new(LockRecursionPolicy.SupportsRecursion); + private readonly List dbContextTypes = new(); + private bool disposed; + private readonly IServiceCollection services; + + // Constructor. + public MongODMConfiguration( + IServiceCollection services) + { + this.services = services; + } + + // Dispose. + public void Dispose() + { + Dispose(true); + GC.SuppressFinalize(this); + } + + protected virtual void Dispose(bool disposing) + { + if (disposed) return; + + // Dispose managed resources. + if (disposing) + configLock.Dispose(); + + disposed = true; + } + + // Properties. + public bool IsFrozen { get; private set; } + + // Methods. + public IMongODMConfiguration AddDbContext( + Action? dbContextOptionsConfig = null) + where TDbContext : DbContext, new() => + AddDbContext(dbContextOptionsConfig); + + public IMongODMConfiguration AddDbContext( + TDbContext dbContext, + Action? dbContextOptionsConfig = null) + where TDbContext : DbContext => + AddDbContext(_ => dbContext, dbContextOptionsConfig); + + public IMongODMConfiguration AddDbContext( + Func dbContextCreator, + Action? dbContextOptionsConfig = null) + where TDbContext : DbContext => + AddDbContext(dbContextCreator, dbContextOptionsConfig); + + public IMongODMConfiguration AddDbContext( + Action? dbContextOptionsConfig = null) + where TDbContext : class, IDbContext + where TDbContextImpl : DbContext, TDbContext, new() => + AddDbContext( + _ => Activator.CreateInstance(), + dbContextOptionsConfig); + + public IMongODMConfiguration AddDbContext( + TDbContextImpl dbContext, + Action? dbContextOptionsConfig = null) + where TDbContext : class, IDbContext + where TDbContextImpl : DbContext, TDbContext => + AddDbContext(_ => dbContext, dbContextOptionsConfig); + + public IMongODMConfiguration AddDbContext( + Func dbContextCreator, + Action? dbContextOptionsConfig) + where TDbContext : class, IDbContext + where TDbContextImpl : DbContext, TDbContext + { + configLock.EnterWriteLock(); + try + { + if (IsFrozen) + throw new InvalidOperationException("Configuration is frozen"); + + // Register dbContext. + services.AddSingleton(sp => + { + // Get dependencies. + var dependencies = sp.GetRequiredService(); + var options = new DbContextOptions(); + dbContextOptionsConfig?.Invoke(options); + + // Get dbcontext. + var dbContext = dbContextCreator(sp); + + // Initialize instance. + dbContext.Initialize( + dependencies, + new MongoClient(options.ConnectionString), + options, + options.ChildDbContextTypes.Select(dbContextType => (IDbContext)sp.GetRequiredService(dbContextType))); + + return dbContext; + }); + services.AddSingleton(sp => sp.GetRequiredService()); + + // Add db context type. + dbContextTypes.Add(typeof(TDbContext)); + + return this; + } + finally + { + configLock.ExitWriteLock(); + } + } + + public void Freeze(IMongODMOptionsBuilder mongODMOptionsBuilder) + { + if (mongODMOptionsBuilder is null) + throw new ArgumentNullException(nameof(mongODMOptionsBuilder)); + + configLock.EnterReadLock(); + try + { + if (IsFrozen) return; + } + finally + { + configLock.ExitReadLock(); + } + + configLock.EnterWriteLock(); + try + { + // Freeze. + IsFrozen = true; + + // Report configuration to options. + mongODMOptionsBuilder.SetDbContextTypes(dbContextTypes); + } + finally + { + configLock.ExitWriteLock(); + } + } + } +} diff --git a/src/MongODM.AspNetCore/ServiceCollectionExtensions.cs b/src/MongODM.AspNetCore/ServiceCollectionExtensions.cs deleted file mode 100644 index d6e01c37..00000000 --- a/src/MongODM.AspNetCore/ServiceCollectionExtensions.cs +++ /dev/null @@ -1,96 +0,0 @@ -// 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.ExecContext; -using Etherna.ExecContext.AsyncLocal; -using Etherna.MongODM.AspNetCore; -using Etherna.MongODM.Core; -using Etherna.MongODM.Core.Domain.Models; -using Etherna.MongODM.Core.Options; -using Etherna.MongODM.Core.ProxyModels; -using Etherna.MongODM.Core.Repositories; -using Etherna.MongODM.Core.Serialization.Mapping; -using Etherna.MongODM.Core.Serialization.Modifiers; -using Etherna.MongODM.Core.Tasks; -using Etherna.MongODM.Core.Utility; -using Microsoft.AspNetCore.Http; -using Microsoft.Extensions.DependencyInjection.Extensions; -using System; - -namespace Microsoft.Extensions.DependencyInjection -{ - public static class ServiceCollectionExtensions - { - public static IMongODMConfiguration AddMongODM( - this IServiceCollection services, - Action? configureOptions = null) - where TTaskRunner : class, ITaskRunner - where TModelBase : class, IModel => //needed because of this https://jira.mongodb.org/browse/CSHARP-3154 - AddMongODM(services, configureOptions); - - public static IMongODMConfiguration AddMongODM( - this IServiceCollection services, - Action? configureOptions = null) - where TProxyGenerator : class, IProxyGenerator - where TTaskRunner : class, ITaskRunner - where TModelBase : class, IModel //needed because of this https://jira.mongodb.org/browse/CSHARP-3154 - { - // MongODM generic configuration. - var configuration = new AspNetCoreMongODMConfiguration(services); - services.TryAddSingleton(configuration); - - var mongODMOptions = new MongODMOptions(); - configureOptions?.Invoke(mongODMOptions); - services.TryAddSingleton(mongODMOptions); - - services.TryAddSingleton(); - - services.TryAddSingleton(serviceProvider => - new ExecutionContextSelector(new IExecutionContext[] //default - { - new HttpContextExecutionContext(serviceProvider.GetRequiredService()), - AsyncLocalContext.Instance - })); - services.TryAddSingleton(); - services.TryAddSingleton(); - - // DbContext internal. - //dependencies - /***** - * Transient dependencies have to be injected only into DbContext instance, - * and passed to other with Initialize() method. This because otherwise inside - * 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.TryAddSingleton(); - - //tasks - services.TryAddTransient(); - services.TryAddTransient(); - - //castle proxy generator - services.TryAddSingleton(new Castle.DynamicProxy.ProxyGenerator()); - - //static configurations - services.TryAddSingleton>(); - - return configuration; - } - } -} diff --git a/src/MongODM.AspNetCore/StaticConfigurationBuilder.cs b/src/MongODM.AspNetCore/StaticConfigurationBuilder.cs deleted file mode 100644 index b008f1fe..00000000 --- a/src/MongODM.AspNetCore/StaticConfigurationBuilder.cs +++ /dev/null @@ -1,41 +0,0 @@ -// 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.Core.Conventions; -using Etherna.MongODM.Core.Domain.Models; -using Etherna.MongODM.Core.ProxyModels; -using Etherna.MongODM.Core.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 index 20f51272..bceb8320 100644 --- a/src/MongODM.Core/Conventions/HierarchicalProxyTolerantDiscriminatorConvention.cs +++ b/src/MongODM.Core/Conventions/HierarchicalProxyTolerantDiscriminatorConvention.cs @@ -12,29 +12,141 @@ // See the License for the specific language governing permissions and // limitations under the License. -using Etherna.MongODM.Core.ProxyModels; -using MongoDB.Bson; -using MongoDB.Bson.Serialization.Conventions; +using Etherna.ExecContext; +using Etherna.MongoDB.Bson; +using Etherna.MongoDB.Bson.IO; +using Etherna.MongoDB.Bson.Serialization; +using Etherna.MongoDB.Bson.Serialization.Conventions; +using Etherna.MongoDB.Bson.Serialization.Serializers; +using Etherna.MongODM.Core.Utility; using System; +using System.Collections.Generic; +using System.Linq; namespace Etherna.MongODM.Core.Conventions { - public class HierarchicalProxyTolerantDiscriminatorConvention : HierarchicalDiscriminatorConvention + public class HierarchicalProxyTolerantDiscriminatorConvention : IDiscriminatorConvention { // Fields. - private readonly IProxyGenerator proxyGenerator; + private readonly IDbContext? _dbContext; //remove nullability with constructors that don't ask it, when will be possible + private readonly IExecutionContext? executionContext; // Constructors. + public HierarchicalProxyTolerantDiscriminatorConvention( + IDbContext dbContext, + string elementName) + { + _dbContext = dbContext; + + ElementName = elementName ?? throw new ArgumentNullException(nameof(elementName)); + if (elementName.IndexOf('\0') != -1) + throw new ArgumentException("Element names cannot contain nulls.", nameof(elementName)); + } + + /// + /// Only needed for static registration on , used when dbcontext is not available. + /// Remove when static call will be removed. + /// + /// Discriminator element name + /// Execution context public HierarchicalProxyTolerantDiscriminatorConvention( string elementName, - IProxyGenerator proxyGenerator) - : base(elementName) + IExecutionContext executionContext) { - this.proxyGenerator = proxyGenerator ?? throw new ArgumentNullException(nameof(proxyGenerator)); + ElementName = elementName ?? throw new ArgumentNullException(nameof(elementName)); + if (elementName.IndexOf('\0') != -1) + throw new ArgumentException("Element names cannot contain nulls.", nameof(elementName)); + + this.executionContext = executionContext; + } + + public IDbContext DbContext + { + get + { + if (_dbContext is not null) + return _dbContext; + + /* If we didn't injected a dbContext, this is an instance retrieved from a static invoke. + * Try to find it from execution contenxt. */ + if (executionContext is null) + throw new InvalidOperationException(); + + var dbContext = DbExecutionContextHandler.TryGetCurrentDbContext(executionContext); + if (dbContext is null) + throw new InvalidOperationException(); + + return dbContext; + } } + public string ElementName { get; } // Methods. - public override BsonValue GetDiscriminator(Type nominalType, Type actualType) => - base.GetDiscriminator(nominalType, proxyGenerator.PurgeProxyType(actualType)); + public Type GetActualType(IBsonReader bsonReader, Type nominalType) + { + if (bsonReader is null) + throw new ArgumentNullException(nameof(bsonReader)); + + //the BsonReader is sitting at the value whose actual type needs to be found + var bsonType = bsonReader.GetCurrentBsonType(); + if (bsonType == BsonType.Document) + { + //we can skip looking for a discriminator if nominalType has no discriminated sub types + if (DbContext.DiscriminatorRegistry.IsTypeDiscriminated(nominalType)) + { + var bookmark = bsonReader.GetBookmark(); + bsonReader.ReadStartDocument(); + var actualType = nominalType; + if (bsonReader.FindElement(ElementName)) + { + var context = BsonDeserializationContext.CreateRoot(bsonReader); + var discriminator = BsonValueSerializer.Instance.Deserialize(context); + if (discriminator.IsBsonArray) + { + discriminator = discriminator.AsBsonArray.Last(); //last item is leaf class discriminator + } + actualType = DbContext.DiscriminatorRegistry.LookupActualType(nominalType, discriminator); + } + bsonReader.ReturnToBookmark(bookmark); + return actualType; + } + } + + return nominalType; + } + + /// + /// Gets the discriminator value for an actual type. + /// + /// The nominal type. + /// The actual type. + /// The discriminator value. + public BsonValue? GetDiscriminator(Type nominalType, Type actualType) + { + // Remove proxy type. + actualType = DbContext.ProxyGenerator.PurgeProxyType(actualType); + + // Find active class map for model type. + var classMap = DbContext.SchemaRegistry.GetActiveClassMap(actualType); + + // Get discriminator from class map. + if (actualType != nominalType || classMap.DiscriminatorIsRequired || classMap.HasRootClass) + { + if (classMap.HasRootClass && !classMap.IsRootClass) + { + var values = new List(); + for (; !classMap.IsRootClass; classMap = classMap.BaseClassMap) + { + values.Add(classMap.Discriminator); + } + values.Add(classMap.Discriminator); //add the root class's discriminator + return new BsonArray(values.Reverse()); //reverse to put leaf class last + } + else + return classMap.Discriminator; + } + + return null; + } } } diff --git a/src/MongODM.Core/DbContext.cs b/src/MongODM.Core/DbContext.cs index a4f949f1..50eaebdd 100644 --- a/src/MongODM.Core/DbContext.cs +++ b/src/MongODM.Core/DbContext.cs @@ -12,8 +12,13 @@ // See the License for the specific language governing permissions and // limitations under the License. +using Etherna.ExecContext; +using Etherna.MongoDB.Bson.Serialization; +using Etherna.MongoDB.Driver; +using Etherna.MongoDB.Driver.Linq; using Etherna.MongODM.Core.Domain.ModelMaps; using Etherna.MongODM.Core.Domain.Models; +using Etherna.MongODM.Core.Exceptions; using Etherna.MongODM.Core.Migration; using Etherna.MongODM.Core.Options; using Etherna.MongODM.Core.ProxyModels; @@ -21,9 +26,8 @@ using Etherna.MongODM.Core.Serialization; using Etherna.MongODM.Core.Serialization.Mapping; using Etherna.MongODM.Core.Serialization.Modifiers; +using Etherna.MongODM.Core.Serialization.Providers; using Etherna.MongODM.Core.Utility; -using MongoDB.Driver; -using MongoDB.Driver.Linq; using System; using System.Collections.Generic; using System.Linq; @@ -33,48 +37,66 @@ namespace Etherna.MongODM.Core { - public abstract class DbContext : IDbContext + public abstract class DbContext : IDbContext, IDbContextBuilder { - // Constructors and initialization. - protected DbContext( + // Fields. + private bool? _isSeeded; + private BsonSerializerRegistry _serializerRegistry = default!; + private IEnumerable childDbContexts = default!; + private bool isInitialized; + private readonly ReaderWriterLockSlim isSeededLock = new(); //support read/write locks + private readonly SemaphoreSlim seedingSemaphore = new(1, 1); //support async/await + + // Constructor and initializer. + protected DbContext() { } + public void Initialize( IDbDependencies dependencies, - DbContextOptions options) + IMongoClient mongoClient, + IDbContextOptions options, + IEnumerable childDbContexts) { + if (isInitialized) + throw new InvalidOperationException("DbContext already initialized"); if (dependencies is null) throw new ArgumentNullException(nameof(dependencies)); if (options is null) throw new ArgumentNullException(nameof(options)); + // Set dependencies. + this.childDbContexts = childDbContexts; DbCache = dependencies.DbCache; DbMaintainer = dependencies.DbMaintainer; DbMigrationManager = dependencies.DbMigrationManager; DbOperations = new CollectionRepository(options.DbOperationsCollectionName); - DocumentSemVerOptions = options.DocumentSemVer; - Identifier = options.Identifier ?? GetType().Name; + DiscriminatorRegistry = dependencies.DiscriminatorRegistry; + ExecutionContext = dependencies.ExecutionContext; LibraryVersion = typeof(DbContext) .GetTypeInfo() .Assembly .GetCustomAttribute() ?.InformationalVersion ?.Split('+')[0] ?? "1.0.0"; - ModelMapVersionOptions = options.ModelMapVersion; + Options = options; ProxyGenerator = dependencies.ProxyGenerator; - RepositoryRegister = dependencies.RepositoryRegister; - SchemaRegister = dependencies.SchemaRegister; + RepositoryRegistry = dependencies.RepositoryRegistry; + SchemaRegistry = dependencies.SchemaRegistry; SerializerModifierAccessor = dependencies.SerializerModifierAccessor; + _serializerRegistry = (BsonSerializerRegistry)dependencies.BsonSerializerRegistry; - // Initialize MongoDB driver. - Client = new MongoClient(options.ConnectionString); - Database = Client.GetDatabase(options.DbName); + // Execute initialization into execution context. + using var dbExecutionContext = new DbExecutionContextHandler(this); // Initialize internal dependencies. + DbCache.Initialize(this); DbMaintainer.Initialize(this); DbMigrationManager.Initialize(this); - RepositoryRegister.Initialize(this); - SchemaRegister.Initialize(this); + DiscriminatorRegistry.Initialize(this); + RepositoryRegistry.Initialize(this); + SchemaRegistry.Initialize(this); + InitializeSerializerRegistry(); // Initialize repositories. - foreach (var repository in RepositoryRegister.ModelRepositoryMap.Values) + foreach (var repository in RepositoryRegistry.RepositoriesByModelType.Values) repository.Initialize(this); // Register model maps. @@ -88,8 +110,18 @@ protected DbContext( foreach (var maps in ModelMapsCollectors) maps.Register(this); - // Build and freeze schemas register. - SchemaRegister.Freeze(); + // Build and freeze schema registry. + SchemaRegistry.Freeze(); + + // Initialize MongoDB database. + Client = mongoClient; + Database = Client.GetDatabase(options.DbName, new MongoDatabaseSettings + { + SerializerRegistry = _serializerRegistry + }); + + // Set as initialized. + isInitialized = true; } // Public properties. @@ -97,21 +129,72 @@ protected DbContext( 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 IDbMigrationManager DbMigrationManager { get; } - public ICollectionRepository DbOperations { get; } + public IMongoClient Client { get; private set; } = default!; + public IMongoDatabase Database { get; private set; } = default!; + public IDbCache DbCache { get; private set; } = default!; + public IDbMaintainer DbMaintainer { get; private set; } = default!; + public IDbMigrationManager DbMigrationManager { get; private set; } = default!; + public ICollectionRepository DbOperations { get; private set; } = default!; + public IDiscriminatorRegistry DiscriminatorRegistry { get; private set; } = default!; public virtual IEnumerable DocumentMigrationList { get; } = Array.Empty(); - public DocumentSemVerOptions DocumentSemVerOptions { get; } - public string Identifier { get; } - public SemanticVersion LibraryVersion { get; } - public ModelMapVersionOptions ModelMapVersionOptions { get; } - public IProxyGenerator ProxyGenerator { get; } - public IRepositoryRegister RepositoryRegister { get; } - public ISchemaRegister SchemaRegister { get; } - public ISerializerModifierAccessor SerializerModifierAccessor { get; } + public IExecutionContext ExecutionContext { get; private set; } = default!; + public string Identifier => Options?.Identifier ?? GetType().Name; + public bool IsSeeded + { + get + { + // Try to read cached. + isSeededLock.EnterReadLock(); + try + { + if (_isSeeded.HasValue) + return _isSeeded.Value; + } + finally + { + isSeededLock.ExitReadLock(); + } + + // Get seeding state from db. + isSeededLock.EnterWriteLock(); + try + { + if (!_isSeeded.HasValue) + { + var task = DbOperations.QueryElementsAsync(elements => + elements.OfType() + .AnyAsync(sop => sop.DbContextName == Identifier)); + task.Wait(); + _isSeeded = task.Result; + } + + return _isSeeded.Value; + } + finally + { + isSeededLock.ExitWriteLock(); + } + } + private set + { + isSeededLock.EnterWriteLock(); + try + { + _isSeeded = value; + } + finally + { + isSeededLock.ExitWriteLock(); + } + } + } + public SemanticVersion LibraryVersion { get; private set; } = default!; + public IDbContextOptions Options { get; private set; } = default!; + public IProxyGenerator ProxyGenerator { get; private set; } = default!; + public IRepositoryRegistry RepositoryRegistry { get; private set; } = default!; + public IBsonSerializerRegistry SerializerRegistry => _serializerRegistry; + public ISchemaRegistry SchemaRegistry { get; private set; } = default!; + public ISerializerModifierAccessor SerializerModifierAccessor { get; private set; } = default!; // Protected properties. protected abstract IEnumerable ModelMapsCollectors { get; } @@ -155,9 +238,9 @@ public virtual async Task SaveChangesAsync(CancellationToken cancellationToken = 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 { - if (RepositoryRegister.ModelCollectionRepositoryMap.ContainsKey(modelType)) + if (RepositoryRegistry.CollectionRepositoriesByModelType.ContainsKey(modelType)) { - var repository = RepositoryRegister.ModelCollectionRepositoryMap[modelType]; + var repository = RepositoryRegistry.CollectionRepositoriesByModelType[modelType]; await repository.ReplaceAsync(model, cancellationToken: cancellationToken).ConfigureAwait(false); break; } @@ -167,24 +250,44 @@ public virtual async Task SaveChangesAsync(CancellationToken cancellationToken = } } } + + // Save changes on child dbcontexts. + foreach (var child in childDbContexts) + { + await child.SaveChangesAsync(cancellationToken).ConfigureAwait(false); + } } public async Task SeedIfNeededAsync() { // Check if already seeded. - if (await DbOperations.QueryElementsAsync(elements => - elements.OfType() - .AnyAsync(sop => sop.DbContextName == Identifier)).ConfigureAwait(false)) + if (IsSeeded) return false; - // Seed. - await SeedAsync().ConfigureAwait(false); + await seedingSemaphore.WaitAsync().ConfigureAwait(false); + try + { + // Check again if seeded. + if (IsSeeded) + return false; + + // Seed. + try { await SeedAsync().ConfigureAwait(false); } + catch (Exception e) { throw new MongodmDbSeedingException($"Error seeding {GetType().Name} dbContext", e); } + + // Report operation. + var seedOperation = new SeedOperation(this); + await DbOperations.CreateAsync(seedOperation).ConfigureAwait(false); - // Report operation. - var seedOperation = new SeedOperation(this); - await DbOperations.CreateAsync(seedOperation).ConfigureAwait(false); + // Cache as seeded. + IsSeeded = true; - return true; + return true; + } + finally + { + seedingSemaphore.Release(); + } } public Task StartSessionAsync(CancellationToken cancellationToken = default) => @@ -193,5 +296,18 @@ public Task StartSessionAsync(CancellationToken cancellati // Protected methods. protected virtual Task SeedAsync() => Task.CompletedTask; + + // Private helpers. + private void InitializeSerializerRegistry() + { + //order matters. It's in reverse order of how they'll get consumed + _serializerRegistry.RegisterSerializationProvider(new ModelMapSerializationProvider(this)); + _serializerRegistry.RegisterSerializationProvider(new DiscriminatedInterfaceSerializationProvider()); + _serializerRegistry.RegisterSerializationProvider(new CollectionsSerializationProvider()); + _serializerRegistry.RegisterSerializationProvider(new PrimitiveSerializationProvider()); + _serializerRegistry.RegisterSerializationProvider(new AttributedSerializationProvider()); + _serializerRegistry.RegisterSerializationProvider(new TypeMappingSerializationProvider()); + _serializerRegistry.RegisterSerializationProvider(new BsonObjectModelSerializationProvider()); + } } } \ No newline at end of file diff --git a/src/MongODM.Core/Domain/ModelMaps/DbMigrationOperationMap.cs b/src/MongODM.Core/Domain/ModelMaps/DbMigrationOperationMap.cs index db25fc76..1ab63a4e 100644 --- a/src/MongODM.Core/Domain/ModelMaps/DbMigrationOperationMap.cs +++ b/src/MongODM.Core/Domain/ModelMaps/DbMigrationOperationMap.cs @@ -22,13 +22,13 @@ class DbMigrationOperationMap : IModelMapsCollector { public void Register(IDbContext dbContext) { - dbContext.SchemaRegister.AddModelMapsSchema("afdb63c9-791b-41f8-8216-556e233df0de"); + dbContext.SchemaRegistry.AddModelMapsSchema("afdb63c9-791b-41f8-8216-556e233df0de"); - dbContext.SchemaRegister.AddModelMapsSchema("1696c0c9-d615-44d9-ab9b-4e3618164185"); + dbContext.SchemaRegistry.AddModelMapsSchema("1696c0c9-d615-44d9-ab9b-4e3618164185"); - dbContext.SchemaRegister.AddModelMapsSchema("d2b49514-464e-4b28-8b38-ad2d0cc69d3e"); + dbContext.SchemaRegistry.AddModelMapsSchema("d2b49514-464e-4b28-8b38-ad2d0cc69d3e"); - dbContext.SchemaRegister.AddModelMapsSchema("24d65670-a3c3-443c-977a-51112df04e2a"); + dbContext.SchemaRegistry.AddModelMapsSchema("24d65670-a3c3-443c-977a-51112df04e2a"); } } } diff --git a/src/MongODM.Core/Domain/ModelMaps/ModelBaseMap.cs b/src/MongODM.Core/Domain/ModelMaps/ModelBaseMap.cs index 1f9a6f10..6601ffce 100644 --- a/src/MongODM.Core/Domain/ModelMaps/ModelBaseMap.cs +++ b/src/MongODM.Core/Domain/ModelMaps/ModelBaseMap.cs @@ -12,11 +12,11 @@ // See the License for the specific language governing permissions and // limitations under the License. +using Etherna.MongoDB.Bson; +using Etherna.MongoDB.Bson.Serialization.IdGenerators; +using Etherna.MongoDB.Bson.Serialization.Serializers; using Etherna.MongODM.Core.Domain.Models; using Etherna.MongODM.Core.Serialization; -using MongoDB.Bson; -using MongoDB.Bson.Serialization.IdGenerators; -using MongoDB.Bson.Serialization.Serializers; namespace Etherna.MongODM.Core.Domain.ModelMaps { @@ -24,10 +24,10 @@ class ModelBaseMap : IModelMapsCollector { public void Register(IDbContext dbContext) { - // register class maps. - dbContext.SchemaRegister.AddModelMapsSchema("bff55d53-0517-4a93-8fda-7bd448181449"); + // Register class maps. + dbContext.SchemaRegistry.AddModelMapsSchema("bff55d53-0517-4a93-8fda-7bd448181449"); - dbContext.SchemaRegister.AddModelMapsSchema>("586b48f5-ba1f-45e3-a812-744f88c1c969", + dbContext.SchemaRegistry.AddModelMapsSchema>("586b48f5-ba1f-45e3-a812-744f88c1c969", modelMap => { modelMap.AutoMap(); diff --git a/src/MongODM.Core/Domain/ModelMaps/OperationBaseMap.cs b/src/MongODM.Core/Domain/ModelMaps/OperationBaseMap.cs index 6498edcd..d8715a6f 100644 --- a/src/MongODM.Core/Domain/ModelMaps/OperationBaseMap.cs +++ b/src/MongODM.Core/Domain/ModelMaps/OperationBaseMap.cs @@ -13,7 +13,6 @@ // limitations under the License. using Etherna.MongODM.Core.Domain.Models; -using Etherna.MongODM.Core.Options; using Etherna.MongODM.Core.Serialization; using Etherna.MongODM.Core.Serialization.Serializers; @@ -23,17 +22,9 @@ class OperationBaseMap : IModelMapsCollector { public void Register(IDbContext dbContext) { - dbContext.SchemaRegister.AddModelMapsSchema("ee726d4f-6e6a-44b0-bf3e-45322534c36d", + dbContext.SchemaRegistry.AddModelMapsSchema("ee726d4f-6e6a-44b0-bf3e-45322534c36d", customSerializer: new ModelMapSerializer( - dbContext.DbCache, - new DocumentSemVerOptions - { - CurrentVersion = dbContext.LibraryVersion, - WriteInDocuments = false - }, - dbContext.ModelMapVersionOptions, - dbContext.SchemaRegister, - dbContext.SerializerModifierAccessor)); + dbContext)); } } } diff --git a/src/MongODM.Core/Domain/ModelMaps/SeedOperationMap.cs b/src/MongODM.Core/Domain/ModelMaps/SeedOperationMap.cs index f26e0a38..3852cbcf 100644 --- a/src/MongODM.Core/Domain/ModelMaps/SeedOperationMap.cs +++ b/src/MongODM.Core/Domain/ModelMaps/SeedOperationMap.cs @@ -21,7 +21,7 @@ class SeedOperationMap : IModelMapsCollector { public void Register(IDbContext dbContext) { - dbContext.SchemaRegister.AddModelMapsSchema("f9bfe56e-8045-4559-b91b-4745c2fd9766"); + dbContext.SchemaRegistry.AddModelMapsSchema("f9bfe56e-8045-4559-b91b-4745c2fd9766"); } } } diff --git a/src/MongODM.Core/Domain/Models/DbMigrationOperation.cs b/src/MongODM.Core/Domain/Models/DbMigrationOperation.cs index 5c931fba..caecdc8b 100644 --- a/src/MongODM.Core/Domain/Models/DbMigrationOperation.cs +++ b/src/MongODM.Core/Domain/Models/DbMigrationOperation.cs @@ -27,6 +27,7 @@ public enum Status New, Running, Completed, + Failed, Cancelled } @@ -64,7 +65,8 @@ public virtual void AddLog(MigrationLogBase log) [PropertyAlterer(nameof(CurrentStatus))] public virtual void TaskCancelled() { - if (CurrentStatus == Status.Completed) + if (CurrentStatus == Status.Completed || + CurrentStatus == Status.Failed) throw new InvalidOperationException(); CurrentStatus = Status.Cancelled; @@ -81,6 +83,16 @@ public virtual void TaskCompleted() CurrentStatus = Status.Completed; } + [PropertyAlterer(nameof(CurrentStatus))] + public virtual void TaskFailed() + { + if (CurrentStatus == Status.Completed || + CurrentStatus == Status.Cancelled) + throw new InvalidOperationException(); + + CurrentStatus = Status.Failed; + } + [PropertyAlterer(nameof(CurrentStatus))] [PropertyAlterer(nameof(TaskId))] public virtual void TaskStarted(string taskId) diff --git a/src/MongODM.Core/Domain/Models/EntityModelBase.cs b/src/MongODM.Core/Domain/Models/EntityModelBase.cs index dac468ab..28769fef 100644 --- a/src/MongODM.Core/Domain/Models/EntityModelBase.cs +++ b/src/MongODM.Core/Domain/Models/EntityModelBase.cs @@ -44,7 +44,7 @@ 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) || + obj is not IEntityModel || EqualityComparer.Default.Equals((obj as IEntityModel)!.Id, default!)) return false; return GetType() == obj.GetType() && EqualityComparer.Default.Equals(Id, (obj as IEntityModel)!.Id); diff --git a/src/ExecutionContext/Exceptions/ExecutionContextNotFoundException.cs b/src/MongODM.Core/Exceptions/MongodmDbSeedingException.cs similarity index 62% rename from src/ExecutionContext/Exceptions/ExecutionContextNotFoundException.cs rename to src/MongODM.Core/Exceptions/MongodmDbSeedingException.cs index 41998bfd..020a132f 100644 --- a/src/ExecutionContext/Exceptions/ExecutionContextNotFoundException.cs +++ b/src/MongODM.Core/Exceptions/MongodmDbSeedingException.cs @@ -14,17 +14,20 @@ using System; -namespace Etherna.ExecContext.Exceptions +namespace Etherna.MongODM.Core.Exceptions { - public class ExecutionContextNotFoundException : Exception + public class MongodmDbSeedingException : Exception { - public ExecutionContextNotFoundException(string message) : base(message) - { } + public MongodmDbSeedingException() + { + } - public ExecutionContextNotFoundException(string message, Exception innerException) : base(message, innerException) - { } + public MongodmDbSeedingException(string message) : base(message) + { + } - public ExecutionContextNotFoundException() - { } + public MongodmDbSeedingException(string message, Exception innerException) : base(message, innerException) + { + } } } diff --git a/src/MongODM.Core/Exceptions/MongodmIndexBuildingException.cs b/src/MongODM.Core/Exceptions/MongodmIndexBuildingException.cs new file mode 100644 index 00000000..2cecc519 --- /dev/null +++ b/src/MongODM.Core/Exceptions/MongodmIndexBuildingException.cs @@ -0,0 +1,33 @@ +// 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.Core.Exceptions +{ + public class MongodmIndexBuildingException : Exception + { + public MongodmIndexBuildingException() + { + } + + public MongodmIndexBuildingException(string message) : base(message) + { + } + + public MongodmIndexBuildingException(string message, Exception innerException) : base(message, innerException) + { + } + } +} diff --git a/src/MongODM.Core/Extensions/BsonClassMapExtensions.cs b/src/MongODM.Core/Extensions/BsonClassMapExtensions.cs index 22a732dd..bd863112 100644 --- a/src/MongODM.Core/Extensions/BsonClassMapExtensions.cs +++ b/src/MongODM.Core/Extensions/BsonClassMapExtensions.cs @@ -12,9 +12,9 @@ // See the License for the specific language governing permissions and // limitations under the License. +using Etherna.MongoDB.Bson.Serialization; using Etherna.MongODM.Core.Domain.Models; using Etherna.MongODM.Core.Serialization.Serializers; -using MongoDB.Bson.Serialization; using System; using System.Linq.Expressions; using System.Reflection; @@ -41,7 +41,6 @@ public static BsonMemberMap SetMemberSerializer( this BsonClassMap classMap, Expression> memberLambda, IBsonSerializer serializer) - where TMember : class { if (classMap is null) throw new ArgumentNullException(nameof(classMap)); diff --git a/src/MongODM.Core/Extensions/BsonMemberMapExtensions.cs b/src/MongODM.Core/Extensions/BsonMemberMapExtensions.cs index ffdfe687..bb1db4b1 100644 --- a/src/MongODM.Core/Extensions/BsonMemberMapExtensions.cs +++ b/src/MongODM.Core/Extensions/BsonMemberMapExtensions.cs @@ -12,7 +12,7 @@ // See the License for the specific language governing permissions and // limitations under the License. -using MongoDB.Bson.Serialization; +using Etherna.MongoDB.Bson.Serialization; using System; namespace Etherna.MongODM.Core.Extensions diff --git a/src/MongODM.Core/Extensions/EnumerableExtensions.cs b/src/MongODM.Core/Extensions/EnumerableExtensions.cs index 190d212e..1a8d531f 100644 --- a/src/MongODM.Core/Extensions/EnumerableExtensions.cs +++ b/src/MongODM.Core/Extensions/EnumerableExtensions.cs @@ -12,7 +12,7 @@ // See the License for the specific language governing permissions and // limitations under the License. -using MongoDB.Driver.Linq; +using Etherna.MongoDB.Driver.Linq; using System; using System.Collections.Generic; using System.Linq; diff --git a/src/MongODM.Core/IDbContext.cs b/src/MongODM.Core/IDbContext.cs index 6f8b240c..a54ad96d 100644 --- a/src/MongODM.Core/IDbContext.cs +++ b/src/MongODM.Core/IDbContext.cs @@ -12,6 +12,9 @@ // See the License for the specific language governing permissions and // limitations under the License. +using Etherna.ExecContext; +using Etherna.MongoDB.Bson.Serialization; +using Etherna.MongoDB.Driver; using Etherna.MongODM.Core.Domain.Models; using Etherna.MongODM.Core.Migration; using Etherna.MongODM.Core.Options; @@ -21,7 +24,6 @@ using Etherna.MongODM.Core.Serialization.Mapping; using Etherna.MongODM.Core.Serialization.Modifiers; using Etherna.MongODM.Core.Utility; -using MongoDB.Driver; using System.Collections.Generic; using System.Threading; using System.Threading.Tasks; @@ -64,24 +66,35 @@ public interface IDbContext /// ICollectionRepository DbOperations { get; } + /// + /// Registry for discriminator configuration. + /// + IDiscriminatorRegistry DiscriminatorRegistry { get; } + /// /// List of registered migration tasks /// IEnumerable DocumentMigrationList { get; } - DocumentSemVerOptions DocumentSemVerOptions { get; } - /// /// DbContext unique identifier. /// string Identifier { get; } /// - /// Current MongODM library version + /// True if it has been seeded. + /// + bool IsSeeded { get; } + + /// + /// Current MongODM library version. /// SemanticVersion LibraryVersion { get; } - ModelMapVersionOptions ModelMapVersionOptions { get; } + /// + /// Db context options. + /// + IDbContextOptions Options { get; } /// /// Current model proxy generator. @@ -89,20 +102,30 @@ public interface IDbContext IProxyGenerator ProxyGenerator { get; } /// - /// Register of available repositories. + /// Registry of available repositories. + /// + IRepositoryRegistry RepositoryRegistry { get; } + + /// + /// Local instance of a serializer registry. /// - IRepositoryRegister RepositoryRegister { get; } + IBsonSerializerRegistry SerializerRegistry { get; } /// - /// Register for model serialization and schema information. + /// Registry for model serialization and schema information. /// - ISchemaRegister SchemaRegister { get; } + ISchemaRegistry SchemaRegistry { get; } /// /// Serializer modifier accessor. /// ISerializerModifierAccessor SerializerModifierAccessor { get; } + /// + /// ExecutionContext handler. + /// + IExecutionContext ExecutionContext { get; } + // Methods. /// /// Save current model changes on db. diff --git a/src/ExecutionContext/IExecutionContext.cs b/src/MongODM.Core/IDbContextBuilder.cs similarity index 63% rename from src/ExecutionContext/IExecutionContext.cs rename to src/MongODM.Core/IDbContextBuilder.cs index 28857b0f..1dcdaa7d 100644 --- a/src/ExecutionContext/IExecutionContext.cs +++ b/src/MongODM.Core/IDbContextBuilder.cs @@ -12,19 +12,18 @@ // See the License for the specific language governing permissions and // limitations under the License. +using Etherna.MongoDB.Driver; +using Etherna.MongODM.Core.Options; using System.Collections.Generic; -namespace Etherna.ExecContext +namespace Etherna.MongODM.Core { - /// - /// Represents an execution context, where information can be put and retrieve alongside - /// the process with a key-value dictionary. - /// - public interface IExecutionContext + public interface IDbContextBuilder { - /// - /// The context dictionary. - /// - IDictionary? Items { get; } + void Initialize( + IDbDependencies dependencies, + IMongoClient mongoClient, + IDbContextOptions options, + IEnumerable childDbContexts); } } diff --git a/src/MongODM.Core/IDbDependencies.cs b/src/MongODM.Core/IDbDependencies.cs index 4caf1e7a..9f0bbb98 100644 --- a/src/MongODM.Core/IDbDependencies.cs +++ b/src/MongODM.Core/IDbDependencies.cs @@ -12,6 +12,8 @@ // See the License for the specific language governing permissions and // limitations under the License. +using Etherna.ExecContext; +using Etherna.MongoDB.Bson.Serialization; using Etherna.MongODM.Core.ProxyModels; using Etherna.MongODM.Core.Repositories; using Etherna.MongODM.Core.Serialization.Mapping; @@ -22,12 +24,15 @@ namespace Etherna.MongODM.Core { public interface IDbDependencies { + IBsonSerializerRegistry BsonSerializerRegistry { get; } IDbCache DbCache { get; } IDbMaintainer DbMaintainer { get; } IDbMigrationManager DbMigrationManager { get; } + IDiscriminatorRegistry DiscriminatorRegistry { get; } + IExecutionContext ExecutionContext { get; } IProxyGenerator ProxyGenerator { get; } - IRepositoryRegister RepositoryRegister { get; } - ISchemaRegister SchemaRegister { get; } + IRepositoryRegistry RepositoryRegistry { get; } + ISchemaRegistry SchemaRegistry { get; } ISerializerModifierAccessor SerializerModifierAccessor { get; } } } \ No newline at end of file diff --git a/src/MongODM.Core/IMongODMConfiguration.cs b/src/MongODM.Core/IMongODMConfiguration.cs deleted file mode 100644 index c2d2f34b..00000000 --- a/src/MongODM.Core/IMongODMConfiguration.cs +++ /dev/null @@ -1,41 +0,0 @@ -// 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.Core.Options; -using System; -using System.Collections.Generic; - -namespace Etherna.MongODM.Core -{ - public interface IMongODMConfiguration - { - IEnumerable DbContextTypes { get; } - bool IsFrozen { get; } - - // Methods. - IMongODMConfiguration AddDbContext( - Action>? dbContextConfig = null) - where TDbContext : class, IDbContext; - - IMongODMConfiguration AddDbContext( - Action>? dbContextConfig = null) - where TDbContext : class, IDbContext - where TDbContextImpl : class, TDbContext; - - /// - /// Freeze configuration. - /// - void Freeze(); - } -} diff --git a/src/MongODM.Core/Migration/DocumentMigration.cs b/src/MongODM.Core/Migration/DocumentMigration.cs index e81e37da..08ba1b5c 100644 --- a/src/MongODM.Core/Migration/DocumentMigration.cs +++ b/src/MongODM.Core/Migration/DocumentMigration.cs @@ -12,9 +12,9 @@ // See the License for the specific language governing permissions and // limitations under the License. +using Etherna.MongoDB.Driver; using Etherna.MongODM.Core.Domain.Models; using Etherna.MongODM.Core.Repositories; -using MongoDB.Driver; using System; using System.Threading; using System.Threading.Tasks; @@ -24,7 +24,7 @@ namespace Etherna.MongODM.Core.Migration public abstract class DocumentMigration { // Properties. - public abstract ICollectionRepository SourceCollection { get; } + public abstract ICollectionRepository SourceRepository { get; } // Methods. /// @@ -32,6 +32,7 @@ public abstract class DocumentMigration /// /// 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 callbackEveryTotDocuments = 0, Func? callbackAsync = null, CancellationToken cancellationToken = default); } @@ -45,73 +46,81 @@ public class DocumentMigration : DocumentMigration where TModel : class, IEntityModel { // Fields. - private readonly ICollectionRepository _sourceCollection; - private readonly Func destinationCollectionSelector; + private readonly ICollectionRepository _sourceRepository; + private readonly Func destinationRepositorySelector; private readonly Func modelConverter; // Constructors. - public DocumentMigration(ICollectionRepository collection) - : this(collection, collection, m => m) + public DocumentMigration(ICollectionRepository repository) + : this(repository, repository, m => m) { } public DocumentMigration( - ICollectionRepository sourceCollection, - ICollectionRepository destinationCollection, + ICollectionRepository sourceRepository, + ICollectionRepository destinationRepository, Func modelConverter) - : this(sourceCollection, _ => destinationCollection, modelConverter) + : this(sourceRepository, _ => destinationRepository, modelConverter) { } public DocumentMigration( - ICollectionRepository sourceCollection, - Func destinationCollectionSelector, + ICollectionRepository sourceRepository, + Func destinationRepositorySelector, Func modelConverter) { - _sourceCollection = sourceCollection ?? throw new ArgumentNullException(nameof(sourceCollection)); - this.destinationCollectionSelector = destinationCollectionSelector ?? throw new ArgumentNullException(nameof(destinationCollectionSelector)); + _sourceRepository = sourceRepository ?? throw new ArgumentNullException(nameof(sourceRepository)); + this.destinationRepositorySelector = destinationRepositorySelector ?? throw new ArgumentNullException(nameof(destinationRepositorySelector)); this.modelConverter = modelConverter ?? throw new ArgumentNullException(nameof(modelConverter)); } // Properties. - public override ICollectionRepository SourceCollection => _sourceCollection; + public override ICollectionRepository SourceRepository => _sourceRepository; // Methods. - public override async Task MigrateAsync( + public override Task MigrateAsync( int callbackEveryTotDocuments = 0, Func? callbackAsync = null, - CancellationToken cancellationToken = default) - { - if (callbackEveryTotDocuments < 0) - throw new ArgumentOutOfRangeException(nameof(callbackEveryTotDocuments), "Value can't be negative"); - - // Migrate documents. - var totMigratedDocuments = 0L; - await _sourceCollection.Collection.Find(FilterDefinition.Empty, new FindOptions { NoCursorTimeout = true }) - .ForEachAsync(async model => + CancellationToken cancellationToken = default) => + _sourceRepository.AccessToCollectionAsync(async sourceCollection => + { + var totMigratedDocuments = 0L; + try { - var destinationCollection = destinationCollectionSelector(model); - - // Verify if needs to skip this model. - if (destinationCollection is null) - return; - - // Replace if it's the same collection, insert one otherwise. - if (SourceCollection == destinationCollection) - await destinationCollection.ReplaceAsync(model, updateDependentDocuments: false).ConfigureAwait(false); - else - await destinationCollection.CreateAsync(modelConverter(model)).ConfigureAwait(false); - - // Increment counter. - totMigratedDocuments++; - - // Execute callback. - if (callbackEveryTotDocuments > 0 && - totMigratedDocuments % callbackEveryTotDocuments == 0 && - callbackAsync != null) - await callbackAsync(totMigratedDocuments).ConfigureAwait(false); - - }, cancellationToken).ConfigureAwait(false); - - return MigrationResult.Succeeded(totMigratedDocuments); - } + if (callbackEveryTotDocuments < 0) + throw new ArgumentOutOfRangeException(nameof(callbackEveryTotDocuments), "Value can't be negative"); + + // Migrate documents. + await sourceCollection.Find(FilterDefinition.Empty, new FindOptions { NoCursorTimeout = true }) + .ForEachAsync(async model => + { + var destinationRepository = destinationRepositorySelector(model); + + // Verify if needs to skip this model. + if (destinationRepository is null) + return; + + // Replace if it's the same collection, insert one otherwise. + if (SourceRepository == destinationRepository) + await destinationRepository.ReplaceAsync(model, updateDependentDocuments: false).ConfigureAwait(false); + else + await destinationRepository.CreateAsync(modelConverter(model)).ConfigureAwait(false); + + // Increment counter. + totMigratedDocuments++; + + // Execute callback. + if (callbackEveryTotDocuments > 0 && + totMigratedDocuments % callbackEveryTotDocuments == 0 && + callbackAsync != null) + await callbackAsync(totMigratedDocuments).ConfigureAwait(false); + + }, cancellationToken).ConfigureAwait(false); + + return MigrationResult.Succeeded(totMigratedDocuments); + } + catch (Exception e) + { + return MigrationResult.Failed(totMigratedDocuments, e); + } + }); } } diff --git a/src/MongODM.Core/Migration/MigrationResult.cs b/src/MongODM.Core/Migration/MigrationResult.cs index 962b135f..9ab0c7fb 100644 --- a/src/MongODM.Core/Migration/MigrationResult.cs +++ b/src/MongODM.Core/Migration/MigrationResult.cs @@ -12,6 +12,8 @@ // See the License for the specific language governing permissions and // limitations under the License. +using System; + namespace Etherna.MongODM.Core.Migration { public class MigrationResult @@ -20,21 +22,24 @@ public class MigrationResult private MigrationResult() { } // Properties. - public bool Succeded { get; private set; } + public Exception? Exception { get; private set; } public long MigratedDocuments { get; private set; } + public bool Succeded { get; private set; } // Methods. - public static MigrationResult Failed() => + public static MigrationResult Failed(long migratedDocuments, Exception? e = null) => new() { + Exception = e, + MigratedDocuments = migratedDocuments, Succeded = false }; public static MigrationResult Succeeded(long migratedDocuments) => new() { - Succeded = true, - MigratedDocuments = migratedDocuments + MigratedDocuments = migratedDocuments, + Succeded = true }; } } \ No newline at end of file diff --git a/src/MongODM.Core/MongODM.Core.csproj b/src/MongODM.Core/MongODM.Core.csproj index 2397e128..fe334434 100644 --- a/src/MongODM.Core/MongODM.Core.csproj +++ b/src/MongODM.Core/MongODM.Core.csproj @@ -19,27 +19,25 @@ LICENSE true true + True - - + + + + + all runtime; build; native; contentfiles; analyzers; buildtransitive - + all runtime; build; native; contentfiles; analyzers; buildtransitive - - - - - - True diff --git a/src/MongODM.Core/MongODMConfiguration.cs b/src/MongODM.Core/MongODMConfiguration.cs deleted file mode 100644 index 13380df8..00000000 --- a/src/MongODM.Core/MongODMConfiguration.cs +++ /dev/null @@ -1,159 +0,0 @@ -// 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.Core.Options; -using System; -using System.Collections.Generic; -using System.Threading; - -namespace Etherna.MongODM.Core -{ - public abstract class MongODMConfiguration : IMongODMConfiguration, IDisposable - { - // Fields. - private readonly ReaderWriterLockSlim configLock = new(LockRecursionPolicy.SupportsRecursion); - private bool disposed; - private readonly List _dbContextTypes = new(); - - // Dispose. - public void Dispose() - { - Dispose(true); - GC.SuppressFinalize(this); - } - - protected virtual void Dispose(bool disposing) - { - if (disposed) return; - - // Dispose managed resources. - if (disposing) - configLock.Dispose(); - - disposed = true; - } - - // Properties. - public IEnumerable DbContextTypes - { - get - { - Freeze(); - return _dbContextTypes; - } - } - - public bool IsFrozen { get; private set; } - - // Methods. - - public IMongODMConfiguration AddDbContext( - Action>? dbContextConfig = null) - where TDbContext : class, IDbContext - { - configLock.EnterWriteLock(); - try - { - if (IsFrozen) - throw new InvalidOperationException("Configuration is frozen"); - - // Register dbContext. - RegisterSingleton(); - - // Register options. - var contextOptions = new DbContextOptions(); - dbContextConfig?.Invoke(contextOptions); - RegisterSingleton(contextOptions); - - // Add db context type. - _dbContextTypes.Add(typeof(TDbContext)); - - return this; - } - finally - { - configLock.ExitWriteLock(); - } - } - - public IMongODMConfiguration AddDbContext( - Action>? dbContextConfig = null) - where TDbContext : class, IDbContext - where TDbContextImpl : class, TDbContext - { - configLock.EnterWriteLock(); - try - { - if (IsFrozen) - throw new InvalidOperationException("Configuration is frozen"); - - // Register dbContext. - RegisterSingleton(); - RegisterSingleton(sp => (TDbContextImpl)sp.GetService(typeof(TDbContext))); - - // Register options. - var contextOptions = new DbContextOptions(); - dbContextConfig?.Invoke(contextOptions); - RegisterSingleton(contextOptions); - - // Add db context type. - _dbContextTypes.Add(typeof(TDbContext)); - - return this; - } - finally - { - configLock.ExitWriteLock(); - } - } - - public void Freeze() - { - configLock.EnterReadLock(); - try - { - if (IsFrozen) return; - } - finally - { - configLock.ExitReadLock(); - } - - configLock.EnterWriteLock(); - try - { - // Freeze. - IsFrozen = true; - } - finally - { - configLock.ExitWriteLock(); - } - } - - // Abstract protected methods. - protected abstract void RegisterSingleton() - where TService : class; - - protected abstract void RegisterSingleton(TService instance) - where TService : class; - - protected abstract void RegisterSingleton() - where TService : class - where TImplementation : class, TService; - - protected abstract void RegisterSingleton(Func implementationFactory) - where TService : class; - } -} diff --git a/src/MongODM.Core/Options/DbContextOptions.cs b/src/MongODM.Core/Options/DbContextOptions.cs index d2abc7ea..77eaf88d 100644 --- a/src/MongODM.Core/Options/DbContextOptions.cs +++ b/src/MongODM.Core/Options/DbContextOptions.cs @@ -12,13 +12,18 @@ // See the License for the specific language governing permissions and // limitations under the License. -using Etherna.MongODM.Core.Extensions; +using System; +using System.Collections.Generic; using System.Linq; namespace Etherna.MongODM.Core.Options { - public class DbContextOptions + public class DbContextOptions : IDbContextOptions { + // Fields. + private readonly List _childDbContextTypes = new(); + + // Properties. public string ConnectionString { get; set; } = "mongodb://localhost/localDb"; public string DbName => ConnectionString.Split('?')[0] .Split('/').Last(); @@ -26,14 +31,14 @@ public class DbContextOptions public DocumentSemVerOptions DocumentSemVer { get; set; } = new DocumentSemVerOptions(); public string? Identifier { get; set; } public ModelMapVersionOptions ModelMapVersion { get; set; } = new ModelMapVersionOptions(); - } + public IEnumerable ChildDbContextTypes => _childDbContextTypes; - public class DbContextOptions : DbContextOptions - where TDbContext : class, IDbContext - { - public DbContextOptions() + // Methods. + public void ParentFor() where + TDbContext : class, IDbContext { - ConnectionString = $"mongodb://localhost/{typeof(TDbContext).Name.ToLowerFirstChar()}"; + if (!_childDbContextTypes.Contains(typeof(TDbContext))) + _childDbContextTypes.Add(typeof(TDbContext)); } } } diff --git a/src/MongODM.Core/Options/IDbContextOptions.cs b/src/MongODM.Core/Options/IDbContextOptions.cs new file mode 100644 index 00000000..b61dc7fc --- /dev/null +++ b/src/MongODM.Core/Options/IDbContextOptions.cs @@ -0,0 +1,26 @@ +// 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.Core.Options +{ + public interface IDbContextOptions + { + public string ConnectionString { get; } + public string DbName { get; } + public string DbOperationsCollectionName { get; } + public DocumentSemVerOptions DocumentSemVer { get; } + public string? Identifier { get; } + public ModelMapVersionOptions ModelMapVersion { get; } + } +} \ No newline at end of file diff --git a/src/MongODM.Core/Options/MongODMOptions.cs b/src/MongODM.Core/Options/MongODMOptions.cs index bd3fa50b..616b72c4 100644 --- a/src/MongODM.Core/Options/MongODMOptions.cs +++ b/src/MongODM.Core/Options/MongODMOptions.cs @@ -12,10 +12,19 @@ // See the License for the specific language governing permissions and // limitations under the License. +using System; +using System.Collections.Generic; + namespace Etherna.MongODM.Core.Options { - public class MongODMOptions + public sealed class MongODMOptions : IMongODMOptionsBuilder { + // Properties. + public IEnumerable DbContextTypes { get; private set; } = Array.Empty(); public string DbMaintenanceQueueName { get; set; } = "default"; + + // Explicit methods. + void IMongODMOptionsBuilder.SetDbContextTypes(IEnumerable dbContextTypes) => + DbContextTypes = dbContextTypes; } } diff --git a/src/ExecutionContext/Properties/AssemblyInfo.cs b/src/MongODM.Core/Options/MongODMOptionsBuilder.cs similarity index 76% rename from src/ExecutionContext/Properties/AssemblyInfo.cs rename to src/MongODM.Core/Options/MongODMOptionsBuilder.cs index 3a8ab07a..94a23358 100644 --- a/src/ExecutionContext/Properties/AssemblyInfo.cs +++ b/src/MongODM.Core/Options/MongODMOptionsBuilder.cs @@ -13,8 +13,12 @@ // limitations under the License. using System; -using System.Runtime.CompilerServices; +using System.Collections.Generic; -[assembly: CLSCompliant(false)] -[assembly: InternalsVisibleTo("DynamicProxyGenAssembly2")] -[assembly: InternalsVisibleTo("ExecutionContext.Tests")] +namespace Etherna.MongODM.Core.Options +{ + public interface IMongODMOptionsBuilder + { + void SetDbContextTypes(IEnumerable dbContextTypes); + } +} diff --git a/src/MongODM.Core/Properties/AssemblyInfo.cs b/src/MongODM.Core/Properties/AssemblyInfo.cs index 83cc064e..dd2b8d00 100644 --- a/src/MongODM.Core/Properties/AssemblyInfo.cs +++ b/src/MongODM.Core/Properties/AssemblyInfo.cs @@ -1,3 +1,17 @@ -using System; +// 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; [assembly: CLSCompliant(false)] \ No newline at end of file diff --git a/src/ExecutionContext/AsyncLocal/IHandledAsyncLocalContext.cs b/src/MongODM.Core/Properties/IsExternalInit.cs similarity index 64% rename from src/ExecutionContext/AsyncLocal/IHandledAsyncLocalContext.cs rename to src/MongODM.Core/Properties/IsExternalInit.cs index 6236049c..252ec65e 100644 --- a/src/ExecutionContext/AsyncLocal/IHandledAsyncLocalContext.cs +++ b/src/MongODM.Core/Properties/IsExternalInit.cs @@ -12,14 +12,8 @@ // See the License for the specific language governing permissions and // limitations under the License. -namespace Etherna.ExecContext.AsyncLocal +// Required because of this https://developercommunity.visualstudio.com/t/error-cs0518-predefined-type-systemruntimecompiler/1244809 +namespace System.Runtime.CompilerServices { - /// - /// Interface used by for comunicate with its - /// creator . - /// - internal interface IHandledAsyncLocalContext - { - void OnDisposed(IAsyncLocalContextHandler context); - } -} + internal static class IsExternalInit { } +} \ No newline at end of file diff --git a/src/MongODM.Core/ProxyModels/IProxyGenerator.cs b/src/MongODM.Core/ProxyModels/IProxyGenerator.cs index b34bb1dd..55d23687 100644 --- a/src/MongODM.Core/ProxyModels/IProxyGenerator.cs +++ b/src/MongODM.Core/ProxyModels/IProxyGenerator.cs @@ -18,6 +18,10 @@ namespace Etherna.MongODM.Core.ProxyModels { public interface IProxyGenerator { + // Properties. + bool DisableCreationWithProxyTypes { get; set; } + + // Methods. object CreateInstance(Type type, IDbContext dbContext, params object[] constructorArguments); TModel CreateInstance(IDbContext dbContext, params object[] constructorArguments); bool IsProxyType(Type type); diff --git a/src/MongODM.Core/ProxyModels/ProxyGenerator.cs b/src/MongODM.Core/ProxyModels/ProxyGenerator.cs index 7cfd148d..7a24f97a 100644 --- a/src/MongODM.Core/ProxyModels/ProxyGenerator.cs +++ b/src/MongODM.Core/ProxyModels/ProxyGenerator.cs @@ -17,6 +17,7 @@ using System; using System.Collections.Generic; using System.Linq; +using System.Reflection; using System.Threading; namespace Etherna.MongODM.Core.ProxyModels @@ -34,14 +35,13 @@ public class ProxyGenerator : IProxyGenerator, IDisposable private readonly Dictionary proxyTypeDictionary = new(); private readonly ReaderWriterLockSlim proxyTypeDictionaryLock = new(LockRecursionPolicy.SupportsRecursion); - // Constructors. + // Constructor and dispose. public ProxyGenerator( Castle.DynamicProxy.IProxyGenerator proxyGeneratorCore) { this.proxyGeneratorCore = proxyGeneratorCore; } - - // Dispose. + public void Dispose() { Dispose(true); @@ -62,6 +62,9 @@ protected virtual void Dispose(bool disposing) disposed = true; } + // Properties. + public bool DisableCreationWithProxyTypes { get; set; } + // Methods. public object CreateInstance( Type type, @@ -73,6 +76,17 @@ public object CreateInstance( if (type is null) throw new ArgumentNullException(nameof(type)); + // If creation of proxy models are disabled, create a simple model instance. + if (DisableCreationWithProxyTypes) + { + return Activator.CreateInstance( + type, + BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance, + null, + constructorArguments, + null); + } + // Get configuration. (Type[] AdditionalInterfaces, Func InterceptorInstancerSelector) configuration = (null!, null!); modelConfigurationDictionaryLock.EnterReadLock(); diff --git a/src/MongODM.Core/ProxyModels/ReferenceableInterceptor.cs b/src/MongODM.Core/ProxyModels/ReferenceableInterceptor.cs index 3728f5d3..0712a319 100644 --- a/src/MongODM.Core/ProxyModels/ReferenceableInterceptor.cs +++ b/src/MongODM.Core/ProxyModels/ReferenceableInterceptor.cs @@ -42,14 +42,14 @@ public ReferenceableInterceptor( throw new ArgumentNullException(nameof(dbContext)); var repositoryModelType = typeof(TModel); - while (!dbContext.RepositoryRegister.ModelRepositoryMap.ContainsKey(repositoryModelType)) + while (!dbContext.RepositoryRegistry.RepositoriesByModelType.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]; + repository = dbContext.RepositoryRegistry.RepositoriesByModelType[repositoryModelType]; } // Protected methods. diff --git a/src/MongODM.Core/ReflectionHelper.cs b/src/MongODM.Core/ReflectionHelper.cs index a20ad33e..88154684 100644 --- a/src/MongODM.Core/ReflectionHelper.cs +++ b/src/MongODM.Core/ReflectionHelper.cs @@ -23,8 +23,8 @@ namespace Etherna.MongODM.Core { public static class ReflectionHelper { - private static readonly Dictionary> propertyRegister = new(); - private static readonly ReaderWriterLockSlim propertyRegisterLock = new(); + private static readonly Dictionary> propertyRegistry = new(); + private static readonly ReaderWriterLockSlim propertyRegistryLock = new(); public static MemberInfo FindProperty(LambdaExpression lambdaExpression) { @@ -169,30 +169,29 @@ public static TMember GetValueFromLambda(TModel source, Express /// /// Return the list of writable instance property of a type /// - /// The model type /// The list of properties public static IEnumerable GetWritableInstanceProperties(Type objectType) { if (objectType is null) throw new ArgumentNullException(nameof(objectType)); - propertyRegisterLock.EnterReadLock(); + propertyRegistryLock.EnterReadLock(); try { - if (propertyRegister.ContainsKey(objectType)) + if (propertyRegistry.ContainsKey(objectType)) { - return propertyRegister[objectType]; + return propertyRegistry[objectType]; } } finally { - propertyRegisterLock.ExitReadLock(); + propertyRegistryLock.ExitReadLock(); } - propertyRegisterLock.EnterWriteLock(); + propertyRegistryLock.EnterWriteLock(); try { - if (!propertyRegister.ContainsKey(objectType)) + if (!propertyRegistry.ContainsKey(objectType)) { var typeStack = new List(); var stackType = objectType; @@ -202,15 +201,15 @@ public static IEnumerable GetWritableInstanceProperties(Type objec stackType = stackType.BaseType; } while (stackType != null); - propertyRegister.Add(objectType, typeStack + propertyRegistry.Add(objectType, typeStack .SelectMany(type => type.GetProperties(BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance)) .Where(prop => prop.CanWrite)); } - return propertyRegister[objectType]; + return propertyRegistry[objectType]; } finally { - propertyRegisterLock.ExitWriteLock(); + propertyRegistryLock.ExitWriteLock(); } } diff --git a/src/MongODM.Core/Repositories/CollectionRepository.cs b/src/MongODM.Core/Repositories/CollectionRepository.cs index 8588fa06..166ef2e2 100644 --- a/src/MongODM.Core/Repositories/CollectionRepository.cs +++ b/src/MongODM.Core/Repositories/CollectionRepository.cs @@ -12,13 +12,14 @@ // See the License for the specific language governing permissions and // limitations under the License. +using Etherna.MongoDB.Bson; +using Etherna.MongoDB.Driver; +using Etherna.MongoDB.Driver.Linq; using Etherna.MongODM.Core.Domain.Models; using Etherna.MongODM.Core.Exceptions; +using Etherna.MongODM.Core.Extensions; using Etherna.MongODM.Core.ProxyModels; -using Etherna.MongODM.Core.Serialization.Mapping; -using MongoDB.Bson; -using MongoDB.Driver; -using MongoDB.Driver.Linq; +using Etherna.MongODM.Core.Utility; using System; using System.Collections.Generic; using System.Linq; @@ -48,71 +49,101 @@ 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(ISchemaRegister schemaRegister, CancellationToken cancellationToken = default) - { - var newIndexes = new List<(string name, CreateIndexModel createIndex)>(); - - // Define new indexes. - //repository defined - newIndexes.AddRange(options.IndexBuilders.Select(pair => + public Task AccessToCollectionAsync(Func, Task> action) => + AccessToCollectionAsync(async collection => { - var (keys, options) = pair; - if (options.Name == null) - { - var renderedKeys = keys.Render(Collection.DocumentSerializer, Collection.Settings.SerializerRegistry); - options.Name = $"doc_{ string.Join("_", renderedKeys.Names) }"; - } + await action(collection).ConfigureAwait(false); + return 0; + }); + + public async Task AccessToCollectionAsync(Func, Task> func) + { + if (func is null) + throw new ArgumentNullException(nameof(func)); - return (options.Name, new CreateIndexModel(keys, options)); - })); + // Initialize collection cache. + if (_collection is null) + _collection = DbContext.Database.GetCollection(options.Name); - //referenced documents - var dependencies = DbContext.SchemaRegister.GetIdMemberDependenciesFromRootModel(typeof(TModel)); + // Execute func into execution context. + using (new DbExecutionContextHandler(DbContext)) + { + return await func(_collection).ConfigureAwait(false); + } + } - var idPaths = dependencies - .Select(dependency => dependency.MemberPathToString()) - .Distinct(); + public override Task BuildIndexesAsync(CancellationToken cancellationToken = default) => + AccessToCollectionAsync(async collection => + { + var newIndexes = new List<(string name, CreateIndexModel createIndex)>(); - newIndexes.AddRange(idPaths.Select(path => - ($"ref_{path}", - new CreateIndexModel( - Builders.IndexKeys.Ascending(path), - new CreateIndexOptions + // Define new indexes. + //repository defined + newIndexes.AddRange(options.IndexBuilders.Select(pair => + { + var (keys, options) = pair; + if (options.Name == null) { - Name = $"ref_{path}", - Sparse = true - })))); - - // Get current indexes. - var currentIndexes = new List(); - using (var indexList = await Collection.Indexes.ListAsync(cancellationToken).ConfigureAwait(false)) - while (indexList.MoveNext(cancellationToken)) - currentIndexes.AddRange(indexList.Current); - - // Remove old indexes. - foreach (var oldIndex in from index in currentIndexes - let indexName = index.GetElement("name").Value.ToString() - where indexName != "_id_" - where !newIndexes.Any(newIndex => newIndex.name == indexName) - select index) - { - await Collection.Indexes.DropOneAsync(oldIndex.GetElement("name").Value.ToString(), cancellationToken).ConfigureAwait(false); - } + try + { + var renderedKeys = keys.Render(collection.DocumentSerializer, collection.Settings.SerializerRegistry); + options.Name = $"doc_{ string.Join("_", renderedKeys.Names) }"; + } + catch (InvalidOperationException) + { + throw new MongodmIndexBuildingException($"Can't build custom index in collection \"{Name}\""); + } + } + + return (options.Name, new CreateIndexModel(keys, options)); + })); + + //referenced documents + var dependencies = DbContext.SchemaRegistry.GetIdMemberDependenciesFromRootModel(typeof(TModel), true); + + var idPaths = dependencies + .Select(dependency => dependency.MemberPathToString()) + .Distinct(); + + newIndexes.AddRange(idPaths.Select(path => + ($"ref_{path}", + new CreateIndexModel( + Builders.IndexKeys.Ascending(path), + new CreateIndexOptions + { + Name = $"ref_{path}", + Sparse = true + })))); + + // Get current indexes. + var currentIndexes = new List(); + using (var indexList = await collection.Indexes.ListAsync(cancellationToken).ConfigureAwait(false)) + while (await indexList.MoveNextAsync(cancellationToken).ConfigureAwait(false)) + currentIndexes.AddRange(indexList.Current); + + // Remove old indexes. + foreach (var oldIndex in from index in currentIndexes + let indexName = index.GetElement("name").Value.ToString() + where indexName != "_id_" + where !newIndexes.Any(newIndex => newIndex.name == indexName) + select index) + { + await collection.Indexes.DropOneAsync(oldIndex.GetElement("name").Value.ToString(), cancellationToken).ConfigureAwait(false); + } - // Build new indexes. - if (newIndexes.Any()) - await Collection.Indexes.CreateManyAsync(newIndexes.Select(i => i.createIndex), cancellationToken).ConfigureAwait(false); - } + // Build new indexes. + if (newIndexes.Any()) + await collection.Indexes.CreateManyAsync(newIndexes.Select(i => i.createIndex), cancellationToken).ConfigureAwait(false); + }); public virtual Task> FindAsync( FilterDefinition filter, FindOptions? options = null, CancellationToken cancellationToken = default) => - Collection.FindAsync(filter, options, cancellationToken); + AccessToCollectionAsync(collection => collection.FindAsync(filter, options, cancellationToken)); public Task FindOneAsync( Expression> predicate, @@ -121,12 +152,43 @@ public Task FindOneAsync( public virtual Task QueryElementsAsync( Func, Task> query, - AggregateOptions? aggregateOptions = null) + AggregateOptions? aggregateOptions = null) => + AccessToCollectionAsync(collection => + { + if (query is null) + throw new ArgumentNullException(nameof(query)); + + return query(collection.AsQueryable(aggregateOptions)); + }); + + public async Task> QueryPaginatedElementsAsync( + Func, IMongoQueryable> filter, + Expression> orderKeySelector, + int page, + int take, + bool useDescendingOrder = false, + CancellationToken cancellationToken = default) { - if (query is null) - throw new ArgumentNullException(nameof(query)); + var elements = await QueryElementsAsync(elements => + + useDescendingOrder ? - return query(Collection.AsQueryable(aggregateOptions)); + filter(elements) + .PaginateDescending(orderKeySelector, page, take) + .ToListAsync(cancellationToken) : + + filter(elements) + .Paginate(orderKeySelector, page, take) + .ToListAsync(cancellationToken)).ConfigureAwait(false); + + var maxPage = (await QueryElementsAsync(elements => filter(elements) + .CountAsync(cancellationToken)).ConfigureAwait(false) - 1) / take; + + return new PaginatedEnumerable( + elements, + page, + take, + maxPage); } public virtual Task ReplaceAsync( @@ -172,20 +234,21 @@ public virtual Task ReplaceAsync( // Protected methods. protected override Task CreateOnDBAsync(IEnumerable models, CancellationToken cancellationToken) => - Collection.InsertManyAsync(models, null, cancellationToken); + AccessToCollectionAsync(collection => collection.InsertManyAsync(models, null, cancellationToken)); protected override Task CreateOnDBAsync(TModel model, CancellationToken cancellationToken) => - Collection.InsertOneAsync(model, null, cancellationToken); + AccessToCollectionAsync(collection => collection.InsertOneAsync(model, null, cancellationToken)); - protected override Task DeleteOnDBAsync(TModel model, CancellationToken cancellationToken) - { - if (model is null) - throw new ArgumentNullException(nameof(model)); + protected override Task DeleteOnDBAsync(TModel model, CancellationToken cancellationToken) => + AccessToCollectionAsync(collection => + { + if (model is null) + throw new ArgumentNullException(nameof(model)); - return Collection.DeleteOneAsync( - Builders.Filter.Eq(m => m.Id, model.Id), - cancellationToken); - } + return collection.DeleteOneAsync( + Builders.Filter.Eq(m => m.Id, model.Id), + cancellationToken); + }); protected override async Task FindOneOnDBAsync(TKey id, CancellationToken cancellationToken = default) { @@ -203,54 +266,57 @@ protected override async Task FindOneOnDBAsync(TKey id, CancellationToke } // Helpers. - private async Task FindOneOnDBAsync( + private Task FindOneOnDBAsync( Expression> predicate, - CancellationToken cancellationToken = default) - { - if (predicate is null) - throw new ArgumentNullException(nameof(predicate)); + CancellationToken cancellationToken = default) => + AccessToCollectionAsync(async collection => + { + if (predicate is null) + throw new ArgumentNullException(nameof(predicate)); - using var cursor = await Collection.FindAsync(predicate, cancellationToken: cancellationToken).ConfigureAwait(false); - var element = await cursor.FirstOrDefaultAsync(cancellationToken).ConfigureAwait(false); + using var cursor = await collection.FindAsync(predicate, cancellationToken: cancellationToken).ConfigureAwait(false); + var element = await cursor.FirstOrDefaultAsync(cancellationToken).ConfigureAwait(false); - if (element == default(TModel)) - throw new MongodmEntityNotFoundException("Can't find element"); + if (element == default(TModel)) + throw new MongodmEntityNotFoundException("Can't find element"); - return element; - } + return element; + }); + - private async Task ReplaceHelperAsync( + private Task ReplaceHelperAsync( TModel model, IClientSessionHandle? session, bool updateDependentDocuments, - CancellationToken cancellationToken) - { - if (model == null) - throw new ArgumentNullException(nameof(model)); - - // Replace on db. - if (session == null) + CancellationToken cancellationToken) => + AccessToCollectionAsync(async collection => { - await Collection.ReplaceOneAsync( - Builders.Filter.Eq(m => m.Id, model.Id), - model, - cancellationToken: cancellationToken).ConfigureAwait(false); - } - else - { - await Collection.ReplaceOneAsync( - session, - Builders.Filter.Eq(m => m.Id, model.Id), - model, - cancellationToken: cancellationToken).ConfigureAwait(false); - } + if (model == null) + throw new ArgumentNullException(nameof(model)); - // Update dependent documents. - if (updateDependentDocuments) - DbContext.DbMaintainer.OnUpdatedModel((IAuditable)model, model.Id); + // Replace on db. + if (session == null) + { + await collection.ReplaceOneAsync( + Builders.Filter.Eq(m => m.Id, model.Id), + model, + cancellationToken: cancellationToken).ConfigureAwait(false); + } + else + { + await collection.ReplaceOneAsync( + session, + Builders.Filter.Eq(m => m.Id, model.Id), + model, + cancellationToken: cancellationToken).ConfigureAwait(false); + } - // Reset changed members. - ((IAuditable)model).ResetChangedMembers(); - } + // Update dependent documents. + if (updateDependentDocuments) + DbContext.DbMaintainer.OnUpdatedModel((IAuditable)model, model.Id); + + // Reset changed members. + ((IAuditable)model).ResetChangedMembers(); + }); } } \ No newline at end of file diff --git a/src/MongODM.Core/Repositories/CollectionRepositoryOptions.cs b/src/MongODM.Core/Repositories/CollectionRepositoryOptions.cs index e10baee9..9683f4bb 100644 --- a/src/MongODM.Core/Repositories/CollectionRepositoryOptions.cs +++ b/src/MongODM.Core/Repositories/CollectionRepositoryOptions.cs @@ -12,7 +12,7 @@ // See the License for the specific language governing permissions and // limitations under the License. -using MongoDB.Driver; +using Etherna.MongoDB.Driver; using System; using System.Collections.Generic; diff --git a/src/MongODM.Core/Repositories/GridFSRepository.cs b/src/MongODM.Core/Repositories/GridFSRepository.cs index e60b8f36..1c2f84b8 100644 --- a/src/MongODM.Core/Repositories/GridFSRepository.cs +++ b/src/MongODM.Core/Repositories/GridFSRepository.cs @@ -12,12 +12,12 @@ // See the License for the specific language governing permissions and // limitations under the License. +using Etherna.MongoDB.Bson; +using Etherna.MongoDB.Driver; +using Etherna.MongoDB.Driver.GridFS; using Etherna.MongODM.Core.Domain.Models; using Etherna.MongODM.Core.Exceptions; -using Etherna.MongODM.Core.Serialization.Mapping; -using MongoDB.Bson; -using MongoDB.Driver; -using MongoDB.Driver.GridFS; +using Etherna.MongODM.Core.Utility; using System; using System.Collections.Generic; using System.IO; @@ -46,18 +46,40 @@ 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(ISchemaRegister schemaRegister, CancellationToken cancellationToken = default) => Task.CompletedTask; + public Task AccessToGridFSBucketAsync(Func action) => + AccessToGridFSBucketAsync(async bucket => + { + await action(bucket).ConfigureAwait(false); + return 0; + }); + + public async Task AccessToGridFSBucketAsync(Func> func) + { + if (func is null) + throw new ArgumentNullException(nameof(func)); + + // Initialize bucket cache. + if (_gridFSBucket is null) + _gridFSBucket = new GridFSBucket(DbContext.Database, new GridFSBucketOptions { BucketName = options.Name }); + + // Execute func into execution context. + using (new DbExecutionContextHandler(DbContext)) + { + return await func(_gridFSBucket).ConfigureAwait(false); + } + } + + public override Task BuildIndexesAsync(CancellationToken cancellationToken = default) => Task.CompletedTask; public virtual Task DownloadAsBytesAsync(string id, CancellationToken cancellationToken = default) => - GridFSBucket.DownloadAsBytesAsync(ObjectId.Parse(id), null, cancellationToken); + AccessToGridFSBucketAsync(bucket => bucket.DownloadAsBytesAsync(ObjectId.Parse(id), null, cancellationToken)); public virtual async Task DownloadAsStreamAsync(string id, CancellationToken cancellationToken = default) => - await GridFSBucket.OpenDownloadStreamAsync(ObjectId.Parse(id), null, cancellationToken).ConfigureAwait(false); + await AccessToGridFSBucketAsync(bucket => + bucket.OpenDownloadStreamAsync(ObjectId.Parse(id), null, cancellationToken)).ConfigureAwait(false); // Protected methods. protected override async Task CreateOnDBAsync(IEnumerable models, CancellationToken cancellationToken) @@ -69,48 +91,52 @@ protected override async Task CreateOnDBAsync(IEnumerable models, Cancel await CreateOnDBAsync(model, cancellationToken).ConfigureAwait(false); } - protected override async Task CreateOnDBAsync(TModel model, CancellationToken cancellationToken) - { - if (model is null) - throw new ArgumentNullException(nameof(model)); - - // Upload. - model.Stream.Position = 0; - var id = await GridFSBucket.UploadFromStreamAsync(model.Name, model.Stream, new GridFSUploadOptions + protected override Task CreateOnDBAsync(TModel model, CancellationToken cancellationToken) => + AccessToGridFSBucketAsync(async bucket => { - Metadata = options.MetadataSerializer?.Invoke(model) - }, cancellationToken).ConfigureAwait(false); - ReflectionHelper.SetValue(model, m => m.Id, id.ToString()); - } + if (model is null) + throw new ArgumentNullException(nameof(model)); - protected override Task DeleteOnDBAsync(TModel model, CancellationToken cancellationToken) - { - if (model is null) - throw new ArgumentNullException(nameof(model)); + // Upload. + model.Stream.Position = 0; + var id = await bucket.UploadFromStreamAsync(model.Name, model.Stream, new GridFSUploadOptions + { + Metadata = options.MetadataSerializer?.Invoke(model) + }, cancellationToken).ConfigureAwait(false); + ReflectionHelper.SetValue(model, m => m.Id, id.ToString()); + }); - return GridFSBucket.DeleteAsync(ObjectId.Parse(model.Id), cancellationToken); - } - protected override async Task FindOneOnDBAsync(string id, CancellationToken cancellationToken = default) - { - if (id == null) - throw new ArgumentNullException(nameof(id)); + protected override Task DeleteOnDBAsync(TModel model, CancellationToken cancellationToken) => + AccessToGridFSBucketAsync(bucket => + { + if (model is null) + throw new ArgumentNullException(nameof(model)); - var filter = Builders.Filter.Eq("_id", ObjectId.Parse(id)); - var mongoFile = await GridFSBucket.Find(filter, cancellationToken: cancellationToken) - .SingleOrDefaultAsync(cancellationToken).ConfigureAwait(false); - if (mongoFile == null) - throw new MongodmEntityNotFoundException($"Can't find key {id}"); + return bucket.DeleteAsync(ObjectId.Parse(model.Id), cancellationToken); + }); - var file = DbContext.ProxyGenerator.CreateInstance(DbContext); - ReflectionHelper.SetValue(file, m => m.Id, mongoFile.Id.ToString()); - ReflectionHelper.SetValue(file, m => m.Length, mongoFile.Length); - ReflectionHelper.SetValue(file, m => m.Name, mongoFile.Filename); + protected override Task FindOneOnDBAsync(string id, CancellationToken cancellationToken = default) => + AccessToGridFSBucketAsync(async bucket => + { + if (id == null) + throw new ArgumentNullException(nameof(id)); - // Deserialize metadata. - options.MetadataDeserializer?.Invoke(mongoFile.Metadata, file); + var filter = Builders.Filter.Eq("_id", ObjectId.Parse(id)); + var mongoFile = await (await bucket.FindAsync(filter, cancellationToken: cancellationToken).ConfigureAwait(false)) + .SingleOrDefaultAsync(cancellationToken).ConfigureAwait(false); + if (mongoFile == null) + throw new MongodmEntityNotFoundException($"Can't find key {id}"); - return file; - } + var file = DbContext.ProxyGenerator.CreateInstance(DbContext); + ReflectionHelper.SetValue(file, m => m.Id, mongoFile.Id.ToString()); + ReflectionHelper.SetValue(file, m => m.Length, mongoFile.Length); + ReflectionHelper.SetValue(file, m => m.Name, mongoFile.Filename); + + // Deserialize metadata. + options.MetadataDeserializer?.Invoke(mongoFile.Metadata, file); + + return file; + }); } } \ No newline at end of file diff --git a/src/MongODM.Core/Repositories/GridFSRepositoryOptions.cs b/src/MongODM.Core/Repositories/GridFSRepositoryOptions.cs index 59d75c56..8f443ff7 100644 --- a/src/MongODM.Core/Repositories/GridFSRepositoryOptions.cs +++ b/src/MongODM.Core/Repositories/GridFSRepositoryOptions.cs @@ -12,7 +12,7 @@ // See the License for the specific language governing permissions and // limitations under the License. -using MongoDB.Bson; +using Etherna.MongoDB.Bson; using System; namespace Etherna.MongODM.Core.Repositories diff --git a/src/MongODM.Core/Repositories/ICollectionRepository.cs b/src/MongODM.Core/Repositories/ICollectionRepository.cs index 6514cbbc..6e786767 100644 --- a/src/MongODM.Core/Repositories/ICollectionRepository.cs +++ b/src/MongODM.Core/Repositories/ICollectionRepository.cs @@ -12,9 +12,9 @@ // See the License for the specific language governing permissions and // limitations under the License. +using Etherna.MongoDB.Driver; +using Etherna.MongoDB.Driver.Linq; using Etherna.MongODM.Core.Domain.Models; -using MongoDB.Driver; -using MongoDB.Driver.Linq; using System; using System.Linq.Expressions; using System.Threading; @@ -39,7 +39,9 @@ Task ReplaceAsync( public interface ICollectionRepository : IRepository, ICollectionRepository where TModel : class, IEntityModel { - IMongoCollection Collection { get; } + Task AccessToCollectionAsync(Func, Task> action); + + Task AccessToCollectionAsync(Func, Task> func); Task> FindAsync( FilterDefinition filter, @@ -54,6 +56,14 @@ Task QueryElementsAsync( Func, Task> query, AggregateOptions? aggregateOptions = null); + Task> QueryPaginatedElementsAsync( + Func, IMongoQueryable> filter, + Expression> orderKeySelector, + int page, + int take, + bool useDescendingOrder = false, + CancellationToken cancellationToken = default); + Task ReplaceAsync( TModel model, bool updateDependentDocuments = true, diff --git a/src/MongODM.Core/Repositories/IGridFSRepository.cs b/src/MongODM.Core/Repositories/IGridFSRepository.cs index 5ed597ef..c3d9b313 100644 --- a/src/MongODM.Core/Repositories/IGridFSRepository.cs +++ b/src/MongODM.Core/Repositories/IGridFSRepository.cs @@ -12,8 +12,9 @@ // See the License for the specific language governing permissions and // limitations under the License. +using Etherna.MongoDB.Driver.GridFS; using Etherna.MongODM.Core.Domain.Models; -using MongoDB.Driver.GridFS; +using System; using System.IO; using System.Threading; using System.Threading.Tasks; @@ -22,7 +23,9 @@ namespace Etherna.MongODM.Core.Repositories { public interface IGridFSRepository : IRepository { - IGridFSBucket GridFSBucket { get; } + Task AccessToGridFSBucketAsync(Func action); + + Task AccessToGridFSBucketAsync(Func> func); Task DownloadAsBytesAsync(string id, CancellationToken cancellationToken = default); diff --git a/src/MongODM.Core/Repositories/IRepository.cs b/src/MongODM.Core/Repositories/IRepository.cs index edda7f9f..f773ad52 100644 --- a/src/MongODM.Core/Repositories/IRepository.cs +++ b/src/MongODM.Core/Repositories/IRepository.cs @@ -13,7 +13,6 @@ // limitations under the License. using Etherna.MongODM.Core.Domain.Models; -using Etherna.MongODM.Core.Serialization.Mapping; using System; using System.Collections.Generic; using System.Threading; @@ -29,7 +28,6 @@ public interface IRepository : IDbContextInitializable string Name { get; } Task BuildIndexesAsync( - ISchemaRegister schemaRegister, CancellationToken cancellationToken = default); Task CreateAsync( diff --git a/src/MongODM.Core/Repositories/IRepositoryRegister.cs b/src/MongODM.Core/Repositories/IRepositoryRegistry.cs similarity index 75% rename from src/MongODM.Core/Repositories/IRepositoryRegister.cs rename to src/MongODM.Core/Repositories/IRepositoryRegistry.cs index 1c41cac6..3fb93031 100644 --- a/src/MongODM.Core/Repositories/IRepositoryRegister.cs +++ b/src/MongODM.Core/Repositories/IRepositoryRegistry.cs @@ -17,21 +17,21 @@ namespace Etherna.MongODM.Core.Repositories { - public interface IRepositoryRegister : IDbContextInitializable + public interface IRepositoryRegistry : IDbContextInitializable { /// /// Model-Repository map for collection types. /// - IReadOnlyDictionary ModelCollectionRepositoryMap { get; } + IReadOnlyDictionary CollectionRepositoriesByModelType { get; } /// /// Model-Repository map for gridfs types. /// - IReadOnlyDictionary ModelGridFSRepositoryMap { get; } + IReadOnlyDictionary GridFSRepositoriesByModelType { get; } /// /// Model-Repository map for both collection and gridfs types. /// - IReadOnlyDictionary ModelRepositoryMap { get; } + IReadOnlyDictionary RepositoriesByModelType { get; } } } \ No newline at end of file diff --git a/src/MongODM.AspNetCore/HttpContextExecutionContext.cs b/src/MongODM.Core/Repositories/PaginatedEnumerable.cs similarity index 55% rename from src/MongODM.AspNetCore/HttpContextExecutionContext.cs rename to src/MongODM.Core/Repositories/PaginatedEnumerable.cs index 62b7f85d..d2f45986 100644 --- a/src/MongODM.AspNetCore/HttpContextExecutionContext.cs +++ b/src/MongODM.Core/Repositories/PaginatedEnumerable.cs @@ -12,24 +12,27 @@ // See the License for the specific language governing permissions and // limitations under the License. -using Etherna.ExecContext; -using Microsoft.AspNetCore.Http; using System.Collections.Generic; -namespace Etherna.MongODM.AspNetCore +namespace Etherna.MongODM.Core.Repositories { - public class HttpContextExecutionContext : IExecutionContext + public class PaginatedEnumerable { - // Fields. - private readonly IHttpContextAccessor httpContextAccessor; - - // Constructors. - public HttpContextExecutionContext(IHttpContextAccessor httpContextAccessor) + public PaginatedEnumerable( + IEnumerable elements, + int currentPage, + int pageSize, + int maxPage) { - this.httpContextAccessor = httpContextAccessor; + CurrentPage = currentPage; + Elements = elements; + MaxPage = maxPage; + PageSize = pageSize; } - // Properties. - public IDictionary? Items => httpContextAccessor?.HttpContext?.Items; + public int CurrentPage { get; } + public IEnumerable Elements { get; } + public int MaxPage { get; } + public int PageSize { get; } } } diff --git a/src/MongODM.Core/Repositories/RepositoryBase.cs b/src/MongODM.Core/Repositories/RepositoryBase.cs index 406195bf..33ac9f2a 100644 --- a/src/MongODM.Core/Repositories/RepositoryBase.cs +++ b/src/MongODM.Core/Repositories/RepositoryBase.cs @@ -49,7 +49,7 @@ public virtual void Initialize(IDbContext dbContext) public abstract string Name { get; } // Methods. - public abstract Task BuildIndexesAsync(ISchemaRegister schemaRegister, CancellationToken cancellationToken = default); + public abstract Task BuildIndexesAsync(CancellationToken cancellationToken = default); public Task CreateAsync(object model, CancellationToken cancellationToken = default) => CreateAsync((TModel)model, cancellationToken); @@ -81,7 +81,7 @@ public virtual async Task DeleteAsync(TModel model, CancellationToken cancellati throw new ArgumentNullException(nameof(model)); // Process cascade delete. - var referencesIdsPaths = DbContext.SchemaRegister.GetIdMemberDependenciesFromRootModel(typeof(TModel)) + var referencesIdsPaths = DbContext.SchemaRegistry.GetIdMemberDependenciesFromRootModel(typeof(TModel)) .Where(d => d.UseCascadeDelete) .Where(d => d.EntityClassMapPath.Count() == 2) //ignore references of references .DistinctBy(d => d.FullPathToString()) @@ -169,7 +169,7 @@ private async Task CascadeDeleteMembersAsync(object currentModel, IEnumerable _modelCollectionRepositoryMap = default!; - private IReadOnlyDictionary _modelGridFSRepositoryMap = default!; - private IReadOnlyDictionary _modelRepositoryMap = default!; + private IReadOnlyDictionary _collectionRepositoriesByModelType = default!; + private IReadOnlyDictionary _gridFSRepositoriesByModelType = default!; + private IReadOnlyDictionary _repositoriesByModelType = default!; // Initializer. public void Initialize(IDbContext dbContext) @@ -40,11 +40,11 @@ public void Initialize(IDbContext dbContext) // Properties. public bool IsInitialized { get; private set; } - public IReadOnlyDictionary ModelCollectionRepositoryMap + public IReadOnlyDictionary CollectionRepositoriesByModelType { get { - if (_modelCollectionRepositoryMap is null) + if (_collectionRepositoriesByModelType is null) { var dbContextType = dbContext.GetType(); @@ -67,20 +67,20 @@ public IReadOnlyDictionary ModelCollectionRepositor return false; }); - // Initialize register. - _modelCollectionRepositoryMap = repos.ToDictionary( + // Initialize registry. + _collectionRepositoriesByModelType = repos.ToDictionary( prop => ((ICollectionRepository)prop.GetValue(dbContext)).GetModelType, prop => (ICollectionRepository)prop.GetValue(dbContext)); } - return _modelCollectionRepositoryMap; + return _collectionRepositoriesByModelType; } } - public IReadOnlyDictionary ModelGridFSRepositoryMap + public IReadOnlyDictionary GridFSRepositoriesByModelType { get { - if (_modelGridFSRepositoryMap is null) + if (_gridFSRepositoriesByModelType is null) { var dbContextType = dbContext.GetType(); @@ -103,33 +103,33 @@ public IReadOnlyDictionary ModelGridFSRepositoryMap return false; }); - //construct register - _modelGridFSRepositoryMap = repos.ToDictionary( + //construct registry + _gridFSRepositoriesByModelType = repos.ToDictionary( prop => ((IGridFSRepository)prop.GetValue(dbContext)).GetModelType, prop => (IGridFSRepository)prop.GetValue(dbContext)); } - return _modelGridFSRepositoryMap; + return _gridFSRepositoriesByModelType; } } - public IReadOnlyDictionary ModelRepositoryMap + public IReadOnlyDictionary RepositoriesByModelType { get { - if (_modelRepositoryMap is null) + if (_repositoriesByModelType is null) { var repoMap = new Dictionary(); - foreach (var pair in ModelCollectionRepositoryMap) + foreach (var pair in CollectionRepositoriesByModelType) repoMap.Add(pair.Key, pair.Value); - foreach (var pair in ModelGridFSRepositoryMap) + foreach (var pair in GridFSRepositoriesByModelType) repoMap.Add(pair.Key, pair.Value); - _modelRepositoryMap = repoMap; + _repositoriesByModelType = repoMap; } - return _modelRepositoryMap; + return _repositoriesByModelType; } } } diff --git a/src/MongODM.Core/Serialization/ExtendedBsonDocumentWriter.cs b/src/MongODM.Core/Serialization/ExtendedBsonDocumentWriter.cs index a3131582..ad4191db 100644 --- a/src/MongODM.Core/Serialization/ExtendedBsonDocumentWriter.cs +++ b/src/MongODM.Core/Serialization/ExtendedBsonDocumentWriter.cs @@ -12,8 +12,8 @@ // See the License for the specific language governing permissions and // limitations under the License. -using MongoDB.Bson; -using MongoDB.Bson.IO; +using Etherna.MongoDB.Bson; +using Etherna.MongoDB.Bson.IO; namespace Etherna.MongODM.Core.Serialization { diff --git a/src/MongODM.Core/Serialization/Mapping/DiscriminatorRegistry.cs b/src/MongODM.Core/Serialization/Mapping/DiscriminatorRegistry.cs new file mode 100644 index 00000000..49ec517c --- /dev/null +++ b/src/MongODM.Core/Serialization/Mapping/DiscriminatorRegistry.cs @@ -0,0 +1,198 @@ +// 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.MongoDB.Bson; +using Etherna.MongoDB.Bson.Serialization.Conventions; +using Etherna.MongODM.Core.Conventions; +using System; +using System.Collections.Generic; +using System.Reflection; +using System.Threading; + +namespace Etherna.MongODM.Core.Serialization.Mapping +{ + public class DiscriminatorRegistry : IDiscriminatorRegistry + { + // Fields. + private readonly ReaderWriterLockSlim configLock = new(LockRecursionPolicy.SupportsRecursion); + private readonly Dictionary discriminatorConventions = new(); + private readonly Dictionary> discriminators = new(); + private readonly HashSet discriminatedTypes = new(); + + private IDbContext dbContext = default!; + + // Constructor and initializer. + 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 void AddDiscriminator(Type type, BsonValue discriminator) + { + // Checks. + if (type is null) + throw new ArgumentNullException(nameof(type)); + if (discriminator is null) + throw new ArgumentNullException(nameof(discriminator)); + + var typeInfo = type.GetTypeInfo(); + if (typeInfo.IsInterface) + throw new BsonSerializationException($"Discriminators can only be registered for classes, not for interface {type.FullName}."); + + // Add discriminator. + configLock.EnterWriteLock(); + try + { + if (!discriminators.TryGetValue(discriminator, out HashSet hashSet)) + { + hashSet = new HashSet(); + discriminators.Add(discriminator, hashSet); + } + + if (!hashSet.Contains(type)) + { + hashSet.Add(type); + + //mark all base types as discriminated (so we know that it's worth reading a discriminator) + for (var baseType = typeInfo.BaseType; baseType != null; baseType = baseType.GetTypeInfo().BaseType) + discriminatedTypes.Add(baseType); + } + } + finally + { + configLock.ExitWriteLock(); + } + } + + public void AddDiscriminatorConvention(Type type, IDiscriminatorConvention convention) + { + if (type is null) + throw new ArgumentNullException(nameof(type)); + if (convention is null) + throw new ArgumentNullException(nameof(convention)); + + configLock.EnterWriteLock(); + try + { + if (!discriminatorConventions.ContainsKey(type)) + discriminatorConventions.Add(type, convention); + else + throw new BsonSerializationException($"There is already a discriminator convention registered for type {type.FullName}."); + } + finally + { + configLock.ExitWriteLock(); + } + } + + public bool IsTypeDiscriminated(Type type) + { + var typeInfo = type.GetTypeInfo(); + return typeInfo.IsInterface || discriminatedTypes.Contains(type); + } + + public Type LookupActualType(Type nominalType, BsonValue? discriminator) + { + if (discriminator == null) + return nominalType; + + configLock.EnterReadLock(); + try + { + Type? actualType = null; + + var nominalTypeInfo = nominalType.GetTypeInfo(); + if (discriminators.TryGetValue(discriminator, out HashSet hashSet)) + { + foreach (var type in hashSet) + { + if (nominalTypeInfo.IsAssignableFrom(type)) + { + if (actualType == null) + actualType = type; + else + throw new BsonSerializationException($"Ambiguous discriminator '{discriminator}'."); + } + } + } + + if (actualType == null) + throw new BsonSerializationException($"Unknown discriminator value '{discriminator}'."); + + return actualType; + } + finally + { + configLock.ExitReadLock(); + } + } + + public IDiscriminatorConvention LookupDiscriminatorConvention(Type type) + { + configLock.EnterReadLock(); + try + { + if (discriminatorConventions.TryGetValue(type, out IDiscriminatorConvention convention)) + return convention; + } + finally + { + configLock.ExitReadLock(); + } + + configLock.EnterWriteLock(); + try + { + if (!discriminatorConventions.TryGetValue(type, out IDiscriminatorConvention convention)) + { + var typeInfo = type.GetTypeInfo(); + if (type == typeof(object)) + { + //if there is no convention registered for object register the default one + convention = new HierarchicalProxyTolerantDiscriminatorConvention(dbContext, "_t"); + AddDiscriminatorConvention(typeof(object), convention); + } + else if (typeInfo.IsInterface) + { + // TODO: should convention for interfaces be inherited from parent interfaces? + convention = LookupDiscriminatorConvention(typeof(object)); + AddDiscriminatorConvention(type, convention); + } + else //type is not typeof(object), or interface + { + //inherit the discriminator convention from the closest parent that has one + convention = LookupDiscriminatorConvention(typeInfo.BaseType); + + //register the convention for current type + AddDiscriminatorConvention(type, convention); + } + } + + return convention; + } + finally + { + configLock.ExitWriteLock(); + } + } + } +} diff --git a/src/MongODM.Core/Serialization/Mapping/IDiscriminatorRegistry.cs b/src/MongODM.Core/Serialization/Mapping/IDiscriminatorRegistry.cs new file mode 100644 index 00000000..a0ee2e1b --- /dev/null +++ b/src/MongODM.Core/Serialization/Mapping/IDiscriminatorRegistry.cs @@ -0,0 +1,44 @@ +// 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.MongoDB.Bson; +using Etherna.MongoDB.Bson.Serialization.Conventions; +using System; + +namespace Etherna.MongODM.Core.Serialization.Mapping +{ + public interface IDiscriminatorRegistry : IDbContextInitializable + { + void AddDiscriminator(Type type, BsonValue discriminator); + + void AddDiscriminatorConvention(Type type, IDiscriminatorConvention convention); + + /// + /// Returns whether the given type has any discriminators registered for any of its subclasses. + /// + /// A Type. + /// True if the type is discriminated. + bool IsTypeDiscriminated(Type type); + + /// + /// Looks up the actual type of an object to be deserialized. + /// + /// The nominal type of the object. + /// The discriminator. + /// The actual type of the object. + Type LookupActualType(Type nominalType, BsonValue? discriminator); + + IDiscriminatorConvention LookupDiscriminatorConvention(Type type); + } +} \ No newline at end of file diff --git a/src/MongODM.Core/Serialization/Mapping/IModelMap.cs b/src/MongODM.Core/Serialization/Mapping/IModelMap.cs index 9008b398..9ba90ce0 100644 --- a/src/MongODM.Core/Serialization/Mapping/IModelMap.cs +++ b/src/MongODM.Core/Serialization/Mapping/IModelMap.cs @@ -12,9 +12,11 @@ // See the License for the specific language governing permissions and // limitations under the License. +using Etherna.MongoDB.Bson.Serialization; +using Etherna.MongODM.Core.Serialization.Serializers; using Etherna.MongODM.Core.Utility; -using MongoDB.Bson.Serialization; using System; +using System.Threading.Tasks; namespace Etherna.MongODM.Core.Serialization.Mapping { @@ -25,12 +27,19 @@ public interface IModelMap : IFreezableConfig string? BaseModelMapId { get; } BsonClassMap BsonClassMap { get; } IBsonSerializer BsonClassMapSerializer { get; } - public bool IsEntity { get; } - public Type ModelType { get; } - public IBsonSerializer? Serializer { get; } + bool IsEntity { get; } + Type ModelType { get; } + IBsonSerializer? Serializer { get; } // Methods. + Task FixDeserializedModelAsync(object model); void SetBaseModelMap(IModelMap baseModelMap); void UseProxyGenerator(IDbContext dbContext); } + + public interface IModelMap : IModelMap + { + // Methods. + Task FixDeserializedModelAsync(TModel model); + } } \ No newline at end of file diff --git a/src/MongODM.Core/Serialization/Mapping/ISchemaRegister.cs b/src/MongODM.Core/Serialization/Mapping/ISchemaRegistry.cs similarity index 86% rename from src/MongODM.Core/Serialization/Mapping/ISchemaRegister.cs rename to src/MongODM.Core/Serialization/Mapping/ISchemaRegistry.cs index 4f36761a..1d86eb9f 100644 --- a/src/MongODM.Core/Serialization/Mapping/ISchemaRegister.cs +++ b/src/MongODM.Core/Serialization/Mapping/ISchemaRegistry.cs @@ -12,10 +12,10 @@ // See the License for the specific language governing permissions and // limitations under the License. +using Etherna.MongoDB.Bson; +using Etherna.MongoDB.Bson.Serialization; using Etherna.MongODM.Core.Serialization.Mapping.Schemas; using Etherna.MongODM.Core.Utility; -using MongoDB.Bson; -using MongoDB.Bson.Serialization; using System; using System.Collections.Generic; using System.Reflection; @@ -23,9 +23,9 @@ namespace Etherna.MongODM.Core.Serialization.Mapping { /// - /// Interface for implementation. + /// Interface for implementation. /// - public interface ISchemaRegister : IDbContextInitializable, IFreezableConfig + public interface ISchemaRegistry : IDbContextInitializable, IFreezableConfig { // Properties. /// @@ -68,6 +68,12 @@ IModelMapsSchemaBuilder AddModelMapsSchema( ModelMap activeModelMap) where TModel : class; + /// + /// Get active class map from schemas, or create a default classMap for model type + /// + /// The active model map + BsonClassMap GetActiveClassMap(Type modelType); + /// /// Return bson element for represent a model map id /// @@ -79,8 +85,9 @@ IModelMapsSchemaBuilder AddModelMapsSchema( /// Get all id member dependencies from a root model type /// /// The model type + /// If true, ignore secondary model maps /// The list of member dependencies - IEnumerable GetIdMemberDependenciesFromRootModel(Type modelType); + IEnumerable GetIdMemberDependenciesFromRootModel(Type modelType, bool onlyFromActiveModelMap = false); /// /// Get all member dependencies that points to a specific member definition diff --git a/src/MongODM.Core/Serialization/Mapping/MemberDependency.cs b/src/MongODM.Core/Serialization/Mapping/MemberDependency.cs index 9584739e..94c4a901 100644 --- a/src/MongODM.Core/Serialization/Mapping/MemberDependency.cs +++ b/src/MongODM.Core/Serialization/Mapping/MemberDependency.cs @@ -12,8 +12,8 @@ // See the License for the specific language governing permissions and // limitations under the License. +using Etherna.MongoDB.Bson.Serialization; using Etherna.MongODM.Core.Extensions; -using MongoDB.Bson.Serialization; using System; using System.Collections.Generic; using System.Linq; @@ -32,6 +32,7 @@ public class MemberDependency // Constructors. public MemberDependency( IModelMap rootModelMap, + bool rootModelMapIsActive, IEnumerable memberPath, bool useCascadeDelete) { @@ -39,6 +40,7 @@ public MemberDependency( if (!memberPath.Any()) throw new ArgumentException("Member path can't be empty", nameof(memberPath)); RootModelMap = rootModelMap ?? throw new ArgumentNullException(nameof(rootModelMap)); + RootModelMapIsActive = rootModelMapIsActive; UseCascadeDelete = useCascadeDelete; } @@ -119,6 +121,11 @@ public IEnumerable MemberPathToLastEntityModelId /// public IModelMap RootModelMap { get; } + /// + /// True if root model map is the currently active in schema + /// + public bool RootModelMapIsActive { get; } + /// /// True if requested to apply cascade delete /// diff --git a/src/MongODM.Core/Serialization/Mapping/ModelMap.cs b/src/MongODM.Core/Serialization/Mapping/ModelMap.cs index e6a612be..b56bd7e2 100644 --- a/src/MongODM.Core/Serialization/Mapping/ModelMap.cs +++ b/src/MongODM.Core/Serialization/Mapping/ModelMap.cs @@ -12,11 +12,14 @@ // See the License for the specific language governing permissions and // limitations under the License. +using Etherna.MongoDB.Bson.Serialization; using Etherna.MongODM.Core.Extensions; using Etherna.MongODM.Core.Serialization.Serializers; using Etherna.MongODM.Core.Utility; -using MongoDB.Bson.Serialization; using System; +using System.Linq; +using System.Reflection; +using System.Threading.Tasks; namespace Etherna.MongODM.Core.Serialization.Mapping { @@ -63,6 +66,9 @@ public IBsonSerializer BsonClassMapSerializer public IBsonSerializer? Serializer { get; } // Methods. + public Task FixDeserializedModelAsync(object model) => + FixDeserializedModelHelperAsync(model); + public void SetBaseModelMap(IModelMap baseModelMap) => ExecuteConfigAction(() => { @@ -81,11 +87,29 @@ public void UseProxyGenerator(IDbContext dbContext) => if (ModelType.IsAbstract) throw new InvalidOperationException("Can't generate proxy of an abstract model"); + // Remove CreatorMaps. + while (BsonClassMap.CreatorMaps.Any()) + { + var memberInfo = BsonClassMap.CreatorMaps.First().MemberInfo; + switch (memberInfo) + { + case ConstructorInfo constructorInfo: + BsonClassMap.UnmapConstructor(constructorInfo); + break; + case MethodInfo methodInfo: + BsonClassMap.UnmapFactoryMethod(methodInfo); + break; + default: throw new InvalidOperationException(); + } + } + // Set creator. BsonClassMap.SetCreator(() => dbContext.ProxyGenerator.CreateInstance(ModelType, dbContext)); }); // Protected methods. + protected abstract Task FixDeserializedModelHelperAsync(object model); + protected override void FreezeAction() { // Freeze bson class maps. @@ -99,24 +123,45 @@ public static IBsonSerializer GetDefaultSerializer(IDbContext db if (dbContext is null) throw new ArgumentNullException(nameof(dbContext)); - return new ModelMapSerializer( - dbContext.DbCache, - dbContext.DocumentSemVerOptions, - dbContext.ModelMapVersionOptions, - dbContext.SchemaRegister, - dbContext.SerializerModifierAccessor); + return new ModelMapSerializer(dbContext); } } - public class ModelMap : ModelMap + public class ModelMap : ModelMap, IModelMap { + private readonly Func>? fixDeserializedModelFunc; + // Constructors. public ModelMap( string id, - BsonClassMap bsonClassMap, + BsonClassMap? bsonClassMap = null, string? baseModelMapId = null, + Func>? fixDeserializedModelFunc = null, IBsonSerializer? serializer = null) - : base(id, baseModelMapId, bsonClassMap, serializer) - { } + : base(id, baseModelMapId, bsonClassMap ?? new BsonClassMap(cm => cm.AutoMap()), serializer) + { + this.fixDeserializedModelFunc = fixDeserializedModelFunc; + } + + // Methods. + public async Task FixDeserializedModelAsync(TModel model) + { + if (model is null) + throw new ArgumentNullException(nameof(model)); + + return (TModel)await FixDeserializedModelHelperAsync(model).ConfigureAwait(false); + } + + // Protected methods. + protected override async Task FixDeserializedModelHelperAsync( + object model) + { + if (model is null) + throw new ArgumentNullException(nameof(model)); + + return fixDeserializedModelFunc is not null ? + (await fixDeserializedModelFunc((TModel)model).ConfigureAwait(false))! : + model; + } } } diff --git a/src/MongODM.Core/Serialization/Mapping/OwnedBsonMemberMap.cs b/src/MongODM.Core/Serialization/Mapping/OwnedBsonMemberMap.cs index b940278e..daa68f1a 100644 --- a/src/MongODM.Core/Serialization/Mapping/OwnedBsonMemberMap.cs +++ b/src/MongODM.Core/Serialization/Mapping/OwnedBsonMemberMap.cs @@ -12,7 +12,7 @@ // See the License for the specific language governing permissions and // limitations under the License. -using MongoDB.Bson.Serialization; +using Etherna.MongoDB.Bson.Serialization; namespace Etherna.MongODM.Core.Serialization.Mapping { diff --git a/src/MongODM.Core/Serialization/Mapping/SchemaRegister.cs b/src/MongODM.Core/Serialization/Mapping/SchemaRegistry.cs similarity index 87% rename from src/MongODM.Core/Serialization/Mapping/SchemaRegister.cs rename to src/MongODM.Core/Serialization/Mapping/SchemaRegistry.cs index 52eebeb4..b9c488cd 100644 --- a/src/MongODM.Core/Serialization/Mapping/SchemaRegister.cs +++ b/src/MongODM.Core/Serialization/Mapping/SchemaRegistry.cs @@ -12,13 +12,14 @@ // See the License for the specific language governing permissions and // limitations under the License. +using Etherna.MongoDB.Bson; +using Etherna.MongoDB.Bson.Serialization; using Etherna.MongODM.Core.Extensions; using Etherna.MongODM.Core.Serialization.Mapping.Schemas; using Etherna.MongODM.Core.Serialization.Serializers; using Etherna.MongODM.Core.Utility; -using MongoDB.Bson; -using MongoDB.Bson.Serialization; using System; +using System.Collections.Concurrent; using System.Collections.Generic; using System.Diagnostics.CodeAnalysis; using System.Linq; @@ -27,12 +28,13 @@ namespace Etherna.MongODM.Core.Serialization.Mapping { - public class SchemaRegister : FreezableConfig, ISchemaRegister + public class SchemaRegistry : FreezableConfig, ISchemaRegistry { // Fields. private readonly Dictionary _schemas = new(); private readonly Dictionary activeModelMapIdBsonElement = new(); + private readonly ConcurrentDictionary defaultClassMapsCache = new(); private readonly Dictionary> memberInfoToMemberMapsDictionary = new(); private readonly Dictionary> modelTypeToReferencedIdMemberMapsDictionary = new(); @@ -77,8 +79,7 @@ public IModelMapsSchemaBuilder AddModelMapsSchema( var modelMap = new ModelMap( activeModelMapId, new BsonClassMap(activeModelMapInitializer ?? (cm => cm.AutoMap())), - null, - customSerializer ?? ModelMap.GetDefaultSerializer(dbContext)); + serializer: customSerializer ?? ModelMap.GetDefaultSerializer(dbContext)); return AddModelMapsSchema(modelMap); } @@ -105,6 +106,29 @@ public IModelMapsSchemaBuilder AddModelMapsSchema( return modelSchemaConfiguration; }); + public BsonClassMap GetActiveClassMap(Type modelType) + { + // If a schema is registered. + if (_schemas.ContainsKey(modelType) && + _schemas[modelType] is IModelMapsSchema modelMapSchema) + return modelMapSchema.ActiveMap.BsonClassMap; + + // If we don't have a model schema, look for a default classmap, or create it. + if (defaultClassMapsCache.ContainsKey(modelType)) + return defaultClassMapsCache[modelType]; + + var classMapDefinition = typeof(BsonClassMap<>); + var classMapType = classMapDefinition.MakeGenericType(modelType); + var classMap = (BsonClassMap)Activator.CreateInstance(classMapType); + classMap.AutoMap(); + + // Register classMap (if doesn't exist) with discriminator. + defaultClassMapsCache.TryAdd(modelType, classMap); + dbContext.DiscriminatorRegistry.AddDiscriminator(modelType, classMap.Discriminator); + + return classMap; + } + public BsonElement GetActiveModelMapIdBsonElement(Type modelType) { if (modelType is null) @@ -119,12 +143,12 @@ public BsonElement GetActiveModelMapIdBsonElement(Type modelType) return activeModelMapIdBsonElement[modelType]; } - public IEnumerable GetIdMemberDependenciesFromRootModel(Type modelType) + public IEnumerable GetIdMemberDependenciesFromRootModel(Type modelType, bool onlyFromActiveModelMap = false) { Freeze(); //needed for initialization if (modelTypeToReferencedIdMemberMapsDictionary.TryGetValue(modelType, out List dependencies)) - return dependencies; + return dependencies.Where(d => !onlyFromActiveModelMap || d.RootModelMapIsActive); return Array.Empty(); } @@ -144,7 +168,7 @@ public IModelMapsSchema GetModelMapsSchema(Type modelType) if (modelType is null) throw new ArgumentNullException(nameof(modelType)); if (!_schemas.ContainsKey(modelType)) - throw new InvalidOperationException(modelType.Name + " schema is not registered"); + throw new KeyNotFoundException(modelType.Name + " schema is not registered"); var schema = _schemas[modelType]; @@ -199,12 +223,12 @@ protected override void FreezeAction() // Register active serializer. if (schema.ActiveSerializer != null) - BsonSerializer.RegisterSerializer(schema.ModelType, schema.ActiveSerializer); + ((BsonSerializerRegistry)dbContext.SerializerRegistry).RegisterSerializer(schema.ModelType, schema.ActiveSerializer); // Register discriminators for all bson class maps. if (schema is IModelMapsSchema modelMapsSchema) foreach (var modelMap in modelMapsSchema.AllMapsDictionary.Values) - BsonSerializer.RegisterDiscriminator(modelMapsSchema.ModelType, modelMap.BsonClassMap.Discriminator); + dbContext.DiscriminatorRegistry.AddDiscriminator(modelMapsSchema.ModelType, modelMap.BsonClassMap.Discriminator); } // Specific for model maps schemas. @@ -220,6 +244,7 @@ protected override void FreezeAction() foreach (var modelMap in schema.AllMapsDictionary.Values) CompileDependencyRegisters( modelMap, + modelMap == schema.ActiveMap, modelMap.BsonClassMap, default, Array.Empty()); @@ -235,7 +260,7 @@ protected override void FreezeAction() activeModelMapIdBsonElement.Add( schema.ModelType, new BsonElement( - dbContext.ModelMapVersionOptions.ElementName, + dbContext.Options.ModelMapVersion.ElementName, new BsonString(notProxySchema.ActiveMap.Id))); } } @@ -243,6 +268,7 @@ protected override void FreezeAction() // Helpers. private void CompileDependencyRegisters( IModelMap modelMap, + bool modelMapIsActive, BsonClassMap currentClassMap, BsonClassMap? lastEntityClassMap, IEnumerable ownedBsonMemberPath, @@ -268,6 +294,7 @@ private void CompileDependencyRegisters( // Identify current member with its root model map, the path from current model map, and cascade delete information. var memberDependency = new MemberDependency( modelMap, + modelMapIsActive, currentMemberPath, useCascadeDeleteSetting ?? false); @@ -306,11 +333,9 @@ private void CompileDependencyRegisters( //model maps schema serializers if (memberSerializer is IModelMapsContainerSerializer schemaSerializer) { -#pragma warning disable CA1508 // Avoid dead conditional code. Here code analyzer is wrong bool? useCascadeDelete = (memberSerializer as IReferenceContainerSerializer)?.UseCascadeDelete; -#pragma warning restore CA1508 // Avoid dead conditional code foreach (var childClassMap in schemaSerializer.AllChildClassMaps) - CompileDependencyRegisters(modelMap, childClassMap, lastEntityClassMap, currentMemberPath, useCascadeDelete); + CompileDependencyRegisters(modelMap, modelMapIsActive, childClassMap, lastEntityClassMap, currentMemberPath, useCascadeDelete); } } } @@ -332,6 +357,7 @@ private IModelMapsSchema CreateNewDefaultModelMapsSchema(Type modelType) Guid.NewGuid().ToString(), //string id classMap, //BsonClassMap bsonClassMap null, //string? baseModelMapId + null, //Func>? fixDeserializedModelFunc null); //IBsonSerializer? serializer //model maps schema diff --git a/src/MongODM.Core/Serialization/Mapping/Schemas/CustomSerializerSchema.cs b/src/MongODM.Core/Serialization/Mapping/Schemas/CustomSerializerSchema.cs index 192b0537..bc3a5b2a 100644 --- a/src/MongODM.Core/Serialization/Mapping/Schemas/CustomSerializerSchema.cs +++ b/src/MongODM.Core/Serialization/Mapping/Schemas/CustomSerializerSchema.cs @@ -12,7 +12,7 @@ // See the License for the specific language governing permissions and // limitations under the License. -using MongoDB.Bson.Serialization; +using Etherna.MongoDB.Bson.Serialization; using System; namespace Etherna.MongODM.Core.Serialization.Mapping.Schemas diff --git a/src/MongODM.Core/Serialization/Mapping/Schemas/IModelMapsSchema.cs b/src/MongODM.Core/Serialization/Mapping/Schemas/IModelMapsSchema.cs index fa31c451..695d631e 100644 --- a/src/MongODM.Core/Serialization/Mapping/Schemas/IModelMapsSchema.cs +++ b/src/MongODM.Core/Serialization/Mapping/Schemas/IModelMapsSchema.cs @@ -12,7 +12,7 @@ // See the License for the specific language governing permissions and // limitations under the License. -using MongoDB.Bson.Serialization; +using Etherna.MongoDB.Bson.Serialization; using System.Collections.Generic; namespace Etherna.MongODM.Core.Serialization.Mapping.Schemas @@ -24,6 +24,7 @@ public interface IModelMapsSchema : ISchema IModelMap ActiveMap { get; } IReadOnlyDictionary AllMapsDictionary { get; } IDbContext DbContext { get; } + IModelMap? FallbackModelMap { get; } IBsonSerializer? FallbackSerializer { get; } IEnumerable SecondaryMaps { get; } } diff --git a/src/MongODM.Core/Serialization/Mapping/Schemas/IModelMapsSchemaBuilder.cs b/src/MongODM.Core/Serialization/Mapping/Schemas/IModelMapsSchemaBuilder.cs index e26ad5d0..5a3f0f7e 100644 --- a/src/MongODM.Core/Serialization/Mapping/Schemas/IModelMapsSchemaBuilder.cs +++ b/src/MongODM.Core/Serialization/Mapping/Schemas/IModelMapsSchemaBuilder.cs @@ -12,17 +12,16 @@ // See the License for the specific language governing permissions and // limitations under the License. -using MongoDB.Bson.Serialization; +using Etherna.MongoDB.Bson.Serialization; using System; namespace Etherna.MongODM.Core.Serialization.Mapping.Schemas { public interface IModelMapsSchemaBuilder - where TModel : class { // Methods. /// - /// Add a fallback serializer invoked in case of undefined schema id + /// Add a fallback serializer invoked in case of unrecognized schema id /// /// Fallback serializer /// This same model schema @@ -30,19 +29,25 @@ IModelMapsSchemaBuilder AddFallbackCustomSerializer( IBsonSerializer fallbackSerializer); /// - /// Register a secondary model map + /// Add a fallback model map invoked in case of unrecognized schema id, and absence of fallback serializer /// - /// The map Id /// The model map inizializer /// Id of the base model map for this model map /// Custom serializer /// This same model schema configuration - IModelMapsSchemaBuilder AddSecondaryMap( - string id, + IModelMapsSchemaBuilder AddFallbackModelMap( Action>? modelMapInitializer = null, string? baseModelMapId = null, IBsonSerializer? customSerializer = null); + /// + /// Add a fallback model map invoked in case of unrecognized schema id, and absence of fallback serializer + /// + /// The model map + /// This same model schema configuration + IModelMapsSchemaBuilder AddFallbackModelMap( + ModelMap modelMap); + /// /// Register a secondary model map /// diff --git a/src/MongODM.Core/Serialization/Mapping/Schemas/IReferenceModelMapsSchemaBuilder.cs b/src/MongODM.Core/Serialization/Mapping/Schemas/IReferenceModelMapsSchemaBuilder.cs index 586def86..36d82ee4 100644 --- a/src/MongODM.Core/Serialization/Mapping/Schemas/IReferenceModelMapsSchemaBuilder.cs +++ b/src/MongODM.Core/Serialization/Mapping/Schemas/IReferenceModelMapsSchemaBuilder.cs @@ -12,7 +12,7 @@ // See the License for the specific language governing permissions and // limitations under the License. -using MongoDB.Bson.Serialization; +using Etherna.MongoDB.Bson.Serialization; using System; namespace Etherna.MongODM.Core.Serialization.Mapping.Schemas @@ -21,24 +21,42 @@ public interface IReferenceModelMapsSchemaBuilder { // Methods. /// - /// Add a fallback serializer invoked in case of undefined schema id + /// Add a fallback serializer invoked in case of unrecognized schema id /// /// Fallback serializer /// This same model schema IReferenceModelMapsSchemaBuilder AddFallbackCustomSerializer( IBsonSerializer fallbackSerializer); + /// + /// Add a fallback model map invoked in case of unrecognized schema id, and absence of fallback serializer + /// + /// The model map inizializer + /// Id of the base model map for this model map + /// This same model schema configuration + IReferenceModelMapsSchemaBuilder AddFallbackModelMap( + Action>? modelMapInitializer = null, + string? baseModelMapId = null); + + /// + /// Add a fallback model map invoked in case of unrecognized schema id, and absence of fallback serializer + /// + /// The model map + /// This same model schema configuration + IReferenceModelMapsSchemaBuilder AddFallbackModelMap( + ModelMap modelMap); + /// /// Register a secondary model map /// /// The map Id - /// Id of the base model map for this model map /// The model map inizializer + /// Id of the base model map for this model map /// This same model schema configuration IReferenceModelMapsSchemaBuilder AddSecondaryMap( string id, - string? baseModelMapId = null, - Action>? modelMapInitializer = null); + Action>? modelMapInitializer = null, + string? baseModelMapId = null); /// /// Register a secondary model map diff --git a/src/MongODM.Core/Serialization/Mapping/Schemas/ISchema.cs b/src/MongODM.Core/Serialization/Mapping/Schemas/ISchema.cs index c65768fe..fb2801e7 100644 --- a/src/MongODM.Core/Serialization/Mapping/Schemas/ISchema.cs +++ b/src/MongODM.Core/Serialization/Mapping/Schemas/ISchema.cs @@ -12,8 +12,8 @@ // See the License for the specific language governing permissions and // limitations under the License. +using Etherna.MongoDB.Bson.Serialization; using Etherna.MongODM.Core.Utility; -using MongoDB.Bson.Serialization; using System; namespace Etherna.MongODM.Core.Serialization.Mapping.Schemas diff --git a/src/MongODM.Core/Serialization/Mapping/Schemas/ModelMapsSchema.cs b/src/MongODM.Core/Serialization/Mapping/Schemas/ModelMapsSchema.cs index 68a07b4c..1cc95dfa 100644 --- a/src/MongODM.Core/Serialization/Mapping/Schemas/ModelMapsSchema.cs +++ b/src/MongODM.Core/Serialization/Mapping/Schemas/ModelMapsSchema.cs @@ -12,7 +12,7 @@ // See the License for the specific language governing permissions and // limitations under the License. -using MongoDB.Bson.Serialization; +using Etherna.MongoDB.Bson.Serialization; using System; using System.Diagnostics.CodeAnalysis; @@ -36,24 +36,20 @@ public IModelMapsSchemaBuilder AddFallbackCustomSerializer(IBsonSerializ } [SuppressMessage("Reliability", "CA2000:Dispose objects before losing scope", Justification = "The new model map instance can't be disposed")] - public IModelMapsSchemaBuilder AddSecondaryMap( - string id, + public IModelMapsSchemaBuilder AddFallbackModelMap( Action>? modelMapInitializer = null, string? baseModelMapId = null, - IBsonSerializer? customSerializer = null) - { - // Verify if needs a default serializer. - if (!typeof(TModel).IsAbstract) - customSerializer ??= ModelMap.GetDefaultSerializer(DbContext); - - // Create model map. - var modelMap = new ModelMap( - id, + IBsonSerializer? customSerializer = null) => + AddFallbackModelMap(new ModelMap( + "fallback", new BsonClassMap(modelMapInitializer ?? (cm => cm.AutoMap())), baseModelMapId, - customSerializer); + serializer: customSerializer)); - return AddSecondaryMap(modelMap); + public IModelMapsSchemaBuilder AddFallbackModelMap(ModelMap modelMap) + { + AddFallbackModelMapHelper(modelMap); + return this; } public IModelMapsSchemaBuilder AddSecondaryMap(ModelMap modelMap) diff --git a/src/MongODM.Core/Serialization/Mapping/Schemas/ModelMapsSchemaBase.cs b/src/MongODM.Core/Serialization/Mapping/Schemas/ModelMapsSchemaBase.cs index a208a8cf..0b567ac2 100644 --- a/src/MongODM.Core/Serialization/Mapping/Schemas/ModelMapsSchemaBase.cs +++ b/src/MongODM.Core/Serialization/Mapping/Schemas/ModelMapsSchemaBase.cs @@ -12,7 +12,7 @@ // See the License for the specific language governing permissions and // limitations under the License. -using MongoDB.Bson.Serialization; +using Etherna.MongoDB.Bson.Serialization; using System; using System.Collections.Generic; using System.Linq; @@ -69,6 +69,7 @@ public IReadOnlyDictionary AllMapsDictionary } } public IDbContext DbContext { get; } + public IModelMap? FallbackModelMap { get; protected set; } public IBsonSerializer? FallbackSerializer { get; protected set; } public override Type? ProxyModelType { get; } public IEnumerable SecondaryMaps => _secondaryMaps; @@ -79,12 +80,23 @@ protected void AddFallbackCustomSerializerHelper(IBsonSerializer fallbackSeriali { if (fallbackSerializer is null) throw new ArgumentNullException(nameof(fallbackSerializer)); - if (FallbackSerializer != null) + if (FallbackSerializer is not null) throw new InvalidOperationException("Fallback serializer already setted"); FallbackSerializer = fallbackSerializer; }); + protected void AddFallbackModelMapHelper(IModelMap fallbackModelMap) => + ExecuteConfigAction(() => + { + if (fallbackModelMap is null) + throw new ArgumentNullException(nameof(fallbackModelMap)); + if (FallbackModelMap is not null) + throw new InvalidOperationException("Fallback model map already setted"); + + FallbackModelMap = fallbackModelMap; + }); + protected void AddSecondaryMapHelper(IModelMap modelMap) => ExecuteConfigAction(() => { @@ -104,8 +116,11 @@ protected override void FreezeAction() { // Freeze model maps. ActiveMap.Freeze(); + foreach (var secondaryMap in _secondaryMaps) secondaryMap.Freeze(); + + FallbackModelMap?.Freeze(); } } } diff --git a/src/MongODM.Core/Serialization/Mapping/Schemas/ReferenceModelMapsSchema.cs b/src/MongODM.Core/Serialization/Mapping/Schemas/ReferenceModelMapsSchema.cs index 3f925241..1a76c537 100644 --- a/src/MongODM.Core/Serialization/Mapping/Schemas/ReferenceModelMapsSchema.cs +++ b/src/MongODM.Core/Serialization/Mapping/Schemas/ReferenceModelMapsSchema.cs @@ -12,7 +12,7 @@ // See the License for the specific language governing permissions and // limitations under the License. -using MongoDB.Bson.Serialization; +using Etherna.MongoDB.Bson.Serialization; using System; using System.Diagnostics.CodeAnalysis; @@ -34,11 +34,26 @@ public IReferenceModelMapsSchemaBuilder AddFallbackCustomSerializer(IBso return this; } + [SuppressMessage("Reliability", "CA2000:Dispose objects before losing scope", Justification = "The new model map instance can't be disposed")] + public IReferenceModelMapsSchemaBuilder AddFallbackModelMap( + Action>? modelMapInitializer = null, + string? baseModelMapId = null) => + AddFallbackModelMap(new ModelMap( + "fallback", + new BsonClassMap(modelMapInitializer ?? (cm => cm.AutoMap())), + baseModelMapId)); + + public IReferenceModelMapsSchemaBuilder AddFallbackModelMap(ModelMap modelMap) + { + AddFallbackModelMapHelper(modelMap); + return this; + } + [SuppressMessage("Reliability", "CA2000:Dispose objects before losing scope", Justification = "The new model map instance can't be disposed")] public IReferenceModelMapsSchemaBuilder AddSecondaryMap( string id, - string? baseModelMapId = null, - Action>? modelMapInitializer = null) => + Action>? modelMapInitializer = null, + string? baseModelMapId = null) => AddSecondaryMap(new ModelMap( id, new BsonClassMap(modelMapInitializer ?? (cm => cm.AutoMap())), diff --git a/src/MongODM.Core/Serialization/Mapping/Schemas/SchemaBase.cs b/src/MongODM.Core/Serialization/Mapping/Schemas/SchemaBase.cs index 48fd499f..1dad67ca 100644 --- a/src/MongODM.Core/Serialization/Mapping/Schemas/SchemaBase.cs +++ b/src/MongODM.Core/Serialization/Mapping/Schemas/SchemaBase.cs @@ -12,8 +12,8 @@ // See the License for the specific language governing permissions and // limitations under the License. +using Etherna.MongoDB.Bson.Serialization; using Etherna.MongODM.Core.Utility; -using MongoDB.Bson.Serialization; using System; namespace Etherna.MongODM.Core.Serialization.Mapping.Schemas diff --git a/src/MongODM.Core/Serialization/Providers/ModelMapSerializationProvider.cs b/src/MongODM.Core/Serialization/Providers/ModelMapSerializationProvider.cs new file mode 100644 index 00000000..5fa0cc2a --- /dev/null +++ b/src/MongODM.Core/Serialization/Providers/ModelMapSerializationProvider.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 Etherna.MongoDB.Bson; +using Etherna.MongoDB.Bson.Serialization; +using Etherna.MongODM.Core.Serialization.Serializers; +using System; +using System.Globalization; +using System.Reflection; + +namespace Etherna.MongODM.Core.Serialization.Providers +{ + public class ModelMapSerializationProvider : BsonSerializationProviderBase + { + // Fields. + private readonly DbContext dbContext; + + // Constructor. + public ModelMapSerializationProvider(DbContext dbContext) + { + this.dbContext = dbContext; + } + + // Methods. + public override IBsonSerializer? GetSerializer(Type type, IBsonSerializerRegistry serializerRegistry) + { + if (type == null) + throw new ArgumentNullException(nameof(type)); + + var typeInfo = type.GetTypeInfo(); + if (typeInfo.IsGenericType && typeInfo.ContainsGenericParameters) + { + var message = string.Format(CultureInfo.InvariantCulture, "Generic type {0} has unassigned type parameters.", BsonUtils.GetFriendlyTypeName(type)); + throw new ArgumentException(message, nameof(type)); + } + + if ((typeInfo.IsClass || (typeInfo.IsValueType && !typeInfo.IsPrimitive)) && + !typeof(Array).GetTypeInfo().IsAssignableFrom(type) && + !typeof(Enum).GetTypeInfo().IsAssignableFrom(type)) + { + var modelMapSerializerDefinition = typeof(ModelMapSerializer<>); + var modelMapSerializerType = modelMapSerializerDefinition.MakeGenericType(type); + return (IBsonSerializer)Activator.CreateInstance(modelMapSerializerType, dbContext); + } + + return null; + } + } +} diff --git a/src/MongODM.Core/Serialization/SemanticVersion.cs b/src/MongODM.Core/Serialization/SemanticVersion.cs index 488fea21..3a91d6f1 100644 --- a/src/MongODM.Core/Serialization/SemanticVersion.cs +++ b/src/MongODM.Core/Serialization/SemanticVersion.cs @@ -12,7 +12,7 @@ // See the License for the specific language governing permissions and // limitations under the License. -using MongoDB.Bson; +using Etherna.MongoDB.Bson; using System; using System.Globalization; using System.Text; diff --git a/src/MongODM.Core/Serialization/Serializers/DictionarySerializer.cs b/src/MongODM.Core/Serialization/Serializers/DictionarySerializer.cs index 0e1eaa4e..93dd8ca2 100644 --- a/src/MongODM.Core/Serialization/Serializers/DictionarySerializer.cs +++ b/src/MongODM.Core/Serialization/Serializers/DictionarySerializer.cs @@ -12,9 +12,9 @@ // See the License for the specific language governing permissions and // limitations under the License. -using MongoDB.Bson.Serialization; -using MongoDB.Bson.Serialization.Options; -using MongoDB.Bson.Serialization.Serializers; +using Etherna.MongoDB.Bson.Serialization; +using Etherna.MongoDB.Bson.Serialization.Options; +using Etherna.MongoDB.Bson.Serialization.Serializers; using System; using System.Collections.Generic; diff --git a/src/MongODM.Core/Serialization/Serializers/EnumerableSerializer.cs b/src/MongODM.Core/Serialization/Serializers/EnumerableSerializer.cs index 10dff32d..27803b36 100644 --- a/src/MongODM.Core/Serialization/Serializers/EnumerableSerializer.cs +++ b/src/MongODM.Core/Serialization/Serializers/EnumerableSerializer.cs @@ -12,8 +12,8 @@ // See the License for the specific language governing permissions and // limitations under the License. -using MongoDB.Bson.Serialization; -using MongoDB.Bson.Serialization.Serializers; +using Etherna.MongoDB.Bson.Serialization; +using Etherna.MongoDB.Bson.Serialization.Serializers; using System; using System.Collections.Generic; diff --git a/src/MongODM.Core/Serialization/Serializers/ExtraElementsSerializer.cs b/src/MongODM.Core/Serialization/Serializers/ExtraElementsSerializer.cs index 9e4d969a..ba63af22 100644 --- a/src/MongODM.Core/Serialization/Serializers/ExtraElementsSerializer.cs +++ b/src/MongODM.Core/Serialization/Serializers/ExtraElementsSerializer.cs @@ -12,19 +12,30 @@ // See the License for the specific language governing permissions and // limitations under the License. -using MongoDB.Bson; -using MongoDB.Bson.IO; -using MongoDB.Bson.Serialization; -using MongoDB.Bson.Serialization.Serializers; +using Etherna.MongoDB.Bson; +using Etherna.MongoDB.Bson.IO; +using Etherna.MongoDB.Bson.Serialization; +using Etherna.MongoDB.Bson.Serialization.Serializers; using System; using System.Collections.Generic; namespace Etherna.MongODM.Core.Serialization.Serializers { + /// + /// Utility serializer used for help into document migration scripts. + /// public class ExtraElementsSerializer : SerializerBase { - private static ExtraElementsSerializer Instance { get; } = new ExtraElementsSerializer(); + // Fields. + private readonly IDbContext dbContext; + // Constructor. + public ExtraElementsSerializer(IDbContext dbContext) + { + this.dbContext = dbContext; + } + + // Methods. public override void Serialize(BsonSerializationContext context, BsonSerializationArgs args, object value) { if (context is null) @@ -51,13 +62,12 @@ public override void Serialize(BsonSerializationContext context, BsonSerializati } else { - BsonSerializer.Serialize(context.Writer, value); + var serializer = dbContext.SerializerRegistry.GetSerializer(); + serializer.Serialize(context, value); } } - // Static methods. - - public static TValue DeserializeValue( + public TValue DeserializeValue( object extraElements, IBsonSerializer? serializer = null) { @@ -71,13 +81,13 @@ public static TValue DeserializeValue( serializationContext.Writer.WriteStartDocument(); serializationContext.Writer.WriteName("container"); - Instance.Serialize(serializationContext, extraElements); + this.Serialize(serializationContext, extraElements); serializationContext.Writer.WriteEndDocument(); // Lookup for a serializer. if (serializer == null) { - serializer = BsonSerializer.LookupSerializer(); + serializer = dbContext.SerializerRegistry.GetSerializer(); } // Deserialize. diff --git a/src/MongODM.Core/Serialization/Serializers/GeoPointSerializer.cs b/src/MongODM.Core/Serialization/Serializers/GeoPointSerializer.cs index abdbe92b..edeaa571 100644 --- a/src/MongODM.Core/Serialization/Serializers/GeoPointSerializer.cs +++ b/src/MongODM.Core/Serialization/Serializers/GeoPointSerializer.cs @@ -12,11 +12,11 @@ // See the License for the specific language governing permissions and // limitations under the License. +using Etherna.MongoDB.Bson.Serialization; +using Etherna.MongoDB.Bson.Serialization.Serializers; +using Etherna.MongoDB.Driver.GeoJsonObjectModel; +using Etherna.MongoDB.Driver.GeoJsonObjectModel.Serializers; using Etherna.MongODM.Core.ProxyModels; -using MongoDB.Bson.Serialization; -using MongoDB.Bson.Serialization.Serializers; -using MongoDB.Driver.GeoJsonObjectModel; -using MongoDB.Driver.GeoJsonObjectModel.Serializers; using System; using System.Linq.Expressions; using System.Reflection; diff --git a/src/MongODM.Core/Serialization/Serializers/HexToBinaryDataSerializer.cs b/src/MongODM.Core/Serialization/Serializers/HexToBinaryDataSerializer.cs index c6fc38d8..9feed504 100644 --- a/src/MongODM.Core/Serialization/Serializers/HexToBinaryDataSerializer.cs +++ b/src/MongODM.Core/Serialization/Serializers/HexToBinaryDataSerializer.cs @@ -12,9 +12,9 @@ // See the License for the specific language governing permissions and // limitations under the License. -using MongoDB.Bson; -using MongoDB.Bson.Serialization; -using MongoDB.Bson.Serialization.Serializers; +using Etherna.MongoDB.Bson; +using Etherna.MongoDB.Bson.Serialization; +using Etherna.MongoDB.Bson.Serialization.Serializers; using System; namespace Etherna.MongODM.Core.Serialization.Serializers diff --git a/src/MongODM.Core/Serialization/Serializers/IModelMapsContainerSerializer.cs b/src/MongODM.Core/Serialization/Serializers/IModelMapsContainerSerializer.cs index f4b14035..b66b9251 100644 --- a/src/MongODM.Core/Serialization/Serializers/IModelMapsContainerSerializer.cs +++ b/src/MongODM.Core/Serialization/Serializers/IModelMapsContainerSerializer.cs @@ -12,7 +12,7 @@ // See the License for the specific language governing permissions and // limitations under the License. -using MongoDB.Bson.Serialization; +using Etherna.MongoDB.Bson.Serialization; using System.Collections.Generic; namespace Etherna.MongODM.Core.Serialization.Serializers diff --git a/src/MongODM.Core/Serialization/Serializers/ModelMapSerializer.cs b/src/MongODM.Core/Serialization/Serializers/ModelMapSerializer.cs index 7ada6e5b..4b7a0cef 100644 --- a/src/MongODM.Core/Serialization/Serializers/ModelMapSerializer.cs +++ b/src/MongODM.Core/Serialization/Serializers/ModelMapSerializer.cs @@ -12,20 +12,18 @@ // See the License for the specific language governing permissions and // limitations under the License. +using Etherna.MongoDB.Bson; +using Etherna.MongoDB.Bson.IO; +using Etherna.MongoDB.Bson.Serialization; +using Etherna.MongoDB.Bson.Serialization.Conventions; +using Etherna.MongoDB.Bson.Serialization.Serializers; using Etherna.MongODM.Core.Domain.Models; -using Etherna.MongODM.Core.Options; using Etherna.MongODM.Core.ProxyModels; using Etherna.MongODM.Core.Serialization.Mapping; -using Etherna.MongODM.Core.Serialization.Modifiers; -using Etherna.MongODM.Core.Utility; -using MongoDB.Bson; -using MongoDB.Bson.IO; -using MongoDB.Bson.Serialization; -using MongoDB.Bson.Serialization.Conventions; -using MongoDB.Bson.Serialization.Serializers; using System; using System.Collections.Generic; using System.Linq; +using System.Threading.Tasks; namespace Etherna.MongODM.Core.Serialization.Serializers { @@ -35,45 +33,36 @@ public class ModelMapSerializer : { // Fields. private IDiscriminatorConvention _discriminatorConvention = default!; - private readonly IDbCache dbCache; private readonly BsonElement documentSemVerElement; - private readonly DocumentSemVerOptions documentSemVerOptions; - private readonly ModelMapVersionOptions modelMapVersionOptions; - private readonly ISchemaRegister schemaRegister; - private readonly ISerializerModifierAccessor serializerModifierAccessor; + private readonly IDbContext dbContext; // Constructor. public ModelMapSerializer( - IDbCache dbCache, - DocumentSemVerOptions documentSemVerOptions, - ModelMapVersionOptions modelMapVersionOptions, - ISchemaRegister schemaRegister, - ISerializerModifierAccessor serializerModifierAccessor) + IDbContext dbContext) { - this.dbCache = dbCache ?? throw new ArgumentNullException(nameof(dbCache)); - this.documentSemVerOptions = documentSemVerOptions ?? throw new ArgumentNullException(nameof(documentSemVerOptions)); - this.modelMapVersionOptions = modelMapVersionOptions ?? throw new ArgumentNullException(nameof(modelMapVersionOptions)); - this.schemaRegister = schemaRegister ?? throw new ArgumentNullException(nameof(schemaRegister)); - this.serializerModifierAccessor = serializerModifierAccessor ?? throw new ArgumentNullException(nameof(serializerModifierAccessor)); + if (dbContext is null) + throw new ArgumentNullException(nameof(dbContext)); + + this.dbContext = dbContext; documentSemVerElement = new BsonElement( - documentSemVerOptions.ElementName, - documentSemVerOptions.CurrentVersion.ToBsonArray()); + dbContext.Options.DocumentSemVer.ElementName, + dbContext.Options.DocumentSemVer.CurrentVersion.ToBsonArray()); } // Properties. - public IEnumerable AllChildClassMaps => schemaRegister.GetModelMapsSchema(typeof(TModel)) + public IEnumerable AllChildClassMaps => dbContext.SchemaRegistry.GetModelMapsSchema(typeof(TModel)) .AllMapsDictionary.Values.Select(map => map.BsonClassMap); public BsonClassMapSerializer DefaultBsonClassMapSerializer => - (BsonClassMapSerializer)schemaRegister.GetModelMapsSchema(typeof(TModel)).ActiveMap.BsonClassMapSerializer; + (BsonClassMapSerializer)dbContext.SchemaRegistry.GetModelMapsSchema(typeof(TModel)).ActiveMap.BsonClassMapSerializer; public IDiscriminatorConvention DiscriminatorConvention { get { if (_discriminatorConvention == null) - _discriminatorConvention = BsonSerializer.LookupDiscriminatorConvention(typeof(TModel)); + _discriminatorConvention = dbContext.DiscriminatorRegistry.LookupDiscriminatorConvention(typeof(TModel)); return _discriminatorConvention; } } @@ -94,14 +83,14 @@ public override TModel Deserialize(BsonDeserializationContext context, BsonDeser // Find pre-deserialization informations. //get actual type and schema var actualType = DiscriminatorConvention.GetActualType(context.Reader, args.NominalType); - var actualTypeSchema = schemaRegister.GetModelMapsSchema(actualType); + var actualTypeSchema = dbContext.SchemaRegistry.GetModelMapsSchema(actualType); //deserialize on document var bsonDocument = BsonDocumentSerializer.Instance.Deserialize(context, args); //get version SemanticVersion? documentSemVer = null; - if (bsonDocument.TryGetElement(documentSemVerOptions.ElementName, out BsonElement versionElement)) + if (bsonDocument.TryGetElement(dbContext.Options.DocumentSemVer.ElementName, out BsonElement versionElement)) { documentSemVer = BsonValueToSemVer(versionElement.Value); bsonDocument.RemoveElement(versionElement); //don't report into extra elements @@ -109,7 +98,7 @@ public override TModel Deserialize(BsonDeserializationContext context, BsonDeser //get model map id string? modelMapId = null; - if (bsonDocument.TryGetElement(modelMapVersionOptions.ElementName, out BsonElement modelMapIdElement)) + if (bsonDocument.TryGetElement(dbContext.Options.ModelMapVersion.ElementName, out BsonElement modelMapIdElement)) { modelMapId = BsonValueToModelMapId(modelMapIdElement.Value); bsonDocument.RemoveElement(modelMapIdElement); //don't report into extra elements @@ -130,8 +119,9 @@ public override TModel Deserialize(BsonDeserializationContext context, BsonDeser //if a correct model map is identified with its id if (modelMapId != null && actualTypeSchema.AllMapsDictionary.ContainsKey(modelMapId)) { - var serializer = actualTypeSchema.AllMapsDictionary[modelMapId].BsonClassMapSerializer; - model = (TModel)serializer.Deserialize(localContext, args); + var task = DeserializeModelMapHelperAsync(actualTypeSchema.AllMapsDictionary[modelMapId], localContext, args); + task.Wait(); + model = task.Result; } //else, if a fallback serializator exists @@ -140,27 +130,42 @@ public override TModel Deserialize(BsonDeserializationContext context, BsonDeser model = (TModel)actualTypeSchema.FallbackSerializer.Deserialize(localContext, args); } + //else, if a fallback model map exists + else if (actualTypeSchema.FallbackModelMap != null) + { + var task = DeserializeModelMapHelperAsync(actualTypeSchema.FallbackModelMap, localContext, args); + task.Wait(); + model = task.Result; + } + //else, deserialize wih current active model map else { - model = (TModel)actualTypeSchema.ActiveMap.BsonClassMapSerializer.Deserialize(localContext, args); + var task = DeserializeModelMapHelperAsync(actualTypeSchema.ActiveMap, localContext, args); + task.Wait(); + model = task.Result; } - // Add model to cache. - if (!serializerModifierAccessor.IsNoCacheEnabled && - GetDocumentId(model, out var id, out _, out _) && id != null) + // Add model to cache (if proxy). + /* Proxy models enable different features. Anyway, if the model as not been created as a proxy + * (for example for tests scope) these additional operations are not possible or required. + * In this case, don't add any not-proxy models in cache. + */ + if (!dbContext.SerializerModifierAccessor.IsNoCacheEnabled && + GetDocumentId(model, out var id, out _, out _) && id != null && + dbContext.ProxyGenerator.IsProxyType(model.GetType())) { - if (dbCache.LoadedModels.ContainsKey(id)) + if (dbContext.DbCache.LoadedModels.ContainsKey(id)) { var fullModel = model; - model = (TModel)dbCache.LoadedModels[id]; + model = (TModel)dbContext.DbCache.LoadedModels[id]; if (((IReferenceable)model).IsSummary) ((IReferenceable)model).MergeFullModel(fullModel); } else { - dbCache.AddModel(id, (IEntityModel)model); + dbContext.DbCache.AddModel(id, (IEntityModel)model); } } @@ -193,7 +198,7 @@ public override void Serialize(BsonSerializationContext context, BsonSerializati var bsonDocument = new BsonDocument(); using var bsonWriter = new ExtendedBsonDocumentWriter(bsonDocument) { - IsRootDocument = !(context.Writer is ExtendedBsonDocumentWriter) + IsRootDocument = context.Writer is not ExtendedBsonDocumentWriter }; var localContext = BsonSerializationContext.CreateRoot( bsonWriter, @@ -201,7 +206,7 @@ public override void Serialize(BsonSerializationContext context, BsonSerializati // Get default schema. var actualType = value.GetType(); - var modelMapsSchema = schemaRegister.GetModelMapsSchema(actualType); + var modelMapsSchema = dbContext.SchemaRegistry.GetModelMapsSchema(actualType); // Serialize. modelMapsSchema.ActiveBsonClassMapSerializer.Serialize(localContext, args, value); @@ -214,14 +219,14 @@ public override void Serialize(BsonSerializationContext context, BsonSerializati * from bson class map serializer. In that case, the right model map id is already be setted, and we * don't have to replace it with the one wrong of the basic collection model type. */ - if (!bsonDocument.Contains(modelMapVersionOptions.ElementName)) - bsonDocument.InsertAt(0, schemaRegister.GetActiveModelMapIdBsonElement(actualType)); + if (!bsonDocument.Contains(dbContext.Options.ModelMapVersion.ElementName)) + bsonDocument.InsertAt(0, dbContext.SchemaRegistry.GetActiveModelMapIdBsonElement(actualType)); //add version - if (documentSemVerOptions.WriteInDocuments && bsonWriter.IsRootDocument) + if (dbContext.Options.DocumentSemVer.WriteInDocuments && bsonWriter.IsRootDocument) { - if (bsonDocument.Contains(documentSemVerOptions.ElementName)) - bsonDocument.Remove(documentSemVerOptions.ElementName); + if (bsonDocument.Contains(dbContext.Options.DocumentSemVer.ElementName)) + bsonDocument.Remove(dbContext.Options.DocumentSemVer.ElementName); bsonDocument.InsertAt(1, documentSemVerElement); } @@ -256,5 +261,23 @@ public bool TryGetMemberSerializationInfo(string memberName, out BsonSerializati bsonArray.Count >= 4 ? bsonArray[3].AsString : null), _ => throw new NotSupportedException(), }; + + private static async Task DeserializeModelMapHelperAsync( + IModelMap modelMap, + BsonDeserializationContext context, + BsonDeserializationArgs args) + { + /* If is mapped a different serializer than the current one, choose it. + * Otherwise, if doesn't exist or is already the current, deserialize with BsonClassMap + */ + var serializer = modelMap.Serializer is not null && modelMap.Serializer.GetType() != typeof(ModelMapSerializer) ? + modelMap.Serializer : + modelMap.BsonClassMapSerializer; + + var model = (TModel)serializer.Deserialize(context, args); + + // Fix model. + return (TModel)await modelMap.FixDeserializedModelAsync(model).ConfigureAwait(false); + } } } diff --git a/src/MongODM.Core/Serialization/Serializers/ReadOnlyDictionarySerializer.cs b/src/MongODM.Core/Serialization/Serializers/ReadOnlyDictionarySerializer.cs index 118346b5..98ffcc89 100644 --- a/src/MongODM.Core/Serialization/Serializers/ReadOnlyDictionarySerializer.cs +++ b/src/MongODM.Core/Serialization/Serializers/ReadOnlyDictionarySerializer.cs @@ -12,9 +12,9 @@ // See the License for the specific language governing permissions and // limitations under the License. -using MongoDB.Bson.Serialization; -using MongoDB.Bson.Serialization.Options; -using MongoDB.Bson.Serialization.Serializers; +using Etherna.MongoDB.Bson.Serialization; +using Etherna.MongoDB.Bson.Serialization.Options; +using Etherna.MongoDB.Bson.Serialization.Serializers; using System; using System.Collections.Generic; diff --git a/src/MongODM.Core/Serialization/Serializers/ReferenceSerializer.cs b/src/MongODM.Core/Serialization/Serializers/ReferenceSerializer.cs index dd993c27..b09dc86b 100644 --- a/src/MongODM.Core/Serialization/Serializers/ReferenceSerializer.cs +++ b/src/MongODM.Core/Serialization/Serializers/ReferenceSerializer.cs @@ -12,13 +12,13 @@ // See the License for the specific language governing permissions and // limitations under the License. +using Etherna.MongoDB.Bson; +using Etherna.MongoDB.Bson.IO; +using Etherna.MongoDB.Bson.Serialization; +using Etherna.MongoDB.Bson.Serialization.Conventions; +using Etherna.MongoDB.Bson.Serialization.Serializers; using Etherna.MongODM.Core.Domain.Models; using Etherna.MongODM.Core.ProxyModels; -using MongoDB.Bson; -using MongoDB.Bson.IO; -using MongoDB.Bson.Serialization; -using MongoDB.Bson.Serialization.Conventions; -using MongoDB.Bson.Serialization.Serializers; using System; using System.Collections.Generic; using System.Linq; @@ -90,7 +90,7 @@ public IDiscriminatorConvention DiscriminatorConvention get { if (_discriminatorConvention == null) - _discriminatorConvention = BsonSerializer.LookupDiscriminatorConvention(typeof(TModelBase)); + _discriminatorConvention = dbContext.DiscriminatorRegistry.LookupDiscriminatorConvention(typeof(TModelBase)); return _discriminatorConvention; } } @@ -126,7 +126,7 @@ public override TModelBase Deserialize(BsonDeserializationContext context, BsonD //get model map id string? modelMapId = null; - if (bsonDocument.TryGetElement(dbContext.ModelMapVersionOptions.ElementName, out BsonElement modelMapIdElement)) + if (bsonDocument.TryGetElement(dbContext.Options.ModelMapVersion.ElementName, out BsonElement modelMapIdElement)) { modelMapId = BsonValueToModelMapId(modelMapIdElement.Value); bsonDocument.RemoveElement(modelMapIdElement); //don't report into extra elements @@ -149,14 +149,17 @@ public override TModelBase Deserialize(BsonDeserializationContext context, BsonD var model = serializer.Deserialize(localContext, args) as TModelBase; - // Process model. - if (model != null) + // Process model (if proxy). + /* Proxy models enable different features. Anyway, if the model as not been created as a proxy + * (for example for tests scope) these additional operations are not possible or required. + * In this case, simply return the model as is. + */ + if (model != null && + dbContext.ProxyGenerator.IsProxyType(model.GetType())) { var id = model.Id; -#pragma warning disable CA1508 // Avoid dead conditional code if (id == null) //ignore refered instances without id return null!; -#pragma warning restore CA1508 // Avoid dead conditional code // Check if model as been loaded in cache. if (dbContext.DbCache.LoadedModels.ContainsKey(id) && @@ -290,8 +293,8 @@ public override void Serialize(BsonSerializationContext context, BsonSerializati // Add additional data. //add model map id - if (bsonDocument.Contains(dbContext.ModelMapVersionOptions.ElementName)) - bsonDocument.Remove(dbContext.ModelMapVersionOptions.ElementName); + if (bsonDocument.Contains(dbContext.Options.ModelMapVersion.ElementName)) + bsonDocument.Remove(dbContext.Options.ModelMapVersion.ElementName); var modelMapIdElement = configuration.GetActiveModelMapIdBsonElement( dbContext.ProxyGenerator.PurgeProxyType(value.GetType())); bsonDocument.InsertAt(0, modelMapIdElement); diff --git a/src/MongODM.Core/Serialization/Serializers/ReferenceSerializerAdapter.cs b/src/MongODM.Core/Serialization/Serializers/ReferenceSerializerAdapter.cs index 51124f68..351ce397 100644 --- a/src/MongODM.Core/Serialization/Serializers/ReferenceSerializerAdapter.cs +++ b/src/MongODM.Core/Serialization/Serializers/ReferenceSerializerAdapter.cs @@ -12,9 +12,9 @@ // See the License for the specific language governing permissions and // limitations under the License. +using Etherna.MongoDB.Bson.Serialization; +using Etherna.MongoDB.Bson.Serialization.Serializers; using Etherna.MongODM.Core.Domain.Models; -using MongoDB.Bson.Serialization; -using MongoDB.Bson.Serialization.Serializers; using System; using System.Collections.Generic; diff --git a/src/MongODM.Core/Serialization/Serializers/ReferenceSerializerConfiguration.cs b/src/MongODM.Core/Serialization/Serializers/ReferenceSerializerConfiguration.cs index d62fb096..cbac623c 100644 --- a/src/MongODM.Core/Serialization/Serializers/ReferenceSerializerConfiguration.cs +++ b/src/MongODM.Core/Serialization/Serializers/ReferenceSerializerConfiguration.cs @@ -12,11 +12,11 @@ // See the License for the specific language governing permissions and // limitations under the License. +using Etherna.MongoDB.Bson; +using Etherna.MongoDB.Bson.Serialization; using Etherna.MongODM.Core.Serialization.Mapping; using Etherna.MongODM.Core.Serialization.Mapping.Schemas; using Etherna.MongODM.Core.Utility; -using MongoDB.Bson; -using MongoDB.Bson.Serialization; using System; using System.Collections.Generic; using System.Diagnostics.CodeAnalysis; @@ -137,7 +137,7 @@ protected override void FreezeAction() activeModelMapIdBsonElement.Add( schema.ModelType, new BsonElement( - dbContext.ModelMapVersionOptions.ElementName, + dbContext.Options.ModelMapVersion.ElementName, new BsonString(notProxySchema.ActiveMap.Id))); } } @@ -160,6 +160,7 @@ private IModelMapsSchema CreateNewDefaultReferenceSchema(Type modelType) Guid.NewGuid().ToString(), //string id classMap, //BsonClassMap bsonClassMap null, //string? baseModelMapId + null, //Func>? fixDeserializedModelFunc null); //IBsonSerializer? serializer //model maps schema diff --git a/src/ExecutionContext/AsyncLocal/IAsyncLocalContextHandler.cs b/src/MongODM.Core/Tasks/ITaskRunnerBuilder.cs similarity index 73% rename from src/ExecutionContext/AsyncLocal/IAsyncLocalContextHandler.cs rename to src/MongODM.Core/Tasks/ITaskRunnerBuilder.cs index 51f1885c..45a32c11 100644 --- a/src/ExecutionContext/AsyncLocal/IAsyncLocalContextHandler.cs +++ b/src/MongODM.Core/Tasks/ITaskRunnerBuilder.cs @@ -12,14 +12,12 @@ // See the License for the specific language governing permissions and // limitations under the License. -using System; +using Etherna.MongODM.Core.Options; -namespace Etherna.ExecContext.AsyncLocal +namespace Etherna.MongODM.Core.Tasks { - /// - /// A disposable interface for - /// - public interface IAsyncLocalContextHandler : IDisposable + public interface ITaskRunnerBuilder { + void SetMongODMOptions(MongODMOptions options); } } diff --git a/src/MongODM.Core/Tasks/MigrateDbContextTask.cs b/src/MongODM.Core/Tasks/MigrateDbContextTask.cs index b0520c71..14e3e707 100644 --- a/src/MongODM.Core/Tasks/MigrateDbContextTask.cs +++ b/src/MongODM.Core/Tasks/MigrateDbContextTask.cs @@ -37,6 +37,7 @@ public async Task RunAsync(string dbMigrationOpId, string taskId) { var dbContext = (TDbContext)serviceProvider.GetService(typeof(TDbContext)); var dbMigrationOp = (DbMigrationOperation)await dbContext.DbOperations.FindOneAsync(dbMigrationOpId).ConfigureAwait(false); + var completedWithErrors = false; // Start migrate operation. dbMigrationOp.TaskStarted(taskId); @@ -50,16 +51,19 @@ public async Task RunAsync(string dbMigrationOpId, string taskId) async procDocs => { dbMigrationOp.AddLog(new DocumentMigrationLog( - docMigration.SourceCollection.Name, + docMigration.SourceRepository.Name, MigrationLogBase.ExecutionState.Executing, procDocs)); await dbContext.SaveChangesAsync().ConfigureAwait(false); }).ConfigureAwait(false); + if (!result.Succeded) + completedWithErrors = true; + //ended document migration log dbMigrationOp.AddLog(new DocumentMigrationLog( - docMigration.SourceCollection.Name, + docMigration.SourceRepository.Name, result.Succeded ? MigrationLogBase.ExecutionState.Succeded : MigrationLogBase.ExecutionState.Failed, @@ -69,23 +73,39 @@ public async Task RunAsync(string dbMigrationOpId, string taskId) } // Build indexes. - foreach (var repository in dbContext.RepositoryRegister.ModelCollectionRepositoryMap.Values) + foreach (var repository in dbContext.RepositoryRegistry.CollectionRepositoriesByModelType.Values) { dbMigrationOp.AddLog(new IndexMigrationLog( repository.Name, MigrationLogBase.ExecutionState.Executing)); await dbContext.SaveChangesAsync().ConfigureAwait(false); - await repository.BuildIndexesAsync(dbContext.SchemaRegister).ConfigureAwait(false); + try + { + await repository.BuildIndexesAsync().ConfigureAwait(false); + + dbMigrationOp.AddLog(new IndexMigrationLog( + repository.Name, + MigrationLogBase.ExecutionState.Succeded)); + } + catch (Exception) + { + completedWithErrors = true; + + dbMigrationOp.AddLog(new IndexMigrationLog( + repository.Name, + MigrationLogBase.ExecutionState.Failed)); + } - dbMigrationOp.AddLog(new IndexMigrationLog( - repository.Name, - MigrationLogBase.ExecutionState.Succeded)); await dbContext.SaveChangesAsync().ConfigureAwait(false); } // Complete task. - dbMigrationOp.TaskCompleted(); + if (!completedWithErrors) + dbMigrationOp.TaskCompleted(); + else + dbMigrationOp.TaskFailed(); + await dbContext.SaveChangesAsync().ConfigureAwait(false); } } diff --git a/src/MongODM.Core/Tasks/UpdateDocDependenciesTask.cs b/src/MongODM.Core/Tasks/UpdateDocDependenciesTask.cs index da017e79..da415c87 100644 --- a/src/MongODM.Core/Tasks/UpdateDocDependenciesTask.cs +++ b/src/MongODM.Core/Tasks/UpdateDocDependenciesTask.cs @@ -12,10 +12,10 @@ // See the License for the specific language governing permissions and // limitations under the License. +using Etherna.MongoDB.Driver; using Etherna.MongODM.Core.Domain.Models; using Etherna.MongODM.Core.Repositories; using Etherna.MongODM.Core.Serialization.Modifiers; -using MongoDB.Driver; using System; using System.Collections.Generic; using System.Threading.Tasks; @@ -51,10 +51,10 @@ public async Task RunAsync( // Get repository. /* Ignore document update if doesn't exists a collection that can handle its type. */ - if (!dbContext.RepositoryRegister.ModelCollectionRepositoryMap.ContainsKey(typeof(TModel))) + if (!dbContext.RepositoryRegistry.CollectionRepositoriesByModelType.ContainsKey(typeof(TModel))) return; - var repository = (ICollectionRepository)dbContext.RepositoryRegister.ModelCollectionRepositoryMap[typeof(TModel)]; + var repository = (ICollectionRepository)dbContext.RepositoryRegistry.CollectionRepositoriesByModelType[typeof(TModel)]; // Update models. HashSet upgradedDocumentsId = new(); diff --git a/src/MongODM.Core/Utility/DbCache.cs b/src/MongODM.Core/Utility/DbCache.cs index 7a420b9e..fa466b15 100644 --- a/src/MongODM.Core/Utility/DbCache.cs +++ b/src/MongODM.Core/Utility/DbCache.cs @@ -16,24 +16,37 @@ using Etherna.MongODM.Core.Domain.Models; using System; using System.Collections.Generic; +using System.Text; namespace Etherna.MongODM.Core.Utility { public class DbCache : IDbCache { // Consts. - private const string CacheKey = "DBCache"; + private const string CacheKeyPrefix = "DBCache-"; // Fields. - private readonly IExecutionContext executionContext; + private string cacheKey = default!; + private IExecutionContext executionContext = default!; // Constructors. - public DbCache(IExecutionContext executionContext) + public void Initialize(IDbContext dbContext) { - this.executionContext = executionContext ?? throw new ArgumentNullException(nameof(executionContext)); + if (dbContext is null) + throw new ArgumentNullException(nameof(dbContext)); + if (IsInitialized) + throw new InvalidOperationException("Instance already initialized"); + + var cacheKeyBuilder = new StringBuilder(CacheKeyPrefix); + cacheKeyBuilder.Append(dbContext.Identifier); + cacheKey = cacheKeyBuilder.ToString(); + executionContext = dbContext.ExecutionContext; + + IsInitialized = true; } // Properties. + public bool IsInitialized { get; private set; } public IReadOnlyDictionary LoadedModels { get @@ -85,10 +98,10 @@ private Dictionary GetScopedCache() if (executionContext.Items is null) throw new InvalidOperationException("Execution context can't have null Items here"); - if (!executionContext.Items.ContainsKey(CacheKey)) - executionContext.Items.Add(CacheKey, new Dictionary()); + if (!executionContext.Items.ContainsKey(cacheKey)) + executionContext.Items.Add(cacheKey, new Dictionary()); - return (Dictionary)executionContext.Items[CacheKey]!; + return (Dictionary)executionContext.Items[cacheKey]!; } } } diff --git a/src/MongODM.Core/Utility/DbExecutionContextHandler.cs b/src/MongODM.Core/Utility/DbExecutionContextHandler.cs new file mode 100644 index 00000000..2eba35b5 --- /dev/null +++ b/src/MongODM.Core/Utility/DbExecutionContextHandler.cs @@ -0,0 +1,82 @@ +// 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.ExecContext; +using Etherna.ExecContext.AsyncLocal; +using System; +using System.Collections; +using System.Collections.Generic; +using System.Linq; + +namespace Etherna.MongODM.Core.Utility +{ + public sealed class DbExecutionContextHandler : IDisposable + { + // Consts. + private const string HandlerKey = "DbContextExecutionContextHandler"; + + // Fields. + private readonly IAsyncLocalContextHandler? asyncLocalContextHandler; + private readonly ICollection requestes; + + // Constructors and dispose. + public DbExecutionContextHandler( + IDbContext dbContext) + { + DbContext = dbContext ?? throw new ArgumentNullException(nameof(dbContext)); + + var executionContext = dbContext.ExecutionContext; + + if (executionContext.Items is null) //if an execution context doesn't exist, create it + asyncLocalContextHandler = AsyncLocalContext.Instance.InitAsyncLocalContext(); + + if (!executionContext.Items!.ContainsKey(HandlerKey)) + executionContext.Items.Add(HandlerKey, new List()); + + requestes = (ICollection)executionContext.Items[HandlerKey]!; + + lock (((ICollection)requestes).SyncRoot) + requestes.Add(this); + } + + public void Dispose() + { + lock (((ICollection)requestes).SyncRoot) + requestes.Remove(this); + + if (asyncLocalContextHandler is not null) + asyncLocalContextHandler.Dispose(); + } + + // Properties. + public IDbContext DbContext { get; } + + // Static methods. + public static IDbContext? TryGetCurrentDbContext(IExecutionContext executionContext) + { + if (executionContext is null) + throw new ArgumentNullException(nameof(executionContext)); + + if (executionContext.Items is null || + !executionContext.Items.ContainsKey(HandlerKey)) + return null; + + var requestes = (ICollection)executionContext.Items[HandlerKey]!; + + //get the last with a stack system, for recursing calls betweem different dbContexts + lock (((ICollection)requestes).SyncRoot) + return requestes.Reverse().FirstOrDefault()?.DbContext; + } + } +} diff --git a/src/MongODM.Core/Utility/DbMaintainer.cs b/src/MongODM.Core/Utility/DbMaintainer.cs index b5bc2c45..148e8392 100644 --- a/src/MongODM.Core/Utility/DbMaintainer.cs +++ b/src/MongODM.Core/Utility/DbMaintainer.cs @@ -12,9 +12,9 @@ // See the License for the specific language governing permissions and // limitations under the License. +using Etherna.MongoDB.Driver; using Etherna.MongODM.Core.ProxyModels; using Etherna.MongODM.Core.Tasks; -using MongoDB.Driver; using System; using System.Linq; @@ -55,12 +55,12 @@ public void OnUpdatedModel(IAuditable updatedModel, TKey modelId) // Find all possible coinvolted member maps with changes. Keep only referenced members var updatedMembers = updatedModel.ChangedMembers; - var referenceMemberMaps = updatedMembers.SelectMany(memberInfo => dbContext.SchemaRegister.GetMemberDependenciesFromMemberInfo(memberInfo)) + var referenceMemberMaps = updatedMembers.SelectMany(memberInfo => dbContext.SchemaRegistry.GetMemberDependenciesFromMemberInfo(memberInfo)) .Where(memberMap => memberMap.IsEntityReferenceMember); // Group by root model type, and select only model types related to a collections. foreach (var dependencyGroup in referenceMemberMaps.GroupBy(memberMap => memberMap.RootModelMap.ModelType) - .Where(group => dbContext.RepositoryRegister.ModelCollectionRepositoryMap.ContainsKey(group.Key))) + .Where(group => dbContext.RepositoryRegistry.CollectionRepositoriesByModelType.ContainsKey(group.Key))) { // Extract only id paths to referenced entities. var idPaths = dependencyGroup diff --git a/src/MongODM.Core/Utility/DbMigrationManager.cs b/src/MongODM.Core/Utility/DbMigrationManager.cs index 20512c88..21ae69f0 100644 --- a/src/MongODM.Core/Utility/DbMigrationManager.cs +++ b/src/MongODM.Core/Utility/DbMigrationManager.cs @@ -12,11 +12,11 @@ // See the License for the specific language governing permissions and // limitations under the License. +using Etherna.MongoDB.Driver; +using Etherna.MongoDB.Driver.Linq; using Etherna.MongODM.Core.Domain.Models; using Etherna.MongODM.Core.Extensions; using Etherna.MongODM.Core.Tasks; -using MongoDB.Driver; -using MongoDB.Driver.Linq; using System; using System.Collections.Generic; using System.Threading.Tasks; diff --git a/src/MongODM.Core/Utility/FreezableConfig.cs b/src/MongODM.Core/Utility/FreezableConfig.cs index 5369c4dc..d14748bb 100644 --- a/src/MongODM.Core/Utility/FreezableConfig.cs +++ b/src/MongODM.Core/Utility/FreezableConfig.cs @@ -76,12 +76,17 @@ public void Freeze() } // Protected methods. - protected void ExecuteConfigAction(Action configAction) => + protected void ExecuteConfigAction(Action configAction) + { + if (configAction is null) + throw new ArgumentNullException(nameof(configAction)); + ExecuteConfigAction(() => { configAction(); return 0; }); + } protected TReturn ExecuteConfigAction(Func configAction) { diff --git a/src/MongODM.Core/Utility/IDbCache.cs b/src/MongODM.Core/Utility/IDbCache.cs index 651bf971..2685d930 100644 --- a/src/MongODM.Core/Utility/IDbCache.cs +++ b/src/MongODM.Core/Utility/IDbCache.cs @@ -20,7 +20,7 @@ namespace Etherna.MongODM.Core.Utility /// /// Interface for implementation. /// - public interface IDbCache + public interface IDbCache : IDbContextInitializable { // Properties. /// diff --git a/src/MongODM.Core/Utility/IStaticConfigurationBuilder.cs b/src/MongODM.Core/Utility/SerializationContextAccessor.cs similarity index 50% rename from src/MongODM.Core/Utility/IStaticConfigurationBuilder.cs rename to src/MongODM.Core/Utility/SerializationContextAccessor.cs index 6cb2fa50..4adf0697 100644 --- a/src/MongODM.Core/Utility/IStaticConfigurationBuilder.cs +++ b/src/MongODM.Core/Utility/SerializationContextAccessor.cs @@ -12,15 +12,27 @@ // See the License for the specific language governing permissions and // limitations under the License. +using Etherna.ExecContext; +using Etherna.MongoDB.Bson.Serialization; + namespace Etherna.MongODM.Core.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. - /// - public interface IStaticConfigurationBuilder + public class SerializationContextAccessor : ISerializationContextAccessor { + // Fields. + private readonly IExecutionContext executionContext; + + // Constructor. + public SerializationContextAccessor(IExecutionContext executionContext) + { + this.executionContext = executionContext; + } + + // Method. + public IBsonSerializerRegistry? TryGetCurrentBsonSerializerRegistry() + { + var dbContext = DbExecutionContextHandler.TryGetCurrentDbContext(executionContext); + return dbContext?.SerializerRegistry; + } } -} \ No newline at end of file +} diff --git a/src/MongODM.Hangfire/Extensions/HangfireGlobalConfigExtension.cs b/src/MongODM.Hangfire/Extensions/HangfireGlobalConfigExtension.cs index 7a88ce78..a527a5e1 100644 --- a/src/MongODM.Hangfire/Extensions/HangfireGlobalConfigExtension.cs +++ b/src/MongODM.Hangfire/Extensions/HangfireGlobalConfigExtension.cs @@ -1,4 +1,18 @@ -using Etherna.ExecContext.AsyncLocal; +// 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.ExecContext.AsyncLocal; using Etherna.MongODM.HF.Filters; namespace Hangfire diff --git a/src/MongODM.Hangfire/MongODM.Hangfire.csproj b/src/MongODM.Hangfire/MongODM.Hangfire.csproj index 6e4a6323..36f5cd67 100644 --- a/src/MongODM.Hangfire/MongODM.Hangfire.csproj +++ b/src/MongODM.Hangfire/MongODM.Hangfire.csproj @@ -19,15 +19,16 @@ LICENSE true true + True - + all runtime; build; native; contentfiles; analyzers; buildtransitive - - + + all runtime; build; native; contentfiles; analyzers; buildtransitive diff --git a/src/MongODM.Hangfire/Properties/AssemblyInfo.cs b/src/MongODM.Hangfire/Properties/AssemblyInfo.cs index 83cc064e..dd2b8d00 100644 --- a/src/MongODM.Hangfire/Properties/AssemblyInfo.cs +++ b/src/MongODM.Hangfire/Properties/AssemblyInfo.cs @@ -1,3 +1,17 @@ -using System; +// 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; [assembly: CLSCompliant(false)] \ No newline at end of file diff --git a/src/MongODM.Hangfire/Tasks/HangfireTaskRunner.cs b/src/MongODM.Hangfire/Tasks/HangfireTaskRunner.cs index 8cfddb88..32ec2327 100644 --- a/src/MongODM.Hangfire/Tasks/HangfireTaskRunner.cs +++ b/src/MongODM.Hangfire/Tasks/HangfireTaskRunner.cs @@ -12,10 +12,8 @@ // See the License for the specific language governing permissions and // limitations under the License. -using Etherna.ExecContext.AsyncLocal; using Etherna.MongODM.Core.Options; using Etherna.MongODM.Core.Tasks; -using Etherna.MongODM.HF.Filters; using Hangfire; using Hangfire.States; using System; @@ -23,19 +21,17 @@ namespace Etherna.MongODM.HF.Tasks { - public class HangfireTaskRunner : ITaskRunner + public sealed class HangfireTaskRunner : ITaskRunner, ITaskRunnerBuilder { // Fields. private readonly IBackgroundJobClient backgroundJobClient; - private readonly MongODMOptions mongODMOptions; + private MongODMOptions mongODMOptions = default!; // Constructors. public HangfireTaskRunner( - IBackgroundJobClient backgroundJobClient, - MongODMOptions mongODMOptions) + IBackgroundJobClient backgroundJobClient) { this.backgroundJobClient = backgroundJobClient; - this.mongODMOptions = mongODMOptions; } // Methods. @@ -48,5 +44,9 @@ public void RunUpdateDocDependenciesTask(Type dbContextType, Type modelType, Typ backgroundJobClient.Create( task => task.RunAsync(dbContextType, modelType, keyType, idPaths, modelId), new EnqueuedState(mongODMOptions.DbMaintenanceQueueName)); + + // Explicit methods. + void ITaskRunnerBuilder.SetMongODMOptions(MongODMOptions options) => + mongODMOptions = options; } } diff --git a/src/MongODM/ServiceCollectionExtensions.cs b/src/MongODM/Extensions/ServiceCollectionExtensions.cs similarity index 84% rename from src/MongODM/ServiceCollectionExtensions.cs rename to src/MongODM/Extensions/ServiceCollectionExtensions.cs index e55ba23b..af304971 100644 --- a/src/MongODM/ServiceCollectionExtensions.cs +++ b/src/MongODM/Extensions/ServiceCollectionExtensions.cs @@ -12,29 +12,27 @@ // See the License for the specific language governing permissions and // limitations under the License. -using Etherna.MongODM; -using Etherna.MongODM.Core; -using Etherna.MongODM.Core.Domain.Models; +using Etherna.MongODM.AspNetCore; using Etherna.MongODM.Core.Options; using Etherna.MongODM.Core.ProxyModels; using Etherna.MongODM.HF.Tasks; using Hangfire; using Hangfire.Mongo; +using Microsoft.Extensions.DependencyInjection; using System; -namespace Microsoft.Extensions.DependencyInjection +namespace Etherna.MongODM { public static class ServiceCollectionExtensions { // Methods. - public static IMongODMConfiguration AddMongODMWithHangfire( + public static IMongODMConfiguration AddMongODMWithHangfire( this IServiceCollection services, Action? configureHangfireOptions = null, Action? configureMongODMOptions = null) - where TModelBase : class, IModel { // Configure MongODM. - var conf = services.AddMongODM(configureMongODMOptions); + var conf = services.AddMongODM(configureMongODMOptions); // Configure Hangfire. AddHangfire(services, configureHangfireOptions); @@ -42,15 +40,14 @@ public static IMongODMConfiguration AddMongODMWithHangfire( return conf; } - public static IMongODMConfiguration AddMongODMWithHangfire( + public static IMongODMConfiguration AddMongODMWithHangfire( this IServiceCollection services, Action? configureHangfireOptions = null, Action? configureMongODMOptions = null) where TProxyGenerator : class, IProxyGenerator - where TModelBase : class, IModel { // Configure MongODM. - var conf = services.AddMongODM(configureMongODMOptions); + var conf = services.AddMongODM(configureMongODMOptions); // Configure Hangfire. AddHangfire(services, configureHangfireOptions); diff --git a/src/MongODM/MongODM.csproj b/src/MongODM/MongODM.csproj index dfde78eb..c72b23f6 100644 --- a/src/MongODM/MongODM.csproj +++ b/src/MongODM/MongODM.csproj @@ -1,7 +1,7 @@  - netcoreapp2.1;netcoreapp3.1;net5.0 + netcoreapp3.1;net6.0 true Etherna.MongODM Etherna Sagl @@ -19,6 +19,7 @@ LICENSE true true + True @@ -26,12 +27,12 @@ - + all runtime; build; native; contentfiles; analyzers; buildtransitive - - + + all runtime; build; native; contentfiles; analyzers; buildtransitive diff --git a/test/ExecutionContext.Tests/AsyncLocal/AsyncLocalContextHandlerTests.cs b/test/ExecutionContext.Tests/AsyncLocal/AsyncLocalContextHandlerTests.cs deleted file mode 100644 index 3e8bea37..00000000 --- a/test/ExecutionContext.Tests/AsyncLocal/AsyncLocalContextHandlerTests.cs +++ /dev/null @@ -1,48 +0,0 @@ -// 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 Moq; -using Xunit; - -namespace Etherna.ExecContext.AsyncLocal -{ - public class AsyncLocalContextHandlerTests - { - private readonly Mock handledContext; - private readonly AsyncLocalContextHandler handler; - - public AsyncLocalContextHandlerTests() - { - handledContext = new Mock(); - handler = new AsyncLocalContextHandler(handledContext.Object); - } - - [Fact] - public void Initialization() - { - // Assert. - Assert.Equal(handledContext.Object, handler.HandledContext); - } - - [Fact] - public void HandlerDispose() - { - // Action. - handler.Dispose(); - - // Assert. - handledContext.Verify(c => c.OnDisposed(handler), Times.Once); - } - } -} diff --git a/test/ExecutionContext.Tests/AsyncLocal/AsyncLocalContextTests.cs b/test/ExecutionContext.Tests/AsyncLocal/AsyncLocalContextTests.cs deleted file mode 100644 index b97c9d9d..00000000 --- a/test/ExecutionContext.Tests/AsyncLocal/AsyncLocalContextTests.cs +++ /dev/null @@ -1,93 +0,0 @@ -// 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.Threading.Tasks; -using Xunit; - -namespace Etherna.ExecContext.AsyncLocal -{ - public class AsyncLocalContextTests - { - private readonly AsyncLocalContext asyncLocalContext; - - public AsyncLocalContextTests() - { - asyncLocalContext = new AsyncLocalContext(); - } - - [Fact] - public void ItemsNullAtCreation() - { - // Assert. - Assert.Null(asyncLocalContext.Items); - } - - [Fact] - public async Task AsyncLocalLifeCycle() - { - await Task.Run(async () => - { - // Action. - asyncLocalContext.InitAsyncLocalContext(); - - // Assert. - Assert.NotNull(asyncLocalContext.Items); - await Task.Run(() => - { - Assert.NotNull(asyncLocalContext.Items); - }); - }); - - // Assert. - Assert.Null(asyncLocalContext.Items); - } - - [Fact] - public void SyncLocalLifeCycle() - { - void localMethod() - { - // Action. - asyncLocalContext.InitAsyncLocalContext(); - - // Assert. - Assert.NotNull(asyncLocalContext.Items); - void subLocalMethod() - { - Assert.NotNull(asyncLocalContext.Items); - } - subLocalMethod(); - } - localMethod(); - - // Assert. - /* Outside of an async invoker, the container is not automatically disposed. */ - Assert.NotNull(asyncLocalContext.Items); - } - - [Fact] - public void ContextDispose() - { - // Action. - using (var handler = asyncLocalContext.InitAsyncLocalContext()) - { - // Assert. - Assert.NotNull(asyncLocalContext.Items); - } - - // Assert. - Assert.Null(asyncLocalContext.Items); - } - } -} diff --git a/test/ExecutionContext.Tests/ExecutionContext.Tests.csproj b/test/ExecutionContext.Tests/ExecutionContext.Tests.csproj deleted file mode 100644 index 62a43fd8..00000000 --- a/test/ExecutionContext.Tests/ExecutionContext.Tests.csproj +++ /dev/null @@ -1,25 +0,0 @@ - - - - net5.0 - true - Etherna.ExecContext - false - enable - - - - - - - - all - runtime; build; native; contentfiles; analyzers; buildtransitive - - - - - - - - diff --git a/test/ExecutionContext.Tests/ExecutionContextSelectorTests.cs b/test/ExecutionContext.Tests/ExecutionContextSelectorTests.cs deleted file mode 100644 index 5310ed92..00000000 --- a/test/ExecutionContext.Tests/ExecutionContextSelectorTests.cs +++ /dev/null @@ -1,50 +0,0 @@ -// 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 Moq; -using System; -using System.Collections.Generic; -using Xunit; - -namespace Etherna.ExecContext -{ - public class ExecutionContextSelectorTests - { - [Theory] - [InlineData(false, false, null)] - [InlineData(false, true, "1")] - [InlineData(true, false, "0")] - [InlineData(true, true, "0")] - public void ContextSelection( - bool enableContext1, - bool enableContext2, - string expectedResult) - { - // Setup. - Mock context0 = new(); - Mock context1 = new(); - context0.SetupGet(c => c.Items) - .Returns(enableContext1 ? new Dictionary { { "val", "0" } } : null); - context1.SetupGet(c => c.Items) - .Returns(enableContext2 ? new Dictionary { { "val", "1" } } : null); - var selector = new ExecutionContextSelector(new[] { context0.Object, context1.Object }); - - // Action. - var result = selector.Items?["val"] as string; - - // Assert. - Assert.Equal(expectedResult, result); - } - } -} diff --git a/test/MongODM.Core.Tests/ModelMapSerializerTest.cs b/test/MongODM.Core.Tests/ModelMapSerializerTest.cs index d1bd586a..edf6b8c9 100644 --- a/test/MongODM.Core.Tests/ModelMapSerializerTest.cs +++ b/test/MongODM.Core.Tests/ModelMapSerializerTest.cs @@ -12,7 +12,11 @@ // See the License for the specific language governing permissions and // limitations under the License. +using Etherna.MongoDB.Bson; +using Etherna.MongoDB.Bson.IO; +using Etherna.MongoDB.Bson.Serialization; using Etherna.MongODM.Core.Comparers; +using Etherna.MongODM.Core.Conventions; using Etherna.MongODM.Core.Models; using Etherna.MongODM.Core.Options; using Etherna.MongODM.Core.Serialization.Mapping; @@ -20,13 +24,11 @@ using Etherna.MongODM.Core.Serialization.Modifiers; using Etherna.MongODM.Core.Serialization.Serializers; using Etherna.MongODM.Core.Utility; -using MongoDB.Bson; -using MongoDB.Bson.IO; -using MongoDB.Bson.Serialization; using Moq; using System; using System.Collections.Generic; using System.Linq; +using System.Threading.Tasks; using Xunit; namespace Etherna.MongODM.Core @@ -78,19 +80,39 @@ public SerializationTestElement( // Fields. private readonly Mock dbCacheMock = new(); + private readonly Mock dbContextMock = new(); + private readonly Mock discriminatorRegistryMock = new(); private readonly DocumentSemVerOptions documentSemVerOptions = new(); private readonly Mock modelMapsSchemaMock = new(); private readonly ModelMapVersionOptions modelMapVersionOptions = new(); - private readonly Mock schemaRegisterMock = new(); + private readonly Mock schemaRegistryMock = new(); private readonly Mock serializerModifierAccessorMock = new(); // Constructor. public ModelMapSerializerTest() { + discriminatorRegistryMock.Setup(r => r.LookupDiscriminatorConvention(It.IsAny())) + .Returns(() => new HierarchicalProxyTolerantDiscriminatorConvention(dbContextMock.Object, "_t")); + dbCacheMock.Setup(c => c.LoadedModels.ContainsKey(It.IsAny())) .Returns(() => false); - schemaRegisterMock.Setup(sr => sr.GetModelMapsSchema(typeof(FakeModel))) + dbContextMock.Setup(c => c.DbCache) + .Returns(() => dbCacheMock.Object); + dbContextMock.Setup(c => c.DiscriminatorRegistry) + .Returns(() => discriminatorRegistryMock.Object); + dbContextMock.Setup(c => c.ProxyGenerator.IsProxyType(It.IsAny())) + .Returns(true); + dbContextMock.Setup(c => c.Options.DocumentSemVer) + .Returns(() => documentSemVerOptions); + dbContextMock.Setup(c => c.Options.ModelMapVersion) + .Returns(() => modelMapVersionOptions); + dbContextMock.Setup(c => c.SchemaRegistry) + .Returns(() => schemaRegistryMock.Object); + dbContextMock.Setup(c => c.SerializerModifierAccessor) + .Returns(() => serializerModifierAccessorMock.Object); + + schemaRegistryMock.Setup(sr => sr.GetModelMapsSchema(typeof(FakeModel))) .Returns(() => modelMapsSchemaMock.Object); } @@ -137,15 +159,12 @@ public void Deserialize(DeserializationTestElement test) // Setup var bsonReader = new BsonDocumentReader(test.Document); var bsonClassMapSerializer = CreateBsonClassMapSerializer(); - var serializer = new ModelMapSerializer( - dbCacheMock.Object, - documentSemVerOptions, - modelMapVersionOptions, - schemaRegisterMock.Object, - serializerModifierAccessorMock.Object); + var serializer = new ModelMapSerializer(dbContextMock.Object); modelMapsSchemaMock.Setup(s => s.ActiveMap.BsonClassMapSerializer) .Returns(bsonClassMapSerializer); + modelMapsSchemaMock.Setup(s => s.ActiveMap.FixDeserializedModelAsync(It.IsAny())) + .Returns(m => Task.FromResult(m)); // Action test.PreAction(bsonReader); @@ -164,12 +183,7 @@ public void GetDocumentId() // Setup var model = new FakeModel { Id = "idVal" }; var bsonClassMapSerializer = CreateBsonClassMapSerializer(); - var serializer = new ModelMapSerializer( - dbCacheMock.Object, - documentSemVerOptions, - modelMapVersionOptions, - schemaRegisterMock.Object, - serializerModifierAccessorMock.Object); + var serializer = new ModelMapSerializer(dbContextMock.Object); modelMapsSchemaMock.Setup(s => s.ActiveMap.BsonClassMapSerializer) .Returns(bsonClassMapSerializer); @@ -200,12 +214,7 @@ public void GetMemberSerializationInfo() // Setup var memberName = nameof(FakeModel.StringProp); var bsonClassMapSerializer = CreateBsonClassMapSerializer(); - var serializer = new ModelMapSerializer( - dbCacheMock.Object, - documentSemVerOptions, - modelMapVersionOptions, - schemaRegisterMock.Object, - serializerModifierAccessorMock.Object); + var serializer = new ModelMapSerializer(dbContextMock.Object); modelMapsSchemaMock.Setup(s => s.ActiveMap.BsonClassMapSerializer) .Returns(bsonClassMapSerializer); @@ -293,19 +302,14 @@ public void Serialize(SerializationTestElement test) { // Setup var bsonClassMapSerializer = CreateBsonClassMapSerializer(); - var serializer = new ModelMapSerializer( - dbCacheMock.Object, - documentSemVerOptions, - modelMapVersionOptions, - schemaRegisterMock.Object, - serializerModifierAccessorMock.Object); + var serializer = new ModelMapSerializer(dbContextMock.Object); modelMapsSchemaMock.Setup(s => s.ActiveBsonClassMapSerializer) .Returns(bsonClassMapSerializer); modelMapsSchemaMock.Setup(s => s.ActiveMap.Id) .Returns("mapId"); - schemaRegisterMock.Setup(sr => sr.GetActiveModelMapIdBsonElement(typeof(FakeModel))) + schemaRegistryMock.Setup(sr => sr.GetActiveModelMapIdBsonElement(typeof(FakeModel))) .Returns(new BsonElement("_m", new BsonString("mapId"))); // Action @@ -327,12 +331,7 @@ public void SetDocumentId() var id = "idVal"; var model = new FakeModel(); var bsonClassMapSerializer = CreateBsonClassMapSerializer(); - var serializer = new ModelMapSerializer( - dbCacheMock.Object, - documentSemVerOptions, - modelMapVersionOptions, - schemaRegisterMock.Object, - serializerModifierAccessorMock.Object); + var serializer = new ModelMapSerializer(dbContextMock.Object); modelMapsSchemaMock.Setup(s => s.ActiveMap.BsonClassMapSerializer) .Returns(bsonClassMapSerializer); diff --git a/test/MongODM.Core.Tests/MongODM.Core.Tests.csproj b/test/MongODM.Core.Tests/MongODM.Core.Tests.csproj index e7b435e0..882fcc64 100644 --- a/test/MongODM.Core.Tests/MongODM.Core.Tests.csproj +++ b/test/MongODM.Core.Tests/MongODM.Core.Tests.csproj @@ -1,7 +1,7 @@  - net5.0 + net6.0 true Etherna.MongODM.Core false @@ -9,10 +9,10 @@ - - + + - + all runtime; build; native; contentfiles; analyzers diff --git a/test/MongODM.Core.Tests/ReferenceableInterceptorTest.cs b/test/MongODM.Core.Tests/ReferenceableInterceptorTest.cs index 4be9745d..d0983800 100644 --- a/test/MongODM.Core.Tests/ReferenceableInterceptorTest.cs +++ b/test/MongODM.Core.Tests/ReferenceableInterceptorTest.cs @@ -39,7 +39,7 @@ public ReferenceableInterceptorTest() repositoryMock = new Mock>(); dbContextMock = new Mock(); - dbContextMock.Setup(c => c.RepositoryRegister.ModelRepositoryMap) + dbContextMock.Setup(c => c.RepositoryRegistry.RepositoriesByModelType) .Returns(() => new Dictionary { [typeof(FakeModel)] = repositoryMock.Object