diff --git a/.github/workflows/build-test-package.yml b/.github/workflows/build-test-package.yml new file mode 100644 index 00000000..55bab03c --- /dev/null +++ b/.github/workflows/build-test-package.yml @@ -0,0 +1,31 @@ +name: Build test and package .Net Core library - MongODM + +on: + push: + branches: [ master, dev ] + +jobs: + build-test-package: + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@master + with: + fetch-depth: 0 + + - name: Setup .NET Core SDK + uses: actions/setup-dotnet@master + with: + dotnet-version: '3.1.x' + + - name: Build with dotnet + run: dotnet build --configuration Release + + - name: Run unit tests + run: dotnet test --configuration Release + + - name: Generate nuget package + run: dotnet pack --configuration Release -o nupkg + + - name: Push packages + run: dotnet nuget push './nupkg/*.nupkg' --api-key ${{secrets.MYGET_APIKEY}} --source https://www.myget.org/F/etherna/api/v3/index.json diff --git a/MongODM.sln b/MongODM.sln new file mode 100644 index 00000000..7fa3b3a5 --- /dev/null +++ b/MongODM.sln @@ -0,0 +1,78 @@ + +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio Version 16 +VisualStudioVersion = 16.0.28803.156 +MinimumVisualStudioVersion = 10.0.40219.1 +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "src", "src", "{490DAED7-DAD8-459A-A20E-F57F2F6F619E}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "test", "test", "{CF1ABDEA-794F-4474-858D-BCB61F367D72}" +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}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "MongODM.Core.Tests", "test\MongODM.Core.Tests\MongODM.Core.Tests.csproj", "{50D6BEE5-54B5-43F3-BA92-6107AB6E311E}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{03C64D98-FF9F-4760-AE82-203953FF4940}" + ProjectSection(SolutionItems) = preProject + .gitignore = .gitignore + EndProjectSection +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "workflows", "workflows", "{D4BB5972-5F7B-43ED-81C1-69ECF0E62D1E}" + ProjectSection(SolutionItems) = preProject + .github\workflows\build-test-package.yml = .github\workflows\build-test-package.yml + EndProjectSection +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Release|Any CPU = Release|Any CPU + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {10897D0D-4898-4A4D-8D1E-B2435E93D9A1}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {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 + {6374F645-5D17-494E-9529-7F83426900B3}.Release|Any CPU.Build.0 = Release|Any CPU + {4F2498A9-D60D-4D49-95B9-BC78EE2917B5}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {4F2498A9-D60D-4D49-95B9-BC78EE2917B5}.Debug|Any CPU.Build.0 = Debug|Any CPU + {4F2498A9-D60D-4D49-95B9-BC78EE2917B5}.Release|Any CPU.ActiveCfg = Release|Any CPU + {4F2498A9-D60D-4D49-95B9-BC78EE2917B5}.Release|Any CPU.Build.0 = Release|Any CPU + {50D6BEE5-54B5-43F3-BA92-6107AB6E311E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {50D6BEE5-54B5-43F3-BA92-6107AB6E311E}.Debug|Any CPU.Build.0 = Debug|Any CPU + {50D6BEE5-54B5-43F3-BA92-6107AB6E311E}.Release|Any CPU.ActiveCfg = Release|Any CPU + {50D6BEE5-54B5-43F3-BA92-6107AB6E311E}.Release|Any CPU.Build.0 = Release|Any CPU + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + 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} + {D4BB5972-5F7B-43ED-81C1-69ECF0E62D1E} = {03C64D98-FF9F-4760-AE82-203953FF4940} + EndGlobalSection + GlobalSection(ExtensibilityGlobals) = postSolution + SolutionGuid = {43A8089E-9445-4DE1-B509-F049E211B555} + EndGlobalSection +EndGlobal diff --git a/MongoDM.sln b/MongoDM.sln deleted file mode 100644 index cbf778ca..00000000 --- a/MongoDM.sln +++ /dev/null @@ -1,39 +0,0 @@ - -Microsoft Visual Studio Solution File, Format Version 12.00 -# Visual Studio Version 16 -VisualStudioVersion = 16.0.28803.156 -MinimumVisualStudioVersion = 10.0.40219.1 -Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "src", "src", "{490DAED7-DAD8-459A-A20E-F57F2F6F619E}" -EndProject -Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "test", "test", "{CF1ABDEA-794F-4474-858D-BCB61F367D72}" -EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "MongoDM", "src\MongoDM\MongoDM.csproj", "{AA9CE7D3-169F-4725-970E-4CE900E91EDD}" -EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "MongoDM.Tests", "test\MongoDM.Tests\MongoDM.Tests.csproj", "{82789357-2EC1-4159-B3F6-0F830ABAFF4C}" -EndProject -Global - GlobalSection(SolutionConfigurationPlatforms) = preSolution - Debug|Any CPU = Debug|Any CPU - Release|Any CPU = Release|Any CPU - EndGlobalSection - GlobalSection(ProjectConfigurationPlatforms) = postSolution - {AA9CE7D3-169F-4725-970E-4CE900E91EDD}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {AA9CE7D3-169F-4725-970E-4CE900E91EDD}.Debug|Any CPU.Build.0 = Debug|Any CPU - {AA9CE7D3-169F-4725-970E-4CE900E91EDD}.Release|Any CPU.ActiveCfg = Release|Any CPU - {AA9CE7D3-169F-4725-970E-4CE900E91EDD}.Release|Any CPU.Build.0 = Release|Any CPU - {82789357-2EC1-4159-B3F6-0F830ABAFF4C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {82789357-2EC1-4159-B3F6-0F830ABAFF4C}.Debug|Any CPU.Build.0 = Debug|Any CPU - {82789357-2EC1-4159-B3F6-0F830ABAFF4C}.Release|Any CPU.ActiveCfg = Release|Any CPU - {82789357-2EC1-4159-B3F6-0F830ABAFF4C}.Release|Any CPU.Build.0 = Release|Any CPU - EndGlobalSection - GlobalSection(SolutionProperties) = preSolution - HideSolutionNode = FALSE - EndGlobalSection - GlobalSection(NestedProjects) = preSolution - {AA9CE7D3-169F-4725-970E-4CE900E91EDD} = {490DAED7-DAD8-459A-A20E-F57F2F6F619E} - {82789357-2EC1-4159-B3F6-0F830ABAFF4C} = {CF1ABDEA-794F-4474-858D-BCB61F367D72} - EndGlobalSection - GlobalSection(ExtensibilityGlobals) = postSolution - SolutionGuid = {43A8089E-9445-4DE1-B509-F049E211B555} - EndGlobalSection -EndGlobal diff --git a/doc/DbContextClassDiagram.drawio b/doc/DbContextClassDiagram.drawio new file mode 100644 index 00000000..3828422c --- /dev/null +++ b/doc/DbContextClassDiagram.drawio @@ -0,0 +1 @@ +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 \ No newline at end of file diff --git a/src/ExecutionContext/AsyncLocal/AsyncLocalContext.cs b/src/ExecutionContext/AsyncLocal/AsyncLocalContext.cs new file mode 100644 index 00000000..9f58fbb9 --- /dev/null +++ b/src/ExecutionContext/AsyncLocal/AsyncLocalContext.cs @@ -0,0 +1,42 @@ +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 AsyncLocal?>(); + + // 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 new file mode 100644 index 00000000..79f45266 --- /dev/null +++ b/src/ExecutionContext/AsyncLocal/AsyncLocalContextHandler.cs @@ -0,0 +1,21 @@ +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 new file mode 100644 index 00000000..f7b12877 --- /dev/null +++ b/src/ExecutionContext/AsyncLocal/IAsyncLocalContext.cs @@ -0,0 +1,18 @@ +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/AsyncLocal/IAsyncLocalContextHandler.cs b/src/ExecutionContext/AsyncLocal/IAsyncLocalContextHandler.cs new file mode 100644 index 00000000..8e9fb773 --- /dev/null +++ b/src/ExecutionContext/AsyncLocal/IAsyncLocalContextHandler.cs @@ -0,0 +1,11 @@ +using System; + +namespace Etherna.ExecContext.AsyncLocal +{ + /// + /// A disposable interface for + /// + public interface IAsyncLocalContextHandler : IDisposable + { + } +} diff --git a/src/ExecutionContext/AsyncLocal/IHandledAsyncLocalContext.cs b/src/ExecutionContext/AsyncLocal/IHandledAsyncLocalContext.cs new file mode 100644 index 00000000..f3564b19 --- /dev/null +++ b/src/ExecutionContext/AsyncLocal/IHandledAsyncLocalContext.cs @@ -0,0 +1,11 @@ +namespace Etherna.ExecContext.AsyncLocal +{ + /// + /// Interface used by for comunicate with its + /// creator . + /// + internal interface IHandledAsyncLocalContext + { + void OnDisposed(IAsyncLocalContextHandler context); + } +} diff --git a/src/ExecutionContext/Exceptions/ExecutionContextNotFoundException.cs b/src/ExecutionContext/Exceptions/ExecutionContextNotFoundException.cs new file mode 100644 index 00000000..60caf978 --- /dev/null +++ b/src/ExecutionContext/Exceptions/ExecutionContextNotFoundException.cs @@ -0,0 +1,16 @@ +using System; + +namespace Etherna.ExecContext.Exceptions +{ + public class ExecutionContextNotFoundException : Exception + { + public ExecutionContextNotFoundException(string message) : base(message) + { } + + public ExecutionContextNotFoundException(string message, Exception innerException) : base(message, innerException) + { } + + public ExecutionContextNotFoundException() + { } + } +} diff --git a/src/ExecutionContext/ExecutionContext.csproj b/src/ExecutionContext/ExecutionContext.csproj new file mode 100644 index 00000000..ae5c181f --- /dev/null +++ b/src/ExecutionContext/ExecutionContext.csproj @@ -0,0 +1,25 @@ + + + + netstandard2.0 + true + Etherna.ExecContext + Etherna Sagl + Execution context provider + 8.0 + enable + https://github.com/Etherna/mongodm + + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + diff --git a/src/ExecutionContext/ExecutionContextSelector.cs b/src/ExecutionContext/ExecutionContextSelector.cs new file mode 100644 index 00000000..db04f1b7 --- /dev/null +++ b/src/ExecutionContext/ExecutionContextSelector.cs @@ -0,0 +1,40 @@ +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/IExecutionContext.cs b/src/ExecutionContext/IExecutionContext.cs new file mode 100644 index 00000000..10a6d294 --- /dev/null +++ b/src/ExecutionContext/IExecutionContext.cs @@ -0,0 +1,16 @@ +using System.Collections.Generic; + +namespace Etherna.ExecContext +{ + /// + /// Represents an execution context, where information can be put and retrieve alongside + /// the process with a key-value dictionary. + /// + public interface IExecutionContext + { + /// + /// The context dictionary. + /// + IDictionary? Items { get; } + } +} diff --git a/src/ExecutionContext/Properties/AssemblyInfo.cs b/src/ExecutionContext/Properties/AssemblyInfo.cs new file mode 100644 index 00000000..fbf5dc36 --- /dev/null +++ b/src/ExecutionContext/Properties/AssemblyInfo.cs @@ -0,0 +1,4 @@ +using System.Runtime.CompilerServices; + +[assembly: InternalsVisibleTo("DynamicProxyGenAssembly2")] +[assembly: InternalsVisibleTo("ExecutionContext.Tests")] diff --git a/src/ExecutionContext/Properties/GlobalSuppressions.cs b/src/ExecutionContext/Properties/GlobalSuppressions.cs new file mode 100644 index 00000000..8035245b --- /dev/null +++ b/src/ExecutionContext/Properties/GlobalSuppressions.cs @@ -0,0 +1,8 @@ +// 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 = "Etherna.ExecContext")] diff --git a/src/MongODM.AspNetCore/HttpContextExecutionContext.cs b/src/MongODM.AspNetCore/HttpContextExecutionContext.cs new file mode 100644 index 00000000..2b7a8262 --- /dev/null +++ b/src/MongODM.AspNetCore/HttpContextExecutionContext.cs @@ -0,0 +1,21 @@ +using Etherna.ExecContext; +using Microsoft.AspNetCore.Http; +using System.Collections.Generic; + +namespace Etherna.MongODM.AspNetCore +{ + public class HttpContextExecutionContext : IExecutionContext + { + // Fields. + private readonly IHttpContextAccessor httpContextAccessor; + + // Constructors. + public HttpContextExecutionContext(IHttpContextAccessor httpContextAccessor) + { + this.httpContextAccessor = httpContextAccessor; + } + + // Properties. + public IDictionary? Items => httpContextAccessor?.HttpContext?.Items; + } +} diff --git a/src/MongODM.AspNetCore/MongODM.AspNetCore.csproj b/src/MongODM.AspNetCore/MongODM.AspNetCore.csproj new file mode 100644 index 00000000..daf416c1 --- /dev/null +++ b/src/MongODM.AspNetCore/MongODM.AspNetCore.csproj @@ -0,0 +1,39 @@ + + + + netstandard2.0;netcoreapp3.0 + true + Etherna.MongODM.AspNetCore + Etherna Sagl + Asp.Net Core adapter for MongODM + 8.0 + enable + https://github.com/Etherna/mongodm + + + + + + + + + + + + + + + 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..7b73a847 --- /dev/null +++ b/src/MongODM.AspNetCore/MongODMConfiguration.cs @@ -0,0 +1,14 @@ +using Microsoft.Extensions.DependencyInjection; + +namespace Etherna.MongODM.AspNetCore +{ + public class MongODMConfiguration + { + public MongODMConfiguration(IServiceCollection services) + { + Services = services; + } + + public IServiceCollection Services { get; } + } +} diff --git a/src/MongODM.AspNetCore/ServiceCollectionExtensions.cs b/src/MongODM.AspNetCore/ServiceCollectionExtensions.cs new file mode 100644 index 00000000..910067e1 --- /dev/null +++ b/src/MongODM.AspNetCore/ServiceCollectionExtensions.cs @@ -0,0 +1,114 @@ +using Etherna.ExecContext; +using Etherna.ExecContext.AsyncLocal; +using Etherna.MongODM; +using Etherna.MongODM.AspNetCore; +using Etherna.MongODM.ProxyModels; +using Etherna.MongODM.Repositories; +using Etherna.MongODM.Serialization; +using Etherna.MongODM.Serialization.Modifiers; +using Etherna.MongODM.Tasks; +using Etherna.MongODM.Utility; +using Microsoft.AspNetCore.Http; +using Microsoft.Extensions.DependencyInjection.Extensions; +using System; +using System.Collections.Generic; +using System.Linq; + +namespace Microsoft.Extensions.DependencyInjection +{ + public static class ServiceCollectionExtensions + { + public static MongODMConfiguration UseMongODM( + this IServiceCollection services, + IEnumerable? executionContexts = null) + where TTaskRunner : class, ITaskRunner => + UseMongODM(services, executionContexts); + + public static MongODMConfiguration UseMongODM( + this IServiceCollection services, + IEnumerable? executionContexts = null) + where TProxyGenerator: class, IProxyGenerator + where TTaskRunner: class, ITaskRunner + { + services.TryAddSingleton(); + + services.TryAddSingleton(serviceProvider => + { + if (executionContexts is null || !executionContexts.Any()) + executionContexts = new IExecutionContext[] //default + { + new HttpContextExecutionContext(serviceProvider.GetService()), + AsyncLocalContext.Instance + }; + + return executionContexts.Count() == 1 ? + executionContexts.First() : + new ExecutionContextSelector(executionContexts); + }); + 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.TryAddSingleton(); + + //tasks + services.TryAddTransient(); + + //castle proxy generator. + services.TryAddSingleton(new Castle.DynamicProxy.ProxyGenerator()); + + return new MongODMConfiguration(services); + } + + public static MongODMConfiguration AddDbContext( + this MongODMConfiguration config, + Action>? dbContextConfig = null) + where TDbContext : class, IDbContext + { + if (config is null) + throw new ArgumentNullException(nameof(config)); + + // Register dbContext. + config.Services.AddSingleton(); + + // Register options. + var contextOptions = new DbContextOptions(); + dbContextConfig?.Invoke(contextOptions); + config.Services.AddSingleton(contextOptions); + + return config; + } + + public static MongODMConfiguration AddDbContext( + this MongODMConfiguration config, + Action>? dbContextConfig = null) + where TDbContext : class, IDbContext + where TDbContextImpl : class, TDbContext + { + if (config is null) + throw new ArgumentNullException(nameof(config)); + + // Register dbContext. + config.Services.AddSingleton(); + config.Services.AddSingleton(sp => sp.GetService() as TDbContextImpl); + + // Register options. + var contextOptions = new DbContextOptions(); + dbContextConfig?.Invoke(contextOptions); + config.Services.AddSingleton(contextOptions); + + return config; + } + } +} diff --git a/src/MongODM.Core/Attributes/PropertyAltererAttribute.cs b/src/MongODM.Core/Attributes/PropertyAltererAttribute.cs new file mode 100644 index 00000000..6059ab62 --- /dev/null +++ b/src/MongODM.Core/Attributes/PropertyAltererAttribute.cs @@ -0,0 +1,19 @@ +using System; + +namespace Etherna.MongODM.Attributes +{ + [AttributeUsage(AttributeTargets.Method, AllowMultiple = true)] + public class PropertyAltererAttribute : Attribute + { + /// + /// The constructor + /// + /// The related property name + public PropertyAltererAttribute(string propertyName) + { + PropertyName = propertyName; + } + + public string PropertyName { get; } + } +} diff --git a/src/MongoDM/DBContextBase.cs b/src/MongODM.Core/DbContext.cs similarity index 56% rename from src/MongoDM/DBContextBase.cs rename to src/MongODM.Core/DbContext.cs index 5b8e1f6f..770c1bb9 100644 --- a/src/MongoDM/DBContextBase.cs +++ b/src/MongODM.Core/DbContext.cs @@ -1,8 +1,10 @@ -using Digicando.MongoDM.Models; -using Digicando.MongoDM.ProxyModels; -using Digicando.MongoDM.Repositories; -using Digicando.MongoDM.Serialization; -using Digicando.MongoDM.Utility; +using Etherna.MongODM.Migration; +using Etherna.MongODM.Models; +using Etherna.MongODM.ProxyModels; +using Etherna.MongODM.Repositories; +using Etherna.MongODM.Serialization; +using Etherna.MongODM.Serialization.Modifiers; +using Etherna.MongODM.Utility; using MongoDB.Bson; using MongoDB.Bson.Serialization.Conventions; using MongoDB.Driver; @@ -12,35 +14,38 @@ using System.Threading; using System.Threading.Tasks; -namespace Digicando.MongoDM +namespace Etherna.MongODM { - public abstract class DBContextBase : IDBContextBase + public abstract class DbContext : IDbContext { // Consts. public const string DocumentVersionElementName = "v"; - // Constructor. - public DBContextBase( - string connectionString, - IDBCache dbCache, - IDBMaintainer dbMaintainer, - string dbName, - IDocumentSchemaRegister documentSchemaRegister, - DocumentVersion documentVersion, - IProxyGenerator proxyGenerator) + // Constructors and initialization. + public DbContext( + IDbContextDependencies dependencies, + DbContextOptions options) { - DBCache = dbCache; - DBMaintainer = dbMaintainer; - DocumentSchemaRegister = documentSchemaRegister; - DocumentVersion = documentVersion; - ProxyGenerator = proxyGenerator; + DBCache = dependencies.DbCache; + DBMaintainer = dependencies.DbMaintainer; + DocumentSchemaRegister = dependencies.DocumentSchemaRegister; + DocumentVersion = options.DocumentVersion; + ProxyGenerator = dependencies.ProxyGenerator; + RepositoryRegister = dependencies.RepositoryRegister; + SerializerModifierAccessor = dependencies.SerializerModifierAccessor; // Initialize MongoDB driver. - Client = new MongoClient(connectionString); - Database = Client.GetDatabase(dbName); + Client = new MongoClient(options.ConnectionString); + Database = Client.GetDatabase(options.DBName); - // Init IoC dependencies. - documentSchemaRegister.Initialize(this); + // Initialize internal dependencies. + DocumentSchemaRegister.Initialize(this); + DBMaintainer.Initialize(this); + RepositoryRegister.Initialize(this); + + // Initialize repositories. + foreach (var repository in RepositoryRegister.ModelRepositoryMap.Values) + repository.Initialize(this); // Customize conventions. ConventionRegistry.Register("Enum string", new ConventionPack @@ -53,47 +58,41 @@ public DBContextBase( serializerCollector.Register(this); // Build and freeze document schema register. - documentSchemaRegister.Freeze(); + DocumentSchemaRegister.Freeze(); } // Public properties. public IReadOnlyCollection ChangedModelsList => DBCache.LoadedModels.Values - .Where(model => (model as IAuditable).IsChanged) + .Where(model => (model as IAuditable)?.IsChanged == true) .ToList(); public IMongoClient Client { get; } public IMongoDatabase Database { get; } - public IDBCache DBCache { get; } - public IDBMaintainer DBMaintainer { get; } + public IDbCache DBCache { get; } + public IDbMaintainer DBMaintainer { get; } public IDocumentSchemaRegister DocumentSchemaRegister { get; } public DocumentVersion DocumentVersion { get; } public bool IsMigrating { get; private set; } - public abstract IReadOnlyDictionary ModelCollectionRepositoryMap { get; } - public abstract IReadOnlyDictionary ModelGridFSRepositoryMap { get; } - public IReadOnlyDictionary ModelRepositoryMap => - Enumerable.Union<(Type ModelType, IRepository Repository)>( - ModelCollectionRepositoryMap.Select(pair => (pair.Key, pair.Value as IRepository)), - ModelGridFSRepositoryMap.Select(pair => (pair.Key, pair.Value as IRepository))) - .ToDictionary(pair => pair.ModelType, pair => pair.Repository); public IProxyGenerator ProxyGenerator { get; } + public IRepositoryRegister RepositoryRegister { get; } + public ISerializerModifierAccessor SerializerModifierAccessor { get; } // Protected properties. + protected virtual IEnumerable MigrationTaskList { get; } = Array.Empty(); protected abstract IEnumerable SerializerCollectors { get; } // Methods. - public async Task MigrateRepositoriesAsync() + public async Task MigrateRepositoriesAsync(CancellationToken cancellationToken = default) { - // Migrate documents. IsMigrating = true; - foreach (var migration in from repository in ModelCollectionRepositoryMap.Values - where repository.MigrationInfo != null - orderby repository.MigrationInfo.PriorityIndex - select repository.MigrationInfo) - await migration.MigrateAsync(); + + // Migrate collections. + foreach (var migration in MigrationTaskList) + await migration.MigrateAsync(cancellationToken); // Build indexes. - foreach (var repository in ModelCollectionRepositoryMap.Values) - await repository.BuildIndexesAsync(DocumentSchemaRegister); + foreach (var repository in RepositoryRegister.ModelCollectionRepositoryMap.Values) + await repository.BuildIndexesAsync(DocumentSchemaRegister, cancellationToken); IsMigrating = false; } @@ -134,9 +133,9 @@ public virtual async Task SaveChangesAsync(CancellationToken cancellationToken = foreach (var model in ChangedModelsList) { var modelType = model.GetType().BaseType; - if (ModelCollectionRepositoryMap.ContainsKey(modelType)) //can't replace if is a file + if (RepositoryRegister.ModelCollectionRepositoryMap.ContainsKey(modelType)) //can't replace if is a file { - var repository = ModelCollectionRepositoryMap[modelType]; + var repository = RepositoryRegister.ModelCollectionRepositoryMap[modelType]; await repository.ReplaceAsync(model); } } diff --git a/src/MongODM.Core/DbContextOptions.cs b/src/MongODM.Core/DbContextOptions.cs new file mode 100644 index 00000000..eaa49bf8 --- /dev/null +++ b/src/MongODM.Core/DbContextOptions.cs @@ -0,0 +1,17 @@ +using Etherna.MongODM.Serialization; +using System.Linq; + +namespace Etherna.MongODM +{ + public class DbContextOptions + { + public string ConnectionString { get; set; } = "mongodb://localhost/localDb"; + public string DBName => ConnectionString.Split('?')[0] + .Split('/').Last(); + public DocumentVersion DocumentVersion { get; set; } = "1.0.0"; + } + + public class DbContextOptions : DbContextOptions + where TDbContext : class, IDbContext + { } +} diff --git a/src/MongODM.Core/Exceptions/EntityNotFoundException.cs b/src/MongODM.Core/Exceptions/EntityNotFoundException.cs new file mode 100644 index 00000000..fbcd8754 --- /dev/null +++ b/src/MongODM.Core/Exceptions/EntityNotFoundException.cs @@ -0,0 +1,17 @@ +using System; + +namespace Etherna.MongODM.Exceptions +{ + public class EntityNotFoundException : Exception + { + // Constructors. + public EntityNotFoundException() + { } + + public EntityNotFoundException(string message) : base(message) + { } + + public EntityNotFoundException(string message, Exception innerException) : base(message, innerException) + { } + } +} diff --git a/src/MongODM.Core/Exceptions/InvalidEntityTypeException.cs b/src/MongODM.Core/Exceptions/InvalidEntityTypeException.cs new file mode 100644 index 00000000..bf1fb4d6 --- /dev/null +++ b/src/MongODM.Core/Exceptions/InvalidEntityTypeException.cs @@ -0,0 +1,13 @@ +using System; + +namespace Etherna.MongODM.Exceptions +{ + public class InvalidEntityTypeException : Exception + { + public InvalidEntityTypeException() + { } + + public InvalidEntityTypeException(string message) : base(message) + { } + } +} diff --git a/src/MongoDM/Extensions/ClassMapExtensions.cs b/src/MongODM.Core/Extensions/ClassMapExtensions.cs similarity index 88% rename from src/MongoDM/Extensions/ClassMapExtensions.cs rename to src/MongODM.Core/Extensions/ClassMapExtensions.cs index ffddb70d..aea66a6e 100644 --- a/src/MongoDM/Extensions/ClassMapExtensions.cs +++ b/src/MongODM.Core/Extensions/ClassMapExtensions.cs @@ -1,10 +1,10 @@ -using Digicando.MongoDM.Models; -using Digicando.MongoDM.Serialization.Serializers; +using Etherna.MongODM.Models; +using Etherna.MongODM.Serialization.Serializers; using MongoDB.Bson.Serialization; using System; using System.Linq.Expressions; -namespace Digicando.MongoDM.Extensions +namespace Etherna.MongODM.Extensions { public static class ClassMapExtensions { @@ -28,7 +28,7 @@ public static BsonMemberMap SetMemberSerializer { if (typeof(TMember) == typeof(TSerializer)) - return classMap.SetMemberSerializer(memberLambda, serializer as IBsonSerializer); + return classMap.SetMemberSerializer(memberLambda, (IBsonSerializer)serializer); else return classMap.SetMemberSerializer(memberLambda, serializer.GetAdapter()); } diff --git a/src/MongoDM/Extensions/EnumerableExtensions.cs b/src/MongODM.Core/Extensions/EnumerableExtensions.cs similarity index 85% rename from src/MongoDM/Extensions/EnumerableExtensions.cs rename to src/MongODM.Core/Extensions/EnumerableExtensions.cs index 838e2c44..def59d78 100644 --- a/src/MongoDM/Extensions/EnumerableExtensions.cs +++ b/src/MongODM.Core/Extensions/EnumerableExtensions.cs @@ -4,7 +4,7 @@ using System.Linq; using System.Linq.Expressions; -namespace Digicando.MongoDM.Extensions +namespace Etherna.MongODM.Extensions { public static class EnumerableExtensions { @@ -18,6 +18,7 @@ public static class EnumerableExtensions /// Page to take /// Elements per page /// Selected elements page + /// Throw with invalid parameter values public static IEnumerable Paginate( this IEnumerable values, Func orderKeySelector, @@ -25,9 +26,9 @@ public static IEnumerable Paginate( int take) { if (page < 0) - throw new ArgumentOutOfRangeException(nameof(page)); + throw new ArgumentOutOfRangeException(nameof(page), page, "Value can't be negative"); if (take < 1) - throw new ArgumentOutOfRangeException(nameof(take)); + throw new ArgumentOutOfRangeException(nameof(take), take, "Value can't be less than 1"); return values.OrderBy(orderKeySelector) .Skip(page * take) @@ -44,6 +45,7 @@ public static IEnumerable Paginate( /// Page to take /// Elements per page /// Selected elements page + /// Throw with invalid parameter values public static IMongoQueryable Paginate( this IMongoQueryable values, Expression> orderKeySelector, @@ -51,9 +53,9 @@ public static IMongoQueryable Paginate( int take) { if (page < 0) - throw new ArgumentOutOfRangeException(nameof(page)); + throw new ArgumentOutOfRangeException(nameof(page), page, "Value can't be negative"); if (take < 1) - throw new ArgumentOutOfRangeException(nameof(take)); + throw new ArgumentOutOfRangeException(nameof(take), take, "Value can't be less than 1"); return values.OrderBy(orderKeySelector) .Skip(page * take) @@ -70,6 +72,7 @@ public static IMongoQueryable Paginate( /// Page to take /// Elements per page /// Selected elements page + /// Throw with invalid parameter values public static IEnumerable PaginateDescending( this IEnumerable values, Func orderKeySelector, @@ -77,9 +80,9 @@ public static IEnumerable PaginateDescending( int take) { if (page < 0) - throw new ArgumentOutOfRangeException(nameof(page)); + throw new ArgumentOutOfRangeException(nameof(page), page, "Value can't be negative"); if (take < 1) - throw new ArgumentOutOfRangeException(nameof(take)); + throw new ArgumentOutOfRangeException(nameof(take), take, "Value can't be less than 1"); return values.OrderByDescending(orderKeySelector) .Skip(page * take) @@ -96,6 +99,7 @@ public static IEnumerable PaginateDescending( /// Page to take /// Elements per page /// Selected elements page + /// Throw with invalid parameter values public static IMongoQueryable PaginateDescending( this IMongoQueryable values, Expression> orderKeySelector, @@ -103,9 +107,9 @@ public static IMongoQueryable PaginateDescending( int take) { if (page < 0) - throw new ArgumentOutOfRangeException(nameof(page)); + throw new ArgumentOutOfRangeException(nameof(page), page, "Value can't be negative"); if (take < 1) - throw new ArgumentOutOfRangeException(nameof(take)); + throw new ArgumentOutOfRangeException(nameof(take), take, "Value can't be less than 1"); return values.OrderByDescending(orderKeySelector) .Skip(page * take) diff --git a/src/MongoDM/GlobalSuppressions.cs b/src/MongODM.Core/GlobalSuppressions.cs similarity index 65% rename from src/MongoDM/GlobalSuppressions.cs rename to src/MongODM.Core/GlobalSuppressions.cs index dbaddd31..174ec059 100644 --- a/src/MongoDM/GlobalSuppressions.cs +++ b/src/MongODM.Core/GlobalSuppressions.cs @@ -4,6 +4,6 @@ // Project-level suppressions either have no target or are given // a specific target and scoped to a namespace, type, member, etc. -[assembly: System.Diagnostics.CodeAnalysis.SuppressMessage("Code Quality", "IDE0051:Remove unused private members", Justification = "", Scope = "member", Target = "~M:Digicando.MongoDM.Serialization.DocumentSchemaRegister.MembersDependenciesToString~System.String")] -[assembly: System.Diagnostics.CodeAnalysis.SuppressMessage("Code Quality", "IDE0051:Remove unused private members", Justification = "", Scope = "member", Target = "~M:Digicando.MongoDM.Serialization.DocumentSchemaRegister.ModelDependenciesToString~System.String")] +[assembly: System.Diagnostics.CodeAnalysis.SuppressMessage("Code Quality", "IDE0051:Remove unused private members", Justification = "", Scope = "member", Target = "~M:Etherna.MongODM.Serialization.DocumentSchemaRegister.MembersDependenciesToString~System.String")] +[assembly: System.Diagnostics.CodeAnalysis.SuppressMessage("Code Quality", "IDE0051:Remove unused private members", Justification = "", Scope = "member", Target = "~M:Etherna.MongODM.Serialization.DocumentSchemaRegister.ModelDependenciesToString~System.String")] diff --git a/src/MongODM.Core/IDbContext.cs b/src/MongODM.Core/IDbContext.cs new file mode 100644 index 00000000..2ffc85df --- /dev/null +++ b/src/MongODM.Core/IDbContext.cs @@ -0,0 +1,88 @@ +using Etherna.MongODM.ProxyModels; +using Etherna.MongODM.Repositories; +using Etherna.MongODM.Serialization; +using Etherna.MongODM.Serialization.Modifiers; +using Etherna.MongODM.Utility; +using MongoDB.Driver; +using System.Threading; +using System.Threading.Tasks; + +namespace Etherna.MongODM +{ + /// + /// Interface of implementation. + /// + public interface IDbContext + { + // Properties. + /// + /// Current MongoDB client. + /// + IMongoClient Client { get; } + + /// + /// Current MongoDB database. + /// + IMongoDatabase Database { get; } + + /// + /// Database cache container. + /// + IDbCache DBCache { get; } + + /// + /// Database operator interested into maintenance tasks. + /// + IDbMaintainer DBMaintainer { get; } + + /// + /// Container for model serialization and document schema information. + /// + IDocumentSchemaRegister DocumentSchemaRegister { get; } + + /// + /// Current operating document version. + /// + DocumentVersion DocumentVersion { get; } + + /// + /// Flag reporting eventual current migration operation. + /// + bool IsMigrating { get; } + + /// + /// Current model proxy generator. + /// + IProxyGenerator ProxyGenerator { get; } + + /// + /// Register of available repositories. + /// + IRepositoryRegister RepositoryRegister { get; } + + /// + /// Serializer modifier accessor. + /// + ISerializerModifierAccessor SerializerModifierAccessor { get; } + + // Methods. + /// + /// Start a database migration process. + /// + /// Cancellation token + Task MigrateRepositoriesAsync(CancellationToken cancellationToken = default); + + /// + /// Save current model changes on db. + /// + /// Cancellation token + Task SaveChangesAsync(CancellationToken cancellationToken = default); + + /// + /// Start a new database transaction session. + /// + /// Cancellation token + /// The session handler + Task StartSessionAsync(CancellationToken cancellationToken = default); + } +} \ No newline at end of file diff --git a/src/MongODM.Core/IDbContextInitializable.cs b/src/MongODM.Core/IDbContextInitializable.cs new file mode 100644 index 00000000..89e61e46 --- /dev/null +++ b/src/MongODM.Core/IDbContextInitializable.cs @@ -0,0 +1,9 @@ +namespace Etherna.MongODM +{ + public interface IDbContextInitializable + { + bool IsInitialized { get; } + + void Initialize(IDbContext dbContext); + } +} \ No newline at end of file diff --git a/src/MongODM.Core/Migration/MongoCollectionMigration.cs b/src/MongODM.Core/Migration/MongoCollectionMigration.cs new file mode 100644 index 00000000..de10a709 --- /dev/null +++ b/src/MongODM.Core/Migration/MongoCollectionMigration.cs @@ -0,0 +1,46 @@ +using Etherna.MongODM.Models; +using Etherna.MongODM.Repositories; +using MongoDB.Driver; +using System; +using System.Threading; +using System.Threading.Tasks; + +namespace Etherna.MongODM.Migration +{ + /// + /// Migrate a collection to another + /// + /// Type of source model + /// Type of source key + /// Type of destination model + /// Type of destination key + public class MongoCollectionMigration : MongoMigrationBase + where TModelSource : class, IEntityModel + where TModelDest : class, IEntityModel + { + private readonly Func converter; + private readonly Func discriminator; + private readonly IMongoCollection destinationCollection; + private readonly IMongoCollection sourceCollection; + + public MongoCollectionMigration( + ICollectionRepository sourceCollection, + ICollectionRepository destinationCollection, + Func converter, + Func discriminator) + { + this.sourceCollection = sourceCollection.Collection; + this.destinationCollection = destinationCollection.Collection; + this.converter = converter; + this.discriminator = discriminator; + } + + public override Task MigrateAsync(CancellationToken cancellationToken = default) => + sourceCollection.Find(Builders.Filter.Empty, new FindOptions { NoCursorTimeout = true }) + .ForEachAsync(obj => + { + if (discriminator(obj)) + destinationCollection.InsertOneAsync(converter(obj)); + }, cancellationToken); + } +} diff --git a/src/MongODM.Core/Migration/MongoDocumentMigration.cs b/src/MongODM.Core/Migration/MongoDocumentMigration.cs new file mode 100644 index 00000000..5ddbb4da --- /dev/null +++ b/src/MongODM.Core/Migration/MongoDocumentMigration.cs @@ -0,0 +1,64 @@ +using Etherna.MongODM.Models; +using Etherna.MongODM.Repositories; +using Etherna.MongODM.Serialization; +using MongoDB.Bson; +using MongoDB.Driver; +using System.Threading; +using System.Threading.Tasks; + +namespace Etherna.MongODM.Migration +{ + /// + /// Migrate documents of a collection from an older version to a newer + /// + /// The model type + /// The model's key type + public class MongoDocumentMigration : MongoMigrationBase + where TModel : class, IEntityModel + { + private readonly DocumentVersion minimumDocumentVersion; + private readonly IMongoCollection sourceCollection; + + public MongoDocumentMigration( + ICollectionRepository sourceCollection, + DocumentVersion minimumDocumentVersion) + { + this.sourceCollection = sourceCollection.Collection; + this.minimumDocumentVersion = minimumDocumentVersion; + } + + /// + /// Fix all documents prev of MinimumDocumentVersion + /// + public override async Task MigrateAsync(CancellationToken cancellationToken = default) + { + var filterBuilder = Builders.Filter; + var filter = filterBuilder.Or( + // No version in document (very old). + filterBuilder.Exists(DbContext.DocumentVersionElementName, false), + + // Version as string (doc.Version < "0.12.0"). + //(can't query directly for string because https://docs.mongodb.com/v3.2/reference/operator/query/type/#arrays) + filterBuilder.Not(filterBuilder.Type(DbContext.DocumentVersionElementName, BsonType.Int32)), + + // Version is an array with values ("0.12.0" <= doc.Version). + //doc.Major < min.Major + filterBuilder.Lt($"{DbContext.DocumentVersionElementName}.0", minimumDocumentVersion.MajorRelease), + + //doc.Major == min.Major && doc.Minor < min.Minor + filterBuilder.And( + filterBuilder.Eq($"{DbContext.DocumentVersionElementName}.0", minimumDocumentVersion.MajorRelease), + filterBuilder.Lt($"{DbContext.DocumentVersionElementName}.1", minimumDocumentVersion.MinorRelease)), + + //doc.Major == min.Major && doc.Minor == min.Minor && doc.Patch < min.Patch + filterBuilder.And( + filterBuilder.Eq($"{DbContext.DocumentVersionElementName}.0", minimumDocumentVersion.MajorRelease), + filterBuilder.Eq($"{DbContext.DocumentVersionElementName}.1", minimumDocumentVersion.MinorRelease), + filterBuilder.Lt($"{DbContext.DocumentVersionElementName}.2", minimumDocumentVersion.PatchRelease))); + + // Replace documents. + await sourceCollection.Find(filter, new FindOptions { NoCursorTimeout = true }) + .ForEachAsync(obj => sourceCollection.ReplaceOneAsync(Builders.Filter.Eq(m => m.Id, obj.Id), obj), cancellationToken); + } + } +} diff --git a/src/MongODM.Core/Migration/MongoMigrationBase.cs b/src/MongODM.Core/Migration/MongoMigrationBase.cs new file mode 100644 index 00000000..53a67fdc --- /dev/null +++ b/src/MongODM.Core/Migration/MongoMigrationBase.cs @@ -0,0 +1,10 @@ +using System.Threading; +using System.Threading.Tasks; + +namespace Etherna.MongODM.Migration +{ + public abstract class MongoMigrationBase + { + public abstract Task MigrateAsync(CancellationToken cancellationToken = default); + } +} diff --git a/src/MongoDM/Models/IEntityModel.cs b/src/MongODM.Core/Models/IEntityModel.cs similarity index 87% rename from src/MongoDM/Models/IEntityModel.cs rename to src/MongODM.Core/Models/IEntityModel.cs index f27c4310..ffd76256 100644 --- a/src/MongoDM/Models/IEntityModel.cs +++ b/src/MongODM.Core/Models/IEntityModel.cs @@ -1,6 +1,6 @@ using System; -namespace Digicando.MongoDM.Models +namespace Etherna.MongODM.Models { public interface IEntityModel : IModel { diff --git a/src/MongoDM/Models/IFileModel.cs b/src/MongODM.Core/Models/IFileModel.cs similarity index 83% rename from src/MongoDM/Models/IFileModel.cs rename to src/MongODM.Core/Models/IFileModel.cs index c1ccc9e4..bddcf289 100644 --- a/src/MongoDM/Models/IFileModel.cs +++ b/src/MongODM.Core/Models/IFileModel.cs @@ -1,6 +1,6 @@ using System.IO; -namespace Digicando.MongoDM.Models +namespace Etherna.MongODM.Models { public interface IFileModel : IEntityModel { diff --git a/src/MongODM.Core/Models/IModel.cs b/src/MongODM.Core/Models/IModel.cs new file mode 100644 index 00000000..5fec005e --- /dev/null +++ b/src/MongODM.Core/Models/IModel.cs @@ -0,0 +1,9 @@ +using System.Collections.Generic; + +namespace Etherna.MongODM.Models +{ + public interface IModel + { + IDictionary? ExtraElements { get; } + } +} diff --git a/src/MongODM.Core/MongODM.Core.csproj b/src/MongODM.Core/MongODM.Core.csproj new file mode 100644 index 00000000..674ffeee --- /dev/null +++ b/src/MongODM.Core/MongODM.Core.csproj @@ -0,0 +1,29 @@ + + + + netstandard2.0 + true + Etherna.MongODM + Etherna Sagl + ODM framework for MongoDB + 8.0 + enable + https://github.com/Etherna/mongodm + + + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + + + + + + + + diff --git a/src/MongoDM/ProxyModels/AuditableInterceptor.cs b/src/MongODM.Core/ProxyModels/AuditableInterceptor.cs similarity index 96% rename from src/MongoDM/ProxyModels/AuditableInterceptor.cs rename to src/MongODM.Core/ProxyModels/AuditableInterceptor.cs index 70e4a140..eaad5579 100644 --- a/src/MongoDM/ProxyModels/AuditableInterceptor.cs +++ b/src/MongODM.Core/ProxyModels/AuditableInterceptor.cs @@ -1,12 +1,11 @@ using Castle.DynamicProxy; -using Digicando.DomainHelper.Attributes; -using Digicando.DomainHelper.ProxyModel; +using Etherna.MongODM.Attributes; using System; using System.Collections.Generic; using System.Linq; using System.Reflection; -namespace Digicando.MongoDM.ProxyModels +namespace Etherna.MongODM.ProxyModels { public class AuditableInterceptor : ModelInterceptorBase { diff --git a/src/MongoDM/ProxyModels/IAuditable.cs b/src/MongODM.Core/ProxyModels/IAuditable.cs similarity index 90% rename from src/MongoDM/ProxyModels/IAuditable.cs rename to src/MongODM.Core/ProxyModels/IAuditable.cs index b18120a2..dd06ab5c 100644 --- a/src/MongoDM/ProxyModels/IAuditable.cs +++ b/src/MongODM.Core/ProxyModels/IAuditable.cs @@ -1,7 +1,7 @@ using System.Collections.Generic; using System.Reflection; -namespace Digicando.MongoDM.ProxyModels +namespace Etherna.MongODM.ProxyModels { public interface IAuditable { diff --git a/src/MongODM.Core/ProxyModels/IProxyGenerator.cs b/src/MongODM.Core/ProxyModels/IProxyGenerator.cs new file mode 100644 index 00000000..10902b19 --- /dev/null +++ b/src/MongODM.Core/ProxyModels/IProxyGenerator.cs @@ -0,0 +1,11 @@ +using System; + +namespace Etherna.MongODM.ProxyModels +{ + public interface IProxyGenerator + { + object CreateInstance(IDbContext dbContext, Type type, params object[] constructorArguments); + TModel CreateInstance(IDbContext dbContext, params object[] constructorArguments); + bool IsProxyType(Type type); + } +} \ No newline at end of file diff --git a/src/MongoDM/ProxyModels/IReferenceable.cs b/src/MongODM.Core/ProxyModels/IReferenceable.cs similarity index 96% rename from src/MongoDM/ProxyModels/IReferenceable.cs rename to src/MongODM.Core/ProxyModels/IReferenceable.cs index 91558403..b842230d 100644 --- a/src/MongoDM/ProxyModels/IReferenceable.cs +++ b/src/MongODM.Core/ProxyModels/IReferenceable.cs @@ -1,6 +1,6 @@ using System.Collections.Generic; -namespace Digicando.MongoDM.ProxyModels +namespace Etherna.MongODM.ProxyModels { public interface IReferenceable { diff --git a/src/MongODM.Core/ProxyModels/ModelInterceptorBase.cs b/src/MongODM.Core/ProxyModels/ModelInterceptorBase.cs new file mode 100644 index 00000000..5f6232fe --- /dev/null +++ b/src/MongODM.Core/ProxyModels/ModelInterceptorBase.cs @@ -0,0 +1,58 @@ +using Castle.DynamicProxy; +using System; +using System.Collections.Generic; +using System.Linq; + +namespace Etherna.MongODM.ProxyModels +{ + public abstract class ModelInterceptorBase : IInterceptor + { + private readonly IEnumerable additionalInterfaces; + + public ModelInterceptorBase(IEnumerable additionalInterfaces) + { + this.additionalInterfaces = additionalInterfaces; + } + + public void Intercept(IInvocation invocation) + { + if (additionalInterfaces.Contains(invocation.Method.DeclaringType)) + { + var handled = InterceptInterface(invocation); + if (handled) + return; + invocation.Proceed(); + } + else + { + // Check model type. + if (invocation.Method.DeclaringType != typeof(TModel) && + !invocation.Method.DeclaringType.IsAssignableFrom(typeof(TModel))) + { + throw new InvalidOperationException(); + } + + InterceptModel(invocation); + } + } + + /// + /// Intercept an extra interface + /// + /// Current invocation + /// Returns true if the call handled the invocation + protected virtual bool InterceptInterface(IInvocation invocation) + { + return false; + } + + /// + /// Intercept a call to the model + /// + /// Current invocation + protected virtual void InterceptModel(IInvocation invocation) + { + invocation.Proceed(); + } + } +} diff --git a/src/MongODM.Core/ProxyModels/ProxyGenerator.cs b/src/MongODM.Core/ProxyModels/ProxyGenerator.cs new file mode 100644 index 00000000..21190269 --- /dev/null +++ b/src/MongODM.Core/ProxyModels/ProxyGenerator.cs @@ -0,0 +1,187 @@ +using Castle.DynamicProxy; +using Etherna.MongODM.Models; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading; + +namespace Etherna.MongODM.ProxyModels +{ + public class ProxyGenerator : IProxyGenerator + { + // Fields. + private readonly Castle.DynamicProxy.IProxyGenerator proxyGeneratorCore; + + private readonly Dictionary InterceptorInstancerSelector)> modelConfigurationDictionary = + new Dictionary InterceptorInstancerSelector)>(); + private readonly ReaderWriterLockSlim modelConfigurationDictionaryLock = new ReaderWriterLockSlim(LockRecursionPolicy.SupportsRecursion); + + private readonly Dictionary proxyTypeDictionary = new Dictionary(); + private readonly ReaderWriterLockSlim proxyTypeDictionaryLock = new ReaderWriterLockSlim(LockRecursionPolicy.SupportsRecursion); + + // Constructors. + public ProxyGenerator( + Castle.DynamicProxy.IProxyGenerator proxyGeneratorCore) + { + this.proxyGeneratorCore = proxyGeneratorCore; + } + + // Methods. + public object CreateInstance( + IDbContext dbContext, + Type type, + params object[] constructorArguments) + { + // Get configuration. + (Type[] AdditionalInterfaces, Func InterceptorInstancerSelector) configuration = (null!, null!); + modelConfigurationDictionaryLock.EnterReadLock(); + bool configurationFound = false; + try + { + if (modelConfigurationDictionary.ContainsKey(type)) + { + configuration = modelConfigurationDictionary[type]; + configurationFound = true; + } + } + finally + { + modelConfigurationDictionaryLock.ExitReadLock(); + } + + if (!configurationFound) + { + modelConfigurationDictionaryLock.EnterWriteLock(); + try + { + if (modelConfigurationDictionary.ContainsKey(type)) + { + configuration = modelConfigurationDictionary[type]; + } + else + { + var additionalInterfaces = GetAdditionalInterfaces(type); + configuration = (additionalInterfaces, GetInterceptorInstancer(type, additionalInterfaces)); + modelConfigurationDictionary.Add(type, configuration); + } + } + finally + { + modelConfigurationDictionaryLock.ExitWriteLock(); + } + } + + // Generate model. + var proxyModel = proxyGeneratorCore.CreateClassProxy( + type, + configuration.AdditionalInterfaces, + ProxyGenerationOptions.Default, + constructorArguments, + configuration.InterceptorInstancerSelector(dbContext)); + + // Add to proxy type dictionary. + var addToproxyTypeDictionary = false; + proxyTypeDictionaryLock.EnterReadLock(); + try + { + addToproxyTypeDictionary = !proxyTypeDictionary.ContainsKey(type); + } + finally + { + proxyTypeDictionaryLock.ExitReadLock(); + } + + if (addToproxyTypeDictionary) + { + proxyTypeDictionaryLock.EnterWriteLock(); + try + { + if (!proxyTypeDictionary.ContainsKey(type)) + { + proxyTypeDictionary.Add(type, proxyModel.GetType()); + } + } + finally + { + proxyTypeDictionaryLock.ExitWriteLock(); + } + } + + return proxyModel; + } + + public TModel CreateInstance(IDbContext dbContext, params object[] constructorArguments) => + (TModel)CreateInstance(dbContext, typeof(TModel), constructorArguments); + + public bool IsProxyType(Type type) + { + proxyTypeDictionaryLock.EnterReadLock(); + try + { + return proxyTypeDictionary.ContainsValue(type); + } + finally + { + proxyTypeDictionaryLock.ExitReadLock(); + } + } + + // Protected virtual methods. + protected virtual IEnumerable GetCustomAdditionalInterfaces(Type modelType) => + Array.Empty(); + + protected virtual IEnumerable> GetCustomInterceptorInstancer(Type modelType, IEnumerable additionalInterfaces) => + Array.Empty>(); + + // Helpers. + private Type[] GetAdditionalInterfaces(Type modelType) + { + var interfaces = new List(); + + // Add custom additional interfaces. + interfaces.AddRange(GetCustomAdditionalInterfaces(modelType)); + + // Add internal additional interfaces. + if (modelType.GetInterfaces().Contains(typeof(IEntityModel))) //only if is IEntityModel. + { + interfaces.Add(typeof(IAuditable)); + interfaces.Add(typeof(IReferenceable)); + } + + return interfaces.ToArray(); + } + + private Func GetInterceptorInstancer( + Type modelType, + IEnumerable additionalInterfaces) + { + var interceptorInstancers = new List>(); + + // Add custom interceptor instancers. + interceptorInstancers.AddRange(GetCustomInterceptorInstancer(modelType, additionalInterfaces)); + + // Add internal interceptor instances. + if (modelType.GetInterfaces().Contains(typeof(IEntityModel))) //only if is IEntityModel. + { + var entityModelType = modelType.GetInterfaces().First( + i => i.IsGenericType && i.GetGenericTypeDefinition() == typeof(IEntityModel<>)); + var entityModelKeyType = entityModelType.GetGenericArguments().Single(); + + //auditableInterceptor + interceptorInstancers.Add(dbContext => (IInterceptor)Activator.CreateInstance( + typeof(AuditableInterceptor<>).MakeGenericType(modelType), + additionalInterfaces)); + + //summarizableInterceptor + interceptorInstancers.Add(dbContext => (IInterceptor)Activator.CreateInstance( + typeof(ReferenceableInterceptor<,>).MakeGenericType(modelType, entityModelKeyType), + additionalInterfaces, + dbContext)); + } + + return dbContext => (from instancer in interceptorInstancers + select instancer(dbContext)).ToArray(); + } + } +} diff --git a/src/MongoDM/ProxyModels/ReferenceableInterceptor.cs b/src/MongODM.Core/ProxyModels/ReferenceableInterceptor.cs similarity index 85% rename from src/MongoDM/ProxyModels/ReferenceableInterceptor.cs rename to src/MongODM.Core/ProxyModels/ReferenceableInterceptor.cs index 449a7db3..3cf8425e 100644 --- a/src/MongoDM/ProxyModels/ReferenceableInterceptor.cs +++ b/src/MongODM.Core/ProxyModels/ReferenceableInterceptor.cs @@ -1,16 +1,14 @@ using Castle.DynamicProxy; -using Digicando.DomainHelper; -using Digicando.DomainHelper.Attributes; -using Digicando.DomainHelper.ProxyModel; -using Digicando.MongoDM.Models; -using Digicando.MongoDM.Repositories; +using Etherna.MongODM.Attributes; +using Etherna.MongODM.Models; +using Etherna.MongODM.Repositories; using System; using System.Collections.Generic; using System.Linq; using System.Reflection; using System.Threading.Tasks; -namespace Digicando.MongoDM.ProxyModels +namespace Etherna.MongODM.ProxyModels { public class ReferenceableInterceptor : ModelInterceptorBase where TModel : class, IEntityModel @@ -23,13 +21,10 @@ public class ReferenceableInterceptor : ModelInterceptorBase additionalInterfaces, - IDBContextBase dBContext) + IDbContext dbContext) : base(additionalInterfaces) { - if (typeof(TModel).GetInterfaces().Contains(typeof(IFileModel))) - repository = dBContext.ModelGridFSRepositoryMap[typeof(TModel)] as IRepository; - else - repository = dBContext.ModelCollectionRepositoryMap[typeof(TModel)] as IRepository; + repository = (IRepository)dbContext.RepositoryRegister.ModelRepositoryMap[typeof(TModel)]; } // Protected methods. @@ -52,7 +47,7 @@ protected override bool InterceptInterface(IInvocation invocation) } else if (invocation.Method.Name == nameof(IReferenceable.MergeFullModel)) { - MergeFullModel(invocation.Proxy as TModel, invocation.GetArgumentValue(0) as TModel); + MergeFullModel((TModel)invocation.Proxy, invocation.GetArgumentValue(0) as TModel); } else if (invocation.Method.Name == nameof(IReferenceable.SetAsSummary)) { @@ -82,7 +77,7 @@ protected override void InterceptModel(IInvocation invocation) // If member is not loaded, load the full object. if (!settedMemberNames.ContainsKey(propertyName)) { - var task = FullLoadAsync(invocation.Proxy as TModel); + var task = FullLoadAsync((TModel)invocation.Proxy); task.Wait(); } } @@ -108,7 +103,7 @@ protected override void InterceptModel(IInvocation invocation) // If member is not setted and is summary, load the full object. if (!settedMemberNames.ContainsKey(propertyName)) { - var task = FullLoadAsync(invocation.Proxy as TModel); + var task = FullLoadAsync((TModel)invocation.Proxy); task.Wait(); break; @@ -136,7 +131,7 @@ private async Task FullLoadAsync(TModel model) } } - private void MergeFullModel(TModel model, TModel fullModel) + private void MergeFullModel(TModel model, TModel? fullModel) { if (fullModel != null) { diff --git a/src/MongODM.Core/ReflectionHelper.cs b/src/MongODM.Core/ReflectionHelper.cs new file mode 100644 index 00000000..887881bd --- /dev/null +++ b/src/MongODM.Core/ReflectionHelper.cs @@ -0,0 +1,263 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Linq.Expressions; +using System.Reflection; +using System.Threading; + +namespace Etherna.MongODM +{ + public static class ReflectionHelper + { + private static readonly Dictionary> propertyRegister = new Dictionary>(); + private static readonly ReaderWriterLockSlim propertyRegisterLock = new ReaderWriterLockSlim(); + + public static TClass CloneModel(TClass srcObj, params Expression>[] memberLambdas) + where TClass : new() + { + var destObj = new TClass(); + CloneModel(srcObj, destObj, memberLambdas); + return destObj; + } + + public static void CloneModel(TClass srcObj, TClass destObj, params Expression>[] memberLambdas) + { + if (srcObj is null) + throw new ArgumentNullException(nameof(srcObj)); + if (destObj is null) + throw new ArgumentNullException(nameof(destObj)); + + IEnumerable membersToClone; + + if (memberLambdas.Any()) + { + membersToClone = memberLambdas.Select(l => GetMemberInfoFromLambda(l, typeof(TClass))); + } + else // clone full object + { + membersToClone = GetWritableInstanceProperties(typeof(TClass)); + } + + foreach (var member in membersToClone) + { + SetValue(destObj, member, GetValue(srcObj, member)); + } + } + + public static void CloneModel(object srcObj, object destObj, Type actualType) => + CloneModel(srcObj, destObj, GetWritableInstanceProperties(actualType)); + + public static void CloneModel(object srcObj, object destObj, IEnumerable members) + { + foreach (var member in members) + { + SetValue(destObj, member, GetValue(srcObj, member)); + } + } + + public static MemberInfo FindProperty(LambdaExpression lambdaExpression) + { + Expression expressionToCheck = lambdaExpression; + + bool done = false; + + while (!done) + { + switch (expressionToCheck.NodeType) + { + case ExpressionType.Convert: + expressionToCheck = ((UnaryExpression)expressionToCheck).Operand; + break; + case ExpressionType.Lambda: + expressionToCheck = ((LambdaExpression)expressionToCheck).Body; + break; + case ExpressionType.MemberAccess: + var memberExpression = ((MemberExpression)expressionToCheck); + + if (memberExpression.Expression.NodeType != ExpressionType.Parameter && + memberExpression.Expression.NodeType != ExpressionType.Convert) + { + throw new ArgumentException( + $"Expression '{lambdaExpression}' must resolve to top-level member and not any child object's properties. Use a custom resolver on the child type or the AfterMap option instead.", + nameof(lambdaExpression)); + } + + MemberInfo member = memberExpression.Member; + + return member; + default: + done = true; + break; + } + } + + throw new InvalidOperationException(); + } + + public static PropertyInfo FindPropertyImplementation(PropertyInfo interfacePropertyInfo, Type actualType) + { + var interfaceType = interfacePropertyInfo.DeclaringType; + + // An interface map must be used because because there is no + // other officially documented way to derive the explicitly + // implemented property name. + var interfaceMap = actualType.GetInterfaceMap(interfaceType); + + var interfacePropertyAccessors = interfacePropertyInfo.GetAccessors(true); + + var actualPropertyAccessors = interfacePropertyAccessors.Select(interfacePropertyAccessor => + { + var index = Array.IndexOf(interfaceMap.InterfaceMethods, interfacePropertyAccessor); + + return interfaceMap.TargetMethods[index]; + }); + + // Binding must be done by accessor methods because interface + // maps only map accessor methods and do not map properties. + return actualType.GetProperties(BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic) + .Single(propertyInfo => + { + // we are looking for a property that implements all the required accessors + var propertyAccessors = propertyInfo.GetAccessors(true); + return actualPropertyAccessors.All(x => propertyAccessors.Contains(x)); + }); + } + + public static object? GetDefaultValue(Type type) + { + if (type.IsValueType) + { + return Activator.CreateInstance(type); + } + return null; + } + + public static MemberInfo GetMemberInfoFromLambda( + Expression> memberLambda, + Type? actualType = null) + { + var body = memberLambda.Body; + MemberExpression memberExpression; + switch (body.NodeType) + { + case ExpressionType.MemberAccess: + memberExpression = (MemberExpression)body; + break; + case ExpressionType.Convert: + var convertExpression = (UnaryExpression)body; + memberExpression = (MemberExpression)convertExpression.Operand; + break; + default: + throw new InvalidOperationException("Invalid lambda expression"); + } + var memberInfo = memberExpression.Member; + switch (memberInfo.MemberType) + { + case MemberTypes.Field: + break; + case MemberTypes.Property: + if (actualType?.IsInterface == false && + memberInfo.DeclaringType.IsInterface) + { + memberInfo = FindPropertyImplementation((PropertyInfo)memberInfo, actualType); + } + break; + default: + memberInfo = null; + break; + } + if (memberInfo == null) + { + throw new InvalidOperationException("Invalid lambda expression"); + } + return memberInfo; + } + + public static object? GetValue(object source, MemberInfo memberInfo) + { + if (memberInfo is FieldInfo fieldInfo) + return fieldInfo.GetValue(source); + + if (memberInfo is PropertyInfo propertyInfo && propertyInfo.CanRead) + return propertyInfo.GetValue(source); + + return null; + } + + public static TMember GetValueFromLambda(TModel source, Expression> memberLambda) + { + if (source is null) + throw new ArgumentNullException(nameof(source)); + + var memberInfo = GetMemberInfoFromLambda(memberLambda, source.GetType()); + return (TMember)GetValue(source, memberInfo)!; + } + + /// + /// Return the list of writable instance property of a type + /// + /// The model type + /// The list of properties + public static IEnumerable GetWritableInstanceProperties(Type objectType) + { + propertyRegisterLock.EnterReadLock(); + try + { + if (propertyRegister.ContainsKey(objectType)) + { + return propertyRegister[objectType]; + } + } + finally + { + propertyRegisterLock.ExitReadLock(); + } + + propertyRegisterLock.EnterWriteLock(); + try + { + if (!propertyRegister.ContainsKey(objectType)) + { + var typeStack = new List(); + var stackType = objectType; + do + { + typeStack.Add(stackType); + stackType = stackType.BaseType; + } while (stackType != null); + + propertyRegister.Add(objectType, typeStack + .SelectMany(type => type.GetProperties(BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance)) + .Where(prop => prop.CanWrite)); + } + return propertyRegister[objectType]; + } + finally + { + propertyRegisterLock.ExitWriteLock(); + } + } + + public static void SetValue(object destination, MemberInfo memberInfo, object? value) + { + if (memberInfo is FieldInfo fieldInfo) + { + if (!fieldInfo.IsInitOnly) + fieldInfo.SetValue(destination, value); + return; + } + + if (memberInfo is PropertyInfo propertyInfo && propertyInfo.CanWrite) + propertyInfo.SetValue(destination, value); + } + + public static void SetValue(TModel destination, Expression> memberLambda, TMember value) + { + if (destination is null) + throw new ArgumentNullException(nameof(destination)); + + var memberInfo = GetMemberInfoFromLambda(memberLambda, destination.GetType()); + SetValue(destination, memberInfo, value); + } + } +} diff --git a/src/MongoDM/Repositories/CollectionRepositoryBase.cs b/src/MongODM.Core/Repositories/CollectionRepository.cs similarity index 63% rename from src/MongoDM/Repositories/CollectionRepositoryBase.cs rename to src/MongODM.Core/Repositories/CollectionRepository.cs index c0257751..04e4a460 100644 --- a/src/MongoDM/Repositories/CollectionRepositoryBase.cs +++ b/src/MongODM.Core/Repositories/CollectionRepository.cs @@ -1,51 +1,49 @@ -using Digicando.MongoDM.Models; -using Digicando.MongoDM.ProxyModels; -using Digicando.MongoDM.Serialization; -using Digicando.MongoDM.Utility; +using Etherna.MongODM.Exceptions; +using Etherna.MongODM.Models; +using Etherna.MongODM.ProxyModels; +using Etherna.MongODM.Serialization; using MongoDB.Bson; using MongoDB.Driver; using MongoDB.Driver.Linq; using System; using System.Collections.Generic; using System.Linq; +using System.Linq.Expressions; using System.Threading; using System.Threading.Tasks; -namespace Digicando.MongoDM.Repositories +namespace Etherna.MongODM.Repositories { - public abstract class CollectionRepositoryBase : + public class CollectionRepository : RepositoryBase, ICollectionRepository where TModel : class, IEntityModel { // Fields. - private readonly IDBMaintainer dbMaintainer; - private readonly IDocumentSchemaRegister documentSchemaRegister; + private readonly CollectionRepositoryOptions options; + private IMongoCollection _collection = default!; // Constructors. - public CollectionRepositoryBase( - string collectionName, - IDBContextBase dbContext) - : base(dbContext) + public CollectionRepository(string name) + : this(new CollectionRepositoryOptions(name)) + { } + + public CollectionRepository(CollectionRepositoryOptions options) { - Collection = dbContext.Database.GetCollection(collectionName); - dbMaintainer = dbContext.DBMaintainer; - documentSchemaRegister = dbContext.DocumentSchemaRegister; + this.options = options ?? throw new ArgumentNullException(nameof(options)); } // Properties. - public IMongoCollection Collection { get; } - protected virtual IEnumerable<(IndexKeysDefinition keys, CreateIndexOptions options)> IndexBuilders => - new (IndexKeysDefinition keys, CreateIndexOptions options)[0]; + public IMongoCollection Collection => _collection ??= DbContext.Database.GetCollection(options.Name); // Public methods. - public override async Task BuildIndexesAsync(IDocumentSchemaRegister schemaRegister) + public override async Task BuildIndexesAsync(IDocumentSchemaRegister schemaRegister, CancellationToken cancellationToken = default) { var newIndexes = new List<(string name, CreateIndexModel createIndex)>(); // Define new indexes. //repository defined - newIndexes.AddRange(IndexBuilders.Select(pair => + newIndexes.AddRange(options.IndexBuilders.Select(pair => { var (keys, options) = pair; if (options.Name == null) @@ -61,11 +59,11 @@ public override async Task BuildIndexesAsync(IDocumentSchemaRegister schemaRegis newIndexes.Add( ("ver", new CreateIndexModel( - Builders.IndexKeys.Ascending(DBContextBase.DocumentVersionElementName), + Builders.IndexKeys.Ascending(MongODM.DbContext.DocumentVersionElementName), new CreateIndexOptions { Name = "ver" }))); //referenced documents - var dependencies = documentSchemaRegister.GetModelEntityReferencesIds(typeof(TModel)); + var dependencies = DbContext.DocumentSchemaRegister.GetModelEntityReferencesIds(typeof(TModel)); var idPaths = dependencies .Select(dependency => dependency.MemberPathToString()) @@ -83,7 +81,7 @@ public override async Task BuildIndexesAsync(IDocumentSchemaRegister schemaRegis // Get current indexes. var currentIndexes = new List(); - using (var indexList = await Collection.Indexes.ListAsync()) + using (var indexList = await Collection.Indexes.ListAsync(cancellationToken)) while (indexList.MoveNext()) currentIndexes.AddRange(indexList.Current); @@ -94,36 +92,41 @@ public override async Task BuildIndexesAsync(IDocumentSchemaRegister schemaRegis where !newIndexes.Any(newIndex => newIndex.name == indexName) select index) { - await Collection.Indexes.DropOneAsync(oldIndex.GetElement("name").Value.ToString()); + await Collection.Indexes.DropOneAsync(oldIndex.GetElement("name").Value.ToString(), cancellationToken); } // Build new indexes. - await Collection.Indexes.CreateManyAsync(newIndexes.Select(i => i.createIndex)); + await Collection.Indexes.CreateManyAsync(newIndexes.Select(i => i.createIndex), cancellationToken); } public virtual Task> FindAsync( FilterDefinition filter, - FindOptions options = null, + FindOptions? options = null, CancellationToken cancellationToken = default) => Collection.FindAsync(filter, options, cancellationToken); + public Task FindOneAsync( + Expression> predicate, + CancellationToken cancellationToken = default) => + FindOneOnDBAsync(predicate, cancellationToken); + public virtual Task QueryElementsAsync( Func, Task> query, - AggregateOptions aggregateOptions = null) => + AggregateOptions? aggregateOptions = null) => query(Collection.AsQueryable(aggregateOptions)); public virtual Task ReplaceAsync( object model, bool updateDependentDocuments = true, CancellationToken cancellationToken = default) => - ReplaceAsync(model as TModel, updateDependentDocuments, cancellationToken); + ReplaceAsync((TModel)model, updateDependentDocuments, cancellationToken); public virtual Task ReplaceAsync( object model, IClientSessionHandle session, bool updateDependentDocuments = true, CancellationToken cancellationToken = default) => - ReplaceAsync(model as TModel, session, updateDependentDocuments, cancellationToken); + ReplaceAsync((TModel)model, session, updateDependentDocuments, cancellationToken); public virtual Task ReplaceAsync( TModel model, @@ -138,6 +141,21 @@ public virtual Task ReplaceAsync( CancellationToken cancellationToken = default) => ReplaceHelperAsync(model, session, updateDependentDocuments, cancellationToken); + public async Task TryFindOneAsync(Expression> predicate, CancellationToken cancellationToken = default) + { + if (predicate is null) + throw new ArgumentNullException(nameof(predicate)); + + try + { + return await FindOneAsync(predicate, cancellationToken); + } + catch (EntityNotFoundException) + { + return null; + } + } + // Protected methods. protected override Task CreateOnDBAsync(IEnumerable models, CancellationToken cancellationToken) => Collection.InsertManyAsync(models, null, cancellationToken); @@ -155,18 +173,36 @@ protected override async Task FindOneOnDBAsync(TKey id, CancellationToke if (id == null) throw new ArgumentNullException(nameof(id)); - var filter = Builders.Filter.Eq(m => m.Id, id); - var element = await Collection.Find(filter).SingleOrDefaultAsync(cancellationToken); - if (element as TModel == default(TModel)) - throw new KeyNotFoundException($"Can't find key {id}"); - - return element as TModel; + try + { + return await FindOneOnDBAsync(m => m.Id!.Equals(id), cancellationToken: cancellationToken); + } + catch (EntityNotFoundException) + { + throw new EntityNotFoundException($"Can't find key {id}"); + } } // Helpers. + private async Task FindOneOnDBAsync( + Expression> predicate, + CancellationToken cancellationToken = default) + { + if (predicate is null) + throw new ArgumentNullException(nameof(predicate)); + + var element = await Collection.AsQueryable() + .Where(predicate) + .SingleOrDefaultAsync(cancellationToken); + if (element == default(TModel)) + throw new EntityNotFoundException("Can't find element"); + + return element; + } + private async Task ReplaceHelperAsync( TModel model, - IClientSessionHandle session, + IClientSessionHandle? session, bool updateDependentDocuments, CancellationToken cancellationToken) { @@ -192,10 +228,10 @@ await Collection.ReplaceOneAsync( // Update dependent documents. if (updateDependentDocuments) - dbMaintainer.OnUpdatedModel(model as IAuditable, model.Id); + DbContext.DBMaintainer.OnUpdatedModel((IAuditable)model, model.Id); // Reset changed members. - (model as IAuditable).ResetChangedMembers(); + (model as IAuditable)?.ResetChangedMembers(); } } } \ No newline at end of file diff --git a/src/MongODM.Core/Repositories/CollectionRepositoryOptions.cs b/src/MongODM.Core/Repositories/CollectionRepositoryOptions.cs new file mode 100644 index 00000000..4cae2fdb --- /dev/null +++ b/src/MongODM.Core/Repositories/CollectionRepositoryOptions.cs @@ -0,0 +1,17 @@ +using MongoDB.Driver; +using System; +using System.Collections.Generic; + +namespace Etherna.MongODM.Repositories +{ + public class CollectionRepositoryOptions : RepositoryOptionsBase + { + public CollectionRepositoryOptions(string name) + : base(name) + { + IndexBuilders = Array.Empty<(IndexKeysDefinition keys, CreateIndexOptions options)>(); + } + + public IEnumerable<(IndexKeysDefinition keys, CreateIndexOptions options)> IndexBuilders { get; set; } + } +} diff --git a/src/MongoDM/Repositories/GridFSRepositoryBase.cs b/src/MongODM.Core/Repositories/GridFSRepository.cs similarity index 57% rename from src/MongoDM/Repositories/GridFSRepositoryBase.cs rename to src/MongODM.Core/Repositories/GridFSRepository.cs index c460eedc..57fb69fd 100644 --- a/src/MongoDM/Repositories/GridFSRepositoryBase.cs +++ b/src/MongODM.Core/Repositories/GridFSRepository.cs @@ -1,10 +1,7 @@ -using Digicando.DomainHelper; -using Digicando.MongoDM.Models; -using Digicando.MongoDM.ProxyModels; -using Digicando.MongoDM.Serialization; +using Etherna.MongODM.Exceptions; +using Etherna.MongODM.Models; +using Etherna.MongODM.Serialization; using MongoDB.Bson; -using MongoDB.Bson.IO; -using MongoDB.Bson.Serialization; using MongoDB.Driver; using MongoDB.Driver.GridFS; using System; @@ -13,35 +10,33 @@ using System.Threading; using System.Threading.Tasks; -namespace Digicando.MongoDM.Repositories +namespace Etherna.MongODM.Repositories { - public abstract class GridFSRepositoryBase : + public class GridFSRepository : RepositoryBase, IGridFSRepository where TModel : class, IFileModel { + // Fields. + private readonly GridFSRepositoryOptions options; + private GridFSBucket _gridFSBucket = default!; + // Constructors. - public GridFSRepositoryBase( - string bucketName, - IDBContextBase dbContext) - : base(dbContext) - { - var bucketOptions = new GridFSBucketOptions(); - if (bucketName != null) - { - bucketOptions.BucketName = bucketName; - } + public GridFSRepository(string name) + : this(new GridFSRepositoryOptions(name)) + { } - GridFSBucket = new GridFSBucket(dbContext.Database, bucketOptions); - ProxyGenerator = dbContext.ProxyGenerator; + public GridFSRepository(GridFSRepositoryOptions options) + { + this.options = options ?? throw new ArgumentNullException(nameof(options)); } // Properties. - protected IGridFSBucket GridFSBucket { get; } - protected IProxyGenerator ProxyGenerator { get; } + public IGridFSBucket GridFSBucket => + _gridFSBucket ??= new GridFSBucket(DbContext.Database, new GridFSBucketOptions { BucketName = options.Name }); // Methods. - public override Task BuildIndexesAsync(IDocumentSchemaRegister schemaRegister) => Task.CompletedTask; + public override Task BuildIndexesAsync(IDocumentSchemaRegister schemaRegister, CancellationToken cancellationToken = default) => Task.CompletedTask; public virtual Task DownloadAsBytesAsync(string id, CancellationToken cancellationToken = default) => GridFSBucket.DownloadAsBytesAsync(ObjectId.Parse(id), null, cancellationToken); @@ -50,9 +45,6 @@ public virtual async Task DownloadAsStreamAsync(string id, CancellationT await GridFSBucket.OpenDownloadStreamAsync(ObjectId.Parse(id), null, cancellationToken); // Protected methods. - protected virtual void CloneMetadataData(TModel src, TModel dest) - { } - protected override async Task CreateOnDBAsync(IEnumerable models, CancellationToken cancellationToken) { foreach (var model in models) @@ -61,10 +53,14 @@ protected override async Task CreateOnDBAsync(IEnumerable models, Cancel 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 { - Metadata = SerializeMetadata(model) + Metadata = options.MetadataSerializer?.Invoke(model) }); ReflectionHelper.SetValue(model, m => m.Id, id.ToString()); } @@ -80,39 +76,17 @@ protected override async Task FindOneOnDBAsync(string id, CancellationTo var filter = Builders.Filter.Eq("_id", ObjectId.Parse(id)); var mongoFile = await GridFSBucket.Find(filter).SingleOrDefaultAsync(cancellationToken); if (mongoFile == null) - throw new KeyNotFoundException($"Can't find key {id}"); + throw new EntityNotFoundException($"Can't find key {id}"); - var file = ProxyGenerator.CreateInstance(); + 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. - DeserializeMetadata(file, mongoFile.Metadata); + options.MetadataDeserializer?.Invoke(mongoFile.Metadata, file); return file; } - - // Private helpers. - private void DeserializeMetadata(TModel obj, BsonDocument metadata) - { - if (metadata != null) - { - var metadataObject = BsonSerializer.Deserialize(metadata); - CloneMetadataData(metadataObject, obj); - } - } - - private BsonDocument SerializeMetadata(TModel obj) - { - if (obj == null) - { - return null; - } - - var document = new BsonDocument(); - BsonSerializer.Serialize(new BsonDocumentWriter(document), obj); - return document; - } } -} +} \ No newline at end of file diff --git a/src/MongODM.Core/Repositories/GridFSRepositoryOptions.cs b/src/MongODM.Core/Repositories/GridFSRepositoryOptions.cs new file mode 100644 index 00000000..5364a889 --- /dev/null +++ b/src/MongODM.Core/Repositories/GridFSRepositoryOptions.cs @@ -0,0 +1,15 @@ +using MongoDB.Bson; +using System; + +namespace Etherna.MongODM.Repositories +{ + public class GridFSRepositoryOptions : RepositoryOptionsBase + { + public GridFSRepositoryOptions(string name) + : base(name) + { } + + public Action? MetadataDeserializer { get; set; } + public Func? MetadataSerializer { get; set; } + } +} diff --git a/src/MongoDM/Repositories/ICollectionRepository.cs b/src/MongODM.Core/Repositories/ICollectionRepository.cs similarity index 62% rename from src/MongoDM/Repositories/ICollectionRepository.cs rename to src/MongODM.Core/Repositories/ICollectionRepository.cs index 987e0289..1eecf98c 100644 --- a/src/MongoDM/Repositories/ICollectionRepository.cs +++ b/src/MongODM.Core/Repositories/ICollectionRepository.cs @@ -1,11 +1,12 @@ -using Digicando.MongoDM.Models; +using Etherna.MongODM.Models; using MongoDB.Driver; using MongoDB.Driver.Linq; using System; +using System.Linq.Expressions; using System.Threading; using System.Threading.Tasks; -namespace Digicando.MongoDM.Repositories +namespace Etherna.MongODM.Repositories { public interface ICollectionRepository : IRepository { @@ -28,12 +29,16 @@ public interface ICollectionRepository : IRepository Task> FindAsync( FilterDefinition filter, - FindOptions options = null, + FindOptions? options = null, + CancellationToken cancellationToken = default); + + Task FindOneAsync( + Expression> predicate, CancellationToken cancellationToken = default); Task QueryElementsAsync( Func, Task> query, - AggregateOptions aggregateOptions = null); + AggregateOptions? aggregateOptions = null); Task ReplaceAsync( TModel model, @@ -45,5 +50,15 @@ Task ReplaceAsync( IClientSessionHandle session, bool updateDependentDocuments = true, CancellationToken cancellationToken = default); + + /// + /// Try to find a model and don't throw exception if it is not found + /// + /// Model find predicate + /// The cancellation token + /// The model, null if it doesn't exist + Task TryFindOneAsync( + Expression> predicate, + CancellationToken cancellationToken = default); } } diff --git a/src/MongoDM/Repositories/IGridFSRepository.cs b/src/MongODM.Core/Repositories/IGridFSRepository.cs similarity index 77% rename from src/MongoDM/Repositories/IGridFSRepository.cs rename to src/MongODM.Core/Repositories/IGridFSRepository.cs index 29528d5f..6b8f4457 100644 --- a/src/MongoDM/Repositories/IGridFSRepository.cs +++ b/src/MongODM.Core/Repositories/IGridFSRepository.cs @@ -1,12 +1,15 @@ -using Digicando.MongoDM.Models; +using Etherna.MongODM.Models; +using MongoDB.Driver.GridFS; using System.IO; using System.Threading; using System.Threading.Tasks; -namespace Digicando.MongoDM.Repositories +namespace Etherna.MongODM.Repositories { public interface IGridFSRepository : IRepository { + IGridFSBucket GridFSBucket { get; } + Task DownloadAsBytesAsync(string id, CancellationToken cancellationToken = default); Task DownloadAsStreamAsync(string id, CancellationToken cancellationToken = default); diff --git a/src/MongoDM/Repositories/IRepository.cs b/src/MongODM.Core/Repositories/IRepository.cs similarity index 69% rename from src/MongoDM/Repositories/IRepository.cs rename to src/MongODM.Core/Repositories/IRepository.cs index 77227817..73fa5760 100644 --- a/src/MongoDM/Repositories/IRepository.cs +++ b/src/MongODM.Core/Repositories/IRepository.cs @@ -1,17 +1,21 @@ -using Digicando.MongoDM.Migration; -using Digicando.MongoDM.Models; -using Digicando.MongoDM.Serialization; +using Etherna.MongODM.Models; +using Etherna.MongODM.Serialization; +using System; using System.Collections.Generic; using System.Threading; using System.Threading.Tasks; -namespace Digicando.MongoDM.Repositories +namespace Etherna.MongODM.Repositories { - public interface IRepository + public interface IRepository : IDbContextInitializable { - MongoMigrationBase MigrationInfo { get; } + IDbContext DbContext { get; } + Type GetKeyType { get; } + Type GetModelType { get; } - Task BuildIndexesAsync(IDocumentSchemaRegister schemaRegister); + Task BuildIndexesAsync( + IDocumentSchemaRegister schemaRegister, + CancellationToken cancellationToken = default); Task DeleteAsync( IEntityModel model, @@ -42,12 +46,12 @@ Task DeleteAsync( CancellationToken cancellationToken = default); /// - /// Find a model and don't throw exception if Id is not found + /// Try to find a model and don't throw exception if it is not found /// /// Model's Id /// The cancellation token /// The model, null if it doesn't exist - Task TryFindOneAsync( + Task TryFindOneAsync( TKey id, CancellationToken cancellationToken = default); } diff --git a/src/MongODM.Core/Repositories/IRepositoryRegister.cs b/src/MongODM.Core/Repositories/IRepositoryRegister.cs new file mode 100644 index 00000000..3a75332e --- /dev/null +++ b/src/MongODM.Core/Repositories/IRepositoryRegister.cs @@ -0,0 +1,23 @@ +using System; +using System.Collections.Generic; + +namespace Etherna.MongODM.Repositories +{ + public interface IRepositoryRegister : IDbContextInitializable + { + /// + /// Model-Repository map for collection types. + /// + IReadOnlyDictionary ModelCollectionRepositoryMap { get; } + + /// + /// Model-Repository map for gridfs types. + /// + IReadOnlyDictionary ModelGridFSRepositoryMap { get; } + + /// + /// Model-Repository map for both collection and gridfs types. + /// + IReadOnlyDictionary ModelRepositoryMap { get; } + } +} \ No newline at end of file diff --git a/src/MongoDM/Repositories/RepositoryBase.cs b/src/MongODM.Core/Repositories/RepositoryBase.cs similarity index 72% rename from src/MongoDM/Repositories/RepositoryBase.cs rename to src/MongODM.Core/Repositories/RepositoryBase.cs index b5acdffa..3d98ee07 100644 --- a/src/MongoDM/Repositories/RepositoryBase.cs +++ b/src/MongODM.Core/Repositories/RepositoryBase.cs @@ -1,8 +1,7 @@ -using Digicando.DomainHelper; -using Digicando.MongoDM.Migration; -using Digicando.MongoDM.Models; -using Digicando.MongoDM.ProxyModels; -using Digicando.MongoDM.Serialization; +using Etherna.MongODM.Exceptions; +using Etherna.MongODM.Models; +using Etherna.MongODM.ProxyModels; +using Etherna.MongODM.Serialization; using MoreLinq; using System; using System.Collections; @@ -11,37 +10,41 @@ using System.Threading; using System.Threading.Tasks; -namespace Digicando.MongoDM.Repositories +namespace Etherna.MongODM.Repositories { public abstract class RepositoryBase : IRepository where TModel : class, IEntityModel { - // Fields. - private readonly IDBContextBase dbContext; - - // Constructors. - public RepositoryBase(IDBContextBase dbContext) + // Initializer. + public virtual void Initialize(IDbContext dbContext) { - this.dbContext = dbContext; + if (IsInitialized) + throw new InvalidOperationException("Instance already initialized"); + DbContext = dbContext; + + IsInitialized = true; } // Properties. - public virtual MongoMigrationBase MigrationInfo { get; } + public IDbContext DbContext { get; private set; } = default!; + public Type GetKeyType => typeof(TKey); + public Type GetModelType => typeof(TModel); + public bool IsInitialized { get; private set; } - // Public methods. - public abstract Task BuildIndexesAsync(IDocumentSchemaRegister schemaRegister); + // Methods. + public abstract Task BuildIndexesAsync(IDocumentSchemaRegister schemaRegister, CancellationToken cancellationToken = default); public virtual async Task CreateAsync(IEnumerable models, CancellationToken cancellationToken = default) { await CreateOnDBAsync(models, cancellationToken); - await dbContext.SaveChangesAsync(); + await DbContext.SaveChangesAsync(); } public virtual async Task CreateAsync(TModel model, CancellationToken cancellationToken = default) { await CreateOnDBAsync(model, cancellationToken); - await dbContext.SaveChangesAsync(); + await DbContext.SaveChangesAsync(); } public async Task DeleteAsync(TKey id, CancellationToken cancellationToken = default) @@ -53,7 +56,7 @@ public async Task DeleteAsync(TKey id, CancellationToken cancellationToken = def public virtual async Task DeleteAsync(TModel model, CancellationToken cancellationToken = default) { // Process cascade delete. - var referencesIdsPaths = dbContext.DocumentSchemaRegister.GetModelEntityReferencesIds(typeof(TModel)) + var referencesIdsPaths = DbContext.DocumentSchemaRegister.GetModelEntityReferencesIds(typeof(TModel)) .Where(d => d.UseCascadeDelete == true) .Where(d => d.EntityClassMapPath.Count() == 2) //ignore references of references .DistinctBy(d => d.FullPathToString()) @@ -64,20 +67,20 @@ public virtual async Task DeleteAsync(TModel model, CancellationToken cancellati // Unlink dependent models. model.DisposeForDelete(); - await dbContext.SaveChangesAsync(); + await DbContext.SaveChangesAsync(); // Delete model. await DeleteOnDBAsync(model, cancellationToken); // Remove from cache. - if (dbContext.DBCache.LoadedModels.ContainsKey(model.Id)) - dbContext.DBCache.RemoveModel(model.Id); + if (DbContext.DBCache.LoadedModels.ContainsKey(model.Id!)) + DbContext.DBCache.RemoveModel(model.Id!); } public async Task DeleteAsync(IEntityModel model, CancellationToken cancellationToken = default) { if (!(model is TModel castedModel)) - throw new ArgumentException("Invalid model type"); + throw new InvalidEntityTypeException("Invalid model type"); await DeleteAsync(castedModel, cancellationToken); } @@ -85,17 +88,17 @@ public virtual async Task FindOneAsync( TKey id, CancellationToken cancellationToken = default) { - if (dbContext.DBCache.LoadedModels.ContainsKey(id)) + if (DbContext.DBCache.LoadedModels.ContainsKey(id!)) { - var cachedModel = dbContext.DBCache.LoadedModels[id] as TModel; + var cachedModel = DbContext.DBCache.LoadedModels[id!] as TModel; if ((cachedModel as IReferenceable)?.IsSummary == false) - return cachedModel; + return cachedModel!; } return await FindOneOnDBAsync(id, cancellationToken); } - public async Task TryFindOneAsync( + public async Task TryFindOneAsync( TKey id, CancellationToken cancellationToken = default) { @@ -108,13 +111,13 @@ public async Task TryFindOneAsync( { return await FindOneAsync(id, cancellationToken); } - catch (KeyNotFoundException) + catch (EntityNotFoundException) { return null; } } - // Protected methods. + // Protected abstract methods. protected abstract Task CreateOnDBAsync(IEnumerable models, CancellationToken cancellationToken); protected abstract Task CreateOnDBAsync(TModel model, CancellationToken cancellationToken); @@ -127,7 +130,7 @@ public async Task TryFindOneAsync( private async Task CascadeDeleteMembersAsync(object currentModel, IEnumerable idPath) { if (!idPath.Any()) - throw new ArgumentException("Member path can't be emty", nameof(idPath)); + throw new ArgumentException("Member path can't be empty", nameof(idPath)); var currentMember = idPath.First(); var memberTail = idPath.Skip(1); @@ -135,8 +138,8 @@ private async Task CascadeDeleteMembersAsync(object currentModel, IEnumerable _modelCollectionRepositoryMap = default!; + private IReadOnlyDictionary _modelGridFSRepositoryMap = default!; + private IReadOnlyDictionary _modelRepositoryMap = default!; + + // 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; } + + public IReadOnlyDictionary ModelCollectionRepositoryMap + { + get + { + if (_modelCollectionRepositoryMap is null) + { + var dbContextType = dbContext.GetType(); + + //select ICollectionRepository<,> implementing properties + var repos = dbContextType.GetProperties(BindingFlags.Public | BindingFlags.Instance) + .Where(prop => + { + var propType = prop.PropertyType; + + if (propType.IsGenericType && + propType.GetGenericTypeDefinition() == typeof(ICollectionRepository<,>)) + return true; + + if (propType.GetInterfaces() + .Where(@interface => @interface.IsGenericType) + .Select(@interface => @interface.GetGenericTypeDefinition()) + .Contains(typeof(ICollectionRepository<,>))) + return true; + + return false; + }); + + //construct register + _modelCollectionRepositoryMap = repos.ToDictionary( + prop => ((ICollectionRepository)prop.GetValue(dbContext)).GetModelType, + prop => (ICollectionRepository)prop.GetValue(dbContext)); + } + + return _modelCollectionRepositoryMap; + } + } + public IReadOnlyDictionary ModelGridFSRepositoryMap + { + get + { + if (_modelGridFSRepositoryMap is null) + { + var dbContextType = dbContext.GetType(); + + //select ICollectionRepository<,> implementing properties + var repos = dbContextType.GetProperties(BindingFlags.Public | BindingFlags.Instance) + .Where(prop => + { + var propType = prop.PropertyType; + + if (propType.IsGenericType && + propType.GetGenericTypeDefinition() == typeof(IGridFSRepository<>)) + return true; + + if (propType.GetInterfaces() + .Where(@interface => @interface.IsGenericType) + .Select(@interface => @interface.GetGenericTypeDefinition()) + .Contains(typeof(IGridFSRepository<>))) + return true; + + return false; + }); + + //construct register + _modelGridFSRepositoryMap = repos.ToDictionary( + prop => ((IGridFSRepository)prop.GetValue(dbContext)).GetModelType, + prop => (IGridFSRepository)prop.GetValue(dbContext)); + } + + return _modelGridFSRepositoryMap; + } + } + public IReadOnlyDictionary ModelRepositoryMap + { + get + { + if (_modelRepositoryMap is null) + { + var repoMap = new Dictionary(); + + foreach (var pair in ModelCollectionRepositoryMap) + repoMap.Add(pair.Key, pair.Value); + + foreach (var pair in ModelGridFSRepositoryMap) + repoMap.Add(pair.Key, pair.Value); + + _modelRepositoryMap = repoMap; + } + + return _modelRepositoryMap; + } + } + } +} diff --git a/src/MongoDM/Serialization/DocumentSchema.cs b/src/MongODM.Core/Serialization/DocumentSchema.cs similarity index 76% rename from src/MongoDM/Serialization/DocumentSchema.cs rename to src/MongODM.Core/Serialization/DocumentSchema.cs index 795a818b..6c597760 100644 --- a/src/MongoDM/Serialization/DocumentSchema.cs +++ b/src/MongODM.Core/Serialization/DocumentSchema.cs @@ -1,12 +1,12 @@ using MongoDB.Bson.Serialization; using System; -namespace Digicando.MongoDM.Serialization +namespace Etherna.MongODM.Serialization { public class DocumentSchema { // Constructors. - public DocumentSchema(BsonClassMap classMap, Type modelType, IBsonSerializer serializer, DocumentVersion version) + public DocumentSchema(BsonClassMap classMap, Type modelType, IBsonSerializer? serializer, DocumentVersion version) { ClassMap = classMap; ModelType = modelType; @@ -17,7 +17,7 @@ public DocumentSchema(BsonClassMap classMap, Type modelType, IBsonSerializer ser // Properties. public BsonClassMap ClassMap { get; } public Type ModelType { get; } - public IBsonSerializer Serializer { get; } + public IBsonSerializer? Serializer { get; } public DocumentVersion Version { get; } } } diff --git a/src/MongoDM/Serialization/DocumentSchemaMemberMap.cs b/src/MongODM.Core/Serialization/DocumentSchemaMemberMap.cs similarity index 90% rename from src/MongoDM/Serialization/DocumentSchemaMemberMap.cs rename to src/MongODM.Core/Serialization/DocumentSchemaMemberMap.cs index a79c176c..cc4729d2 100644 --- a/src/MongoDM/Serialization/DocumentSchemaMemberMap.cs +++ b/src/MongODM.Core/Serialization/DocumentSchemaMemberMap.cs @@ -4,7 +4,7 @@ using System.Linq; using System.Text; -namespace Digicando.MongoDM.Serialization +namespace Etherna.MongODM.Serialization { public class DocumentSchemaMemberMap { @@ -22,7 +22,7 @@ public DocumentSchemaMemberMap( } // Properties. - public IEnumerable EntityClassMapPath => MemberPath.Select(m => m.EntityClassMap) + public IEnumerable EntityClassMapPath => MemberPath.Select(m => m.EntityClassMap!) .Where(cm => cm != null) .Distinct(); public bool IsIdMember => MemberPath.Last().IsId; @@ -32,7 +32,7 @@ public IEnumerable MemberPathToEntity { get { - var lastEntityNestedMembers = MemberPath.Aggregate( + var lastEntityNestedMembers = MemberPath.Aggregate( (1, null), (acc, member) => member.EntityClassMap == acc.lastEntityClassMap ? (acc.counter++, acc.lastEntityClassMap) : @@ -51,6 +51,9 @@ public IEnumerable MemberPathToId return MemberPath; var lastEntityClassMap = MemberPath.Last().EntityClassMap; + if (lastEntityClassMap is null) + throw new InvalidOperationException("This model is not related to a an entity model with an Id"); + return MemberPathToEntity.Append(new EntityMember(lastEntityClassMap, lastEntityClassMap.IdMemberMap)); } } diff --git a/src/MongoDM/Serialization/DocumentSchemaRegister.cs b/src/MongODM.Core/Serialization/DocumentSchemaRegister.cs similarity index 84% rename from src/MongoDM/Serialization/DocumentSchemaRegister.cs rename to src/MongODM.Core/Serialization/DocumentSchemaRegister.cs index 9cafffdd..22eb9520 100644 --- a/src/MongoDM/Serialization/DocumentSchemaRegister.cs +++ b/src/MongODM.Core/Serialization/DocumentSchemaRegister.cs @@ -1,6 +1,6 @@ -using Digicando.MongoDM.Models; -using Digicando.MongoDM.Serialization.Modifiers; -using Digicando.MongoDM.Serialization.Serializers; +using Etherna.MongODM.Models; +using Etherna.MongODM.Serialization.Modifiers; +using Etherna.MongODM.Serialization.Serializers; using MongoDB.Bson.Serialization; using MongoDB.Bson.Serialization.Serializers; using System; @@ -11,11 +11,12 @@ using System.Threading; using System.Threading.Tasks; -namespace Digicando.MongoDM.Serialization +namespace Etherna.MongODM.Serialization { - class DocumentSchemaRegister : IDocumentSchemaRegister + public class DocumentSchemaRegister : IDocumentSchemaRegister { // Fields. + private readonly ReaderWriterLockSlim configLock = new ReaderWriterLockSlim(LockRecursionPolicy.SupportsRecursion); private readonly Dictionary> memberDependenciesMap = new Dictionary>(); private readonly Dictionary> modelDependenciesMap = @@ -23,7 +24,7 @@ class DocumentSchemaRegister : IDocumentSchemaRegister private readonly Dictionary> modelEntityReferencesIdsMap = new Dictionary>(); - private IDBContextBase dbContext; + private IDbContext dbContext = default!; private readonly ISerializerModifierAccessor serializerModifierAccessor; private readonly List schemas = new List(); @@ -34,36 +35,34 @@ public DocumentSchemaRegister( this.serializerModifierAccessor = serializerModifierAccessor; } - //here for circular dependency injection with DBContext - public void Initialize(IDBContextBase dbContext) + public void Initialize(IDbContext dbContext) { - if (this.dbContext != null) + if (IsInitialized) throw new InvalidOperationException("Instance already initialized"); this.dbContext = dbContext; + + IsInitialized = true; } // Properties. - public ReaderWriterLockSlim ConfigLock { get; } = new ReaderWriterLockSlim(LockRecursionPolicy.SupportsRecursion); public bool IsFrozen { get; private set; } + public bool IsInitialized { get; private set; } public IEnumerable Schemas => schemas; // Methods. - public IDocumentSchemaRegister Freeze() + public void Freeze() { - ConfigLock.EnterReadLock(); + configLock.EnterReadLock(); try { - if (IsFrozen) - { - return this; - } + if (IsFrozen) return; } finally { - ConfigLock.ExitReadLock(); + configLock.ExitReadLock(); } - ConfigLock.EnterWriteLock(); + configLock.EnterWriteLock(); try { if (!IsFrozen) @@ -98,10 +97,8 @@ public IDocumentSchemaRegister Freeze() } finally { - ConfigLock.ExitWriteLock(); + configLock.ExitWriteLock(); } - - return this; } public IEnumerable GetMemberDependencies(MemberInfo memberInfo) @@ -133,8 +130,8 @@ public IEnumerable GetModelEntityReferencesIds(Type mod public void RegisterModelSchema( DocumentVersion fromVersion, - Func> initCustomSerializer = null, - Func> modelMigrationAsync = null) + Func>? initCustomSerializer = null, + Func>? modelMigrationAsync = null) where TModel : class => RegisterModelSchema( fromVersion, @@ -144,31 +141,31 @@ public void RegisterModelSchema( public void RegisterModelSchema( DocumentVersion fromVersion, - Action, ISerializerModifierAccessor> classMapInitializer, - Func> initCustomSerializer = null, - Func> modelMigrationAsync = null) + Action> classMapInitializer, + Func>? initCustomSerializer = null, + Func>? modelMigrationAsync = null) where TModel : class => RegisterModelSchema( fromVersion, - new BsonClassMap(cm => classMapInitializer(cm, serializerModifierAccessor)), + new BsonClassMap(classMapInitializer), initCustomSerializer, modelMigrationAsync); public void RegisterModelSchema( DocumentVersion fromVersion, BsonClassMap classMap, - Func> initCustomSerializer = null, - Func> modelMigrationAsync = null) + Func>? initCustomSerializer = null, + Func>? modelMigrationAsync = null) where TModel : class { - ConfigLock.EnterWriteLock(); + configLock.EnterWriteLock(); try { if (IsFrozen) throw new InvalidOperationException("Register is frozen"); // Generate model serializer. - IBsonSerializer serializer = null; + IBsonSerializer? serializer = null; if (initCustomSerializer != null) //if custom is setted, keep it serializer = initCustomSerializer(); @@ -176,10 +173,10 @@ public void RegisterModelSchema( else if (!typeof(TModel).IsAbstract) //else if can deserialize, set default serializer serializer = new ExtendedClassMapSerializer( - dbContext, + dbContext.DBCache, + dbContext.DocumentVersion, serializerModifierAccessor, - (m, v) => modelMigrationAsync?.Invoke( - m, v, serializerModifierAccessor) ?? Task.FromResult(m)) + (m, v) => modelMigrationAsync?.Invoke(m, v) ?? Task.FromResult(m)) { AddVersion = typeof(IEntityModel).IsAssignableFrom(typeof(TModel)) }; //true only for entity models // Register schema. @@ -187,7 +184,7 @@ public void RegisterModelSchema( } finally { - ConfigLock.ExitWriteLock(); + configLock.ExitWriteLock(); } } @@ -282,7 +279,7 @@ private string MembersDependenciesToString() { strBuilder.AppendLine($"{dependencies.Key.DeclaringType.Name}.{dependencies.Key.Name}"); foreach (var dependency in dependencies.Value) - strBuilder.AppendLine($" {dependency.ToString()}"); + strBuilder.AppendLine($" {dependency}"); } return strBuilder.ToString(); } @@ -296,7 +293,7 @@ private string ModelDependenciesToString() { strBuilder.AppendLine($"{dependencies.Key.Name}"); foreach (var dependency in dependencies.Value) - strBuilder.AppendLine($" {dependency.ToString()}"); + strBuilder.AppendLine($" {dependency}"); } return strBuilder.ToString(); } diff --git a/src/MongoDM/Serialization/DocumentVersion.cs b/src/MongODM.Core/Serialization/DocumentVersion.cs similarity index 88% rename from src/MongoDM/Serialization/DocumentVersion.cs rename to src/MongODM.Core/Serialization/DocumentVersion.cs index ddb78c7b..b31d4082 100644 --- a/src/MongoDM/Serialization/DocumentVersion.cs +++ b/src/MongODM.Core/Serialization/DocumentVersion.cs @@ -2,7 +2,7 @@ using System.Text; using System.Text.RegularExpressions; -namespace Digicando.MongoDM.Serialization +namespace Etherna.MongODM.Serialization { public class DocumentVersion : IComparable { @@ -18,9 +18,10 @@ public DocumentVersion(string version) // * 3.1 // * 3.1.4 // * 3.1.4-alpha1 + // * 3.1.4-beta.2 // * 3.1-DEV var match = Regex.Match(version, - @"^(?\d+)(\.(?\d+))?(\.(?\d+))?(-(?