├── .codecov.yml ├── .editorconfig ├── .github ├── FUNDING.yml ├── release.yml ├── renovate.json └── workflows │ └── build.yml ├── .gitignore ├── CodeCoverage.runsettings ├── License.txt ├── MongoFramework.sln ├── README.md ├── images └── icon.png ├── src ├── Directory.Build.props ├── MongoFramework.Profiling.MiniProfiler │ ├── MiniProfilerDiagnosticListener.cs │ └── MongoFramework.Profiling.MiniProfiler.csproj └── MongoFramework │ ├── AssemblyInternals.cs │ ├── Attributes │ ├── BucketSetOptionsAttribute.cs │ ├── DbSetOptionsAttribute.cs │ ├── ExtraElementsAttribute.cs │ ├── IgnoreExtraElementsAttribute.cs │ ├── IndexAttribute.cs │ ├── MappingAdapterAttribute.cs │ └── RuntimeTypeDiscoveryAttribute.cs │ ├── Bson │ ├── BsonDiff.cs │ └── DiffResult.cs │ ├── BucketSetOptions.cs │ ├── EntityBucket.cs │ ├── EntityDefinitionBuilderExtensions.cs │ ├── IDbSetOptions.cs │ ├── IHaveTenantId.cs │ ├── IMongoDbBucketSet.cs │ ├── IMongoDbConnection.cs │ ├── IMongoDbContext.cs │ ├── IMongoDbSet.cs │ ├── IMongoDbTenantContext.cs │ ├── IMongoDbTenantSet.cs │ ├── IndexSortOrder.cs │ ├── IndexType.cs │ ├── Infrastructure │ ├── Commands │ │ ├── AddEntityCommand.cs │ │ ├── AddToBucketCommand.cs │ │ ├── EntityDefinitionExtensions.cs │ │ ├── IWriteCommand.cs │ │ ├── RemoveBucketCommand.cs │ │ ├── RemoveEntityByIdCommand.cs │ │ ├── RemoveEntityCommand.cs │ │ ├── RemoveEntityRangeCommand.cs │ │ ├── UpdateEntityCommand.cs │ │ └── WriteModelOptions.cs │ ├── DefinitionHelpers │ │ └── UpdateDefinitionHelper.cs │ ├── Diagnostics │ │ ├── DiagnosticCommand.cs │ │ ├── DiagnosticRunner.cs │ │ ├── IDiagnosticListener.cs │ │ └── NoOpDiagnosticListener.cs │ ├── DriverAbstractionRules.cs │ ├── EntityCommandBuilder.cs │ ├── EntityCommandStaging.cs │ ├── EntityCommandWriter.cs │ ├── EntityEntry.cs │ ├── EntityEntryContainer.cs │ ├── EntityEntryState.cs │ ├── Indexing │ │ ├── EntityIndexWriter.cs │ │ └── IndexModelBuilder.cs │ ├── Internal │ │ ├── DbSetInitializer.cs │ │ ├── GenericsHelper.cs │ │ └── TypeExtensions.cs │ ├── Linq │ │ ├── AggregateExecutionModel.cs │ │ ├── EntityProcessorCollection.cs │ │ ├── ILinqProcessor.cs │ │ ├── IMongoFrameworkQueryProvider.cs │ │ ├── IMongoFrameworkQueryable.cs │ │ ├── MethodInfoCache.cs │ │ ├── MongoFrameworkQueryProvider.cs │ │ ├── MongoFrameworkQueryable.cs │ │ ├── Processors │ │ │ └── EntityTrackingProcessor.cs │ │ ├── QueryHelper.cs │ │ └── ResultTransformers.cs │ ├── Mapping │ │ ├── DefaultMappingProcessors.cs │ │ ├── DriverMappingInterop.cs │ │ ├── EntityDefinition.cs │ │ ├── EntityDefinitionBuilder.cs │ │ ├── EntityDefinitionExtensions.cs │ │ ├── EntityKeyGenerator.cs │ │ ├── EntityMapping.MappingBuilder.cs │ │ ├── EntityMapping.MappingProcessors.cs │ │ ├── EntityMapping.cs │ │ ├── IMappingProcessor.cs │ │ ├── Processors │ │ │ ├── BsonKnownTypesProcessor.cs │ │ │ ├── CollectionNameProcessor.cs │ │ │ ├── EntityIdProcessor.cs │ │ │ ├── ExtraElementsProcessor.cs │ │ │ ├── HierarchyProcessor.cs │ │ │ ├── IndexProcessor.cs │ │ │ ├── MappingAdapterProcessor.cs │ │ │ ├── NestedTypeProcessor.cs │ │ │ ├── PropertyMappingProcessor.cs │ │ │ └── SkipMappingProcessor.cs │ │ ├── PropertyPath.cs │ │ └── PropertyTraversalExtensions.cs │ └── Serialization │ │ ├── TypeDiscoverySerializationProvider.cs │ │ └── TypeDiscoverySerializer.cs │ ├── IsExternalInit.cs │ ├── Linq │ ├── ExpressionExtensions.cs │ ├── LinqExtensions.cs │ └── QueryableAsyncExtensions.cs │ ├── MappingBuilder.cs │ ├── MongoDbBucketSet.cs │ ├── MongoDbConnection.cs │ ├── MongoDbContext.cs │ ├── MongoDbSet.cs │ ├── MongoDbTenantContext.cs │ ├── MongoDbTenantSet.cs │ ├── MongoDbUtility.cs │ ├── MongoFramework.csproj │ ├── MultiTenantException.cs │ └── Utilities │ └── Check.cs └── tests ├── Directory.Build.props ├── MongoFramework.Benchmarks ├── BenchmarkDb.cs ├── Infrastructure │ ├── DefinitionHelpers │ │ └── UpdateDefinitionHelperBenchmark.cs │ ├── Indexing │ │ └── IndexModelBuilderBenchmark.cs │ ├── Internal │ │ └── GenericMethodInvokeBenchmark.cs │ ├── Linq │ │ └── LinqBenchmark.cs │ ├── Mapping │ │ └── EntityMappingBenchmark.cs │ └── Serialization │ │ └── TypeDiscovery_FindTypeBenchmark.cs ├── MongoDbDriverHelper.cs ├── MongoDbSetComparisonBenchmark.cs ├── MongoFramework.Benchmarks.csproj ├── Program.cs └── SerializationComparisonBenchmark.cs └── MongoFramework.Tests ├── AssertExtensions.cs ├── Bson ├── GetDifferencesTests.cs └── HasDifferencesTests.cs ├── ExpectedExceptionPatternAttribute.cs ├── Infrastructure ├── Commands │ ├── AddEntityCommandTests.cs │ ├── RemoveEntityByIdCommandTests.cs │ ├── RemoveEntityCommandTests.cs │ ├── RemoveEntityRangeCommandTests.cs │ └── UpdateEntityCommandTests.cs ├── DefinitionHelpers │ └── UpdateDefinitionHelperTests.cs ├── EntityEntryContainerTests.cs ├── Indexing │ ├── EntityIndexWriterTests.cs │ └── IndexModelBuilderTests.cs ├── Linq │ ├── MongoFrameworkQueryableTests.cs │ └── Processors │ │ └── EntityTrackingProcessorTests.cs ├── Mapping │ ├── EntityDefinitionExtensionTests.cs │ ├── EntityMappingTests.cs │ ├── MappingTestBase.cs │ ├── Processors │ │ ├── BsonKnownTypesProcessorTests.cs │ │ ├── CollectionNameProcessorTests.cs │ │ ├── EntityIdProcessorTests.cs │ │ ├── ExtraElementsProcessorTests.cs │ │ ├── HierarchyProcessorTests.cs │ │ ├── MappingAdapterProcessorTests.cs │ │ ├── NestedTypeProcessorTests.cs │ │ ├── PropertyMappingProcessorTests.cs │ │ └── SkipMappingProcessorTests.cs │ └── PropertyTraversalExtensionTests.cs └── Serialization │ ├── TypeDiscoveryIntegrationTests.cs │ └── TypeDiscoverySerializationTests.cs ├── Linq ├── LinqExtensionsTests.cs ├── LinqExtensions_SearchGeoTests.cs ├── QueryableAsyncExtensionsMultiTenantTests.cs ├── QueryableAsyncExtensionsTests.cs └── QueryableAsyncExtensions_SumTests.cs ├── MappingBuilderExtensionTests.cs ├── MappingBuilderTests.cs ├── MongoDbBucketSetTests.cs ├── MongoDbConnectionTests.cs ├── MongoDbContextTenantTests.cs ├── MongoDbContextTests.cs ├── MongoDbDriverHelper.cs ├── MongoDbSetTests.cs ├── MongoDbTenantSetTests.cs ├── MongoDbUtilityTests.cs ├── MongoFramework.Tests.csproj ├── Profiling └── MiniProfiler │ └── MiniProfilerDiagnosticListenerTests.cs ├── TestBase.cs ├── TestConfiguration.cs ├── Utilities └── CheckTests.cs └── app.config /.codecov.yml: -------------------------------------------------------------------------------- 1 | comment: off -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # Based on the EditorConfig from Roslyn 2 | # top-most EditorConfig file 3 | root = true 4 | 5 | [*.cs] 6 | indent_style = tab 7 | 8 | # Sort using and Import directives with System.* appearing first 9 | dotnet_sort_system_directives_first = true 10 | # Avoid "this." and "Me." if not necessary 11 | dotnet_style_qualification_for_field = false:suggestion 12 | dotnet_style_qualification_for_property = false:suggestion 13 | dotnet_style_qualification_for_method = false:suggestion 14 | dotnet_style_qualification_for_event = false:suggestion 15 | 16 | # Use language keywords instead of framework type names for type references 17 | dotnet_style_predefined_type_for_locals_parameters_members = true:suggestion 18 | dotnet_style_predefined_type_for_member_access = true:suggestion 19 | 20 | # Suggest more modern language features when available 21 | dotnet_style_object_initializer = true:suggestion 22 | dotnet_style_collection_initializer = true:suggestion 23 | dotnet_style_coalesce_expression = true:suggestion 24 | dotnet_style_null_propagation = true:suggestion 25 | dotnet_style_explicit_tuple_names = true:suggestion 26 | 27 | # Prefer "var" everywhere 28 | csharp_style_var_for_built_in_types = true:suggestion 29 | csharp_style_var_when_type_is_apparent = true:suggestion 30 | csharp_style_var_elsewhere = true:suggestion 31 | 32 | # Prefer method-like constructs to have a block body 33 | csharp_style_expression_bodied_methods = false:none 34 | csharp_style_expression_bodied_constructors = false:none 35 | csharp_style_expression_bodied_operators = false:none 36 | 37 | # Prefer property-like constructs to have an expression-body 38 | csharp_style_expression_bodied_properties = when_on_single_line:suggestion 39 | csharp_style_expression_bodied_indexers = true:none 40 | csharp_style_expression_bodied_accessors = when_on_single_line:suggestion 41 | 42 | # Suggest more modern language features when available 43 | csharp_style_pattern_matching_over_is_with_cast_check = true:suggestion 44 | csharp_style_pattern_matching_over_as_with_null_check = true:suggestion 45 | csharp_style_inlined_variable_declaration = true:suggestion 46 | csharp_style_throw_expression = true:suggestion 47 | csharp_style_conditional_delegate_call = true:suggestion 48 | 49 | # Newline settings 50 | csharp_new_line_before_open_brace = all 51 | csharp_new_line_before_else = true 52 | csharp_new_line_before_catch = true 53 | csharp_new_line_before_finally = true 54 | csharp_new_line_before_members_in_object_initializers = true 55 | csharp_new_line_before_members_in_anonymous_types = true 56 | 57 | # Misc 58 | csharp_space_after_keywords_in_control_flow_statements = true 59 | csharp_space_between_method_declaration_parameter_list_parentheses = false 60 | csharp_space_between_method_call_parameter_list_parentheses = false 61 | csharp_space_between_parentheses = false 62 | csharp_preserve_single_line_statements = false 63 | csharp_preserve_single_line_blocks = true 64 | csharp_indent_case_contents = true 65 | csharp_indent_switch_labels = true 66 | csharp_indent_labels = no_change 67 | 68 | # Custom naming conventions 69 | dotnet_naming_rule.non_field_members_must_be_capitalized.symbols = non_field_member_symbols 70 | dotnet_naming_symbols.non_field_member_symbols.applicable_kinds = property,method,event,delegate 71 | dotnet_naming_symbols.non_field_member_symbols.applicable_accessibilities = * 72 | 73 | dotnet_naming_rule.non_field_members_must_be_capitalized.style = pascal_case_style 74 | dotnet_naming_style.pascal_case_style.capitalization = pascal_case 75 | 76 | dotnet_naming_rule.non_field_members_must_be_capitalized.severity = suggestion -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | github: Turnerj -------------------------------------------------------------------------------- /.github/release.yml: -------------------------------------------------------------------------------- 1 | changelog: 2 | exclude: 3 | labels: 4 | - ignore-for-release 5 | categories: 6 | - title: ⚠ Breaking Changes 7 | labels: 8 | - breaking-change 9 | - title: Features and Improvements 10 | labels: 11 | - enhancement 12 | - title: Bug Fixes 13 | labels: 14 | - bug 15 | - title: Dependency Updates 16 | labels: 17 | - dependencies 18 | - title: Other Changes 19 | labels: 20 | - "*" -------------------------------------------------------------------------------- /.github/renovate.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://docs.renovatebot.com/renovate-schema.json", 3 | "extends": [ 4 | "github>TurnerSoftware/.github:renovate-shared" 5 | ] 6 | } 7 | -------------------------------------------------------------------------------- /CodeCoverage.runsettings: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | cobertura 8 | [MongoFramework.Tests]* 9 | [MongoFramework]*,[MongoFramework.*]* 10 | Obsolete,GeneratedCodeAttribute,CompilerGeneratedAttribute,DebuggerNonUserCode,DebuggerStepThrough 11 | true 12 | true 13 | 14 | 15 | 16 | 17 | -------------------------------------------------------------------------------- /License.txt: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2017 Turner Software 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /MongoFramework.sln: -------------------------------------------------------------------------------- 1 |  2 | Microsoft Visual Studio Solution File, Format Version 12.00 3 | # Visual Studio Version 17 4 | VisualStudioVersion = 17.5.33209.295 5 | MinimumVisualStudioVersion = 10.0.40219.1 6 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "MongoFramework", "src\MongoFramework\MongoFramework.csproj", "{D871CD75-CC1E-4482-934F-42E74B2BF255}" 7 | EndProject 8 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "MongoFramework.Tests", "tests\MongoFramework.Tests\MongoFramework.Tests.csproj", "{FAAAAE78-22D5-42EB-824F-4B8CEB109023}" 9 | EndProject 10 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "MongoFramework.Profiling.MiniProfiler", "src\MongoFramework.Profiling.MiniProfiler\MongoFramework.Profiling.MiniProfiler.csproj", "{A8438535-7E88-4578-B47A-D1A016E43416}" 11 | EndProject 12 | Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "src", "src", "{D007577A-7BDB-4658-BCB6-7CA96908058F}" 13 | ProjectSection(SolutionItems) = preProject 14 | src\Directory.build.props = src\Directory.build.props 15 | EndProjectSection 16 | EndProject 17 | Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "tests", "tests", "{9CBF5A6D-EC65-4289-B2C9-875BDB654FC4}" 18 | ProjectSection(SolutionItems) = preProject 19 | tests\Directory.build.props = tests\Directory.build.props 20 | EndProjectSection 21 | EndProject 22 | Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "global", "global", "{BA117612-DD9B-4296-8F56-5D790AB07D72}" 23 | ProjectSection(SolutionItems) = preProject 24 | .codecov.yml = .codecov.yml 25 | .editorconfig = .editorconfig 26 | .gitignore = .gitignore 27 | CodeCoverage.runsettings = CodeCoverage.runsettings 28 | License.txt = License.txt 29 | README.md = README.md 30 | EndProjectSection 31 | EndProject 32 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "MongoFramework.Benchmarks", "tests\MongoFramework.Benchmarks\MongoFramework.Benchmarks.csproj", "{0177C18B-96AB-45E1-B9FB-1D734B2B7504}" 33 | EndProject 34 | Global 35 | GlobalSection(SolutionConfigurationPlatforms) = preSolution 36 | Debug|Any CPU = Debug|Any CPU 37 | Release|Any CPU = Release|Any CPU 38 | EndGlobalSection 39 | GlobalSection(ProjectConfigurationPlatforms) = postSolution 40 | {D871CD75-CC1E-4482-934F-42E74B2BF255}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 41 | {D871CD75-CC1E-4482-934F-42E74B2BF255}.Debug|Any CPU.Build.0 = Debug|Any CPU 42 | {D871CD75-CC1E-4482-934F-42E74B2BF255}.Release|Any CPU.ActiveCfg = Release|Any CPU 43 | {D871CD75-CC1E-4482-934F-42E74B2BF255}.Release|Any CPU.Build.0 = Release|Any CPU 44 | {FAAAAE78-22D5-42EB-824F-4B8CEB109023}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 45 | {FAAAAE78-22D5-42EB-824F-4B8CEB109023}.Debug|Any CPU.Build.0 = Debug|Any CPU 46 | {FAAAAE78-22D5-42EB-824F-4B8CEB109023}.Release|Any CPU.ActiveCfg = Release|Any CPU 47 | {FAAAAE78-22D5-42EB-824F-4B8CEB109023}.Release|Any CPU.Build.0 = Release|Any CPU 48 | {A8438535-7E88-4578-B47A-D1A016E43416}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 49 | {A8438535-7E88-4578-B47A-D1A016E43416}.Debug|Any CPU.Build.0 = Debug|Any CPU 50 | {A8438535-7E88-4578-B47A-D1A016E43416}.Release|Any CPU.ActiveCfg = Release|Any CPU 51 | {A8438535-7E88-4578-B47A-D1A016E43416}.Release|Any CPU.Build.0 = Release|Any CPU 52 | {0177C18B-96AB-45E1-B9FB-1D734B2B7504}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 53 | {0177C18B-96AB-45E1-B9FB-1D734B2B7504}.Debug|Any CPU.Build.0 = Debug|Any CPU 54 | {0177C18B-96AB-45E1-B9FB-1D734B2B7504}.Release|Any CPU.ActiveCfg = Release|Any CPU 55 | {0177C18B-96AB-45E1-B9FB-1D734B2B7504}.Release|Any CPU.Build.0 = Release|Any CPU 56 | EndGlobalSection 57 | GlobalSection(SolutionProperties) = preSolution 58 | HideSolutionNode = FALSE 59 | EndGlobalSection 60 | GlobalSection(NestedProjects) = preSolution 61 | {D871CD75-CC1E-4482-934F-42E74B2BF255} = {D007577A-7BDB-4658-BCB6-7CA96908058F} 62 | {FAAAAE78-22D5-42EB-824F-4B8CEB109023} = {9CBF5A6D-EC65-4289-B2C9-875BDB654FC4} 63 | {A8438535-7E88-4578-B47A-D1A016E43416} = {D007577A-7BDB-4658-BCB6-7CA96908058F} 64 | {0177C18B-96AB-45E1-B9FB-1D734B2B7504} = {9CBF5A6D-EC65-4289-B2C9-875BDB654FC4} 65 | EndGlobalSection 66 | GlobalSection(ExtensibilityGlobals) = postSolution 67 | SolutionGuid = {A202BFC6-FE6A-4ED0-AD06-90C7BAB29E28} 68 | EndGlobalSection 69 | EndGlobal 70 | -------------------------------------------------------------------------------- /images/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TurnerSoftware/MongoFramework/916f3bdd30ccf2a38b7979a0859050b215c0353e/images/icon.png -------------------------------------------------------------------------------- /src/Directory.Build.props: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | MongoFramework 5 | 6 | Turner Software 7 | 8 | $(AssemblyName) 9 | true 10 | MIT 11 | icon.png 12 | https://github.com/TurnerSoftware/MongoFramework 13 | mongo;mongodb;data;database;orm 14 | 15 | 16 | true 17 | true 18 | embedded 19 | 20 | Latest 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | -------------------------------------------------------------------------------- /src/MongoFramework.Profiling.MiniProfiler/MongoFramework.Profiling.MiniProfiler.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | netstandard2.0;net6.0 5 | MongoFramework.Profiling.MiniProfiler 6 | MiniProfiler for MongoFramework 7 | MongoFramework integration for MiniProfiler 8 | miniprofiler;profiler;profiling;timing;performance;$(PackageBaseTags) 9 | James Turner 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | -------------------------------------------------------------------------------- /src/MongoFramework/AssemblyInternals.cs: -------------------------------------------------------------------------------- 1 | using System.Runtime.CompilerServices; 2 | 3 | [assembly: InternalsVisibleTo("MongoFramework.Tests")] 4 | [assembly: InternalsVisibleTo("MongoFramework.Benchmarks")] -------------------------------------------------------------------------------- /src/MongoFramework/Attributes/BucketSetOptionsAttribute.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace MongoFramework.Attributes 4 | { 5 | [AttributeUsage(AttributeTargets.Property)] 6 | public class BucketSetOptionsAttribute : DbSetOptionsAttribute 7 | { 8 | public int BucketSize { get; } 9 | public string EntityTimeProperty { get; } 10 | 11 | public BucketSetOptionsAttribute(int bucketSize, string entityTimeProperty) 12 | { 13 | BucketSize = bucketSize; 14 | EntityTimeProperty = entityTimeProperty; 15 | } 16 | 17 | public override IDbSetOptions GetOptions() 18 | { 19 | return new BucketSetOptions 20 | { 21 | BucketSize = BucketSize, 22 | EntityTimeProperty = EntityTimeProperty 23 | }; 24 | } 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /src/MongoFramework/Attributes/DbSetOptionsAttribute.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace MongoFramework.Attributes 4 | { 5 | public abstract class DbSetOptionsAttribute : Attribute 6 | { 7 | public abstract IDbSetOptions GetOptions(); 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /src/MongoFramework/Attributes/ExtraElementsAttribute.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace MongoFramework.Attributes 4 | { 5 | [AttributeUsage(AttributeTargets.Property)] 6 | public class ExtraElementsAttribute : Attribute 7 | { 8 | 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /src/MongoFramework/Attributes/IgnoreExtraElementsAttribute.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace MongoFramework.Attributes 4 | { 5 | /// 6 | /// Instructs the MongoDb Driver to ignore extra elements (properties defined in the DB that aren't on the model) 7 | /// 8 | [AttributeUsage(AttributeTargets.Class)] 9 | public class IgnoreExtraElementsAttribute : Attribute 10 | { 11 | /// 12 | /// Sets whether the value should be inherited by derived classes. 13 | /// 14 | public bool IgnoreInherited { get; set; } 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /src/MongoFramework/Attributes/IndexAttribute.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace MongoFramework.Attributes 4 | { 5 | [AttributeUsage(AttributeTargets.Property, AllowMultiple = true)] 6 | public class IndexAttribute : Attribute 7 | { 8 | /// 9 | /// The name of the index. (Optional) 10 | /// 11 | public string Name { get; } 12 | /// 13 | /// Whether the index has a unique constraint. 14 | /// 15 | public bool IsUnique { get; set; } 16 | /// 17 | /// For standard indexes, defines the sort order of the index. 18 | /// 19 | public IndexSortOrder SortOrder { get; } 20 | /// 21 | /// THe priority of this index in relation to indexes with the same name. 22 | /// 23 | public int IndexPriority { get; set; } 24 | /// 25 | /// The type of index to be applied. 26 | /// 27 | public IndexType IndexType { get; } 28 | /// 29 | /// Whether the index should add a tenant key. 30 | /// 31 | public bool IsTenantExclusive { get; set; } 32 | 33 | /// 34 | /// Applies a standard index to the property with the specified sort order. 35 | /// 36 | /// 37 | public IndexAttribute(IndexSortOrder sortOrder) 38 | { 39 | SortOrder = sortOrder; 40 | } 41 | /// 42 | /// Applies the specified type of index to the property. 43 | /// 44 | /// 45 | public IndexAttribute(IndexType indexType) 46 | { 47 | IndexType = indexType; 48 | } 49 | 50 | /// 51 | /// Applies a standard index to the property with the specified name and sort order. 52 | /// 53 | /// 54 | /// 55 | public IndexAttribute(string name, IndexSortOrder sortOrder) 56 | { 57 | Name = name; 58 | SortOrder = sortOrder; 59 | } 60 | /// 61 | /// Applies the specified type of index to the property with the specified name. 62 | /// 63 | /// 64 | /// 65 | public IndexAttribute(string name, IndexType indexType) 66 | { 67 | Name = name; 68 | IndexType = indexType; 69 | } 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /src/MongoFramework/Attributes/MappingAdapterAttribute.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using MongoFramework.Infrastructure.Mapping; 3 | 4 | namespace MongoFramework.Attributes; 5 | 6 | /// 7 | /// Applies the specific on the entity. 8 | /// Runs after attribute processing, so the adapter can override attributes. 9 | /// Adapter type must have a parameterless constructor. 10 | /// 11 | [AttributeUsage(AttributeTargets.Class)] 12 | public class MappingAdapterAttribute : Attribute 13 | { 14 | /// 15 | /// Gets the adapter type for the attached class 16 | /// 17 | public Type MappingAdapter { get; } 18 | 19 | public MappingAdapterAttribute(Type adapterType) 20 | { 21 | MappingAdapter = adapterType; 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /src/MongoFramework/Attributes/RuntimeTypeDiscoveryAttribute.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace MongoFramework.Attributes 4 | { 5 | [AttributeUsage(AttributeTargets.Class)] 6 | public class RuntimeTypeDiscoveryAttribute : Attribute 7 | { 8 | 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /src/MongoFramework/Bson/BsonDiff.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Linq; 3 | using MongoDB.Bson; 4 | 5 | namespace MongoFramework.Bson 6 | { 7 | public static class BsonDiff 8 | { 9 | public static bool HasDifferences(BsonDocument documentA, BsonDocument documentB) 10 | { 11 | if (documentA == null && documentB == null) 12 | { 13 | return false; 14 | } 15 | else if (documentA == null || documentB == null || documentA.ElementCount != documentB.ElementCount) 16 | { 17 | return true; 18 | } 19 | 20 | var propertyNames = documentA.Names.Union(documentB.Names); 21 | 22 | foreach (var propertyName in propertyNames) 23 | { 24 | if (!documentB.Contains(propertyName)) 25 | { 26 | return true; 27 | } 28 | else 29 | { 30 | var propertyHasDifference = HasDifferences(documentA[propertyName], documentB[propertyName]); 31 | if (propertyHasDifference) 32 | { 33 | return true; 34 | } 35 | } 36 | } 37 | return false; 38 | } 39 | public static bool HasDifferences(BsonValue valueA, BsonValue valueB) 40 | { 41 | if (valueA == null && valueB == null) 42 | { 43 | return false; 44 | } 45 | else if (valueA != valueB || valueA.BsonType != valueB.BsonType) 46 | { 47 | return true; 48 | } 49 | 50 | var bsonType = valueA.BsonType; 51 | if (bsonType == BsonType.Array) 52 | { 53 | return HasDifferences(valueA.AsBsonArray, valueB.AsBsonArray); 54 | } 55 | else if (bsonType == BsonType.Document) 56 | { 57 | return HasDifferences(valueA.AsBsonDocument, valueB.AsBsonDocument); 58 | } 59 | 60 | return false; 61 | } 62 | public static bool HasDifferences(BsonArray arrayA, BsonArray arrayB) 63 | { 64 | if (arrayA == null && arrayB == null) 65 | { 66 | return false; 67 | } 68 | else if (arrayA == null || arrayB == null || arrayA.Count != arrayB.Count) 69 | { 70 | return true; 71 | } 72 | 73 | for (int i = 0, l = arrayA.Count; i < l; i++) 74 | { 75 | var itemHasDifference = HasDifferences(arrayA[i], arrayB[i]); 76 | if (itemHasDifference) 77 | { 78 | return true; 79 | } 80 | } 81 | 82 | return false; 83 | } 84 | 85 | public static DiffResult GetDifferences(BsonDocument documentA, BsonDocument documentB) 86 | { 87 | var result = new BsonDocument(); 88 | var documentAProperties = documentA?.Names ?? Enumerable.Empty(); 89 | var documentBProperties = documentB?.Names ?? Enumerable.Empty(); 90 | var propertyNames = documentAProperties.Union(documentBProperties); 91 | 92 | foreach (var propertyName in propertyNames) 93 | { 94 | if (documentB == null || !documentB.Contains(propertyName)) 95 | { 96 | result.Add(propertyName, BsonUndefined.Value); 97 | } 98 | else if (documentA == null || !documentA.Contains(propertyName)) 99 | { 100 | result.Add(propertyName, documentB[propertyName]); 101 | } 102 | else 103 | { 104 | var diffResult = GetDifferences(documentA[propertyName], documentB[propertyName]); 105 | if (diffResult.HasDifference) 106 | { 107 | result.Add(propertyName, diffResult.Difference); 108 | } 109 | } 110 | } 111 | 112 | if (result.ElementCount > 0) 113 | { 114 | return new DiffResult(result); 115 | } 116 | 117 | return DiffResult.NoDifferences; 118 | } 119 | public static DiffResult GetDifferences(BsonValue valueA, BsonValue valueB) 120 | { 121 | if (valueA == null || valueB == null || valueA.BsonType != valueB.BsonType) 122 | { 123 | return new DiffResult(valueB); 124 | } 125 | 126 | var bsonType = valueA.BsonType; 127 | if (bsonType == BsonType.Array) 128 | { 129 | return GetDifferences(valueA.AsBsonArray, valueB.AsBsonArray); 130 | } 131 | else if (bsonType == BsonType.Document) 132 | { 133 | return GetDifferences(valueA.AsBsonDocument, valueB.AsBsonDocument); 134 | } 135 | else if (valueA != valueB) 136 | { 137 | return new DiffResult(valueB); 138 | } 139 | 140 | return DiffResult.NoDifferences; 141 | } 142 | public static DiffResult GetDifferences(BsonArray arrayA, BsonArray arrayB) 143 | { 144 | var result = new BsonDocument(); 145 | 146 | var arrayACount = arrayA?.Count ?? 0; 147 | var arrayBCount = arrayB?.Count ?? 0; 148 | 149 | for (int i = 0, l = Math.Max(arrayACount, arrayBCount); i < l; i++) 150 | { 151 | if (i >= arrayACount) 152 | { 153 | result[i.ToString()] = arrayB[i]; 154 | } 155 | else if (i >= arrayBCount) 156 | { 157 | result[i.ToString()] = BsonUndefined.Value; 158 | } 159 | else 160 | { 161 | var diffResult = GetDifferences(arrayA[i], arrayB[i]); 162 | if (diffResult.HasDifference) 163 | { 164 | result[i.ToString()] = diffResult.Difference; 165 | } 166 | } 167 | } 168 | 169 | if (result.ElementCount > 0) 170 | { 171 | return new DiffResult(result); 172 | } 173 | 174 | return DiffResult.NoDifferences; 175 | } 176 | } 177 | } 178 | -------------------------------------------------------------------------------- /src/MongoFramework/Bson/DiffResult.cs: -------------------------------------------------------------------------------- 1 | using MongoDB.Bson; 2 | 3 | namespace MongoFramework.Bson 4 | { 5 | public class DiffResult 6 | { 7 | public bool HasDifference { get; private set; } 8 | public BsonValue Difference { get; private set; } 9 | 10 | public static DiffResult NoDifferences { get; } = new DiffResult(); 11 | 12 | /// 13 | /// Creates a DiffResult with no differences. 14 | /// 15 | internal DiffResult() { } 16 | /// 17 | /// Creates a DiffResult with the specified differences. 18 | /// 19 | /// 20 | public DiffResult(BsonValue difference) 21 | { 22 | HasDifference = true; 23 | Difference = difference; 24 | } 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /src/MongoFramework/BucketSetOptions.cs: -------------------------------------------------------------------------------- 1 |  2 | namespace MongoFramework 3 | { 4 | public class BucketSetOptions : IDbSetOptions 5 | { 6 | public int BucketSize { get; set; } 7 | public string EntityTimeProperty { get; set; } 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /src/MongoFramework/EntityBucket.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | 4 | namespace MongoFramework 5 | { 6 | public class EntityBucket where TGroup : class 7 | { 8 | public string Id { get; set; } 9 | public TGroup Group { get; set; } 10 | public DateTime Min { get; set; } 11 | public DateTime Max { get; set; } 12 | public int ItemCount { get; set; } 13 | public int BucketSize { get; set; } 14 | public List Items { get; set; } 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /src/MongoFramework/EntityDefinitionBuilderExtensions.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Reflection; 5 | using MongoFramework.Infrastructure.Mapping; 6 | 7 | namespace MongoFramework; 8 | 9 | public static class EntityDefinitionBuilderExtensions 10 | { 11 | private static PropertyInfo GetPropertyInfo(Type entityType, string propertyName) 12 | { 13 | return entityType.GetProperty(propertyName) ?? throw new ArgumentException($"Property \"{propertyName}\" can not be found on \"{entityType.Name}\".", nameof(propertyName)); 14 | } 15 | 16 | public static EntityDefinitionBuilder HasKey(this EntityDefinitionBuilder definitionBuilder, string propertyName, Action builder = null) 17 | => definitionBuilder.HasKey(GetPropertyInfo(definitionBuilder.EntityType, propertyName), builder); 18 | 19 | public static EntityDefinitionBuilder Ignore(this EntityDefinitionBuilder definitionBuilder, string propertyName) 20 | => definitionBuilder.Ignore(GetPropertyInfo(definitionBuilder.EntityType, propertyName)); 21 | 22 | public static EntityDefinitionBuilder HasProperty(this EntityDefinitionBuilder definitionBuilder, string propertyName, Action builder = null) 23 | => definitionBuilder.HasProperty(GetPropertyInfo(definitionBuilder.EntityType, propertyName), builder); 24 | 25 | public static EntityDefinitionBuilder HasIndex(this EntityDefinitionBuilder definitionBuilder, IEnumerable propertyPaths, Action builder = null) 26 | { 27 | var properties = new List(); 28 | foreach (var propertyPath in propertyPaths) 29 | { 30 | properties.Add( 31 | new IndexProperty( 32 | PropertyPath.FromString(definitionBuilder.EntityType, propertyPath) 33 | ) 34 | ); 35 | } 36 | 37 | return definitionBuilder.HasIndex(properties, builder); 38 | } 39 | public static EntityDefinitionBuilder HasIndex(this EntityDefinitionBuilder definitionBuilder, IEnumerable properties, Action builder = null) 40 | { 41 | return definitionBuilder.HasIndex(properties.Select(p => new IndexProperty(p)), builder); 42 | } 43 | 44 | public static EntityDefinitionBuilder HasExtraElements(this EntityDefinitionBuilder definitionBuilder, string propertyName) 45 | => definitionBuilder.HasExtraElements(GetPropertyInfo(definitionBuilder.EntityType, propertyName)); 46 | 47 | } 48 | -------------------------------------------------------------------------------- /src/MongoFramework/IDbSetOptions.cs: -------------------------------------------------------------------------------- 1 | namespace MongoFramework 2 | { 3 | public interface IDbSetOptions 4 | { 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /src/MongoFramework/IHaveTenantId.cs: -------------------------------------------------------------------------------- 1 | namespace MongoFramework 2 | { 3 | public interface IHaveTenantId 4 | { 5 | public string TenantId { get; set; } 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /src/MongoFramework/IMongoDbBucketSet.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | using System.Linq; 3 | 4 | namespace MongoFramework 5 | { 6 | public interface IMongoDbBucketSet : IMongoDbSet, IQueryable> 7 | where TGroup : class 8 | where TSubEntity : class 9 | { 10 | void Add(TGroup group, TSubEntity entity); 11 | void AddRange(TGroup group, IEnumerable entities); 12 | void Remove(TGroup group); 13 | IQueryable WithGroup(TGroup group); 14 | IQueryable Groups(); 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /src/MongoFramework/IMongoDbConnection.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using MongoDB.Driver; 3 | using MongoFramework.Infrastructure.Diagnostics; 4 | 5 | namespace MongoFramework 6 | { 7 | public interface IMongoDbConnection : IDisposable 8 | { 9 | IMongoClient Client { get; } 10 | IMongoDatabase GetDatabase(); 11 | IDiagnosticListener DiagnosticListener { get; set; } 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /src/MongoFramework/IMongoDbContext.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | using System.Linq; 3 | using System.Threading; 4 | using System.Threading.Tasks; 5 | using MongoFramework.Infrastructure; 6 | 7 | namespace MongoFramework 8 | { 9 | public interface IMongoDbContext 10 | { 11 | IMongoDbConnection Connection { get; } 12 | 13 | EntityEntryContainer ChangeTracker { get; } 14 | EntityCommandStaging CommandStaging { get; } 15 | 16 | IMongoDbSet Set() where TEntity : class; 17 | IQueryable Query() where TEntity : class; 18 | 19 | void SaveChanges(); 20 | Task SaveChangesAsync(CancellationToken cancellationToken = default(CancellationToken)); 21 | 22 | void Attach(TEntity entity) where TEntity : class; 23 | void AttachRange(IEnumerable entities) where TEntity : class; 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /src/MongoFramework/IMongoDbSet.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Linq.Expressions; 5 | using System.Threading; 6 | using System.Threading.Tasks; 7 | 8 | namespace MongoFramework 9 | { 10 | public interface IMongoDbSet 11 | { 12 | [Obsolete("Use SaveChanges on the IMongoDbContext")] 13 | void SaveChanges(); 14 | [Obsolete("Use SaveChangesAsync on the IMongoDbContext")] 15 | Task SaveChangesAsync(CancellationToken cancellationToken = default); 16 | } 17 | 18 | public interface IMongoDbSet : IMongoDbSet, IQueryable where TEntity : class 19 | { 20 | IMongoDbContext Context { get; } 21 | TEntity Find(object id); 22 | ValueTask FindAsync(object id); 23 | TEntity Create(); 24 | void Add(TEntity entity); 25 | void AddRange(IEnumerable entities); 26 | void Update(TEntity entity); 27 | void UpdateRange(IEnumerable entities); 28 | void Remove(TEntity entity); 29 | void RemoveRange(IEnumerable entities); 30 | void RemoveRange(Expression> predicate); 31 | void RemoveById(object entityId); 32 | IQueryable AsNoTracking(); 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /src/MongoFramework/IMongoDbTenantContext.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | 3 | namespace MongoFramework 4 | { 5 | public interface IMongoDbTenantContext : IMongoDbContext 6 | { 7 | string TenantId { get; } 8 | void CheckEntity(IHaveTenantId entity); 9 | void CheckEntities(IEnumerable entity); 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /src/MongoFramework/IMongoDbTenantSet.cs: -------------------------------------------------------------------------------- 1 | using System.Linq; 2 | 3 | namespace MongoFramework 4 | { 5 | public interface IMongoDbTenantSet : IMongoDbSet where TEntity : class 6 | { 7 | new IMongoDbTenantContext Context { get; } 8 | IQueryable GetSearchTextQueryable(string search); 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /src/MongoFramework/IndexSortOrder.cs: -------------------------------------------------------------------------------- 1 | namespace MongoFramework 2 | { 3 | public enum IndexSortOrder 4 | { 5 | Ascending, 6 | Descending 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /src/MongoFramework/IndexType.cs: -------------------------------------------------------------------------------- 1 | namespace MongoFramework 2 | { 3 | public enum IndexType 4 | { 5 | Standard = 0, 6 | Text = 1, 7 | Geo2dSphere = 2 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /src/MongoFramework/Infrastructure/Commands/AddEntityCommand.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.ComponentModel.DataAnnotations; 4 | using MongoDB.Driver; 5 | 6 | namespace MongoFramework.Infrastructure.Commands 7 | { 8 | public class AddEntityCommand : IWriteCommand where TEntity : class 9 | { 10 | private EntityEntry EntityEntry { get; } 11 | 12 | public Type EntityType => typeof(TEntity); 13 | 14 | public AddEntityCommand(EntityEntry entityEntry) 15 | { 16 | EntityEntry = entityEntry; 17 | } 18 | 19 | public IEnumerable> GetModel(WriteModelOptions options) 20 | { 21 | var entity = EntityEntry.Entity as TEntity; 22 | 23 | var validationContext = new ValidationContext(entity); 24 | Validator.ValidateObject(entity, validationContext); 25 | 26 | yield return new InsertOneModel(entity); 27 | } 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /src/MongoFramework/Infrastructure/Commands/AddToBucketCommand.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using MongoDB.Driver; 4 | using MongoFramework.Infrastructure.Mapping; 5 | 6 | namespace MongoFramework.Infrastructure.Commands 7 | { 8 | public class AddToBucketCommand : IWriteCommand> 9 | where TGroup : class 10 | where TSubEntity : class 11 | { 12 | private TGroup Group { get; } 13 | private TSubEntity SubEntity { get; } 14 | private PropertyDefinition EntityTimeProperty { get; } 15 | private int BucketSize { get; } 16 | 17 | public Type EntityType => typeof(EntityBucket); 18 | 19 | public AddToBucketCommand(TGroup group, TSubEntity subEntity, PropertyDefinition entityTimeProperty, int bucketSize) 20 | { 21 | Group = group; 22 | SubEntity = subEntity; 23 | BucketSize = bucketSize; 24 | EntityTimeProperty = entityTimeProperty; 25 | } 26 | 27 | public IEnumerable>> GetModel(WriteModelOptions options) 28 | { 29 | var filterBuilder = Builders>.Filter; 30 | var filter = filterBuilder.And( 31 | filterBuilder.Eq(b => b.Group, Group), 32 | filterBuilder.Where(b => b.ItemCount < BucketSize) 33 | ); 34 | 35 | var entityDefinition = EntityMapping.GetOrCreateDefinition(typeof(EntityBucket)); 36 | 37 | var itemTimeValue = (DateTime)EntityTimeProperty.GetValue(SubEntity); 38 | 39 | var updateDefinition = Builders>.Update 40 | .Inc(b => b.ItemCount, 1) 41 | .Push(b => b.Items, SubEntity) 42 | .Min(b => b.Min, itemTimeValue) 43 | .Max(b => b.Max, itemTimeValue) 44 | .SetOnInsert(b => b.BucketSize, BucketSize) 45 | .SetOnInsert(b => b.Id, entityDefinition.FindNearestKey().KeyGenerator.Generate()); 46 | 47 | yield return new UpdateOneModel>(filter, updateDefinition) 48 | { 49 | IsUpsert = true 50 | }; 51 | } 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /src/MongoFramework/Infrastructure/Commands/EntityDefinitionExtensions.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Text; 4 | using MongoDB.Driver; 5 | using MongoFramework.Infrastructure.Mapping; 6 | 7 | namespace MongoFramework.Infrastructure.Commands 8 | { 9 | public static class EntityDefinitionExtensions 10 | { 11 | public static FilterDefinition CreateIdFilterFromEntity(this EntityDefinition definition, TEntity entity) 12 | { 13 | return Builders.Filter.Eq(definition.GetIdName(), definition.GetIdValue(entity)); 14 | } 15 | public static FilterDefinition CreateIdFilter(this EntityDefinition definition, object entityId, string tenantId = null) 16 | { 17 | if (typeof(IHaveTenantId).IsAssignableFrom(typeof(TEntity)) && tenantId == null) 18 | { 19 | throw new ArgumentException("Tenant ID required for Tenant Entity"); 20 | } 21 | if (tenantId == null) 22 | { 23 | return Builders.Filter.Eq(definition.GetIdName(), entityId); 24 | } 25 | else 26 | { 27 | return Builders.Filter.And( 28 | Builders.Filter.Eq(definition.GetIdName(), entityId), 29 | Builders.Filter.Eq("TenantId", tenantId) 30 | ); 31 | } 32 | } 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /src/MongoFramework/Infrastructure/Commands/IWriteCommand.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Text; 4 | using MongoDB.Driver; 5 | 6 | namespace MongoFramework.Infrastructure.Commands 7 | { 8 | public interface IWriteCommand 9 | { 10 | Type EntityType { get; } 11 | } 12 | 13 | public interface IWriteCommand : IWriteCommand where TEntity : class 14 | { 15 | IEnumerable> GetModel(WriteModelOptions options); 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /src/MongoFramework/Infrastructure/Commands/RemoveBucketCommand.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Text; 4 | using MongoDB.Driver; 5 | 6 | namespace MongoFramework.Infrastructure.Commands 7 | { 8 | public class RemoveBucketCommand : IWriteCommand> 9 | where TGroup : class 10 | where TSubEntity : class 11 | { 12 | private TGroup Group { get; } 13 | 14 | public Type EntityType => typeof(EntityBucket); 15 | 16 | public RemoveBucketCommand(TGroup group) 17 | { 18 | Group = group; 19 | } 20 | 21 | public IEnumerable>> GetModel(WriteModelOptions options) 22 | { 23 | var filterBuilder = Builders>.Filter; 24 | var filter = filterBuilder.Eq(b => b.Group, Group); 25 | yield return new DeleteManyModel>(filter); 26 | } 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /src/MongoFramework/Infrastructure/Commands/RemoveEntityByIdCommand.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Text; 4 | using MongoDB.Driver; 5 | using MongoFramework.Infrastructure.DefinitionHelpers; 6 | using MongoFramework.Infrastructure.Mapping; 7 | 8 | namespace MongoFramework.Infrastructure.Commands 9 | { 10 | public class RemoveEntityByIdCommand : IWriteCommand where TEntity : class 11 | { 12 | private object EntityId { get; } 13 | 14 | public Type EntityType => typeof(TEntity); 15 | 16 | public RemoveEntityByIdCommand(object entityId) 17 | { 18 | EntityId = entityId; 19 | } 20 | 21 | public IEnumerable> GetModel(WriteModelOptions options) 22 | { 23 | var definition = EntityMapping.GetOrCreateDefinition(typeof(TEntity)); 24 | yield return new DeleteOneModel(definition.CreateIdFilter(EntityId, options?.TenantId)); 25 | } 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /src/MongoFramework/Infrastructure/Commands/RemoveEntityCommand.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Text; 4 | using MongoDB.Driver; 5 | using MongoFramework.Infrastructure.DefinitionHelpers; 6 | using MongoFramework.Infrastructure.Mapping; 7 | 8 | namespace MongoFramework.Infrastructure.Commands 9 | { 10 | public class RemoveEntityCommand : IWriteCommand where TEntity : class 11 | { 12 | private EntityEntry EntityEntry { get; } 13 | 14 | public Type EntityType => typeof(TEntity); 15 | 16 | public RemoveEntityCommand(EntityEntry entityEntry) 17 | { 18 | EntityEntry = entityEntry; 19 | } 20 | 21 | public IEnumerable> GetModel(WriteModelOptions options) 22 | { 23 | var definition = EntityMapping.GetOrCreateDefinition(typeof(TEntity)); 24 | yield return new DeleteOneModel(definition.CreateIdFilterFromEntity(EntityEntry.Entity as TEntity)); 25 | } 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /src/MongoFramework/Infrastructure/Commands/RemoveEntityRangeCommand.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq.Expressions; 4 | using System.Text; 5 | using MongoDB.Driver; 6 | 7 | namespace MongoFramework.Infrastructure.Commands 8 | { 9 | public class RemoveEntityRangeCommand : IWriteCommand where TEntity : class 10 | { 11 | private Expression> Predicate { get; } 12 | 13 | public Type EntityType => typeof(TEntity); 14 | 15 | public RemoveEntityRangeCommand(Expression> predicate) 16 | { 17 | Predicate = predicate; 18 | } 19 | 20 | public IEnumerable> GetModel(WriteModelOptions options) 21 | { 22 | yield return new DeleteManyModel(Predicate); 23 | } 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /src/MongoFramework/Infrastructure/Commands/UpdateEntityCommand.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.ComponentModel.DataAnnotations; 4 | using MongoDB.Driver; 5 | using MongoFramework.Infrastructure.DefinitionHelpers; 6 | using MongoFramework.Infrastructure.Mapping; 7 | 8 | namespace MongoFramework.Infrastructure.Commands 9 | { 10 | public class UpdateEntityCommand : IWriteCommand where TEntity : class 11 | { 12 | private EntityEntry EntityEntry { get; } 13 | 14 | public Type EntityType => typeof(TEntity); 15 | 16 | public UpdateEntityCommand(EntityEntry entityEntry) 17 | { 18 | EntityEntry = entityEntry; 19 | } 20 | 21 | public IEnumerable> GetModel(WriteModelOptions options) 22 | { 23 | var entity = EntityEntry.Entity as TEntity; 24 | 25 | var validationContext = new ValidationContext(entity); 26 | Validator.ValidateObject(entity, validationContext); 27 | 28 | var definition = EntityMapping.GetOrCreateDefinition(typeof(TEntity)); 29 | var updateDefinition = UpdateDefinitionHelper.CreateFromDiff(EntityEntry.OriginalValues, EntityEntry.CurrentValues); 30 | yield return new UpdateOneModel(definition.CreateIdFilterFromEntity(entity), updateDefinition); 31 | } 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /src/MongoFramework/Infrastructure/Commands/WriteModelOptions.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Text; 4 | 5 | namespace MongoFramework.Infrastructure.Commands 6 | { 7 | public class WriteModelOptions 8 | { 9 | public static WriteModelOptions Default { get; } = new WriteModelOptions(); 10 | public string TenantId { get; set; } 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /src/MongoFramework/Infrastructure/DefinitionHelpers/UpdateDefinitionHelper.cs: -------------------------------------------------------------------------------- 1 | using System.Linq; 2 | using MongoDB.Bson; 3 | using MongoDB.Driver; 4 | 5 | namespace MongoFramework.Infrastructure.DefinitionHelpers 6 | { 7 | public static class UpdateDefinitionHelper 8 | { 9 | public static UpdateDefinition CreateFromDiff(BsonDocument documentA, BsonDocument documentB) where TEntity : class 10 | { 11 | var definition = new BsonDocument(); 12 | ApplyDiffUpdate(definition, string.Empty, documentA, documentB); 13 | return new BsonDocumentUpdateDefinition(definition); 14 | } 15 | private static void ApplyDiffUpdate(BsonDocument updateDefinition, string name, BsonDocument documentA, BsonDocument documentB) 16 | { 17 | var documentAProperties = documentA?.Names ?? Enumerable.Empty(); 18 | var documentBProperties = documentB?.Names ?? Enumerable.Empty(); 19 | var propertyNames = documentAProperties.Union(documentBProperties); 20 | 21 | var baseName = name; 22 | if (!string.IsNullOrEmpty(baseName)) 23 | { 24 | baseName += "."; 25 | } 26 | 27 | foreach (var propertyName in propertyNames) 28 | { 29 | var fullName = baseName + propertyName; 30 | 31 | if (documentB == null || !documentB.Contains(propertyName)) 32 | { 33 | ApplyPropertyUnset(updateDefinition, fullName); 34 | } 35 | else if (documentA == null || !documentA.Contains(propertyName)) 36 | { 37 | ApplyPropertySet(updateDefinition, fullName, documentB[propertyName]); 38 | } 39 | else 40 | { 41 | ApplyDiffUpdate(updateDefinition, fullName, documentA[propertyName], documentB[propertyName]); 42 | } 43 | } 44 | } 45 | private static void ApplyDiffUpdate(BsonDocument updateDefinition, string name, BsonValue valueA, BsonValue valueB) 46 | { 47 | if (valueB == null) 48 | { 49 | ApplyPropertySet(updateDefinition, name, BsonNull.Value); 50 | } 51 | else if (valueA?.BsonType != valueB?.BsonType) 52 | { 53 | ApplyPropertySet(updateDefinition, name, valueB); 54 | } 55 | else 56 | { 57 | var bsonType = valueA?.BsonType; 58 | if (bsonType == BsonType.Array) 59 | { 60 | ApplyDiffUpdate(updateDefinition, name, valueA.AsBsonArray, valueB.AsBsonArray); 61 | } 62 | else if (bsonType == BsonType.Document) 63 | { 64 | ApplyDiffUpdate(updateDefinition, name, valueA.AsBsonDocument, valueB.AsBsonDocument); 65 | } 66 | else if (valueA != valueB) 67 | { 68 | ApplyPropertySet(updateDefinition, name, valueB); 69 | } 70 | } 71 | } 72 | private static void ApplyDiffUpdate(BsonDocument updateDefinition, string name, BsonArray arrayA, BsonArray arrayB) 73 | { 74 | var arrayACount = arrayA.Count; 75 | var arrayBCount = arrayB.Count; 76 | 77 | //Due to limitations of MongoDB, we can't pull/push at the same time. 78 | //As highlighted on task SERVER-1014 (MongoDB Jira), you can't pull at an index, only at a value match. 79 | //You could avoid the pull by simply pop-ing items off the list for the length difference between "arrayA" and "arrayB". 80 | //That said, we can't run "conflicting" updates on the same path (eg. pull and push) at the same time. 81 | //Instead, if the arrays are the same length, we check differences per index. 82 | //If the arrays are different lengths, we set the whole array in the update. 83 | 84 | if (arrayACount == arrayBCount) 85 | { 86 | for (int i = 0, l = arrayBCount; i < l; i++) 87 | { 88 | var fullName = name + "." + i; 89 | ApplyDiffUpdate(updateDefinition, fullName, arrayA[i], arrayB[i]); 90 | } 91 | } 92 | else 93 | { 94 | ApplyPropertySet(updateDefinition, name, arrayB); 95 | } 96 | } 97 | 98 | private static void ApplyPropertySet(BsonDocument updateDefinition, string name, BsonValue value) 99 | { 100 | if (updateDefinition.TryGetElement("$set", out var element)) 101 | { 102 | element.Value.AsBsonDocument.Add(name, value); 103 | } 104 | else 105 | { 106 | updateDefinition.Set("$set", new BsonDocument 107 | { 108 | { name, value } 109 | }); 110 | } 111 | } 112 | private static void ApplyPropertyUnset(BsonDocument updateDefinition, string name) 113 | { 114 | if (updateDefinition.TryGetElement("$unset", out var element)) 115 | { 116 | element.Value.AsBsonDocument.Add(name, 1); 117 | } 118 | else 119 | { 120 | updateDefinition.Set("$unset", new BsonDocument 121 | { 122 | { name, 1 } 123 | }); 124 | } 125 | } 126 | } 127 | } 128 | -------------------------------------------------------------------------------- /src/MongoFramework/Infrastructure/Diagnostics/DiagnosticCommand.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using MongoDB.Driver; 4 | 5 | namespace MongoFramework.Infrastructure.Diagnostics 6 | { 7 | public class DiagnosticCommand 8 | { 9 | public Guid CommandId { get; set; } 10 | public CommandState CommandState { get; set; } 11 | public Type EntityType { get; set; } 12 | } 13 | 14 | public enum CommandState 15 | { 16 | Start, 17 | FirstResult, 18 | End, 19 | Error 20 | } 21 | 22 | public class ReadDiagnosticCommand : DiagnosticCommand 23 | { 24 | public string Query { get; set; } 25 | } 26 | 27 | public abstract class WriteDiagnosticCommandBase : DiagnosticCommand { } 28 | public class WriteDiagnosticCommand : WriteDiagnosticCommandBase 29 | { 30 | public IEnumerable> WriteModel { get; set; } 31 | } 32 | 33 | public abstract class IndexDiagnosticCommandBase : DiagnosticCommand { } 34 | public class IndexDiagnosticCommand : IndexDiagnosticCommandBase 35 | { 36 | public IEnumerable> IndexModel { get; set; } 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /src/MongoFramework/Infrastructure/Diagnostics/DiagnosticRunner.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using MongoDB.Driver; 4 | using MongoFramework.Infrastructure.Linq; 5 | 6 | namespace MongoFramework.Infrastructure.Diagnostics 7 | { 8 | public class DiagnosticRunner : IDisposable 9 | { 10 | public Guid CommandId { get; } = Guid.NewGuid(); 11 | public IMongoDbConnection Connection { get; } 12 | public bool HasErrored { get; private set; } 13 | 14 | private DiagnosticRunner(IMongoDbConnection connection) 15 | { 16 | Connection = connection; 17 | } 18 | public static DiagnosticRunner Start(IMongoDbConnection connection, AggregateExecutionModel model) where TEntity : class 19 | { 20 | var runner = new DiagnosticRunner(connection); 21 | connection.DiagnosticListener.OnNext(new ReadDiagnosticCommand 22 | { 23 | CommandId = runner.CommandId, 24 | CommandState = CommandState.Start, 25 | EntityType = model.Serializer.ValueType, 26 | Query = QueryHelper.GetQuery(model) 27 | }); 28 | return runner; 29 | } 30 | public static DiagnosticRunner Start(IMongoDbConnection connection, IEnumerable> model) where TEntity : class 31 | { 32 | var runner = new DiagnosticRunner(connection); 33 | connection.DiagnosticListener.OnNext(new WriteDiagnosticCommand 34 | { 35 | CommandId = runner.CommandId, 36 | CommandState = CommandState.Start, 37 | EntityType = typeof(TEntity), 38 | WriteModel = model 39 | }); 40 | return runner; 41 | } 42 | public static DiagnosticRunner Start(IMongoDbConnection connection, IEnumerable> model) where TEntity : class 43 | { 44 | var runner = new DiagnosticRunner(connection); 45 | connection.DiagnosticListener.OnNext(new IndexDiagnosticCommand 46 | { 47 | CommandId = runner.CommandId, 48 | CommandState = CommandState.Start, 49 | EntityType = typeof(TEntity), 50 | IndexModel = model 51 | }); 52 | return runner; 53 | } 54 | 55 | public void FirstReadResult() 56 | { 57 | Connection.DiagnosticListener.OnNext(new ReadDiagnosticCommand 58 | { 59 | CommandId = CommandId, 60 | CommandState = CommandState.FirstResult, 61 | EntityType = typeof(TOutput) 62 | }); 63 | } 64 | 65 | public void Error(Exception exception = null) 66 | { 67 | HasErrored = true; 68 | Connection.DiagnosticListener.OnNext(new DiagnosticCommand 69 | { 70 | CommandId = CommandId, 71 | CommandState = CommandState.Error 72 | }); 73 | 74 | if (exception != null) 75 | { 76 | Connection.DiagnosticListener.OnError(exception); 77 | } 78 | } 79 | 80 | public void Dispose() 81 | { 82 | if (!HasErrored) 83 | { 84 | Connection.DiagnosticListener.OnNext(new DiagnosticCommand 85 | { 86 | CommandId = CommandId, 87 | CommandState = CommandState.End 88 | }); 89 | } 90 | } 91 | } 92 | } 93 | -------------------------------------------------------------------------------- /src/MongoFramework/Infrastructure/Diagnostics/IDiagnosticListener.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace MongoFramework.Infrastructure.Diagnostics 4 | { 5 | public interface IDiagnosticListener : IObserver 6 | { 7 | 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /src/MongoFramework/Infrastructure/Diagnostics/NoOpDiagnosticListener.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace MongoFramework.Infrastructure.Diagnostics 4 | { 5 | public class NoOpDiagnosticListener : IDiagnosticListener 6 | { 7 | public void OnCompleted() { /* No-Op */ } 8 | 9 | public void OnError(Exception error) { /* No-Op */ } 10 | 11 | public void OnNext(DiagnosticCommand value) { /* No-Op */ } 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /src/MongoFramework/Infrastructure/DriverAbstractionRules.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using MongoDB.Bson.Serialization.Serializers; 3 | using MongoDB.Bson.Serialization; 4 | using MongoDB.Bson; 5 | using System.Diagnostics; 6 | using MongoFramework.Infrastructure.Serialization; 7 | 8 | namespace MongoFramework.Infrastructure; 9 | 10 | /// 11 | /// Provides a single entry point to configure common areas of the driver for MongoFramework 12 | /// 13 | internal static class DriverAbstractionRules 14 | { 15 | public static void ApplyRules() 16 | { 17 | RegisterSerializer(new DecimalSerializer(BsonType.Decimal128)); 18 | RegisterSerializer(new NullableSerializer(new DecimalSerializer(BsonType.Decimal128))); 19 | 20 | BsonSerializer.RegisterSerializationProvider(TypeDiscoverySerializationProvider.Instance); 21 | } 22 | 23 | private static void RegisterSerializer(IBsonSerializer serializer) 24 | { 25 | try 26 | { 27 | BsonSerializer.RegisterSerializer(typeof(TTarget), serializer); 28 | } 29 | catch (BsonSerializationException ex) when (ex.Message.Contains("already a serializer registered")) 30 | { 31 | // Already registered 32 | } 33 | catch (Exception ex) 34 | { 35 | Debug.WriteLine(ex.Message, "MongoFramework"); 36 | } 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /src/MongoFramework/Infrastructure/EntityCommandBuilder.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using MongoFramework.Infrastructure.Commands; 3 | using MongoFramework.Infrastructure.Internal; 4 | 5 | namespace MongoFramework.Infrastructure 6 | { 7 | public static class EntityCommandBuilder 8 | { 9 | public static IWriteCommand CreateCommand(EntityEntry entityEntry) 10 | { 11 | var entityType = entityEntry.EntityType; 12 | var method = GenericsHelper.GetMethodDelegate>( 13 | typeof(EntityCommandBuilder), nameof(InternalCreateCommand), entityType 14 | ); 15 | return method(entityEntry); 16 | } 17 | 18 | private static IWriteCommand InternalCreateCommand(EntityEntry entityEntry) where TEntity : class 19 | { 20 | if (entityEntry.State == EntityEntryState.Added) 21 | { 22 | return new AddEntityCommand(entityEntry); 23 | } 24 | else if (entityEntry.State == EntityEntryState.Updated) 25 | { 26 | return new UpdateEntityCommand(entityEntry); 27 | } 28 | else if (entityEntry.State == EntityEntryState.Deleted) 29 | { 30 | return new RemoveEntityCommand(entityEntry); 31 | } 32 | 33 | return null; 34 | } 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /src/MongoFramework/Infrastructure/EntityCommandStaging.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | using MongoFramework.Infrastructure.Commands; 3 | 4 | namespace MongoFramework.Infrastructure 5 | { 6 | public class EntityCommandStaging 7 | { 8 | private HashSet Commands { get; } = new HashSet(); 9 | 10 | public void Add(IWriteCommand command) 11 | { 12 | Commands.Add(command); 13 | } 14 | 15 | public void Clear() 16 | { 17 | Commands.Clear(); 18 | } 19 | 20 | public IEnumerable GetCommands() 21 | { 22 | return Commands; 23 | } 24 | 25 | internal void CommitChanges() 26 | { 27 | Clear(); 28 | } 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /src/MongoFramework/Infrastructure/EntityCommandWriter.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Threading; 5 | using System.Threading.Tasks; 6 | using MongoDB.Driver; 7 | using MongoFramework.Infrastructure.Commands; 8 | using MongoFramework.Infrastructure.Diagnostics; 9 | using MongoFramework.Infrastructure.Mapping; 10 | 11 | namespace MongoFramework.Infrastructure 12 | { 13 | public static class EntityCommandWriter 14 | { 15 | public static void Write(IMongoDbConnection connection, IEnumerable commands, WriteModelOptions options) where TEntity : class 16 | { 17 | var writeModels = commands.OfType>().SelectMany(c => c.GetModel(options)).ToArray(); 18 | if (writeModels.Any()) 19 | { 20 | var entityDefinition = EntityMapping.GetOrCreateDefinition(typeof(TEntity)); 21 | var collection = connection.GetDatabase().GetCollection(entityDefinition.CollectionName); 22 | using (var diagnostics = DiagnosticRunner.Start(connection, writeModels)) 23 | { 24 | try 25 | { 26 | collection.BulkWrite(writeModels); 27 | } 28 | catch (Exception exception) 29 | { 30 | diagnostics.Error(exception); 31 | throw; 32 | } 33 | } 34 | } 35 | } 36 | public static async Task WriteAsync(IMongoDbConnection connection, IEnumerable commands, WriteModelOptions options, CancellationToken cancellationToken = default) where TEntity : class 37 | { 38 | var writeModels = commands.OfType>().SelectMany(c => c.GetModel(options)).ToArray(); 39 | if (writeModels.Any()) 40 | { 41 | var entityDefinition = EntityMapping.GetOrCreateDefinition(typeof(TEntity)); 42 | var collection = connection.GetDatabase().GetCollection(entityDefinition.CollectionName); 43 | using (var diagnostics = DiagnosticRunner.Start(connection, writeModels)) 44 | { 45 | try 46 | { 47 | await collection.BulkWriteAsync(writeModels, cancellationToken: cancellationToken); 48 | } 49 | catch (Exception exception) 50 | { 51 | diagnostics.Error(exception); 52 | throw; 53 | } 54 | } 55 | } 56 | } 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /src/MongoFramework/Infrastructure/EntityEntry.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using MongoDB.Bson; 3 | using MongoFramework.Infrastructure.Mapping; 4 | 5 | namespace MongoFramework.Infrastructure 6 | { 7 | public class EntityEntry 8 | { 9 | /// 10 | /// The entity that forms this object. 11 | /// 12 | public object Entity { get; } 13 | 14 | public Type EntityType { get; } 15 | 16 | /// 17 | /// The state of the entity in this object. 18 | /// 19 | public EntityEntryState State { get; internal set; } 20 | 21 | /// 22 | /// The original values of the entity. 23 | /// 24 | public BsonDocument OriginalValues { get; private set; } 25 | 26 | /// 27 | /// Creates a new with the specified entity and state information. 28 | /// 29 | /// 30 | /// 31 | public EntityEntry(object entity, EntityEntryState state) 32 | { 33 | State = state; 34 | Entity = entity; 35 | EntityType = entity.GetType(); 36 | 37 | // Ensure the entity type is registered before we potentially serialize the entity to a BSON document 38 | EntityMapping.TryRegisterType(EntityType, out _); 39 | 40 | if (state == EntityEntryState.NoChanges) 41 | { 42 | OriginalValues = Entity.ToBsonDocument(); 43 | } 44 | } 45 | 46 | /// 47 | /// Creates a new with the specified entity, type and state information. 48 | /// 49 | /// 50 | /// 51 | /// 52 | public EntityEntry(object entity, Type entityType, EntityEntryState state) : this(entity, state) 53 | { 54 | EntityType = entityType; 55 | } 56 | 57 | /// 58 | /// Resets the state of the entry, ready for tracking new changes. 59 | /// 60 | public void ResetState() 61 | { 62 | OriginalValues = Entity.ToBsonDocument(); 63 | State = EntityEntryState.NoChanges; 64 | } 65 | 66 | /// 67 | /// The current values of the entity. 68 | /// 69 | public BsonDocument CurrentValues => Entity.ToBsonDocument(); 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /src/MongoFramework/Infrastructure/EntityEntryState.cs: -------------------------------------------------------------------------------- 1 | namespace MongoFramework.Infrastructure 2 | { 3 | public enum EntityEntryState 4 | { 5 | /// 6 | /// There are no known changes to the entity 7 | /// 8 | NoChanges, 9 | /// 10 | /// The entity is known to be added during the next save 11 | /// 12 | Added, 13 | /// 14 | /// The entity is known to be updated during the next save 15 | /// 16 | Updated, 17 | /// 18 | /// The entity is known to be deleted during the next save 19 | /// 20 | Deleted, 21 | /// 22 | /// The entity is not attached for tracking 23 | /// 24 | Detached 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /src/MongoFramework/Infrastructure/Indexing/EntityIndexWriter.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Concurrent; 3 | using System.Linq; 4 | using System.Threading; 5 | using System.Threading.Tasks; 6 | using MongoFramework.Infrastructure.Diagnostics; 7 | using MongoFramework.Infrastructure.Mapping; 8 | 9 | namespace MongoFramework.Infrastructure.Indexing 10 | { 11 | public static class EntityIndexWriter 12 | { 13 | private static readonly ConcurrentDictionary HasAppliedIndexes = new ConcurrentDictionary(); 14 | 15 | public static void ClearCache() 16 | { 17 | HasAppliedIndexes.Clear(); 18 | } 19 | 20 | public static void ApplyIndexing(IMongoDbConnection connection) where TEntity : class 21 | { 22 | if (HasAppliedIndexes.TryGetValue(typeof(TEntity), out var hasApplied) && hasApplied) 23 | { 24 | return; 25 | } 26 | 27 | var indexModel = IndexModelBuilder.BuildModel().ToArray(); 28 | if (indexModel.Length > 0) 29 | { 30 | var definition = EntityMapping.GetOrCreateDefinition(typeof(TEntity)); 31 | using (var diagnostics = DiagnosticRunner.Start(connection, indexModel)) 32 | { 33 | try 34 | { 35 | var collection = connection.GetDatabase().GetCollection(definition.CollectionName); 36 | collection.Indexes.CreateMany(indexModel); 37 | HasAppliedIndexes.TryAdd(typeof(TEntity), true); 38 | } 39 | catch (Exception exception) 40 | { 41 | diagnostics.Error(exception); 42 | throw; 43 | } 44 | } 45 | } 46 | } 47 | 48 | public static async Task ApplyIndexingAsync(IMongoDbConnection connection, CancellationToken cancellationToken = default) where TEntity : class 49 | { 50 | if (HasAppliedIndexes.TryGetValue(typeof(TEntity), out var hasApplied) && hasApplied) 51 | { 52 | return; 53 | } 54 | 55 | var indexModel = IndexModelBuilder.BuildModel().ToArray(); 56 | if (indexModel.Length > 0) 57 | { 58 | var definition = EntityMapping.GetOrCreateDefinition(typeof(TEntity)); 59 | using (var diagnostics = DiagnosticRunner.Start(connection, indexModel)) 60 | { 61 | try 62 | { 63 | var collection = connection.GetDatabase().GetCollection(definition.CollectionName); 64 | await collection.Indexes.CreateManyAsync(indexModel, cancellationToken).ConfigureAwait(false); 65 | HasAppliedIndexes.TryAdd(typeof(TEntity), true); 66 | } 67 | catch (Exception exception) 68 | { 69 | diagnostics.Error(exception); 70 | throw; 71 | } 72 | } 73 | } 74 | } 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /src/MongoFramework/Infrastructure/Indexing/IndexModelBuilder.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using MongoDB.Driver; 4 | using MongoFramework.Infrastructure.Mapping; 5 | 6 | namespace MongoFramework.Infrastructure.Indexing; 7 | 8 | public static class IndexModelBuilder 9 | { 10 | public static IEnumerable> BuildModel() 11 | { 12 | var indexBuilder = Builders.IndexKeys; 13 | var indexes = EntityMapping.GetOrCreateDefinition(typeof(TEntity)).Indexes; 14 | 15 | foreach (var index in indexes) 16 | { 17 | var indexKeyCount = index.IndexPaths.Count + (index.IsTenantExclusive ? 1 : 0); 18 | var indexKeys = new IndexKeysDefinition[indexKeyCount]; 19 | for (var i = 0; i < index.IndexPaths.Count; i++) 20 | { 21 | indexKeys[i] = CreateIndexKey(index.IndexPaths[i]); 22 | } 23 | 24 | if (index.IsTenantExclusive) 25 | { 26 | indexKeys[indexKeys.Length - 1] = Builders.IndexKeys.Ascending(nameof(IHaveTenantId.TenantId)); 27 | } 28 | 29 | var combinedKeyDefinition = indexBuilder.Combine(indexKeys); 30 | yield return new CreateIndexModel(combinedKeyDefinition, new CreateIndexOptions 31 | { 32 | Name = index.IndexName, 33 | Unique = index.IsUnique, 34 | Background = true 35 | }); 36 | } 37 | } 38 | 39 | private static IndexKeysDefinition CreateIndexKey(IndexPathDefinition indexPathDefinition) 40 | { 41 | var builder = Builders.IndexKeys; 42 | Func, IndexKeysDefinition> builderMethod = indexPathDefinition.IndexType switch 43 | { 44 | IndexType.Standard when indexPathDefinition.SortOrder == IndexSortOrder.Ascending => builder.Ascending, 45 | IndexType.Standard when indexPathDefinition.SortOrder == IndexSortOrder.Descending => builder.Descending, 46 | IndexType.Text => builder.Text, 47 | IndexType.Geo2dSphere => builder.Geo2DSphere, 48 | _ => throw new ArgumentException($"Unsupported index type \"{indexPathDefinition.IndexType}\"", nameof(indexPathDefinition)) 49 | }; 50 | return builderMethod(indexPathDefinition.Path); 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /src/MongoFramework/Infrastructure/Internal/DbSetInitializer.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Concurrent; 3 | using System.Collections.Generic; 4 | using System.Linq.Expressions; 5 | using System.Reflection; 6 | using MongoFramework.Attributes; 7 | 8 | namespace MongoFramework.Infrastructure.Internal 9 | { 10 | public static class DbSetInitializer 11 | { 12 | private static readonly ConcurrentDictionary> PropertyCache = new ConcurrentDictionary>(); 13 | private static readonly ConcurrentDictionary ConstructorCache = new ConcurrentDictionary(); 14 | 15 | public static IEnumerable GetDbSetProperties(IMongoDbContext context) 16 | { 17 | var contextType = context.GetType(); 18 | return PropertyCache.GetOrAdd(contextType, targetType => 19 | { 20 | var allProperties = targetType.GetProperties(BindingFlags.Instance | BindingFlags.Public); 21 | var dbSetProperties = new List(); 22 | var mongoDbSetType = typeof(IMongoDbSet); 23 | 24 | foreach (var property in allProperties) 25 | { 26 | var propertyType = property.PropertyType; 27 | if (propertyType.IsGenericType && mongoDbSetType.IsAssignableFrom(propertyType)) 28 | { 29 | dbSetProperties.Add(property); 30 | } 31 | } 32 | 33 | return dbSetProperties; 34 | }); 35 | } 36 | 37 | public static IDbSetOptions GetDefaultDbSetOptions(PropertyInfo propertyInfo) 38 | { 39 | var optionsAttribute = propertyInfo.GetCustomAttribute(); 40 | return optionsAttribute?.GetOptions(); 41 | } 42 | 43 | public static IMongoDbSet CreateDbSet(Type dbSetType, IMongoDbContext context, IDbSetOptions options = null) 44 | { 45 | var constructorDetails = ConstructorCache.GetOrAdd(dbSetType, type => 46 | { 47 | var constructorWithOptions = dbSetType.GetConstructor(new[] { typeof(IMongoDbContext), typeof(IDbSetOptions) }); 48 | if (constructorWithOptions != null) 49 | { 50 | var parameters = new[] { Expression.Parameter(typeof(IMongoDbContext)), Expression.Parameter(typeof(IDbSetOptions)) }; 51 | var constructorDelegate = Expression.Lambda>( 52 | Expression.Convert( 53 | Expression.New(constructorWithOptions, parameters), 54 | typeof(IMongoDbSet) 55 | ), 56 | parameters 57 | ).Compile(); 58 | return (true, constructorDelegate); 59 | } 60 | else 61 | { 62 | var constructorNoOptions = dbSetType.GetConstructor(new[] { typeof(IMongoDbContext) }); 63 | if (constructorNoOptions == null) 64 | { 65 | throw new InvalidOperationException("No valid constructor available for IMongoDbSet"); 66 | } 67 | 68 | var parameters = new[] { Expression.Parameter(typeof(IMongoDbContext)) }; 69 | var constructorDelegate = Expression.Lambda>( 70 | Expression.Convert( 71 | Expression.New(constructorNoOptions, parameters), 72 | typeof(IMongoDbSet) 73 | ), 74 | parameters 75 | ).Compile(); 76 | return (false, constructorDelegate); 77 | } 78 | }); 79 | 80 | if (constructorDetails.WithOptions) 81 | { 82 | var constructor = constructorDetails.ConstructorDelegate as Func; 83 | return constructor(context, options); 84 | } 85 | else 86 | { 87 | var constructor = constructorDetails.ConstructorDelegate as Func; 88 | return constructor(context); 89 | } 90 | } 91 | } 92 | } 93 | -------------------------------------------------------------------------------- /src/MongoFramework/Infrastructure/Internal/GenericsHelper.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Concurrent; 3 | using System.Linq; 4 | using System.Reflection; 5 | 6 | namespace MongoFramework.Infrastructure.Internal 7 | { 8 | internal static class GenericsHelper 9 | { 10 | private static readonly ConcurrentDictionary<(Type DeclaringType, string MethodName, Type GenericType), Delegate> MethodDelegateLookup = new ConcurrentDictionary<(Type, string, Type), Delegate>(); 11 | 12 | /// 13 | /// Creates a delegate for a method which includes a generic argument. 14 | /// If a corresponding delegate already exists, it is returned. 15 | /// 16 | /// The target delegate type. 17 | /// The type where the static target method resides. 18 | /// The name of the target method. 19 | /// The generic type for the target method. 20 | /// 21 | public static TDelegate GetMethodDelegate(Type declaringType, string methodName, Type genericArgument) where TDelegate : Delegate 22 | { 23 | return (TDelegate)MethodDelegateLookup.GetOrAdd((declaringType, methodName, genericArgument), o => 24 | { 25 | var baseMethod = o.DeclaringType 26 | .GetMethods(BindingFlags.NonPublic | BindingFlags.Static) 27 | .Where(m => m.IsGenericMethod && m.Name == methodName) 28 | .FirstOrDefault(); 29 | return baseMethod.MakeGenericMethod(genericArgument).CreateDelegate(typeof(TDelegate)); 30 | }); 31 | } 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /src/MongoFramework/Infrastructure/Internal/TypeExtensions.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | 4 | namespace MongoFramework.Infrastructure.Internal; 5 | 6 | internal static class TypeExtensions 7 | { 8 | private static readonly HashSet CommonGenericEnumerables = new() 9 | { 10 | typeof(IEnumerable<>), 11 | typeof(IList<>), 12 | typeof(ICollection<>), 13 | typeof(IReadOnlyList<>), 14 | typeof(IReadOnlyCollection<>) 15 | }; 16 | 17 | /// 18 | /// Attempts to unwrap enumerable types (like ) from the current , returning the actual item type. 19 | /// 20 | /// 21 | /// Unwrapped types include:
22 | /// -
23 | /// -
24 | /// -
25 | /// -
26 | /// -
27 | /// - 28 | ///
29 | /// 30 | /// 31 | public static Type UnwrapEnumerableTypes(this Type type) 32 | { 33 | if (type.IsArray) 34 | { 35 | return type.GetElementType(); 36 | } 37 | else if (type.IsGenericType) 38 | { 39 | if (CommonGenericEnumerables.Contains(type.GetGenericTypeDefinition())) 40 | { 41 | return type.GetGenericArguments()[0]; 42 | } 43 | else 44 | { 45 | //Unlike when the type is directly a known generic enumerable interface, if we start making assumptions 46 | //like that on the interfaces of the type, we can hit edge cases where a type implements multiple interfaces. 47 | foreach (var interfaceType in type.GetInterfaces()) 48 | { 49 | if (interfaceType.IsGenericType && interfaceType.GetGenericTypeDefinition() == typeof(IEnumerable<>)) 50 | { 51 | return type.GetGenericArguments()[0]; 52 | } 53 | } 54 | } 55 | } 56 | 57 | return type; 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /src/MongoFramework/Infrastructure/Linq/AggregateExecutionModel.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | using System.Linq.Expressions; 3 | using MongoDB.Bson; 4 | using MongoDB.Bson.Serialization; 5 | 6 | namespace MongoFramework.Infrastructure.Linq 7 | { 8 | public class AggregateExecutionModel 9 | { 10 | public IEnumerable Stages { get; set; } 11 | 12 | public IBsonSerializer Serializer { get; set; } 13 | 14 | public LambdaExpression ResultTransformer { get; set; } 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /src/MongoFramework/Infrastructure/Linq/EntityProcessorCollection.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | 3 | namespace MongoFramework.Infrastructure.Linq 4 | { 5 | public class EntityProcessorCollection : List>, ILinqProcessor where TEntity : class 6 | { 7 | public void ProcessEntity(TEntity entity, IMongoDbConnection connection) 8 | { 9 | foreach (var processor in this) 10 | { 11 | processor.ProcessEntity(entity, connection); 12 | } 13 | } 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /src/MongoFramework/Infrastructure/Linq/ILinqProcessor.cs: -------------------------------------------------------------------------------- 1 | namespace MongoFramework.Infrastructure.Linq 2 | { 3 | public interface ILinqProcessor where TEntity : class 4 | { 5 | void ProcessEntity(TEntity entity, IMongoDbConnection connection); 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /src/MongoFramework/Infrastructure/Linq/IMongoFrameworkQueryProvider.cs: -------------------------------------------------------------------------------- 1 | using System.Linq; 2 | using System.Linq.Expressions; 3 | using System.Threading; 4 | 5 | namespace MongoFramework.Infrastructure.Linq 6 | { 7 | public interface IMongoFrameworkQueryProvider : IQueryProvider 8 | { 9 | IMongoDbConnection Connection { get; } 10 | Expression GetBaseExpression(); 11 | object ExecuteAsync(Expression expression, CancellationToken cancellationToken = default); 12 | string ToQuery(Expression expression); 13 | } 14 | 15 | public interface IMongoFrameworkQueryProvider : IMongoFrameworkQueryProvider where TEntity : class 16 | { 17 | EntityProcessorCollection EntityProcessors { get; } 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /src/MongoFramework/Infrastructure/Linq/IMongoFrameworkQueryable.cs: -------------------------------------------------------------------------------- 1 | using System.Linq; 2 | 3 | namespace MongoFramework.Infrastructure.Linq 4 | { 5 | public interface IMongoFrameworkQueryable : IOrderedQueryable 6 | { 7 | string ToQuery(); 8 | } 9 | 10 | public interface IMongoFrameworkQueryable : IMongoFrameworkQueryable, IOrderedQueryable 11 | { 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /src/MongoFramework/Infrastructure/Linq/MongoFrameworkQueryable.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections; 3 | using System.Collections.Generic; 4 | using System.Linq; 5 | using System.Linq.Expressions; 6 | 7 | namespace MongoFramework.Infrastructure.Linq 8 | { 9 | public class MongoFrameworkQueryable : IMongoFrameworkQueryable 10 | { 11 | private IMongoFrameworkQueryProvider InternalProvider { get; } 12 | 13 | public Type ElementType => typeof(TOutput); 14 | public Expression Expression { get; } 15 | public IQueryProvider Provider => InternalProvider; 16 | 17 | public MongoFrameworkQueryable(IMongoFrameworkQueryProvider provider) 18 | { 19 | InternalProvider = provider; 20 | Expression = provider.GetBaseExpression(); 21 | } 22 | 23 | public MongoFrameworkQueryable(IMongoFrameworkQueryProvider provider, Expression expression) 24 | { 25 | InternalProvider = provider; 26 | Expression = expression; 27 | } 28 | 29 | public IEnumerator GetEnumerator() 30 | { 31 | var result = (IEnumerable)InternalProvider.Execute(Expression); 32 | return result.GetEnumerator(); 33 | } 34 | 35 | IEnumerator IEnumerable.GetEnumerator() 36 | { 37 | return GetEnumerator(); 38 | } 39 | 40 | public string ToQuery() 41 | { 42 | return InternalProvider.ToQuery(Expression); 43 | } 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /src/MongoFramework/Infrastructure/Linq/Processors/EntityTrackingProcessor.cs: -------------------------------------------------------------------------------- 1 | namespace MongoFramework.Infrastructure.Linq.Processors 2 | { 3 | public class EntityTrackingProcessor : ILinqProcessor where TEntity : class 4 | { 5 | public IMongoDbContext Context { get; } 6 | 7 | public EntityTrackingProcessor(IMongoDbContext context) 8 | { 9 | Context = context; 10 | } 11 | 12 | public void ProcessEntity(TEntity entity, IMongoDbConnection connection) 13 | { 14 | var entry = Context.ChangeTracker.GetEntry(entity); 15 | if (entry == null || entry.State == EntityEntryState.NoChanges) 16 | { 17 | Context.ChangeTracker.SetEntityState(entity, EntityEntryState.NoChanges); 18 | } 19 | } 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /src/MongoFramework/Infrastructure/Linq/QueryHelper.cs: -------------------------------------------------------------------------------- 1 | using System.Linq; 2 | using MongoFramework.Infrastructure.Mapping; 3 | 4 | namespace MongoFramework.Infrastructure.Linq 5 | { 6 | public static class QueryHelper 7 | { 8 | public static string GetQuery(AggregateExecutionModel model) 9 | { 10 | var entityDefinition = EntityMapping.GetOrCreateDefinition(typeof(TEntity)); 11 | var stages = string.Join(", ", model.Stages.Select(x => x.ToString())); 12 | return $"db.{entityDefinition.CollectionName}.aggregate([{stages}])"; 13 | } 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /src/MongoFramework/Infrastructure/Linq/ResultTransformers.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Linq.Expressions; 5 | using System.Threading; 6 | 7 | namespace MongoFramework.Infrastructure.Linq 8 | { 9 | public static class ResultTransformers 10 | { 11 | public static Expression Transform(Expression expression, Type sourceType, bool isAsync) 12 | { 13 | if (expression is MethodCallExpression methodCallExpression) 14 | { 15 | var transformName = methodCallExpression.Method.Name; 16 | if (isAsync) 17 | { 18 | var sourceParameter = Expression.Parameter( 19 | typeof(IAsyncEnumerable<>).MakeGenericType(sourceType), 20 | "source" 21 | ); 22 | var cancellationTokenParameter = Expression.Parameter( 23 | typeof(CancellationToken), 24 | "cancellationToken" 25 | ); 26 | 27 | var methodInfo = (transformName switch 28 | { 29 | nameof(Queryable.First) => MethodInfoCache.AsyncEnumerable.First_1, 30 | nameof(Queryable.FirstOrDefault) => MethodInfoCache.AsyncEnumerable.FirstOrDefault_1, 31 | 32 | nameof(Queryable.Single) => MethodInfoCache.AsyncEnumerable.Single_1, 33 | nameof(Queryable.SingleOrDefault) => MethodInfoCache.AsyncEnumerable.SingleOrDefault_1, 34 | 35 | nameof(Queryable.Count) => MethodInfoCache.AsyncEnumerable.SingleOrDefault_1, 36 | nameof(Queryable.Max) => MethodInfoCache.AsyncEnumerable.Single_1, 37 | nameof(Queryable.Min) => MethodInfoCache.AsyncEnumerable.Single_1, 38 | nameof(Queryable.Sum) => MethodInfoCache.AsyncEnumerable.SingleOrDefault_1, 39 | 40 | nameof(Queryable.Any) => MethodInfoCache.AsyncEnumerable.Any_1, 41 | 42 | _ => throw new InvalidOperationException($"No transform available for {transformName}") 43 | }).MakeGenericMethod(sourceType); 44 | 45 | return Expression.Lambda( 46 | Expression.Call( 47 | null, 48 | methodInfo, 49 | sourceParameter, 50 | cancellationTokenParameter 51 | ), 52 | sourceParameter, 53 | cancellationTokenParameter 54 | ); 55 | } 56 | else 57 | { 58 | var sourceParameter = Expression.Parameter( 59 | typeof(IEnumerable<>).MakeGenericType(sourceType), 60 | "source" 61 | ); 62 | 63 | var methodInfo = (transformName switch 64 | { 65 | nameof(Queryable.First) => MethodInfoCache.Enumerable.First_1, 66 | nameof(Queryable.FirstOrDefault) => MethodInfoCache.Enumerable.FirstOrDefault_1, 67 | 68 | nameof(Queryable.Single) => MethodInfoCache.Enumerable.Single_1, 69 | nameof(Queryable.SingleOrDefault) => MethodInfoCache.Enumerable.SingleOrDefault_1, 70 | 71 | nameof(Queryable.Count) => MethodInfoCache.Enumerable.SingleOrDefault_1, 72 | nameof(Queryable.Max) => MethodInfoCache.Enumerable.Single_1, 73 | nameof(Queryable.Min) => MethodInfoCache.Enumerable.Single_1, 74 | nameof(Queryable.Sum) => MethodInfoCache.Enumerable.SingleOrDefault_1, 75 | 76 | nameof(Queryable.Any) => MethodInfoCache.Enumerable.Any_1, 77 | 78 | _ => throw new InvalidOperationException($"No transform available for {transformName}") 79 | }).MakeGenericMethod(sourceType); 80 | 81 | return Expression.Lambda( 82 | Expression.Call( 83 | null, 84 | methodInfo, 85 | sourceParameter 86 | ), 87 | sourceParameter 88 | ); 89 | } 90 | } 91 | 92 | throw new InvalidOperationException($"Result transformation unavailable for expression type {expression.NodeType}"); 93 | } 94 | } 95 | } -------------------------------------------------------------------------------- /src/MongoFramework/Infrastructure/Mapping/DefaultMappingProcessors.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | using MongoFramework.Infrastructure.Mapping.Processors; 3 | 4 | namespace MongoFramework.Infrastructure.Mapping; 5 | 6 | public static class DefaultMappingProcessors 7 | { 8 | public static readonly IReadOnlyList Processors = new IMappingProcessor[] 9 | { 10 | new SkipMappingProcessor(), 11 | new CollectionNameProcessor(), 12 | new HierarchyProcessor(), 13 | new PropertyMappingProcessor(), 14 | new EntityIdProcessor(), 15 | new NestedTypeProcessor(), 16 | new ExtraElementsProcessor(), 17 | new BsonKnownTypesProcessor(), 18 | new IndexProcessor(), 19 | new MappingAdapterProcessor() 20 | }; 21 | } 22 | -------------------------------------------------------------------------------- /src/MongoFramework/Infrastructure/Mapping/DriverMappingInterop.cs: -------------------------------------------------------------------------------- 1 | using MongoDB.Bson.Serialization; 2 | 3 | namespace MongoFramework.Infrastructure.Mapping; 4 | 5 | /// 6 | /// Maps the MongoFramework definition into something the MongoDB Driver will understand 7 | /// 8 | internal static class DriverMappingInterop 9 | { 10 | /// 11 | /// Registers the as a with all appropriate properties configured. 12 | /// 13 | /// 14 | public static void RegisterDefinition(EntityDefinition definition) 15 | { 16 | var classMap = new BsonClassMap(definition.EntityType); 17 | 18 | // Hierarchy 19 | if (!EntityMapping.IsValidTypeToMap(definition.EntityType.BaseType)) 20 | { 21 | classMap.SetIsRootClass(true); 22 | } 23 | 24 | // Properties 25 | foreach (var property in definition.Properties) 26 | { 27 | var memberMap = classMap.MapMember(property.PropertyInfo); 28 | memberMap.SetElementName(property.ElementName); 29 | } 30 | 31 | // Key / ID 32 | if (definition.Key is not null) 33 | { 34 | var idMemberMap = classMap.MapIdMember(definition.Key.Property.PropertyInfo); 35 | if (definition.Key.KeyGenerator is not null) 36 | { 37 | idMemberMap.SetIdGenerator(new DriverKeyGeneratorWrapper(definition.Key.KeyGenerator)); 38 | } 39 | } 40 | 41 | // Extra Elements 42 | if (definition.ExtraElements is not null) 43 | { 44 | if (definition.ExtraElements.IgnoreExtraElements) 45 | { 46 | classMap.SetIgnoreExtraElements(true); 47 | classMap.SetIgnoreExtraElementsIsInherited(definition.ExtraElements.IgnoreInherited); 48 | } 49 | else 50 | { 51 | classMap.SetIgnoreExtraElements(false); 52 | 53 | var extraElementsProperty = definition.ExtraElements.Property; 54 | foreach (var memberMap in classMap.DeclaredMemberMaps) 55 | { 56 | if (memberMap.ElementName == extraElementsProperty.ElementName) 57 | { 58 | classMap.SetExtraElementsMember(memberMap); 59 | break; 60 | } 61 | } 62 | } 63 | } 64 | 65 | BsonClassMap.RegisterClassMap(classMap); 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /src/MongoFramework/Infrastructure/Mapping/EntityDefinition.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Diagnostics; 4 | using System.Reflection; 5 | 6 | namespace MongoFramework.Infrastructure.Mapping; 7 | 8 | [DebuggerDisplay("{DebuggerDisplay,nq}")] 9 | public sealed record EntityDefinition 10 | { 11 | public Type EntityType { get; init; } 12 | public string CollectionName { get; init; } 13 | public KeyDefinition Key { get; init; } 14 | public IReadOnlyList Properties { get; init; } = Array.Empty(); 15 | public IReadOnlyList Indexes { get; init; } = Array.Empty(); 16 | public ExtraElementsDefinition ExtraElements { get; init; } 17 | 18 | [DebuggerNonUserCode] 19 | private string DebuggerDisplay => $"EntityType = {EntityType.Name}, Collection = {CollectionName}, Properties = {Properties.Count}, Indexes = {Indexes.Count}"; 20 | } 21 | 22 | [DebuggerDisplay("{DebuggerDisplay,nq}")] 23 | public sealed record PropertyDefinition 24 | { 25 | public PropertyInfo PropertyInfo { get; init; } 26 | public string ElementName { get; init; } 27 | 28 | public object GetValue(object entity) 29 | { 30 | return PropertyInfo.GetValue(entity); 31 | } 32 | 33 | public void SetValue(object entity, object value) 34 | { 35 | PropertyInfo.SetValue(entity, value); 36 | } 37 | 38 | [DebuggerNonUserCode] 39 | private string DebuggerDisplay => $"PropertyInfo = {PropertyInfo.Name}, ElementName = {ElementName}"; 40 | } 41 | 42 | [DebuggerDisplay("{DebuggerDisplay,nq}")] 43 | public sealed record IndexDefinition 44 | { 45 | public IReadOnlyList IndexPaths { get; init; } 46 | public string IndexName { get; init; } 47 | public bool IsUnique { get; init; } 48 | public bool IsTenantExclusive { get; init; } 49 | 50 | [DebuggerNonUserCode] 51 | private string DebuggerDisplay => $"IndexName = {IndexName}, IndexPaths = {IndexPaths.Count}, IsUnique = {IsUnique}"; 52 | } 53 | 54 | [DebuggerDisplay("{DebuggerDisplay,nq}")] 55 | public sealed record IndexPathDefinition 56 | { 57 | public string Path { get; init; } 58 | public IndexType IndexType { get; init; } 59 | public IndexSortOrder SortOrder { get; init; } 60 | 61 | [DebuggerNonUserCode] 62 | private string DebuggerDisplay => $"Path = {Path}, IndexType = {IndexType}, SortOrder = {SortOrder}"; 63 | } 64 | 65 | [DebuggerDisplay("{DebuggerDisplay,nq}")] 66 | public sealed record KeyDefinition 67 | { 68 | public PropertyDefinition Property { get; init; } 69 | public IEntityKeyGenerator KeyGenerator { get; init; } 70 | 71 | [DebuggerNonUserCode] 72 | private string DebuggerDisplay => $"PropertyInfo = {Property.PropertyInfo.Name}, ElementName = {Property.ElementName}"; 73 | } 74 | 75 | [DebuggerDisplay("{DebuggerDisplay,nq}")] 76 | public sealed record ExtraElementsDefinition 77 | { 78 | public PropertyDefinition Property { get; init; } 79 | public bool IgnoreExtraElements { get; init; } 80 | public bool IgnoreInherited { get; init; } 81 | 82 | [DebuggerNonUserCode] 83 | private string DebuggerDisplay 84 | { 85 | get 86 | { 87 | if (IgnoreExtraElements) 88 | { 89 | return "IgnoreExtraElements = true"; 90 | } 91 | else 92 | { 93 | return $"PropertyInfo = {Property.PropertyInfo.Name}, ElementName = {Property.ElementName}"; 94 | } 95 | } 96 | } 97 | } -------------------------------------------------------------------------------- /src/MongoFramework/Infrastructure/Mapping/EntityDefinitionExtensions.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | 5 | namespace MongoFramework.Infrastructure.Mapping 6 | { 7 | public static class EntityDefinitionExtensions 8 | { 9 | /// 10 | /// Finds the nearest from , recursively searching the base if one exists. 11 | /// 12 | /// The to start the search from. 13 | /// The key definition; otherwise if one can not be found. 14 | public static KeyDefinition FindNearestKey(this EntityDefinition definition) 15 | { 16 | if (definition.Key is null) 17 | { 18 | return EntityMapping.GetOrCreateDefinition(definition.EntityType.BaseType).FindNearestKey(); 19 | } 20 | 21 | return definition.Key; 22 | } 23 | 24 | public static PropertyDefinition GetIdProperty(this EntityDefinition definition) 25 | { 26 | return definition.FindNearestKey()?.Property; 27 | } 28 | 29 | public static string GetIdName(this EntityDefinition definition) 30 | { 31 | return definition.GetIdProperty()?.ElementName; 32 | } 33 | 34 | public static object GetIdValue(this EntityDefinition definition, object entity) 35 | { 36 | return definition.GetIdProperty()?.GetValue(entity); 37 | } 38 | 39 | public static object GetDefaultId(this EntityDefinition definition) 40 | { 41 | var idPropertyType = definition.GetIdProperty()?.PropertyInfo.PropertyType; 42 | if (idPropertyType is { IsValueType: true }) 43 | { 44 | return Activator.CreateInstance(idPropertyType); 45 | } 46 | return null; 47 | } 48 | 49 | public static IEnumerable GetInheritedProperties(this EntityDefinition definition) 50 | { 51 | var currentType = definition.EntityType.BaseType; 52 | while (currentType != typeof(object) && currentType != null) 53 | { 54 | var currentDefinition = EntityMapping.GetOrCreateDefinition(currentType); 55 | foreach (var property in currentDefinition.Properties) 56 | { 57 | yield return property; 58 | } 59 | 60 | currentType = currentType.BaseType; 61 | } 62 | } 63 | 64 | public static IEnumerable GetAllProperties(this EntityDefinition definition) 65 | { 66 | foreach (var property in definition.Properties) 67 | { 68 | yield return property; 69 | } 70 | 71 | foreach (var property in definition.GetInheritedProperties()) 72 | { 73 | yield return property; 74 | } 75 | } 76 | 77 | public static PropertyDefinition GetProperty(this EntityDefinition definition, string name) 78 | { 79 | foreach (var property in definition.GetAllProperties()) 80 | { 81 | if (property.PropertyInfo.Name == name) 82 | { 83 | return property; 84 | } 85 | } 86 | 87 | return default; 88 | } 89 | } 90 | } 91 | -------------------------------------------------------------------------------- /src/MongoFramework/Infrastructure/Mapping/EntityKeyGenerator.cs: -------------------------------------------------------------------------------- 1 | using MongoDB.Bson.Serialization; 2 | using MongoDB.Bson.Serialization.IdGenerators; 3 | 4 | namespace MongoFramework.Infrastructure.Mapping; 5 | 6 | public interface IEntityKeyGenerator 7 | { 8 | public object Generate(); 9 | public bool IsEmpty(object id); 10 | } 11 | 12 | public static class EntityKeyGenerators 13 | { 14 | public static readonly IEntityKeyGenerator StringKeyGenerator = new EntityKeyGenerator(StringObjectIdGenerator.Instance); 15 | public static readonly IEntityKeyGenerator GuidKeyGenerator = new EntityKeyGenerator(CombGuidGenerator.Instance); 16 | public static readonly IEntityKeyGenerator ObjectIdKeyGenerator = new EntityKeyGenerator(ObjectIdGenerator.Instance); 17 | } 18 | 19 | internal sealed class EntityKeyGenerator : IEntityKeyGenerator 20 | { 21 | private readonly IIdGenerator idGenerator; 22 | 23 | public EntityKeyGenerator(IIdGenerator idGenerator) 24 | { 25 | this.idGenerator = idGenerator; 26 | } 27 | 28 | public object Generate() 29 | { 30 | return idGenerator.GenerateId(null, null); 31 | } 32 | 33 | public bool IsEmpty(object id) 34 | { 35 | return idGenerator.IsEmpty(id); 36 | } 37 | } 38 | 39 | internal sealed class DriverKeyGeneratorWrapper : IIdGenerator 40 | { 41 | private readonly IEntityKeyGenerator entityKeyGenerator; 42 | 43 | public DriverKeyGeneratorWrapper(IEntityKeyGenerator entityKeyGenerator) 44 | { 45 | this.entityKeyGenerator = entityKeyGenerator; 46 | } 47 | 48 | public object GenerateId(object container, object document) => entityKeyGenerator.Generate(); 49 | 50 | public bool IsEmpty(object id) => entityKeyGenerator.IsEmpty(id); 51 | } 52 | -------------------------------------------------------------------------------- /src/MongoFramework/Infrastructure/Mapping/EntityMapping.MappingProcessors.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Buffers; 3 | using System.Collections.Generic; 4 | using System.Linq; 5 | 6 | namespace MongoFramework.Infrastructure.Mapping; 7 | 8 | public static partial class EntityMapping 9 | { 10 | private static readonly List MappingProcessors = new(); 11 | 12 | private static void WithMappingWriteLock(Action action) 13 | { 14 | MappingLock.EnterWriteLock(); 15 | try 16 | { 17 | action(); 18 | } 19 | finally 20 | { 21 | MappingLock.ExitWriteLock(); 22 | } 23 | } 24 | 25 | public static void AddMappingProcessor(IMappingProcessor mappingProcessor) 26 | { 27 | WithMappingWriteLock(() => MappingProcessors.Add(mappingProcessor)); 28 | } 29 | 30 | public static void AddMappingProcessors(IEnumerable mappingProcessors) 31 | { 32 | WithMappingWriteLock(() => MappingProcessors.AddRange(mappingProcessors)); 33 | } 34 | 35 | public static void RemoveMappingProcessor() where TProcessor : IMappingProcessor 36 | { 37 | WithMappingWriteLock(() => 38 | { 39 | var matchingItems = MappingProcessors.Where(p => p.GetType() == typeof(TProcessor)).ToArray(); 40 | foreach (var matchingItem in matchingItems) 41 | { 42 | MappingProcessors.Remove(matchingItem); 43 | } 44 | }); 45 | } 46 | 47 | public static void RemoveAllMappingProcessors() 48 | { 49 | WithMappingWriteLock(MappingProcessors.Clear); 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /src/MongoFramework/Infrastructure/Mapping/EntityMapping.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Concurrent; 3 | using System.Runtime.CompilerServices; 4 | using System.Threading; 5 | using MongoDB.Bson; 6 | using MongoDB.Bson.Serialization; 7 | 8 | namespace MongoFramework.Infrastructure.Mapping; 9 | 10 | public static partial class EntityMapping 11 | { 12 | private static ReaderWriterLockSlim MappingLock { get; } = new(LockRecursionPolicy.SupportsRecursion); 13 | private static readonly ConcurrentDictionary EntityDefinitions = new(); 14 | 15 | static EntityMapping() 16 | { 17 | DriverAbstractionRules.ApplyRules(); 18 | AddMappingProcessors(DefaultMappingProcessors.Processors); 19 | } 20 | 21 | internal static void RemoveAllDefinitions() 22 | { 23 | EntityDefinitions.Clear(); 24 | } 25 | 26 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 27 | public static bool IsValidTypeToMap(Type entityType) 28 | { 29 | return entityType.IsClass && 30 | entityType != typeof(object) && 31 | entityType != typeof(string) && 32 | !typeof(BsonValue).IsAssignableFrom(entityType); 33 | } 34 | 35 | public static bool IsRegistered(Type entityType) 36 | { 37 | return EntityDefinitions.ContainsKey(entityType); 38 | } 39 | 40 | public static EntityDefinition GetOrCreateDefinition(Type entityType) 41 | { 42 | MappingLock.EnterUpgradeableReadLock(); 43 | try 44 | { 45 | if (EntityDefinitions.TryGetValue(entityType, out var definition)) 46 | { 47 | return definition; 48 | } 49 | 50 | return RegisterType(entityType); 51 | } 52 | finally 53 | { 54 | MappingLock.ExitUpgradeableReadLock(); 55 | } 56 | } 57 | 58 | public static bool TryRegisterType(Type entityType, out EntityDefinition definition) 59 | { 60 | if (!IsValidTypeToMap(entityType)) 61 | { 62 | definition = null; 63 | return false; 64 | } 65 | 66 | MappingLock.EnterUpgradeableReadLock(); 67 | try 68 | { 69 | if (EntityDefinitions.ContainsKey(entityType) || BsonClassMap.IsClassMapRegistered(entityType)) 70 | { 71 | definition = null; 72 | return false; 73 | } 74 | 75 | MappingLock.EnterWriteLock(); 76 | try 77 | { 78 | //Now we have the write lock, do one super last minute check 79 | if (EntityDefinitions.TryGetValue(entityType, out definition)) 80 | { 81 | //We will treat success of this check as if we have registered it just now 82 | return true; 83 | } 84 | 85 | var mappingBuilder = new MappingBuilder(MappingProcessors); 86 | mappingBuilder.Entity(entityType); 87 | 88 | RegisterMapping(mappingBuilder); 89 | 90 | return EntityDefinitions.TryGetValue(entityType, out definition); 91 | } 92 | catch 93 | { 94 | definition = null; 95 | return false; 96 | } 97 | finally 98 | { 99 | MappingLock.ExitWriteLock(); 100 | } 101 | } 102 | finally 103 | { 104 | MappingLock.ExitUpgradeableReadLock(); 105 | } 106 | } 107 | 108 | public static EntityDefinition RegisterType(Type entityType) 109 | { 110 | if (!IsValidTypeToMap(entityType)) 111 | { 112 | throw new ArgumentException("Type is not a valid type to map", nameof(entityType)); 113 | } 114 | 115 | MappingLock.EnterUpgradeableReadLock(); 116 | try 117 | { 118 | if (EntityDefinitions.ContainsKey(entityType)) 119 | { 120 | throw new ArgumentException("Type is already registered", nameof(entityType)); 121 | } 122 | 123 | if (BsonClassMap.IsClassMapRegistered(entityType)) 124 | { 125 | throw new ArgumentException($"Type is already registered as a {nameof(BsonClassMap)}"); 126 | } 127 | 128 | MappingLock.EnterWriteLock(); 129 | try 130 | { 131 | //Now we have the write lock, do one super last minute check 132 | if (EntityDefinitions.TryGetValue(entityType, out var definition)) 133 | { 134 | //We will treat success of this check as if we have registered it just now 135 | return definition; 136 | } 137 | 138 | var mappingBuilder = new MappingBuilder(MappingProcessors); 139 | mappingBuilder.Entity(entityType); 140 | 141 | RegisterMapping(mappingBuilder); 142 | 143 | if (EntityDefinitions.TryGetValue(entityType, out definition)) 144 | { 145 | return definition; 146 | } 147 | 148 | throw new ArgumentException($"Registration of type \"{entityType}\" was skipped", nameof(entityType)); 149 | } 150 | finally 151 | { 152 | MappingLock.ExitWriteLock(); 153 | } 154 | } 155 | finally 156 | { 157 | MappingLock.ExitUpgradeableReadLock(); 158 | } 159 | } 160 | } 161 | -------------------------------------------------------------------------------- /src/MongoFramework/Infrastructure/Mapping/IMappingProcessor.cs: -------------------------------------------------------------------------------- 1 | namespace MongoFramework.Infrastructure.Mapping; 2 | 3 | public interface IMappingProcessor 4 | { 5 | void ApplyMapping(EntityDefinitionBuilder definitionBuilder); 6 | } 7 | -------------------------------------------------------------------------------- /src/MongoFramework/Infrastructure/Mapping/Processors/BsonKnownTypesProcessor.cs: -------------------------------------------------------------------------------- 1 | using System.Reflection; 2 | using MongoDB.Bson.Serialization.Attributes; 3 | 4 | namespace MongoFramework.Infrastructure.Mapping.Processors; 5 | 6 | public class BsonKnownTypesProcessor : IMappingProcessor 7 | { 8 | public void ApplyMapping(EntityDefinitionBuilder definitionBuilder) 9 | { 10 | var entityType = definitionBuilder.EntityType; 11 | var bsonKnownTypesAttribute = entityType.GetCustomAttribute(); 12 | if (bsonKnownTypesAttribute != null) 13 | { 14 | foreach (var type in bsonKnownTypesAttribute.KnownTypes) 15 | { 16 | definitionBuilder.MappingBuilder.Entity(type); 17 | } 18 | } 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /src/MongoFramework/Infrastructure/Mapping/Processors/CollectionNameProcessor.cs: -------------------------------------------------------------------------------- 1 | using System.ComponentModel.DataAnnotations.Schema; 2 | using System.Reflection; 3 | 4 | namespace MongoFramework.Infrastructure.Mapping.Processors; 5 | 6 | public class CollectionNameProcessor : IMappingProcessor 7 | { 8 | public void ApplyMapping(EntityDefinitionBuilder definitionBuilder) 9 | { 10 | var entityType = definitionBuilder.EntityType; 11 | var collectionName = entityType.Name; 12 | 13 | var tableAttribute = entityType.GetCustomAttribute(); 14 | 15 | if (tableAttribute == null && entityType.IsGenericType && entityType.GetGenericTypeDefinition() == typeof(EntityBucket<,>)) 16 | { 17 | var groupType = entityType.GetGenericArguments()[0]; 18 | tableAttribute = groupType.GetCustomAttribute(); 19 | if (tableAttribute == null) 20 | { 21 | collectionName = groupType.Name; 22 | } 23 | } 24 | 25 | if (tableAttribute != null) 26 | { 27 | if (string.IsNullOrEmpty(tableAttribute.Schema)) 28 | { 29 | collectionName = tableAttribute.Name; 30 | } 31 | else 32 | { 33 | collectionName = tableAttribute.Schema + "." + tableAttribute.Name; 34 | } 35 | } 36 | 37 | definitionBuilder.ToCollection(collectionName); 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /src/MongoFramework/Infrastructure/Mapping/Processors/EntityIdProcessor.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.ComponentModel.DataAnnotations; 3 | using System.Reflection; 4 | using MongoDB.Bson; 5 | 6 | namespace MongoFramework.Infrastructure.Mapping.Processors; 7 | 8 | public class EntityIdProcessor : IMappingProcessor 9 | { 10 | public void ApplyMapping(EntityDefinitionBuilder definitionBuilder) 11 | { 12 | foreach (var propertyBuilder in definitionBuilder.Properties) 13 | { 14 | if (Attribute.IsDefined(propertyBuilder.PropertyInfo, typeof(KeyAttribute))) 15 | { 16 | definitionBuilder.HasKey( 17 | propertyBuilder.PropertyInfo, 18 | AutoPickKeyGenerator 19 | ); 20 | return; 21 | } 22 | 23 | if (propertyBuilder.ElementName.Equals("id", StringComparison.InvariantCultureIgnoreCase)) 24 | { 25 | //We don't stop here just in case another property has the KeyAttribute 26 | //We preference the attribute over the name match 27 | definitionBuilder.HasKey( 28 | propertyBuilder.PropertyInfo, 29 | AutoPickKeyGenerator 30 | ); 31 | } 32 | } 33 | } 34 | 35 | private static void AutoPickKeyGenerator(EntityKeyBuilder keyBuilder) 36 | { 37 | var propertyType = keyBuilder.Property.PropertyType; 38 | if (propertyType == typeof(string)) 39 | { 40 | keyBuilder.HasKeyGenerator(EntityKeyGenerators.StringKeyGenerator); 41 | } 42 | else if (propertyType == typeof(Guid)) 43 | { 44 | keyBuilder.HasKeyGenerator(EntityKeyGenerators.GuidKeyGenerator); 45 | } 46 | else if (propertyType == typeof(ObjectId)) 47 | { 48 | keyBuilder.HasKeyGenerator(EntityKeyGenerators.ObjectIdKeyGenerator); 49 | } 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /src/MongoFramework/Infrastructure/Mapping/Processors/ExtraElementsProcessor.cs: -------------------------------------------------------------------------------- 1 | using System.Reflection; 2 | using MongoFramework.Attributes; 3 | 4 | namespace MongoFramework.Infrastructure.Mapping.Processors; 5 | 6 | public class ExtraElementsProcessor : IMappingProcessor 7 | { 8 | public void ApplyMapping(EntityDefinitionBuilder definitionBuilder) 9 | { 10 | var entityType = definitionBuilder.EntityType; 11 | 12 | //Ignore extra elements when the "IgnoreExtraElementsAttribute" is on the Entity 13 | var ignoreExtraElements = entityType.GetCustomAttribute(); 14 | if (ignoreExtraElements != null) 15 | { 16 | definitionBuilder.IgnoreExtraElements(); 17 | } 18 | else 19 | { 20 | //If any of the Entity's properties have the "ExtraElementsAttribute", use that 21 | foreach (var propertyBuilder in definitionBuilder.Properties) 22 | { 23 | var extraElementsAttribute = propertyBuilder.PropertyInfo.GetCustomAttribute(); 24 | if (extraElementsAttribute != null) 25 | { 26 | definitionBuilder.HasExtraElements(propertyBuilder.PropertyInfo); 27 | break; 28 | } 29 | } 30 | } 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /src/MongoFramework/Infrastructure/Mapping/Processors/HierarchyProcessor.cs: -------------------------------------------------------------------------------- 1 | namespace MongoFramework.Infrastructure.Mapping.Processors; 2 | 3 | public class HierarchyProcessor : IMappingProcessor 4 | { 5 | public void ApplyMapping(EntityDefinitionBuilder definitionBuilder) 6 | { 7 | var baseType = definitionBuilder.EntityType.BaseType; 8 | if (EntityMapping.IsValidTypeToMap(baseType)) 9 | { 10 | definitionBuilder.MappingBuilder.Entity(baseType); 11 | } 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /src/MongoFramework/Infrastructure/Mapping/Processors/MappingAdapterProcessor.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Reflection; 3 | using MongoFramework.Attributes; 4 | 5 | namespace MongoFramework.Infrastructure.Mapping.Processors; 6 | 7 | public class MappingAdapterProcessor : IMappingProcessor 8 | { 9 | public void ApplyMapping(EntityDefinitionBuilder definitionBuilder) 10 | { 11 | var adapterAttribute = definitionBuilder.EntityType.GetCustomAttribute(); 12 | if (adapterAttribute == null) 13 | { 14 | return; 15 | } 16 | 17 | var adapterType = adapterAttribute.MappingAdapter; 18 | if (!typeof(IMappingProcessor).IsAssignableFrom(adapterType)) 19 | { 20 | throw new InvalidOperationException($"Mapping adapter \"{adapterType}\" doesn't implement IMappingProcessor"); 21 | } 22 | 23 | var instance = (IMappingProcessor)Activator.CreateInstance(adapterType); 24 | instance?.ApplyMapping(definitionBuilder); 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /src/MongoFramework/Infrastructure/Mapping/Processors/NestedTypeProcessor.cs: -------------------------------------------------------------------------------- 1 | using MongoFramework.Infrastructure.Internal; 2 | 3 | namespace MongoFramework.Infrastructure.Mapping.Processors 4 | { 5 | public class NestedTypeProcessor : IMappingProcessor 6 | { 7 | public void ApplyMapping(EntityDefinitionBuilder definitionBuilder) 8 | { 9 | var entityType = definitionBuilder.EntityType; 10 | var properties = definitionBuilder.Properties; 11 | 12 | foreach (var property in properties) 13 | { 14 | var propertyType = property.PropertyInfo.PropertyType; 15 | propertyType = propertyType.UnwrapEnumerableTypes(); 16 | 17 | //Maps the property type for handling property nesting 18 | if (propertyType != entityType && EntityMapping.IsValidTypeToMap(propertyType)) 19 | { 20 | definitionBuilder.MappingBuilder.Entity(propertyType); 21 | } 22 | } 23 | } 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /src/MongoFramework/Infrastructure/Mapping/Processors/PropertyMappingProcessor.cs: -------------------------------------------------------------------------------- 1 | using System.ComponentModel.DataAnnotations.Schema; 2 | using System.Reflection; 3 | 4 | namespace MongoFramework.Infrastructure.Mapping.Processors 5 | { 6 | public class PropertyMappingProcessor : IMappingProcessor 7 | { 8 | public void ApplyMapping(EntityDefinitionBuilder definitionBuilder) 9 | { 10 | var entityType = definitionBuilder.EntityType; 11 | var properties = entityType.GetProperties(BindingFlags.Public | BindingFlags.Instance | BindingFlags.DeclaredOnly); 12 | 13 | foreach (var property in properties) 14 | { 15 | if (!property.CanRead || !property.CanWrite) 16 | { 17 | continue; 18 | } 19 | 20 | //Skip overridden properties 21 | var getMethod = property.GetMethod; 22 | if (property.GetMethod.IsVirtual && getMethod.GetBaseDefinition().DeclaringType != entityType) 23 | { 24 | continue; 25 | } 26 | 27 | //Skip indexer properties (eg. "this[int index]") 28 | if (property.GetIndexParameters().Length > 0) 29 | { 30 | continue; 31 | } 32 | 33 | //Skip properties with the "NotMappedAttribute" 34 | var notMappedAttribute = property.GetCustomAttribute(); 35 | if (notMappedAttribute != null) 36 | { 37 | continue; 38 | } 39 | 40 | definitionBuilder.HasProperty(property, builder => 41 | { 42 | var elementName = property.Name; 43 | 44 | //Set custom element name with the "ColumnAttribute" 45 | var columnAttribute = property.GetCustomAttribute(); 46 | if (columnAttribute != null) 47 | { 48 | elementName = columnAttribute.Name; 49 | } 50 | 51 | builder.HasElementName(elementName); 52 | }); 53 | } 54 | } 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /src/MongoFramework/Infrastructure/Mapping/Processors/SkipMappingProcessor.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.ComponentModel.DataAnnotations.Schema; 3 | 4 | namespace MongoFramework.Infrastructure.Mapping.Processors; 5 | 6 | public class SkipMappingProcessor : IMappingProcessor 7 | { 8 | public void ApplyMapping(EntityDefinitionBuilder definitionBuilder) 9 | { 10 | var entityType = definitionBuilder.EntityType; 11 | 12 | if (Attribute.IsDefined(entityType, typeof(NotMappedAttribute), true)) 13 | { 14 | definitionBuilder.SkipMapping(true); 15 | } 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /src/MongoFramework/Infrastructure/Mapping/PropertyPath.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Linq.Expressions; 5 | using System.Reflection; 6 | using MongoFramework.Infrastructure.Internal; 7 | using MongoFramework.Infrastructure.Linq; 8 | 9 | namespace MongoFramework.Infrastructure.Mapping; 10 | 11 | public readonly record struct PropertyPath(IReadOnlyList Properties) 12 | { 13 | /// 14 | /// Returns the entity types found through the property path. 15 | /// 16 | /// 17 | public IEnumerable GetEntityTypes() 18 | { 19 | foreach (var property in Properties) 20 | { 21 | var possibleEntityType = property.PropertyType.UnwrapEnumerableTypes(); 22 | if (EntityMapping.IsValidTypeToMap(possibleEntityType)) 23 | { 24 | yield return possibleEntityType; 25 | } 26 | } 27 | } 28 | 29 | public bool Contains(PropertyInfo propertyInfo) => Properties.Contains(propertyInfo); 30 | 31 | /// 32 | /// Returns a based on the resolved properties through the . 33 | /// 34 | /// 35 | /// 36 | /// For example, take the expression body: v.Thing.Items.First().Name
37 | /// We want [Thing, Items, Name] but the expression is actually: Name.First().Items.Thing.v
38 | /// This is also expressed as [MemberExpression, MethodCallExpression, MemberExpression, MemberExpression, ParameterExpression]. 39 | ///
40 | /// This is why we have a stack (for our result to be the "correct" order) and we exit on . 41 | ///
42 | /// 43 | /// 44 | /// 45 | public static PropertyPath FromExpression(Expression pathExpression) 46 | { 47 | var propertyInfoChain = new Stack(); 48 | var current = pathExpression; 49 | 50 | while (current is not ParameterExpression) 51 | { 52 | if (current is MemberExpression memberExpression && memberExpression.Member is PropertyInfo propertyInfo) 53 | { 54 | propertyInfoChain.Push(propertyInfo); 55 | current = memberExpression.Expression; 56 | } 57 | else if (current is MethodCallExpression methodExpression) 58 | { 59 | var genericMethodDefinition = methodExpression.Method.GetGenericMethodDefinition(); 60 | if (genericMethodDefinition == MethodInfoCache.Enumerable.First_1 || genericMethodDefinition == MethodInfoCache.Enumerable.Single_1) 61 | { 62 | var callerExpression = methodExpression.Arguments[0]; 63 | current = callerExpression; 64 | } 65 | else 66 | { 67 | throw new ArgumentException($"Invalid method \"{methodExpression.Method.Name}\". Only \"Enumerable.First()\" and \"Enumerable.Single()\" methods are allowed in chained expressions", nameof(pathExpression)); 68 | } 69 | 70 | } 71 | else if (current is UnaryExpression unaryExpression && unaryExpression.NodeType == ExpressionType.Convert) 72 | { 73 | current = unaryExpression.Operand; 74 | } 75 | else 76 | { 77 | throw new ArgumentException($"Unexpected expression \"{current}\" when processing chained expression", nameof(pathExpression)); 78 | } 79 | } 80 | 81 | return new(propertyInfoChain.ToArray()); 82 | } 83 | 84 | /// 85 | /// Returns a based on the resolved properties (by name) through the provided string. 86 | /// 87 | /// 88 | /// For example, take this string: Thing.Items.Name
89 | /// This would be resolved as [Thing, Items, Name] including going through any array/enumerable that might exist. 90 | ///
91 | /// 92 | /// 93 | public static PropertyPath FromString(Type baseType, string propertyPath) 94 | { 95 | var inputChain = propertyPath.Split('.'); 96 | var propertyInfoChain = new PropertyInfo[inputChain.Length]; 97 | 98 | var currentType = baseType; 99 | for (var i = 0; i < inputChain.Length; i++) 100 | { 101 | var propertyName = inputChain[i]; 102 | var property = currentType.GetProperty(propertyName) ?? throw new ArgumentException($"Property \"{propertyName}\" is not found on reflected entity types", nameof(propertyPath)); 103 | propertyInfoChain[i] = property; 104 | 105 | var propertyType = property.PropertyType.UnwrapEnumerableTypes(); 106 | currentType = propertyType; 107 | } 108 | 109 | return new(propertyInfoChain); 110 | } 111 | } 112 | -------------------------------------------------------------------------------- /src/MongoFramework/Infrastructure/Mapping/PropertyTraversalExtensions.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | using System; 3 | using MongoFramework.Infrastructure.Internal; 4 | using System.Linq; 5 | using System.Buffers; 6 | using System.Diagnostics; 7 | 8 | namespace MongoFramework.Infrastructure.Mapping; 9 | 10 | [DebuggerDisplay("{DebuggerDisplay,nq}")] 11 | public sealed record TraversedProperty 12 | { 13 | private static readonly string ElementSeparator = "."; 14 | 15 | public TraversedProperty Parent { get; init; } 16 | public PropertyDefinition Property { get; init; } 17 | public int Depth { get; init; } 18 | 19 | public string GetPath() 20 | { 21 | if (Depth == 0) 22 | { 23 | return Property.ElementName; 24 | } 25 | 26 | var pool = ArrayPool.Shared.Rent(Depth + 1); 27 | try 28 | { 29 | var current = this; 30 | for (var i = Depth; i >= 0; i--) 31 | { 32 | pool[i] = current.Property.ElementName; 33 | current = current.Parent; 34 | } 35 | 36 | return string.Join(ElementSeparator, pool, 0, Depth + 1); 37 | } 38 | finally 39 | { 40 | ArrayPool.Shared.Return(pool); 41 | } 42 | } 43 | 44 | [DebuggerNonUserCode] 45 | private string DebuggerDisplay => $"Property = {Property.ElementName}, Parent = {Parent?.Property?.ElementName}, Depth = {Depth}"; 46 | } 47 | 48 | public static class PropertyTraversalExtensions 49 | { 50 | private readonly record struct TraversalState 51 | { 52 | public HashSet SeenTypes { get; init; } 53 | public IEnumerable Properties { get; init; } 54 | } 55 | 56 | public static IEnumerable TraverseProperties(this EntityDefinition definition) 57 | { 58 | var stack = new Stack(); 59 | stack.Push(new TraversalState 60 | { 61 | SeenTypes = new HashSet { definition.EntityType }, 62 | Properties = definition.GetAllProperties().Select(p => new TraversedProperty 63 | { 64 | Property = p, 65 | Depth = 0 66 | }) 67 | }); 68 | 69 | while (stack.Count > 0) 70 | { 71 | var state = stack.Pop(); 72 | foreach (var traversedProperty in state.Properties) 73 | { 74 | yield return traversedProperty; 75 | 76 | var propertyType = traversedProperty.Property.PropertyInfo.PropertyType; 77 | propertyType = propertyType.UnwrapEnumerableTypes(); 78 | 79 | if (EntityMapping.IsValidTypeToMap(propertyType) && !state.SeenTypes.Contains(propertyType)) 80 | { 81 | var nestedProperties = EntityMapping.GetOrCreateDefinition(propertyType) 82 | .GetAllProperties() 83 | .Select(p => new TraversedProperty 84 | { 85 | Parent = traversedProperty, 86 | Property = p, 87 | Depth = traversedProperty.Depth + 1 88 | }); 89 | 90 | stack.Push(new TraversalState 91 | { 92 | SeenTypes = new HashSet(state.SeenTypes) 93 | { 94 | propertyType 95 | }, 96 | Properties = nestedProperties 97 | }); 98 | } 99 | } 100 | } 101 | } 102 | } -------------------------------------------------------------------------------- /src/MongoFramework/Infrastructure/Serialization/TypeDiscoverySerializationProvider.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using MongoDB.Bson.Serialization; 3 | using MongoFramework.Attributes; 4 | using MongoFramework.Utilities; 5 | 6 | namespace MongoFramework.Infrastructure.Serialization 7 | { 8 | public class TypeDiscoverySerializationProvider : BsonSerializationProviderBase 9 | { 10 | public static TypeDiscoverySerializationProvider Instance { get; } = new TypeDiscoverySerializationProvider(); 11 | 12 | public override IBsonSerializer GetSerializer(Type type, IBsonSerializerRegistry serializerRegistry) 13 | { 14 | Check.NotNull(type, nameof(type)); 15 | 16 | if (Attribute.IsDefined(type, typeof(RuntimeTypeDiscoveryAttribute)) || type == typeof(object)) 17 | { 18 | var serializerType = typeof(TypeDiscoverySerializer<>).MakeGenericType(type); 19 | return (IBsonSerializer)Activator.CreateInstance(serializerType); 20 | } 21 | 22 | return null; 23 | } 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /src/MongoFramework/IsExternalInit.cs: -------------------------------------------------------------------------------- 1 | using System.ComponentModel; 2 | 3 | namespace System.Runtime.CompilerServices 4 | { 5 | /// 6 | /// Reserved to be used by the compiler for tracking metadata. 7 | /// This class should not be used by developers in source code. 8 | /// 9 | [EditorBrowsable(EditorBrowsableState.Never)] 10 | internal static class IsExternalInit 11 | { 12 | } 13 | } -------------------------------------------------------------------------------- /src/MongoFramework/Linq/ExpressionExtensions.cs: -------------------------------------------------------------------------------- 1 | // 2 | // source: https://stackoverflow.com/questions/457316/combining-two-expressions-expressionfunct-bool 3 | // 4 | using System; 5 | using System.Linq.Expressions; 6 | 7 | namespace MongoFramework.Linq 8 | { 9 | public static class ExpressionExtensions 10 | { 11 | public static Expression> AndAlso(this Expression> expr1, Expression> expr2) 12 | { 13 | var parameter1 = expr1.Parameters[0]; 14 | var visitor = new ReplaceParameterVisitor(expr2.Parameters[0], parameter1); 15 | var body2WithParam1 = visitor.Visit(expr2.Body); 16 | return Expression.Lambda>(Expression.AndAlso(expr1.Body, body2WithParam1), parameter1); 17 | } 18 | 19 | private class ReplaceParameterVisitor : ExpressionVisitor 20 | { 21 | private readonly ParameterExpression _oldParameter; 22 | private readonly ParameterExpression _newParameter; 23 | 24 | public ReplaceParameterVisitor(ParameterExpression oldParameter, ParameterExpression newParameter) 25 | { 26 | _oldParameter = oldParameter; 27 | _newParameter = newParameter; 28 | } 29 | 30 | protected override Expression VisitParameter(ParameterExpression node) 31 | { 32 | if (ReferenceEquals(node, _oldParameter)) 33 | { 34 | return _newParameter; 35 | } 36 | 37 | return base.VisitParameter(node); 38 | } 39 | } 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /src/MongoFramework/MappingBuilder.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using MongoFramework.Infrastructure.Mapping; 4 | 5 | namespace MongoFramework; 6 | 7 | public class MappingBuilder 8 | { 9 | private readonly IEnumerable mappingConventions; 10 | private readonly List builders = new(); 11 | 12 | public IReadOnlyList Definitions => builders; 13 | 14 | public MappingBuilder(IEnumerable mappingProcessors) 15 | { 16 | mappingConventions = mappingProcessors; 17 | } 18 | 19 | private bool TryGetBuilder(Type entityType, out EntityDefinitionBuilder builder) 20 | { 21 | builder = builders.Find(b => b.EntityType == entityType); 22 | return builder != null; 23 | } 24 | 25 | private void UpdateBuilder(Type entityType, EntityDefinitionBuilder builder) 26 | { 27 | var index = builders.FindIndex(b => b.EntityType == entityType); 28 | builders[index] = builder; 29 | } 30 | 31 | private void ApplyMappingConventions(EntityDefinitionBuilder definitionBuilder) 32 | { 33 | foreach (var processor in mappingConventions) 34 | { 35 | processor.ApplyMapping(definitionBuilder); 36 | } 37 | } 38 | 39 | public EntityDefinitionBuilder Entity(Type entityType) 40 | { 41 | if (!TryGetBuilder(entityType, out var definitionBuilder)) 42 | { 43 | definitionBuilder = new EntityDefinitionBuilder(entityType, this); 44 | builders.Add(definitionBuilder); 45 | ApplyMappingConventions(definitionBuilder); 46 | } 47 | 48 | return definitionBuilder; 49 | } 50 | 51 | public EntityDefinitionBuilder Entity() 52 | { 53 | if (!TryGetBuilder(typeof(TEntity), out var definitionBuilder)) 54 | { 55 | definitionBuilder = new EntityDefinitionBuilder(this); 56 | builders.Add(definitionBuilder); 57 | ApplyMappingConventions(definitionBuilder); 58 | } 59 | 60 | //Allow upgrading from non-generic entity definition 61 | if (definitionBuilder is not EntityDefinitionBuilder) 62 | { 63 | definitionBuilder = EntityDefinitionBuilder.CreateFrom(definitionBuilder); 64 | UpdateBuilder(typeof(TEntity), definitionBuilder); 65 | } 66 | 67 | return definitionBuilder as EntityDefinitionBuilder; 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /src/MongoFramework/MongoDbBucketSet.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections; 3 | using System.Collections.Generic; 4 | using System.Linq; 5 | using System.Linq.Expressions; 6 | using System.Threading; 7 | using System.Threading.Tasks; 8 | using MongoFramework.Infrastructure.Commands; 9 | using MongoFramework.Infrastructure.Mapping; 10 | using MongoFramework.Utilities; 11 | 12 | namespace MongoFramework 13 | { 14 | public class MongoDbBucketSet : IMongoDbBucketSet 15 | where TGroup : class 16 | where TSubEntity : class 17 | { 18 | private IMongoDbContext Context { get; } 19 | 20 | internal int BucketSize { get; } 21 | 22 | internal PropertyDefinition EntityTimeProperty { get; } 23 | 24 | public MongoDbBucketSet(IMongoDbContext context, IDbSetOptions options) 25 | { 26 | Check.NotNull(context, nameof(context)); 27 | Context = context; 28 | 29 | if (options is BucketSetOptions bucketOptions) 30 | { 31 | if (bucketOptions.BucketSize < 1) 32 | { 33 | throw new ArgumentException($"Invalid bucket size of {bucketOptions.BucketSize}"); 34 | } 35 | BucketSize = bucketOptions.BucketSize; 36 | 37 | var property = EntityMapping.GetOrCreateDefinition(typeof(TSubEntity)).GetProperty(bucketOptions.EntityTimeProperty); 38 | if (property == null) 39 | { 40 | throw new ArgumentException($"Property {bucketOptions.EntityTimeProperty} doesn't exist on bucket item."); 41 | } 42 | 43 | if (property.PropertyInfo.PropertyType != typeof(DateTime)) 44 | { 45 | throw new ArgumentException($"Property {bucketOptions.EntityTimeProperty} on bucket item isn't of type DateTime"); 46 | } 47 | 48 | EntityTimeProperty = property; 49 | } 50 | else 51 | { 52 | throw new ArgumentException("Invalid DbSet options supplied", nameof(options)); 53 | } 54 | } 55 | 56 | public virtual void Add(TGroup group, TSubEntity entity) 57 | { 58 | Check.NotNull(group, nameof(group)); 59 | Check.NotNull(entity, nameof(entity)); 60 | 61 | Context.CommandStaging.Add(new AddToBucketCommand(group, entity, EntityTimeProperty, BucketSize)); 62 | } 63 | 64 | public virtual void AddRange(TGroup group, IEnumerable entities) 65 | { 66 | Check.NotNull(group, nameof(group)); 67 | Check.NotNull(entities, nameof(entities)); 68 | 69 | foreach (var entity in entities) 70 | { 71 | Context.CommandStaging.Add(new AddToBucketCommand(group, entity, EntityTimeProperty, BucketSize)); 72 | } 73 | } 74 | 75 | public virtual void Remove(TGroup group) 76 | { 77 | Check.NotNull(group, nameof(group)); 78 | 79 | Context.CommandStaging.Add(new RemoveBucketCommand(group)); 80 | } 81 | 82 | public virtual IQueryable WithGroup(TGroup group) 83 | { 84 | return GetQueryable() 85 | .Where(e => e.Group == group) 86 | .OrderBy(e => e.Min) 87 | .SelectMany(e => e.Items); 88 | } 89 | 90 | public virtual IQueryable Groups() 91 | { 92 | return GetQueryable() 93 | .Select(e => e.Group) 94 | .Distinct(); 95 | } 96 | 97 | [Obsolete("Use SaveChanges on the IMongoDbContext")] 98 | public void SaveChanges() 99 | { 100 | Context.SaveChanges(); 101 | } 102 | 103 | [Obsolete("Use SaveChangesAsync on the IMongoDbContext")] 104 | public async Task SaveChangesAsync(CancellationToken cancellationToken = default) 105 | { 106 | await Context.SaveChangesAsync(cancellationToken); 107 | } 108 | 109 | #region IQueryable Implementation 110 | 111 | private IQueryable> GetQueryable() 112 | { 113 | return Context.Query>(); 114 | } 115 | 116 | public Type ElementType => GetQueryable().ElementType; 117 | 118 | public Expression Expression => GetQueryable().Expression; 119 | 120 | public IQueryProvider Provider => GetQueryable().Provider; 121 | 122 | public IEnumerator> GetEnumerator() 123 | { 124 | return GetQueryable().GetEnumerator(); 125 | } 126 | 127 | IEnumerator IEnumerable.GetEnumerator() 128 | { 129 | return GetEnumerator(); 130 | } 131 | 132 | #endregion 133 | } 134 | } 135 | -------------------------------------------------------------------------------- /src/MongoFramework/MongoDbConnection.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using MongoDB.Driver; 3 | using MongoFramework.Infrastructure.Diagnostics; 4 | using MongoFramework.Utilities; 5 | 6 | namespace MongoFramework 7 | { 8 | public class MongoDbConnection : IMongoDbConnection 9 | { 10 | public MongoUrl Url { get; protected set; } 11 | private bool IsDisposed { get; set; } 12 | 13 | private Action ConfigureSettings { get; init; } 14 | 15 | private IMongoClient InternalClient; 16 | public IMongoClient Client 17 | { 18 | get 19 | { 20 | if (IsDisposed) 21 | { 22 | throw new ObjectDisposedException(nameof(MongoDbConnection)); 23 | } 24 | 25 | if (InternalClient == null) 26 | { 27 | var settings = MongoClientSettings.FromUrl(Url); 28 | ConfigureSettings?.Invoke(settings); 29 | settings.LinqProvider = MongoDB.Driver.Linq.LinqProvider.V2; 30 | InternalClient = new MongoClient(settings); 31 | } 32 | 33 | return InternalClient; 34 | } 35 | } 36 | 37 | public IDiagnosticListener DiagnosticListener { get; set; } = new NoOpDiagnosticListener(); 38 | 39 | public static MongoDbConnection FromUrl(MongoUrl mongoUrl) => FromUrl(mongoUrl, configureSettings: null); 40 | public static MongoDbConnection FromUrl(MongoUrl mongoUrl, Action configureSettings) 41 | { 42 | Check.NotNull(mongoUrl, nameof(mongoUrl)); 43 | 44 | return new MongoDbConnection 45 | { 46 | Url = mongoUrl, 47 | ConfigureSettings = configureSettings 48 | }; 49 | } 50 | 51 | public static MongoDbConnection FromConnectionString(string connectionString) => FromConnectionString(connectionString, configureSettings: null); 52 | public static MongoDbConnection FromConnectionString(string connectionString, Action configureSettings) => FromUrl(new MongoUrl(connectionString), configureSettings); 53 | 54 | public IMongoDatabase GetDatabase() 55 | { 56 | if (IsDisposed) 57 | { 58 | throw new ObjectDisposedException(nameof(MongoDbConnection)); 59 | } 60 | 61 | return Client.GetDatabase(Url.DatabaseName); 62 | } 63 | 64 | public void Dispose() 65 | { 66 | Dispose(true); 67 | GC.SuppressFinalize(this); 68 | } 69 | 70 | protected virtual void Dispose(bool disposing) 71 | { 72 | if (IsDisposed) 73 | { 74 | return; 75 | } 76 | 77 | if (disposing) 78 | { 79 | InternalClient = null; 80 | IsDisposed = true; 81 | } 82 | } 83 | 84 | ~MongoDbConnection() 85 | { 86 | Dispose(false); 87 | } 88 | } 89 | } 90 | -------------------------------------------------------------------------------- /src/MongoFramework/MongoDbTenantContext.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | using MongoFramework.Infrastructure.Commands; 3 | using MongoFramework.Utilities; 4 | 5 | namespace MongoFramework 6 | { 7 | public class MongoDbTenantContext : MongoDbContext, IMongoDbTenantContext 8 | { 9 | public virtual string TenantId { get; } 10 | 11 | public MongoDbTenantContext(IMongoDbConnection connection, string tenantId) : base(connection) 12 | { 13 | Check.NotNull(tenantId, nameof(tenantId)); 14 | TenantId = tenantId; 15 | } 16 | 17 | protected override void AfterDetectChanges() 18 | { 19 | ChangeTracker.EnforceMultiTenant(TenantId); 20 | } 21 | 22 | protected override WriteModelOptions GetWriteModelOptions() 23 | { 24 | return new WriteModelOptions { TenantId = TenantId }; 25 | } 26 | 27 | public virtual void CheckEntity(IHaveTenantId entity) 28 | { 29 | Check.NotNull(entity, nameof(entity)); 30 | 31 | if (entity.TenantId != TenantId) 32 | { 33 | throw new MultiTenantException($"Entity type {entity.GetType().Name}, tenant ID does not match. Expected: {TenantId}, Entity has: {entity.TenantId}"); 34 | } 35 | } 36 | 37 | public virtual void CheckEntities(IEnumerable entities) 38 | { 39 | Check.NotNull(entities, nameof(entities)); 40 | 41 | foreach (var entity in entities) 42 | { 43 | CheckEntity(entity); 44 | } 45 | } 46 | 47 | /// 48 | /// Marks the entity as unchanged in the change tracker and starts tracking. 49 | /// 50 | /// 51 | public override void Attach(TEntity entity) where TEntity : class 52 | { 53 | if (typeof(IHaveTenantId).IsAssignableFrom(typeof(TEntity))) 54 | { 55 | CheckEntity(entity as IHaveTenantId); 56 | } 57 | base.Attach(entity); 58 | } 59 | 60 | /// 61 | /// Marks the collection of entities as unchanged in the change tracker and starts tracking. 62 | /// 63 | /// 64 | public override void AttachRange(IEnumerable entities) where TEntity : class 65 | { 66 | if (typeof(IHaveTenantId).IsAssignableFrom(typeof(TEntity))) 67 | { 68 | CheckEntities(entities as IEnumerable); 69 | } 70 | base.AttachRange(entities); 71 | } 72 | 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /src/MongoFramework/MongoDbUtility.cs: -------------------------------------------------------------------------------- 1 | using MongoDB.Bson; 2 | 3 | namespace MongoFramework 4 | { 5 | public static class MongoDbUtility 6 | { 7 | /// 8 | /// Checks whether the provided string matches the 24-character hexadecimal format of an ObjectId 9 | /// 10 | /// 11 | /// 12 | public static bool IsValidObjectId(string id) 13 | { 14 | return ObjectId.TryParse(id, out ObjectId result); 15 | } 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /src/MongoFramework/MongoFramework.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | netstandard2.0;netstandard2.1;net6.0 5 | MongoFramework 6 | MongoFramework 7 | An "Entity Framework"-like interface for the MongoDB C# Driver 8 | $(PackageBaseTags) 9 | James Turner 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | -------------------------------------------------------------------------------- /src/MongoFramework/MultiTenantException.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace MongoFramework 4 | { 5 | public class MultiTenantException : Exception 6 | { 7 | public MultiTenantException(string message, Exception innerException = null) 8 | : base(message, innerException) { } 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /src/MongoFramework/Utilities/Check.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) .NET Foundation. All rights reserved. 2 | // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. 3 | // https://github.com/dotnet/efcore/blob/main/src/Shared/Check.cs 4 | 5 | using System; 6 | using System.Collections.Generic; 7 | using System.Diagnostics; 8 | using System.Linq; 9 | 10 | namespace MongoFramework.Utilities 11 | { 12 | [DebuggerStepThrough] 13 | public static class Check 14 | { 15 | public static T NotNull(T value, string parameterName) 16 | { 17 | if (value is null) 18 | { 19 | NotEmpty(parameterName, nameof(parameterName)); 20 | throw new ArgumentNullException(parameterName); 21 | } 22 | 23 | return value; 24 | } 25 | 26 | public static IReadOnlyList NotEmpty(IReadOnlyList value, string parameterName) 27 | { 28 | NotNull(value, parameterName); 29 | 30 | if (value.Count == 0) 31 | { 32 | NotEmpty(parameterName, nameof(parameterName)); 33 | 34 | throw new ArgumentException($"The collection argument '{parameterName}' must contain at least one element."); 35 | } 36 | 37 | return value; 38 | } 39 | 40 | public static string NotEmpty(string value, string parameterName) 41 | { 42 | Exception e = null; 43 | if (value is null) 44 | { 45 | e = new ArgumentNullException(parameterName); 46 | } 47 | else if (value.Trim().Length == 0) 48 | { 49 | e = new ArgumentException($"The string argument '{parameterName}' cannot be empty."); 50 | } 51 | 52 | if (e != null) 53 | { 54 | NotEmpty(parameterName, nameof(parameterName)); 55 | 56 | throw e; 57 | } 58 | 59 | return value; 60 | } 61 | 62 | public static string NullButNotEmpty(string value, string parameterName) 63 | { 64 | if (!(value is null) 65 | && value.Length == 0) 66 | { 67 | NotEmpty(parameterName, nameof(parameterName)); 68 | 69 | throw new ArgumentException("The string argument '{argumentName}' cannot be empty."); 70 | } 71 | 72 | return value; 73 | } 74 | 75 | public static IReadOnlyList HasNoNulls(IReadOnlyList value, string parameterName) 76 | where T : class 77 | { 78 | NotNull(value, parameterName); 79 | 80 | if (value.Any(e => e == null)) 81 | { 82 | NotEmpty(parameterName, nameof(parameterName)); 83 | 84 | throw new ArgumentException(parameterName); 85 | } 86 | 87 | return value; 88 | } 89 | 90 | public static IReadOnlyList HasNoEmptyElements( 91 | IReadOnlyList value, 92 | string parameterName) 93 | { 94 | NotNull(value, parameterName); 95 | 96 | if (value.Any(s => string.IsNullOrWhiteSpace(s))) 97 | { 98 | NotEmpty(parameterName, nameof(parameterName)); 99 | 100 | throw new ArgumentException($"The collection argument '{parameterName}' must not contain any empty elements."); 101 | } 102 | 103 | return value; 104 | } 105 | 106 | [Conditional("DEBUG")] 107 | public static void DebugAssert(bool condition, string message) 108 | { 109 | if (!condition) 110 | { 111 | throw new Exception($"Check.DebugAssert failed: {message}"); 112 | } 113 | } 114 | } 115 | } 116 | -------------------------------------------------------------------------------- /tests/Directory.Build.props: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Latest 5 | 6 | 7 | -------------------------------------------------------------------------------- /tests/MongoFramework.Benchmarks/BenchmarkDb.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using MongoDB.Driver; 3 | 4 | namespace MongoFramework.Benchmarks 5 | { 6 | static class BenchmarkDb 7 | { 8 | public static string ConnectionString => Environment.GetEnvironmentVariable("MONGODB_URI") ?? "mongodb://localhost"; 9 | 10 | public static string GetDatabaseName() 11 | { 12 | return "MongoFrameworkBenchmarks"; 13 | } 14 | 15 | public static IMongoDbConnection GetConnection() 16 | { 17 | var urlBuilder = new MongoUrlBuilder(ConnectionString) 18 | { 19 | DatabaseName = GetDatabaseName() 20 | }; 21 | return MongoDbConnection.FromUrl(urlBuilder.ToMongoUrl()); 22 | } 23 | 24 | public static void DropDatabase() 25 | { 26 | GetConnection().Client.DropDatabase(GetDatabaseName()); 27 | } 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /tests/MongoFramework.Benchmarks/Infrastructure/DefinitionHelpers/UpdateDefinitionHelperBenchmark.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using BenchmarkDotNet.Attributes; 4 | using BenchmarkDotNet.Jobs; 5 | using MongoDB.Bson; 6 | using MongoDB.Driver; 7 | using MongoFramework.Infrastructure.DefinitionHelpers; 8 | 9 | namespace MongoFramework.Benchmarks.Infrastructure.DefinitionHelpers 10 | { 11 | [SimpleJob(RuntimeMoniker.Net50), MemoryDiagnoser] 12 | public class UpdateDefinitionHelperBenchmark 13 | { 14 | [Benchmark] 15 | public UpdateDefinition Benchmark() 16 | { 17 | return UpdateDefinitionHelper.CreateFromDiff( 18 | new BsonDocument(new Dictionary 19 | { 20 | {"Age", 20}, 21 | {"Name", "Peter"}, 22 | {"FavouriteNumber", 8}, 23 | {"RegisteredDate", new DateTime(2017, 10, 1)}, 24 | {"FavouriteDate", new DateTime(2019, 8, 1)}, 25 | {"SomeBoolean", true}, 26 | {"RelatedIds", new int[] { 1, 3, 5, 7 }}, 27 | {"Status", "Active"}, 28 | {"DescriptionOne", null}, 29 | {"DescriptionTwo", "Hello World"}, 30 | {"ExclusivePropertyToFirst", "Hello World"} 31 | }), 32 | new BsonDocument(new Dictionary 33 | { 34 | {"Age", 21}, 35 | {"Name", "Simon"}, 36 | {"FavouriteNumber", 8}, 37 | {"RegisteredDate", new DateTime(2017, 8, 1)}, 38 | {"FavouriteDate", new DateTime(2019, 8, 1)}, 39 | {"SomeBoolean", false}, 40 | {"RelatedIds", new int[] { 1, 3, 6, 7 }}, 41 | {"MoreNumbers", new int[] { 1, 3, 5, 7, 9 }}, 42 | {"Status", "Active"}, 43 | {"DescriptionOne", "Hello World"}, 44 | {"DescriptionTwo", null}, 45 | {"ExclusivePropertyToSecond", "Hello World"} 46 | }) 47 | ); 48 | } 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /tests/MongoFramework.Benchmarks/Infrastructure/Indexing/IndexModelBuilderBenchmark.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using BenchmarkDotNet.Attributes; 5 | using BenchmarkDotNet.Jobs; 6 | using MongoFramework.Attributes; 7 | using MongoFramework.Infrastructure.Indexing; 8 | using MongoFramework.Infrastructure.Mapping; 9 | 10 | namespace MongoFramework.Benchmarks.Infrastructure.Indexing 11 | { 12 | [SimpleJob(RuntimeMoniker.Net50), MemoryDiagnoser] 13 | public class IndexModelBuilderBenchmark 14 | { 15 | public class FlatIndexModel 16 | { 17 | [Index(IndexSortOrder.Ascending)] 18 | public string NoNameIndex { get; set; } 19 | [Index("MyCustomIndexName", IndexSortOrder.Ascending)] 20 | public string NamedIndex { get; set; } 21 | 22 | [Index("MyCompoundIndex", IndexSortOrder.Ascending, IndexPriority = 1)] 23 | public string FirstPriority { get; set; } 24 | 25 | [Index("MyCompoundIndex", IndexSortOrder.Ascending, IndexPriority = 3)] 26 | public string ThirdPriority { get; set; } 27 | 28 | [Index("MyCompoundIndex", IndexSortOrder.Ascending, IndexPriority = 2)] 29 | public string SecondPriority { get; set; } 30 | } 31 | 32 | public class NestedIndexParentModel 33 | { 34 | [Index(IndexSortOrder.Ascending)] 35 | public string NoNameIndex { get; set; } 36 | [Index("MyCustomIndexName", IndexSortOrder.Ascending)] 37 | public string NamedIndex { get; set; } 38 | public IEnumerable ChildEnumerable { get; set; } 39 | public NestedIndexChildModel[] ChildArray { get; set; } 40 | public List ChildList { get; set; } 41 | } 42 | public class NestedIndexChildModel 43 | { 44 | [Index(IndexSortOrder.Ascending)] 45 | public string ChildId { get; set; } 46 | } 47 | 48 | [GlobalSetup] 49 | public void Setup() 50 | { 51 | EntityMapping.RemoveAllDefinitions(); 52 | MongoDbDriverHelper.ResetDriver(); 53 | EntityMapping.RegisterType(typeof(FlatIndexModel)); 54 | EntityMapping.RegisterType(typeof(NestedIndexParentModel)); 55 | } 56 | 57 | [Benchmark] 58 | public void FlatModel() 59 | { 60 | IndexModelBuilder.BuildModel().ToArray(); 61 | } 62 | 63 | [Benchmark] 64 | public void NestedModel() 65 | { 66 | IndexModelBuilder.BuildModel().ToArray(); 67 | } 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /tests/MongoFramework.Benchmarks/Infrastructure/Internal/GenericMethodInvokeBenchmark.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Linq; 3 | using System.Reflection; 4 | using BenchmarkDotNet.Attributes; 5 | using BenchmarkDotNet.Jobs; 6 | using MongoFramework.Infrastructure.Internal; 7 | 8 | namespace MongoFramework.Benchmarks.Infrastructure.Internal 9 | { 10 | [ShortRunJob(RuntimeMoniker.Net50), MemoryDiagnoser] 11 | public class GenericMethodInvokeBenchmark 12 | { 13 | [Benchmark] 14 | public string DirectReflection() 15 | { 16 | var baseMethod = typeof(GenericMethodInvokeBenchmark) 17 | .GetMethods(BindingFlags.NonPublic | BindingFlags.Static) 18 | .Where(m => m.IsGenericMethod && m.Name == nameof(MyGenericMethod)) 19 | .FirstOrDefault(); 20 | var method = baseMethod.MakeGenericMethod(typeof(int)); 21 | return method.Invoke(null, new object[] { typeof(int), "1" }) as string; 22 | } 23 | 24 | [Benchmark] 25 | public string GenericHelper() 26 | { 27 | var method = GenericsHelper.GetMethodDelegate>( 28 | typeof(GenericMethodInvokeBenchmark), 29 | nameof(MyGenericMethod), 30 | typeof(int) 31 | ); 32 | 33 | return method(typeof(int), "1"); 34 | } 35 | 36 | private static string MyGenericMethod(Type type, string output) 37 | { 38 | if (type == typeof(TType)) 39 | { 40 | return output; 41 | } 42 | 43 | throw new Exception("Types don't match"); 44 | } 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /tests/MongoFramework.Benchmarks/Infrastructure/Linq/LinqBenchmark.cs: -------------------------------------------------------------------------------- 1 | using System.Linq; 2 | using BenchmarkDotNet.Attributes; 3 | using BenchmarkDotNet.Jobs; 4 | using MongoFramework.Infrastructure.Linq; 5 | using MongoFramework.Infrastructure.Mapping; 6 | 7 | namespace MongoFramework.Benchmarks.Infrastructure.Linq 8 | { 9 | [SimpleJob(RuntimeMoniker.Net50), MemoryDiagnoser] 10 | public class LinqBenchmark 11 | { 12 | private IMongoDbConnection Connection { get; set; } 13 | 14 | public class TestModel 15 | { 16 | public string Id { get; set; } 17 | } 18 | 19 | [GlobalSetup] 20 | public void Setup() 21 | { 22 | EntityMapping.TryRegisterType(typeof(TestModel), out _); 23 | Connection = BenchmarkDb.GetConnection(); 24 | 25 | //Pre-JIT the benchmarks to avoid issues with benchmark 26 | FirstOrDefault(); 27 | ToArray(); 28 | Count(); 29 | Any(); 30 | Where_OfType_OrderBy_Select_FirstOrDefault(); 31 | } 32 | 33 | private IMongoFrameworkQueryable GetQueryable() 34 | { 35 | var provider = new MongoFrameworkQueryProvider(Connection); 36 | return new MongoFrameworkQueryable(provider); 37 | } 38 | 39 | [Benchmark] 40 | public TestModel FirstOrDefault() 41 | { 42 | return GetQueryable().FirstOrDefault(); 43 | } 44 | 45 | [Benchmark] 46 | public TestModel[] ToArray() 47 | { 48 | return GetQueryable().ToArray(); 49 | } 50 | 51 | [Benchmark] 52 | public int Count() 53 | { 54 | return GetQueryable().Count(); 55 | } 56 | 57 | [Benchmark] 58 | public bool Any() 59 | { 60 | return GetQueryable().Any(); 61 | } 62 | 63 | [Benchmark] 64 | public object Where_OfType_OrderBy_Select_FirstOrDefault() 65 | { 66 | return GetQueryable().Where(e => e.Id == "123").OfType().OrderBy(e => e.Id).Select(e => new { A = e.Id, B = e.Id }).FirstOrDefault(); 67 | } 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /tests/MongoFramework.Benchmarks/Infrastructure/Mapping/EntityMappingBenchmark.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using BenchmarkDotNet.Attributes; 4 | using BenchmarkDotNet.Jobs; 5 | using MongoDB.Bson; 6 | using MongoDB.Bson.Serialization; 7 | using MongoFramework.Infrastructure.Mapping; 8 | 9 | namespace MongoFramework.Benchmarks.Infrastructure.Mapping 10 | { 11 | [SimpleJob(RuntimeMoniker.Net50), MemoryDiagnoser] 12 | public class EntityMappingBenchmark 13 | { 14 | public class PersonModel 15 | { 16 | public ObjectId Id { get; set; } 17 | public string Name { get; set; } 18 | public DateTime DateOfBirth { get; set; } 19 | public List Addresses { get; set; } 20 | } 21 | public class AddressModel 22 | { 23 | public string Street { get; set; } 24 | public string Suburb { get; set; } 25 | public string State { get; set; } 26 | public string Country { get; set; } 27 | } 28 | 29 | [Benchmark] 30 | public void ResetOverhead() 31 | { 32 | EntityMapping.RemoveAllDefinitions(); 33 | MongoDbDriverHelper.ResetDriver(); 34 | } 35 | 36 | [Benchmark] 37 | public void ClassMapAutoMap() 38 | { 39 | new BsonClassMap().AutoMap(); 40 | MongoDbDriverHelper.ResetDriver(); 41 | } 42 | 43 | [Benchmark] 44 | public void EntityMappingRegister() 45 | { 46 | EntityMapping.RegisterType(typeof(PersonModel)); 47 | EntityMapping.RemoveAllDefinitions(); 48 | MongoDbDriverHelper.ResetDriver(); 49 | } 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /tests/MongoFramework.Benchmarks/Infrastructure/Serialization/TypeDiscovery_FindTypeBenchmark.cs: -------------------------------------------------------------------------------- 1 | using BenchmarkDotNet.Attributes; 2 | using BenchmarkDotNet.Jobs; 3 | using MongoFramework.Infrastructure.Serialization; 4 | 5 | namespace MongoFramework.Benchmarks.Infrastructure.Serialization 6 | { 7 | [SimpleJob(RuntimeMoniker.Net50), MemoryDiagnoser] 8 | public class TypeDiscovery_FindTypeBenchmark 9 | { 10 | private class LocalClass { } 11 | 12 | [Benchmark] 13 | public void Overhead() 14 | { 15 | TypeDiscovery.ClearCache(); 16 | } 17 | 18 | [Benchmark] 19 | public void FindString() 20 | { 21 | TypeDiscovery.FindTypeByDiscriminator("string", typeof(object)); 22 | TypeDiscovery.ClearCache(); 23 | } 24 | 25 | [Benchmark] 26 | public void FindMongoDbContext() 27 | { 28 | TypeDiscovery.FindTypeByDiscriminator("MongoDbContext", typeof(object)); 29 | TypeDiscovery.ClearCache(); 30 | } 31 | 32 | [Benchmark] 33 | public void FindLocalClass() 34 | { 35 | TypeDiscovery.FindTypeByDiscriminator("LocalClass", typeof(object)); 36 | TypeDiscovery.ClearCache(); 37 | } 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /tests/MongoFramework.Benchmarks/MongoDbDriverHelper.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Concurrent; 3 | using System.Collections.Generic; 4 | using System.Reflection; 5 | using MongoDB.Bson; 6 | using MongoDB.Bson.Serialization; 7 | 8 | namespace MongoFramework.Benchmarks 9 | { 10 | public static class MongoDbDriverHelper 11 | { 12 | public static void ResetDriver() 13 | { 14 | //Primarily introduced to better test TypeDiscoverySerializer, this is designed to reset the MongoDB driver 15 | //as if the assembly just loaded. It is likely incomplete and would be easily subject to breaking in future 16 | //driver updates. If someone knows a better way to reset the MongoDB driver, please open a pull request! 17 | 18 | var classMapField = typeof(BsonClassMap).GetField("__classMaps", BindingFlags.NonPublic | BindingFlags.Static); 19 | if (classMapField.GetValue(null) is Dictionary classMaps) 20 | { 21 | classMaps.Clear(); 22 | } 23 | 24 | var knownTypesField = typeof(BsonSerializer).GetField("__typesWithRegisteredKnownTypes", BindingFlags.NonPublic | BindingFlags.Static); 25 | if (knownTypesField.GetValue(null) is HashSet knownTypes) 26 | { 27 | knownTypes.Clear(); 28 | } 29 | 30 | var discriminatorTypesField = typeof(BsonSerializer).GetField("__discriminatedTypes", BindingFlags.NonPublic | BindingFlags.Static); 31 | if (discriminatorTypesField.GetValue(null) is HashSet discriminatorTypes) 32 | { 33 | discriminatorTypes.Clear(); 34 | } 35 | 36 | var discriminatorsField = typeof(BsonSerializer).GetField("__discriminators", BindingFlags.NonPublic | BindingFlags.Static); 37 | if (discriminatorsField.GetValue(null) is Dictionary> discriminators) 38 | { 39 | discriminators.Clear(); 40 | } 41 | 42 | var serializerRegistryField = typeof(BsonSerializer).GetField("__serializerRegistry", BindingFlags.NonPublic | BindingFlags.Static); 43 | if (serializerRegistryField.GetValue(null) is BsonSerializerRegistry registry) 44 | { 45 | var cacheField = typeof(BsonSerializerRegistry).GetField("_cache", BindingFlags.NonPublic | BindingFlags.Instance); 46 | var registryCache = cacheField.GetValue(registry) as ConcurrentDictionary; 47 | registryCache.Clear(); 48 | } 49 | } 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /tests/MongoFramework.Benchmarks/MongoDbSetComparisonBenchmark.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Linq; 3 | using BenchmarkDotNet.Attributes; 4 | using BenchmarkDotNet.Jobs; 5 | using MongoFramework.Attributes; 6 | 7 | namespace MongoFramework.Benchmarks 8 | { 9 | [SimpleJob(RuntimeMoniker.Net50), MemoryDiagnoser] 10 | public class MongoDbSetComparisonBenchmark 11 | { 12 | private class TestModel 13 | { 14 | public string Id { get; set; } 15 | [Index(IndexSortOrder.Ascending)] 16 | public string Name { get; set; } 17 | public DateTime Date { get; set; } 18 | public int OtherReference { get; set; } 19 | public NestedTestModel NestedModel { get; set; } 20 | } 21 | 22 | private class NestedTestModel 23 | { 24 | public string NestedName { get; set; } 25 | public int NestedReference { get; set; } 26 | } 27 | 28 | private class TestBucketGroup 29 | { 30 | [Index(IndexSortOrder.Ascending)] 31 | public string Name { get; set; } 32 | } 33 | 34 | private class TestBucketItem 35 | { 36 | public DateTime Date { get; set; } 37 | public int OtherReference { get; set; } 38 | public NestedTestModel NestedModel { get; set; } 39 | } 40 | 41 | private class CustomDbContext : MongoDbContext 42 | { 43 | public CustomDbContext(IMongoDbConnection connection) : base(connection) { } 44 | public MongoDbSet TestModels { get; set; } 45 | [BucketSetOptions(200, nameof(TestBucketItem.Date))] 46 | public MongoDbBucketSet TestBucketModels { get; set; } 47 | } 48 | 49 | public int NumberOfGroups = 8; 50 | public int EntitiesPerGroup = 200; 51 | 52 | [GlobalSetup] 53 | public void Setup() 54 | { 55 | BenchmarkDb.DropDatabase(); 56 | } 57 | 58 | [GlobalCleanup] 59 | public void Cleanup() 60 | { 61 | BenchmarkDb.DropDatabase(); 62 | } 63 | 64 | [Benchmark] 65 | public void MongoDbSet() 66 | { 67 | var connection = BenchmarkDb.GetConnection(); 68 | var date = new DateTime(2000, 1, 1); 69 | using (var context = new CustomDbContext(connection)) 70 | { 71 | //Add 72 | for (var i = 0; i < NumberOfGroups; i++) 73 | { 74 | for (var j = 0; j < EntitiesPerGroup; j++) 75 | { 76 | var v = i * EntitiesPerGroup + j; 77 | context.TestModels.Add(new TestModel 78 | { 79 | Date = date.AddDays(v), 80 | Name = "TestModel", 81 | OtherReference = i * EntitiesPerGroup + v, 82 | NestedModel = new NestedTestModel 83 | { 84 | NestedName = $"TestModel {v}", 85 | NestedReference = v 86 | } 87 | }); 88 | } 89 | context.SaveChanges(); 90 | } 91 | 92 | //Read 93 | for (var i = 0; i < EntitiesPerGroup; i++) 94 | { 95 | context.TestModels 96 | .Where(t => t.Name == "TestModel") 97 | .Sum(t => t.OtherReference); 98 | } 99 | 100 | //Remove 101 | context.TestModels.RemoveRange(t => true); 102 | context.SaveChanges(); 103 | } 104 | } 105 | 106 | [Benchmark] 107 | public void MongoDbBucketSet() 108 | { 109 | var connection = BenchmarkDb.GetConnection(); 110 | var date = new DateTime(2000, 1, 1); 111 | using (var context = new CustomDbContext(connection)) 112 | { 113 | //Add 114 | for (var i = 0; i < NumberOfGroups; i++) 115 | { 116 | for (var j = 0; j < EntitiesPerGroup; j++) 117 | { 118 | var v = i * EntitiesPerGroup + j; 119 | context.TestBucketModels.Add(new TestBucketGroup 120 | { 121 | Name = "TestModel" 122 | }, 123 | new TestBucketItem 124 | { 125 | Date = date.AddDays(v), 126 | OtherReference = v, 127 | NestedModel = new NestedTestModel 128 | { 129 | NestedName = $"TestModel {v}", 130 | NestedReference = v 131 | } 132 | }); 133 | } 134 | context.SaveChanges(); 135 | } 136 | 137 | //Read 138 | for (var i = 0; i < EntitiesPerGroup; i++) 139 | { 140 | context.TestBucketModels.WithGroup(new TestBucketGroup 141 | { 142 | Name = "TestModel" 143 | }).Sum(t => t.OtherReference); 144 | } 145 | 146 | //Remove 147 | context.TestBucketModels.Remove(new TestBucketGroup 148 | { 149 | Name = "TestModel" 150 | }); 151 | context.SaveChanges(); 152 | } 153 | } 154 | } 155 | } 156 | -------------------------------------------------------------------------------- /tests/MongoFramework.Benchmarks/MongoFramework.Benchmarks.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | Exe 5 | net6.0 6 | False 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | -------------------------------------------------------------------------------- /tests/MongoFramework.Benchmarks/Program.cs: -------------------------------------------------------------------------------- 1 | using BenchmarkDotNet.Running; 2 | 3 | namespace MongoFramework.Benchmarks 4 | { 5 | class Program 6 | { 7 | static void Main(string[] args) 8 | { 9 | BenchmarkSwitcher.FromAssembly(typeof(Program).Assembly).Run(args); 10 | } 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /tests/MongoFramework.Tests/AssertExtensions.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Threading.Tasks; 3 | using Microsoft.VisualStudio.TestTools.UnitTesting; 4 | 5 | namespace MongoFramework.Tests 6 | { 7 | public class AssertExtensions 8 | { 9 | /// 10 | /// Author: Gilles Leblanc 11 | /// Source: https://gillesleblanc.wordpress.com/2014/03/17/testing-that-an-exception-isnt-thrown-in-c/ 12 | /// 13 | /// 14 | /// 15 | /// 16 | public static void DoesNotThrow(Action expressionUnderTest, string exceptionMessage = "Expected exception was thrown by target of invocation.") where T : Exception 17 | { 18 | try 19 | { 20 | expressionUnderTest(); 21 | } 22 | catch (T) 23 | { 24 | Assert.Fail(exceptionMessage); 25 | } 26 | catch (Exception) 27 | { 28 | Assert.IsTrue(true); 29 | } 30 | 31 | Assert.IsTrue(true); 32 | } 33 | 34 | /// 35 | /// Author: Gilles Leblanc 36 | /// Source: https://gillesleblanc.wordpress.com/2014/03/17/testing-that-an-exception-isnt-thrown-in-c/ 37 | /// 38 | /// 39 | /// 40 | /// 41 | public static async Task DoesNotThrowAsync(Func expressionUnderTest, string exceptionMessage = "Expected exception was thrown by target of invocation.") where T : Exception 42 | { 43 | try 44 | { 45 | await expressionUnderTest().ConfigureAwait(false); 46 | } 47 | catch (T) 48 | { 49 | Assert.Fail(exceptionMessage); 50 | } 51 | catch (Exception) 52 | { 53 | Assert.IsTrue(true); 54 | } 55 | 56 | Assert.IsTrue(true); 57 | } 58 | } 59 | } -------------------------------------------------------------------------------- /tests/MongoFramework.Tests/ExpectedExceptionPatternAttribute.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Text.RegularExpressions; 3 | using Microsoft.VisualStudio.TestTools.UnitTesting; 4 | 5 | namespace MongoFramework.Tests 6 | { 7 | public class ExpectedExceptionPatternAttribute : ExpectedExceptionBaseAttribute 8 | { 9 | private Type ExpectedExceptionType { get; } 10 | private Regex MessagePattern { get; } 11 | private string RawPattern { get; } 12 | 13 | public ExpectedExceptionPatternAttribute(Type expectedExceptionType, string exceptionMessagePattern) 14 | { 15 | ExpectedExceptionType = expectedExceptionType; 16 | MessagePattern = new Regex(exceptionMessagePattern); 17 | RawPattern = exceptionMessagePattern; 18 | } 19 | 20 | protected override void Verify(Exception exception) 21 | { 22 | Assert.IsNotNull(exception, $"\"{nameof(exception)}\" is null"); 23 | 24 | var thrownExceptionType = exception.GetType(); 25 | 26 | if (ExpectedExceptionType != thrownExceptionType) 27 | { 28 | throw new Exception($"Test method threw exception {thrownExceptionType.FullName}, but exception {ExpectedExceptionType.FullName} was expected. Exception message: {exception.Message}"); 29 | } 30 | 31 | if (!MessagePattern.IsMatch(exception.Message)) 32 | { 33 | throw new Exception($"Thrown exception message \"{exception.Message}\" does not match pattern \"{RawPattern}\"."); 34 | } 35 | } 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /tests/MongoFramework.Tests/Infrastructure/Commands/AddEntityCommandTests.cs: -------------------------------------------------------------------------------- 1 | using System.ComponentModel.DataAnnotations; 2 | using System.Linq; 3 | using Microsoft.VisualStudio.TestTools.UnitTesting; 4 | using MongoFramework.Infrastructure; 5 | using MongoFramework.Infrastructure.Commands; 6 | 7 | namespace MongoFramework.Tests.Infrastructure.Commands 8 | { 9 | [TestClass] 10 | public class AddEntityCommandTests : TestBase 11 | { 12 | public class TestModel 13 | { 14 | public string Id { get; set; } 15 | public string Title { get; set; } 16 | } 17 | 18 | public class TestValidationModel 19 | { 20 | public string Id { get; set; } 21 | 22 | [Required] 23 | public string RequiredField { get; set; } 24 | public bool BooleanField { get; set; } 25 | } 26 | 27 | [TestMethod] 28 | public void AddEntity() 29 | { 30 | var connection = TestConfiguration.GetConnection(); 31 | var context = new MongoDbContext(connection); 32 | 33 | var entity = new TestModel 34 | { 35 | Title = "AddEntityCommandTests.AddEntity" 36 | }; 37 | 38 | context.CommandStaging.Add(new AddEntityCommand(new EntityEntry(entity, EntityEntryState.Added))); 39 | context.SaveChanges(); 40 | 41 | Assert.IsNotNull(entity.Id); 42 | } 43 | 44 | [TestMethod, ExpectedException(typeof(ValidationException))] 45 | public void ValidationExceptionOnInvalidModel() 46 | { 47 | var entity = new TestValidationModel { }; 48 | var command = new AddEntityCommand(new EntityEntry(entity, EntityEntryState.Added)); 49 | command.GetModel(WriteModelOptions.Default).FirstOrDefault(); 50 | } 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /tests/MongoFramework.Tests/Infrastructure/Commands/RemoveEntityByIdCommandTests.cs: -------------------------------------------------------------------------------- 1 | using System.Linq; 2 | using Microsoft.VisualStudio.TestTools.UnitTesting; 3 | using MongoFramework.Infrastructure; 4 | using MongoFramework.Infrastructure.Commands; 5 | 6 | namespace MongoFramework.Tests.Infrastructure.Commands 7 | { 8 | [TestClass] 9 | public class RemoveEntityByIdCommandTests : TestBase 10 | { 11 | public class TestModel 12 | { 13 | public string Id { get; set; } 14 | public string Title { get; set; } 15 | } 16 | 17 | [TestMethod] 18 | public void RemoveEntity() 19 | { 20 | var connection = TestConfiguration.GetConnection(); 21 | var context = new MongoDbContext(connection); 22 | 23 | var entity = new TestModel 24 | { 25 | Title = "RemoveEntityByIdCommandTests.RemoveEntity" 26 | }; 27 | 28 | context.CommandStaging.Add(new AddEntityCommand(new EntityEntry(entity, EntityEntryState.Added))); 29 | context.SaveChanges(); 30 | 31 | context.CommandStaging.Add(new RemoveEntityByIdCommand(entity.Id)); 32 | context.SaveChanges(); 33 | 34 | var dbEntity = context.Query().Where(e => e.Id == entity.Id).FirstOrDefault(); 35 | Assert.IsNull(dbEntity); 36 | } 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /tests/MongoFramework.Tests/Infrastructure/Commands/RemoveEntityCommandTests.cs: -------------------------------------------------------------------------------- 1 | using System.Linq; 2 | using Microsoft.VisualStudio.TestTools.UnitTesting; 3 | using MongoFramework.Infrastructure; 4 | using MongoFramework.Infrastructure.Commands; 5 | 6 | namespace MongoFramework.Tests.Infrastructure.Commands 7 | { 8 | [TestClass] 9 | public class RemoveEntityCommandTests : TestBase 10 | { 11 | public class TestModel 12 | { 13 | public string Id { get; set; } 14 | public string Title { get; set; } 15 | } 16 | 17 | [TestMethod] 18 | public void RemoveEntity() 19 | { 20 | var connection = TestConfiguration.GetConnection(); 21 | var context = new MongoDbContext(connection); 22 | 23 | var entity = new TestModel 24 | { 25 | Title = "RemoveEntityCommandTests.RemoveEntity" 26 | }; 27 | 28 | context.CommandStaging.Add(new AddEntityCommand(new EntityEntry(entity, EntityEntryState.Added))); 29 | context.SaveChanges(); 30 | 31 | context.CommandStaging.Add(new RemoveEntityCommand(new EntityEntry(entity, EntityEntryState.Deleted))); 32 | context.SaveChanges(); 33 | 34 | var dbEntity = context.Query().Where(e => e.Id == entity.Id).FirstOrDefault(); 35 | Assert.IsNull(dbEntity); 36 | } 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /tests/MongoFramework.Tests/Infrastructure/Commands/RemoveEntityRangeCommandTests.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Linq; 3 | using Microsoft.VisualStudio.TestTools.UnitTesting; 4 | using MongoFramework.Infrastructure; 5 | using MongoFramework.Infrastructure.Commands; 6 | 7 | namespace MongoFramework.Tests.Infrastructure.Commands 8 | { 9 | [TestClass] 10 | public class RemoveEntityRangeCommandTests : TestBase 11 | { 12 | public class TestModel 13 | { 14 | public string Id { get; set; } 15 | public string Title { get; set; } 16 | public DateTime DateTime { get; set; } 17 | } 18 | 19 | [TestMethod] 20 | public void RemoveEntities() 21 | { 22 | var connection = TestConfiguration.GetConnection(); 23 | var context = new MongoDbContext(connection); 24 | 25 | var entities = new[] 26 | { 27 | new TestModel 28 | { 29 | Title = "RemoveEntityRangeCommandTests.RemoveEntities", 30 | DateTime = DateTime.UtcNow.AddDays(-1) 31 | }, 32 | new TestModel 33 | { 34 | Title = "RemoveEntityRangeCommandTests.RemoveEntities", 35 | DateTime = DateTime.UtcNow.AddDays(1) 36 | } 37 | }; 38 | 39 | context.CommandStaging.Add(new AddEntityCommand(new EntityEntry(entities[0], EntityEntryState.Added))); 40 | context.CommandStaging.Add(new AddEntityCommand(new EntityEntry(entities[1], EntityEntryState.Added))); 41 | context.SaveChanges(); 42 | 43 | context.CommandStaging.Add(new RemoveEntityRangeCommand(e => e.DateTime < DateTime.UtcNow)); 44 | context.SaveChanges(); 45 | 46 | var removedEntity = context.Query().Where(e => e.Id == entities[0].Id).FirstOrDefault(); 47 | Assert.IsNull(removedEntity); 48 | 49 | var nonRemovedEntity = context.Query().Where(e => e.Id == entities[1].Id).FirstOrDefault(); 50 | Assert.IsNotNull(nonRemovedEntity); 51 | } 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /tests/MongoFramework.Tests/Infrastructure/Commands/UpdateEntityCommandTests.cs: -------------------------------------------------------------------------------- 1 | using System.ComponentModel.DataAnnotations; 2 | using System.Linq; 3 | using Microsoft.VisualStudio.TestTools.UnitTesting; 4 | using MongoFramework.Infrastructure; 5 | using MongoFramework.Infrastructure.Commands; 6 | 7 | namespace MongoFramework.Tests.Infrastructure.Commands 8 | { 9 | [TestClass] 10 | public class UpdateEntityCommandTests : TestBase 11 | { 12 | public class TestModel 13 | { 14 | public string Id { get; set; } 15 | public string Title { get; set; } 16 | } 17 | 18 | public class TestValidationModel 19 | { 20 | public string Id { get; set; } 21 | 22 | [Required] 23 | public string RequiredField { get; set; } 24 | public bool BooleanField { get; set; } 25 | } 26 | 27 | [TestMethod] 28 | public void UpdateEntity() 29 | { 30 | var connection = TestConfiguration.GetConnection(); 31 | var context = new MongoDbContext(connection); 32 | 33 | var entity = new TestModel 34 | { 35 | Title = "UpdateEntityCommandTests.UpdateEntity" 36 | }; 37 | 38 | context.CommandStaging.Add(new AddEntityCommand(new EntityEntry(entity, EntityEntryState.Added))); 39 | context.SaveChanges(); 40 | 41 | var updatedEntity = new TestModel 42 | { 43 | Id = entity.Id, 44 | Title = "UpdateEntityCommandTests.UpdateEntity-Updated" 45 | }; 46 | 47 | context.CommandStaging.Add(new UpdateEntityCommand(new EntityEntry(updatedEntity, EntityEntryState.Updated))); 48 | context.SaveChanges(); 49 | 50 | var dbEntity = context.Query().Where(e => e.Id == entity.Id).FirstOrDefault(); 51 | Assert.AreEqual("UpdateEntityCommandTests.UpdateEntity-Updated", dbEntity.Title); 52 | } 53 | 54 | [TestMethod, ExpectedException(typeof(ValidationException))] 55 | public void ValidationExceptionOnInvalidModel() 56 | { 57 | var entity = new TestValidationModel { }; 58 | var command = new UpdateEntityCommand(new EntityEntry(entity, EntityEntryState.Updated)); 59 | command.GetModel(WriteModelOptions.Default).FirstOrDefault(); 60 | } 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /tests/MongoFramework.Tests/Infrastructure/Indexing/EntityIndexWriterTests.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Threading.Tasks; 3 | using Microsoft.VisualStudio.TestTools.UnitTesting; 4 | using MongoDB.Driver; 5 | using MongoDB.Driver.GeoJsonObjectModel; 6 | using MongoFramework.Attributes; 7 | using MongoFramework.Infrastructure.Indexing; 8 | 9 | namespace MongoFramework.Tests.Infrastructure.Indexing 10 | { 11 | [TestClass] 12 | public class EntityIndexWriterTests : TestBase 13 | { 14 | public class IndexModel 15 | { 16 | public string Id { get; set; } 17 | [Index(IndexSortOrder.Ascending)] 18 | public string IndexedPropertyOne { get; set; } 19 | [Index("MyIndexedProperty", IndexSortOrder.Descending)] 20 | public string IndexedPropertyTwo { get; set; } 21 | [Index(IndexType.Text)] 22 | public string IndexedText { get; set; } 23 | [Index(IndexType.Geo2dSphere)] 24 | public GeoJsonPoint IndexedGeo { get; set; } 25 | } 26 | 27 | public class MultipleTextIndexModel 28 | { 29 | [Index(IndexType.Text)] 30 | public string IndexedTextOne { get; set; } 31 | [Index(IndexType.Text)] 32 | public string IndexedTextTwo { get; set; } 33 | } 34 | 35 | public class NoIndexModel 36 | { 37 | public string Id { get; set; } 38 | } 39 | 40 | [TestMethod] 41 | public void WriteIndexSync() 42 | { 43 | var connection = TestConfiguration.GetConnection(); 44 | 45 | EntityIndexWriter.ApplyIndexing(connection); 46 | 47 | var collection = connection.GetDatabase().GetCollection("IndexModel"); 48 | var dbIndexes = collection.Indexes.List().ToList(); 49 | Assert.AreEqual(5, dbIndexes.Count); 50 | } 51 | 52 | [TestMethod] 53 | public async Task WriteIndexAsync() 54 | { 55 | var connection = TestConfiguration.GetConnection(); 56 | 57 | 58 | await EntityIndexWriter.ApplyIndexingAsync(connection); 59 | 60 | var collection = connection.GetDatabase().GetCollection("IndexModel"); 61 | var dbIndexes = await collection.Indexes.List().ToListAsync().ConfigureAwait(false); 62 | Assert.AreEqual(5, dbIndexes.Count); 63 | } 64 | 65 | [TestMethod] 66 | public void NoIndexSync() 67 | { 68 | var connection = TestConfiguration.GetConnection(); 69 | AssertExtensions.DoesNotThrow(() => EntityIndexWriter.ApplyIndexing(connection)); 70 | } 71 | 72 | [TestMethod] 73 | public async Task NoIndexAsync() 74 | { 75 | var connection = TestConfiguration.GetConnection(); 76 | await AssertExtensions.DoesNotThrowAsync(async () => await EntityIndexWriter.ApplyIndexingAsync(connection)).ConfigureAwait(false); 77 | } 78 | 79 | 80 | [TestMethod, ExpectedException(typeof(Exception), AllowDerivedTypes = true)] 81 | public void FailureFromMultipleTextIndexes() 82 | { 83 | var connection = TestConfiguration.GetConnection(); 84 | EntityIndexWriter.ApplyIndexing(connection); 85 | } 86 | } 87 | } -------------------------------------------------------------------------------- /tests/MongoFramework.Tests/Infrastructure/Linq/Processors/EntityTrackingProcessorTests.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.VisualStudio.TestTools.UnitTesting; 2 | using MongoFramework.Infrastructure; 3 | using MongoFramework.Infrastructure.Linq.Processors; 4 | 5 | namespace MongoFramework.Tests.Infrastructure.Linq.Processors 6 | { 7 | [TestClass] 8 | public class EntityTrackingProcessorTests : TestBase 9 | { 10 | public class TestEntity 11 | { 12 | public string Id { get; set; } 13 | public string Description { get; set; } 14 | } 15 | 16 | [TestMethod] 17 | public void TrackUnseenEntities() 18 | { 19 | var connection = TestConfiguration.GetConnection(); 20 | var context = new MongoDbContext(connection); 21 | var processor = new EntityTrackingProcessor(context); 22 | 23 | processor.ProcessEntity(new TestEntity { Id = "123", Description = "Database" }, null); 24 | 25 | var collectionEntity = context.ChangeTracker.GetEntry(new TestEntity { Id = "123" }).Entity as TestEntity; 26 | Assert.AreEqual("Database", collectionEntity.Description); 27 | } 28 | 29 | [TestMethod] 30 | public void RefreshEntityIfMarkedAsNoChanges() 31 | { 32 | var connection = TestConfiguration.GetConnection(); 33 | var context = new MongoDbContext(connection); 34 | var processor = new EntityTrackingProcessor(context); 35 | 36 | context.ChangeTracker.SetEntityState(new TestEntity 37 | { 38 | Id = "123", 39 | Description = "1" 40 | }, EntityEntryState.NoChanges); 41 | 42 | processor.ProcessEntity(new TestEntity { Id = "123", Description = "2" }, null); 43 | 44 | var collectionEntity = context.ChangeTracker.GetEntry(new TestEntity { Id = "123" }).Entity as TestEntity; 45 | Assert.AreEqual("2", collectionEntity.Description); 46 | } 47 | 48 | [TestMethod] 49 | public void DontRefreshEntityIfMarkedForDeletion() 50 | { 51 | var connection = TestConfiguration.GetConnection(); 52 | var context = new MongoDbContext(connection); 53 | var processor = new EntityTrackingProcessor(context); 54 | 55 | context.ChangeTracker.SetEntityState(new TestEntity 56 | { 57 | Id = "123", 58 | Description = "Deleted" 59 | }, EntityEntryState.Deleted); 60 | 61 | processor.ProcessEntity(new TestEntity { Id = "123", Description = "Database" }, null); 62 | 63 | var collectionEntity = context.ChangeTracker.GetEntry(new TestEntity { Id = "123" }).Entity as TestEntity; 64 | Assert.AreEqual("Deleted", collectionEntity.Description); 65 | } 66 | 67 | [TestMethod] 68 | public void DontRefreshEntityIfMarkedForUpdate() 69 | { 70 | var connection = TestConfiguration.GetConnection(); 71 | var context = new MongoDbContext(connection); 72 | var processor = new EntityTrackingProcessor(context); 73 | 74 | context.ChangeTracker.SetEntityState(new TestEntity 75 | { 76 | Id = "123", 77 | Description = "Updated" 78 | }, EntityEntryState.Updated); 79 | 80 | processor.ProcessEntity(new TestEntity { Id = "123", Description = "Database" }, null); 81 | 82 | var collectionEntity = context.ChangeTracker.GetEntry(new TestEntity { Id = "123" }).Entity as TestEntity; 83 | Assert.AreEqual("Updated", collectionEntity.Description); 84 | } 85 | 86 | [TestMethod] 87 | public void DontRefreshEntityIfMarkedForAdded() 88 | { 89 | var connection = TestConfiguration.GetConnection(); 90 | var context = new MongoDbContext(connection); 91 | var processor = new EntityTrackingProcessor(context); 92 | 93 | context.ChangeTracker.SetEntityState(new TestEntity 94 | { 95 | Id = "123", 96 | Description = "Added" 97 | }, EntityEntryState.Added); 98 | 99 | processor.ProcessEntity(new TestEntity { Id = "123", Description = "Database" }, null); 100 | 101 | var collectionEntity = context.ChangeTracker.GetEntry(new TestEntity { Id = "123" }).Entity as TestEntity; 102 | Assert.AreEqual("Added", collectionEntity.Description); 103 | } 104 | } 105 | } 106 | -------------------------------------------------------------------------------- /tests/MongoFramework.Tests/Infrastructure/Mapping/EntityDefinitionExtensionTests.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using Microsoft.VisualStudio.TestTools.UnitTesting; 5 | using MongoFramework.Infrastructure.Commands; 6 | using MongoFramework.Infrastructure.Mapping; 7 | 8 | namespace MongoFramework.Tests.Infrastructure.Mapping 9 | { 10 | [TestClass] 11 | public class EntityDefinitionExtensionTests : TestBase 12 | { 13 | public class IdNameParentModel 14 | { 15 | public string Id { get; set; } 16 | } 17 | 18 | public class IdNameChildModel : IdNameParentModel 19 | { 20 | public string Description { get; set; } 21 | } 22 | 23 | public class OverridePropertyBaseModel 24 | { 25 | public virtual string TargetProperty { get; set; } 26 | } 27 | 28 | public class OverridePropertyChildModel : OverridePropertyBaseModel 29 | { 30 | public override string TargetProperty { get; set; } 31 | } 32 | 33 | public class OverridePropertyGrandChildModel : OverridePropertyChildModel 34 | { 35 | 36 | } 37 | 38 | public class TenantModel : IHaveTenantId 39 | { 40 | public string Id { get; set; } 41 | public string TenantId { get; set; } 42 | } 43 | 44 | [TestMethod] 45 | public void GetIdNameChecksInheritence() 46 | { 47 | var definition = EntityMapping.RegisterType(typeof(IdNameChildModel)); 48 | var parentDefinition = EntityMapping.GetOrCreateDefinition(typeof(IdNameParentModel)); 49 | 50 | Assert.AreEqual("Id", definition.GetIdName()); 51 | Assert.AreEqual("Id", parentDefinition.GetIdName()); 52 | } 53 | 54 | [TestMethod] 55 | public void GetInheritedPropertiesTakesBaseProperties() 56 | { 57 | var definition = EntityMapping.RegisterType(typeof(OverridePropertyGrandChildModel)); 58 | var inheritedProperties = definition.GetInheritedProperties().ToArray(); 59 | Assert.AreEqual(1, inheritedProperties.Length); 60 | Assert.AreEqual(typeof(OverridePropertyBaseModel), inheritedProperties[0].PropertyInfo.DeclaringType); 61 | } 62 | [TestMethod] 63 | public void GetAllPropertiesTakesBaseProperties() 64 | { 65 | var definition = EntityMapping.RegisterType(typeof(OverridePropertyChildModel)); 66 | var allProperties = definition.GetAllProperties().ToArray(); 67 | Assert.AreEqual(1, allProperties.Length); 68 | Assert.AreEqual(typeof(OverridePropertyBaseModel), allProperties[0].PropertyInfo.DeclaringType); 69 | } 70 | 71 | [TestMethod] 72 | public void GetTenantModelIdRequiresTenant() 73 | { 74 | var definition = EntityMapping.RegisterType(typeof(TenantModel)); 75 | Assert.ThrowsException(() => definition.CreateIdFilter("id")); 76 | } 77 | 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /tests/MongoFramework.Tests/Infrastructure/Mapping/EntityMappingTests.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Threading.Tasks; 3 | using Microsoft.VisualStudio.TestTools.UnitTesting; 4 | using MongoFramework.Infrastructure.Mapping; 5 | 6 | namespace MongoFramework.Tests.Infrastructure.Mapping 7 | { 8 | [TestClass] 9 | public class EntityMappingTests : TestBase 10 | { 11 | public class MappingLockModel 12 | { 13 | public string Id { get; set; } 14 | } 15 | 16 | /// 17 | /// A potentially common issue for web application startup, this tests that multiple threads 18 | /// can map a class at the same time without concurrency issues. 19 | /// 20 | /// Relates to: https://github.com/TurnerSoftware/MongoFramework/issues/7 21 | /// 22 | [TestMethod] 23 | public void MappingLocks() 24 | { 25 | var connection = TestConfiguration.GetConnection(); 26 | AssertExtensions.DoesNotThrow(() => 27 | { 28 | Parallel.For(1, 10, i => { EntityMapping.GetOrCreateDefinition(typeof(MappingLockModel)); }); 29 | }); 30 | } 31 | } 32 | } -------------------------------------------------------------------------------- /tests/MongoFramework.Tests/Infrastructure/Mapping/MappingTestBase.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.VisualStudio.TestTools.UnitTesting; 2 | using MongoFramework.Infrastructure.Mapping; 3 | 4 | namespace MongoFramework.Tests.Infrastructure.Mapping 5 | { 6 | [TestClass] 7 | public abstract class MappingTestBase : TestBase 8 | { 9 | [TestInitialize] 10 | public void MappingProcessorReset() 11 | { 12 | EntityMapping.RemoveAllMappingProcessors(); 13 | } 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /tests/MongoFramework.Tests/Infrastructure/Mapping/Processors/BsonKnownTypesProcessorTests.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.VisualStudio.TestTools.UnitTesting; 2 | using MongoDB.Bson.Serialization; 3 | using MongoDB.Bson.Serialization.Attributes; 4 | using MongoFramework.Infrastructure.Mapping; 5 | using MongoFramework.Infrastructure.Mapping.Processors; 6 | 7 | namespace MongoFramework.Tests.Infrastructure.Mapping.Processors 8 | { 9 | [TestClass] 10 | public class BsonKnownTypesProcessorTests : MappingTestBase 11 | { 12 | [BsonKnownTypes(typeof(KnownTypesChildModel))] 13 | class KnownTypesBaseModel 14 | { 15 | public string Id { get; set; } 16 | } 17 | 18 | class KnownTypesChildModel : KnownTypesBaseModel 19 | { 20 | 21 | } 22 | 23 | class UnknownTypesBaseModel 24 | { 25 | 26 | } 27 | 28 | class UnknownTypesChildModel : UnknownTypesBaseModel 29 | { 30 | 31 | } 32 | 33 | [TestMethod] 34 | public void WithAttribute() 35 | { 36 | EntityMapping.AddMappingProcessor(new BsonKnownTypesProcessor()); 37 | Assert.IsFalse(BsonClassMap.IsClassMapRegistered(typeof(KnownTypesChildModel))); 38 | EntityMapping.RegisterType(typeof(KnownTypesBaseModel)); 39 | Assert.IsTrue(BsonClassMap.IsClassMapRegistered(typeof(KnownTypesChildModel))); 40 | } 41 | 42 | [TestMethod] 43 | public void WithoutAttribute() 44 | { 45 | EntityMapping.AddMappingProcessor(new BsonKnownTypesProcessor()); 46 | Assert.IsFalse(BsonClassMap.IsClassMapRegistered(typeof(UnknownTypesChildModel))); 47 | EntityMapping.RegisterType(typeof(UnknownTypesBaseModel)); 48 | Assert.IsFalse(BsonClassMap.IsClassMapRegistered(typeof(UnknownTypesChildModel))); 49 | } 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /tests/MongoFramework.Tests/Infrastructure/Mapping/Processors/CollectionNameProcessorTests.cs: -------------------------------------------------------------------------------- 1 | using System.ComponentModel.DataAnnotations.Schema; 2 | using Microsoft.VisualStudio.TestTools.UnitTesting; 3 | using MongoFramework.Infrastructure.Mapping; 4 | using MongoFramework.Infrastructure.Mapping.Processors; 5 | 6 | namespace MongoFramework.Tests.Infrastructure.Mapping.Processors 7 | { 8 | [TestClass] 9 | public class CollectionNameProcessorTests : MappingTestBase 10 | { 11 | [Table("CustomCollection")] 12 | public class CustomCollectionModel 13 | { 14 | } 15 | 16 | [Table("CustomCollection", Schema = "CustomSchema")] 17 | public class CustomCollectionAndSchemaModel 18 | { 19 | } 20 | 21 | public class DefaultCollectionNameModel 22 | { 23 | } 24 | 25 | [TestMethod] 26 | public void CollectionNameFromClassName() 27 | { 28 | EntityMapping.AddMappingProcessor(new CollectionNameProcessor()); 29 | var definition = EntityMapping.RegisterType(typeof(DefaultCollectionNameModel)); 30 | Assert.AreEqual("DefaultCollectionNameModel", definition.CollectionName); 31 | 32 | definition = EntityMapping.RegisterType(typeof(EntityBucket)); 33 | Assert.AreEqual("DefaultCollectionNameModel", definition.CollectionName); 34 | } 35 | 36 | [TestMethod] 37 | public void CollectionNameFromAttribute() 38 | { 39 | EntityMapping.AddMappingProcessor(new CollectionNameProcessor()); 40 | var definition = EntityMapping.RegisterType(typeof(CustomCollectionModel)); 41 | Assert.AreEqual("CustomCollection", definition.CollectionName); 42 | 43 | definition = EntityMapping.RegisterType(typeof(EntityBucket)); 44 | Assert.AreEqual("CustomCollection", definition.CollectionName); 45 | } 46 | 47 | [TestMethod] 48 | public void CollectionNameAndSchemaFromAttribute() 49 | { 50 | EntityMapping.AddMappingProcessor(new CollectionNameProcessor()); 51 | var definition = EntityMapping.RegisterType(typeof(CustomCollectionAndSchemaModel)); 52 | Assert.AreEqual("CustomSchema.CustomCollection", definition.CollectionName); 53 | 54 | definition = EntityMapping.RegisterType(typeof(EntityBucket)); 55 | Assert.AreEqual("CustomSchema.CustomCollection", definition.CollectionName); 56 | } 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /tests/MongoFramework.Tests/Infrastructure/Mapping/Processors/ExtraElementsProcessorTests.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | using System.ComponentModel.DataAnnotations.Schema; 3 | using System.Linq; 4 | using Microsoft.VisualStudio.TestTools.UnitTesting; 5 | using MongoDB.Bson.Serialization; 6 | using MongoFramework.Attributes; 7 | using MongoFramework.Infrastructure; 8 | using MongoFramework.Infrastructure.Mapping; 9 | using MongoFramework.Infrastructure.Mapping.Processors; 10 | 11 | namespace MongoFramework.Tests.Infrastructure.Mapping.Processors 12 | { 13 | [TestClass] 14 | public class ExtraElementsProcessorTests : MappingTestBase 15 | { 16 | [Table("ExtraElementsModel")] 17 | public class ExtraElementsAttrModel 18 | { 19 | public string Id { get; set; } 20 | [ExtraElements] 21 | public IDictionary AdditionalElements { get; set; } 22 | } 23 | 24 | [Table("ExtraElementsModel")] 25 | public class ModelWithExtraElements 26 | { 27 | public string Id { get; set; } 28 | public string PropertyOne { get; set; } 29 | public int PropertyTwo { get; set; } 30 | } 31 | 32 | [IgnoreExtraElements] 33 | public class IgnoreExtraElementsModel 34 | { 35 | public string Id { get; set; } 36 | } 37 | 38 | [TestMethod] 39 | public void ObeysIgnoreExtraElementsAttribute() 40 | { 41 | EntityMapping.AddMappingProcessor(new PropertyMappingProcessor()); 42 | EntityMapping.AddMappingProcessor(new ExtraElementsProcessor()); 43 | EntityMapping.RegisterType(typeof(IgnoreExtraElementsModel)); 44 | 45 | var classMap = BsonClassMap.GetRegisteredClassMaps() 46 | .Where(cm => cm.ClassType == typeof(IgnoreExtraElementsModel)).FirstOrDefault(); 47 | Assert.IsTrue(classMap.IgnoreExtraElements); 48 | 49 | EntityMapping.RegisterType(typeof(ExtraElementsAttrModel)); 50 | classMap = BsonClassMap.GetRegisteredClassMaps() 51 | .Where(cm => cm.ClassType == typeof(ExtraElementsAttrModel)).FirstOrDefault(); 52 | Assert.IsFalse(classMap.IgnoreExtraElements); 53 | } 54 | 55 | [TestMethod] 56 | public void ObeysExtraElementsAttribute() 57 | { 58 | EntityMapping.AddMappingProcessor(new PropertyMappingProcessor()); 59 | EntityMapping.AddMappingProcessor(new ExtraElementsProcessor()); 60 | EntityMapping.RegisterType(typeof(ExtraElementsAttrModel)); 61 | 62 | var classMap = BsonClassMap.GetRegisteredClassMaps() 63 | .Where(cm => cm.ClassType == typeof(ExtraElementsAttrModel)).FirstOrDefault(); 64 | Assert.AreEqual("AdditionalElements", classMap.ExtraElementsMemberMap.ElementName); 65 | 66 | EntityMapping.RegisterType(typeof(IgnoreExtraElementsModel)); 67 | classMap = BsonClassMap.GetRegisteredClassMaps() 68 | .Where(cm => cm.ClassType == typeof(IgnoreExtraElementsModel)).FirstOrDefault(); 69 | Assert.AreEqual(null, classMap.ExtraElementsMemberMap); 70 | } 71 | 72 | [TestMethod] 73 | public void ExtraElementsSerializationIntegrationTest() 74 | { 75 | EntityMapping.AddMappingProcessor(new CollectionNameProcessor()); 76 | EntityMapping.AddMappingProcessor(new PropertyMappingProcessor()); 77 | EntityMapping.AddMappingProcessor(new ExtraElementsProcessor()); 78 | EntityMapping.RegisterType(typeof(ExtraElementsAttrModel)); 79 | EntityMapping.RegisterType(typeof(ModelWithExtraElements)); 80 | 81 | var connection = TestConfiguration.GetConnection(); 82 | var context = new MongoDbContext(connection); 83 | 84 | var entity = new ModelWithExtraElements 85 | { 86 | PropertyOne = "ModelWithExtraElements", 87 | PropertyTwo = 123 88 | }; 89 | context.ChangeTracker.SetEntityState(entity, EntityEntryState.Added); 90 | context.SaveChanges(); 91 | 92 | var dbEntity = context.Query().Where(e => e.Id == entity.Id).FirstOrDefault(); 93 | Assert.AreEqual("ModelWithExtraElements", dbEntity.AdditionalElements[nameof(ModelWithExtraElements.PropertyOne)]); 94 | Assert.AreEqual(123, dbEntity.AdditionalElements[nameof(ModelWithExtraElements.PropertyTwo)]); 95 | } 96 | } 97 | } -------------------------------------------------------------------------------- /tests/MongoFramework.Tests/Infrastructure/Mapping/Processors/HierarchyProcessorTests.cs: -------------------------------------------------------------------------------- 1 | using System.Linq; 2 | using Microsoft.VisualStudio.TestTools.UnitTesting; 3 | using MongoDB.Bson.Serialization; 4 | using MongoFramework.Infrastructure.Mapping; 5 | using MongoFramework.Infrastructure.Mapping.Processors; 6 | 7 | namespace MongoFramework.Tests.Infrastructure.Mapping.Processors 8 | { 9 | [TestClass] 10 | public class HierarchyProcessorTests : MappingTestBase 11 | { 12 | public class ParentTestModel 13 | { 14 | public string Id { get; set; } 15 | } 16 | 17 | public class ChildTestModel : ParentTestModel 18 | { 19 | public string DeclaredProperty { get; set; } 20 | } 21 | 22 | [TestMethod] 23 | public void ParentClassIsMapped() 24 | { 25 | EntityMapping.AddMappingProcessor(new HierarchyProcessor()); 26 | 27 | Assert.IsFalse(BsonClassMap.IsClassMapRegistered(typeof(ChildTestModel))); 28 | Assert.IsFalse(BsonClassMap.IsClassMapRegistered(typeof(ParentTestModel))); 29 | 30 | EntityMapping.RegisterType(typeof(ChildTestModel)); 31 | 32 | Assert.IsTrue(BsonClassMap.IsClassMapRegistered(typeof(ChildTestModel))); 33 | Assert.IsTrue(BsonClassMap.IsClassMapRegistered(typeof(ParentTestModel))); 34 | } 35 | 36 | [TestMethod] 37 | public void AccessToInherittedProperty() 38 | { 39 | EntityMapping.AddMappingProcessor(new HierarchyProcessor()); 40 | EntityMapping.AddMappingProcessor(new PropertyMappingProcessor()); 41 | 42 | var definition = EntityMapping.RegisterType(typeof(ChildTestModel)); 43 | 44 | var allProperties = definition.GetAllProperties(); 45 | Assert.IsTrue(allProperties.Any(p => p.ElementName == "Id")); 46 | 47 | var declaredProperties = definition.Properties; 48 | Assert.IsFalse(declaredProperties.Any(p => p.ElementName == "Id")); 49 | } 50 | 51 | [TestMethod] 52 | public void AccessToDeclaredProperty() 53 | { 54 | EntityMapping.AddMappingProcessor(new HierarchyProcessor()); 55 | EntityMapping.AddMappingProcessor(new PropertyMappingProcessor()); 56 | var definition = EntityMapping.RegisterType(typeof(ChildTestModel)); 57 | 58 | var mappedProperties = definition.GetAllProperties(); 59 | Assert.IsTrue(mappedProperties.Any(p => p.ElementName == "DeclaredProperty")); 60 | 61 | var inherittedProperties = definition.GetInheritedProperties(); 62 | Assert.IsFalse(inherittedProperties.Any(p => p.ElementName == "DeclaredProperty")); 63 | } 64 | } 65 | } -------------------------------------------------------------------------------- /tests/MongoFramework.Tests/Infrastructure/Mapping/Processors/MappingAdapterProcessorTests.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.ComponentModel.DataAnnotations.Schema; 3 | using System.Linq; 4 | using Microsoft.VisualStudio.TestTools.UnitTesting; 5 | using MongoFramework.Attributes; 6 | using MongoFramework.Infrastructure.Mapping; 7 | using MongoFramework.Infrastructure.Mapping.Processors; 8 | 9 | namespace MongoFramework.Tests.Infrastructure.Mapping.Processors 10 | { 11 | [TestClass] 12 | public class MappingAdapterProcessorTests : MappingTestBase 13 | { 14 | 15 | public class AdapterTestModelMappingAdapter : IMappingProcessor 16 | { 17 | public void ApplyMapping(EntityDefinitionBuilder definitionBuilder) 18 | { 19 | definitionBuilder 20 | .ToCollection("Custom") 21 | .HasIndex(new[] { "UserName" }, b => b.IsUnique()); 22 | } 23 | } 24 | 25 | [Table("TestModels")] 26 | [MappingAdapter(typeof(AdapterTestModelMappingAdapter))] 27 | public class AdapterTestModel 28 | { 29 | public Guid MyCustomId { get; set; } 30 | public string UserName { get; set; } 31 | 32 | } 33 | 34 | public class AdapterTestModelMappingAdapterNoInterface 35 | { 36 | // no interface 37 | } 38 | 39 | [MappingAdapter(typeof(AdapterTestModelMappingAdapterNoInterface))] 40 | public class AdapterTestModelNoInterface 41 | { 42 | // broken adapter 43 | } 44 | 45 | public class AdapterTestModelMappingAdapterConstructor : IMappingProcessor 46 | { 47 | public AdapterTestModelMappingAdapterConstructor(string test) 48 | { 49 | 50 | } 51 | 52 | public void ApplyMapping(EntityDefinitionBuilder definitionBuilder) 53 | { 54 | throw new NotImplementedException(); 55 | } 56 | } 57 | 58 | [MappingAdapter(typeof(AdapterTestModelMappingAdapterConstructor))] 59 | public class AdapterTestModelConstructor 60 | { 61 | // broken adapter 62 | } 63 | 64 | [TestMethod] 65 | public void AdapterRequiresIMappingProcessor() 66 | { 67 | EntityMapping.AddMappingProcessor(new CollectionNameProcessor()); 68 | EntityMapping.AddMappingProcessor(new PropertyMappingProcessor()); 69 | EntityMapping.AddMappingProcessor(new EntityIdProcessor()); 70 | EntityMapping.AddMappingProcessor(new MappingAdapterProcessor()); 71 | Assert.ThrowsException(() => EntityMapping.RegisterType(typeof(AdapterTestModelNoInterface))); 72 | } 73 | 74 | [TestMethod] 75 | public void AdapterRequiresParameterlessConstructor() 76 | { 77 | EntityMapping.AddMappingProcessor(new CollectionNameProcessor()); 78 | EntityMapping.AddMappingProcessor(new PropertyMappingProcessor()); 79 | EntityMapping.AddMappingProcessor(new EntityIdProcessor()); 80 | EntityMapping.AddMappingProcessor(new MappingAdapterProcessor()); 81 | Assert.ThrowsException(() => EntityMapping.RegisterType(typeof(AdapterTestModelConstructor))); 82 | } 83 | 84 | [TestMethod] 85 | public void AdapterOverridesAttributes() 86 | { 87 | EntityMapping.AddMappingProcessor(new CollectionNameProcessor()); 88 | EntityMapping.AddMappingProcessor(new PropertyMappingProcessor()); 89 | EntityMapping.AddMappingProcessor(new EntityIdProcessor()); 90 | EntityMapping.AddMappingProcessor(new MappingAdapterProcessor()); 91 | var definition = EntityMapping.RegisterType(typeof(AdapterTestModel)); 92 | 93 | Assert.AreEqual("Custom", definition.CollectionName); 94 | Assert.AreEqual(1, definition.Indexes.Count()); 95 | } 96 | 97 | } 98 | } -------------------------------------------------------------------------------- /tests/MongoFramework.Tests/Infrastructure/Mapping/Processors/NestedTypeProcessorTests.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | using Microsoft.VisualStudio.TestTools.UnitTesting; 3 | using MongoDB.Bson.Serialization; 4 | using MongoFramework.Infrastructure.Mapping; 5 | using MongoFramework.Infrastructure.Mapping.Processors; 6 | 7 | namespace MongoFramework.Tests.Infrastructure.Mapping.Processors 8 | { 9 | [TestClass] 10 | public class NestedTypeProcessorTests : MappingTestBase 11 | { 12 | public class CollectionBaseModel 13 | { 14 | public ICollection CollectionModel { get; set; } 15 | } 16 | 17 | public class CollectionNestedModel 18 | { 19 | public string HelloWorld { get; set; } 20 | } 21 | 22 | public class PropertyBaseModel 23 | { 24 | public PropertyNestedModel Model { get; set; } 25 | } 26 | 27 | public class PropertyNestedModel 28 | { 29 | public string HelloWorld { get; set; } 30 | } 31 | 32 | [TestMethod] 33 | public void MapsNestedStandardPropertyModel() 34 | { 35 | EntityMapping.AddMappingProcessor(new PropertyMappingProcessor()); 36 | EntityMapping.AddMappingProcessor(new NestedTypeProcessor()); 37 | Assert.IsFalse(BsonClassMap.IsClassMapRegistered(typeof(PropertyNestedModel))); 38 | EntityMapping.RegisterType(typeof(PropertyBaseModel)); 39 | Assert.IsTrue(BsonClassMap.IsClassMapRegistered(typeof(PropertyNestedModel))); 40 | } 41 | 42 | [TestMethod] 43 | public void MapsNestedCollectionPropertyModel() 44 | { 45 | EntityMapping.AddMappingProcessor(new PropertyMappingProcessor()); 46 | EntityMapping.AddMappingProcessor(new NestedTypeProcessor()); 47 | Assert.IsFalse(BsonClassMap.IsClassMapRegistered(typeof(CollectionNestedModel))); 48 | EntityMapping.RegisterType(typeof(CollectionBaseModel)); 49 | Assert.IsTrue(BsonClassMap.IsClassMapRegistered(typeof(CollectionNestedModel))); 50 | } 51 | } 52 | } -------------------------------------------------------------------------------- /tests/MongoFramework.Tests/Infrastructure/Mapping/Processors/PropertyMappingProcessorTests.cs: -------------------------------------------------------------------------------- 1 | using System.ComponentModel.DataAnnotations.Schema; 2 | using System.Linq; 3 | using Microsoft.VisualStudio.TestTools.UnitTesting; 4 | using MongoDB.Bson; 5 | using MongoDB.Bson.Serialization; 6 | using MongoFramework.Infrastructure.Mapping; 7 | using MongoFramework.Infrastructure.Mapping.Processors; 8 | 9 | namespace MongoFramework.Tests.Infrastructure.Mapping.Processors 10 | { 11 | [TestClass] 12 | public class PropertyMappingProcessorTests : MappingTestBase 13 | { 14 | public class ColumnAttributePropertyModel 15 | { 16 | public string Id { get; set; } 17 | [Column("CustomPropertyName")] 18 | public string MyProperty { get; set; } 19 | } 20 | 21 | public class NotMappedPropertiesModel 22 | { 23 | public string Id { get; set; } 24 | [NotMapped] 25 | public string NotMapped { get; set; } 26 | } 27 | public class BaseModel 28 | { 29 | public virtual string TestProperty { get => BaseProperty; set => BaseProperty = value; } 30 | public string BaseProperty { get; set; } 31 | } 32 | 33 | public class ChildModel : BaseModel 34 | { 35 | public override string TestProperty { get => ChildProperty; set => ChildProperty = value; } 36 | public string ChildProperty { get; set; } 37 | } 38 | 39 | [TestMethod] 40 | public void ObeysNotMappedAttribute() 41 | { 42 | EntityMapping.AddMappingProcessor(new PropertyMappingProcessor()); 43 | var definition = EntityMapping.RegisterType(typeof(NotMappedPropertiesModel)); 44 | Assert.IsFalse(definition.Properties.Any(p => p.ElementName == "NotMapped")); 45 | } 46 | 47 | [TestMethod] 48 | public void ObeysColumnAttributeRemap() 49 | { 50 | EntityMapping.AddMappingProcessor(new PropertyMappingProcessor()); 51 | var definition = EntityMapping.RegisterType(typeof(ColumnAttributePropertyModel)); 52 | Assert.IsTrue(definition.Properties.Any(p => p.ElementName == "CustomPropertyName")); 53 | } 54 | 55 | [TestMethod] 56 | public void OverriddenPropertyDeserializationAppliesToChild() 57 | { 58 | EntityMapping.AddMappingProcessor(new PropertyMappingProcessor()); 59 | EntityMapping.RegisterType(typeof(ChildModel)); 60 | 61 | var document = new BsonDocument 62 | { 63 | { "_t", "ChildModel" }, 64 | { "TestProperty", "ChildDeserialization" } 65 | }; 66 | 67 | var deserializedResult = BsonSerializer.Deserialize(document); 68 | Assert.AreEqual("ChildDeserialization", deserializedResult.ChildProperty); 69 | Assert.IsNull(deserializedResult.BaseProperty); 70 | } 71 | 72 | [TestMethod] 73 | public void OverriddenPropertyDeserializationAppliesToBase() 74 | { 75 | EntityMapping.AddMappingProcessor(new PropertyMappingProcessor()); 76 | EntityMapping.RegisterType(typeof(ChildModel)); 77 | 78 | var document = new BsonDocument 79 | { 80 | { "_t", "BaseModel" }, 81 | { "TestProperty", "BaseDeserialization" } 82 | }; 83 | 84 | var deserializedResult = BsonSerializer.Deserialize(document); 85 | Assert.AreEqual("BaseDeserialization", deserializedResult.BaseProperty); 86 | } 87 | 88 | [TestMethod] 89 | public void DerivedTypeDeserializationAppliesToDiscriminatorType() 90 | { 91 | EntityMapping.AddMappingProcessor(new PropertyMappingProcessor()); 92 | EntityMapping.RegisterType(typeof(ChildModel)); 93 | 94 | var document = new BsonDocument 95 | { 96 | { "_t", "ChildModel" }, 97 | { "TestProperty", "ChildDeserialization" } 98 | }; 99 | 100 | var deserializedResult = BsonSerializer.Deserialize(document); 101 | Assert.AreEqual("ChildDeserialization", (deserializedResult as ChildModel).ChildProperty); 102 | Assert.IsNull(deserializedResult.BaseProperty); 103 | } 104 | } 105 | } -------------------------------------------------------------------------------- /tests/MongoFramework.Tests/Infrastructure/Mapping/Processors/SkipMappingProcessorTests.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.ComponentModel.DataAnnotations.Schema; 3 | using Microsoft.VisualStudio.TestTools.UnitTesting; 4 | using MongoFramework.Infrastructure.Mapping; 5 | using MongoFramework.Infrastructure.Mapping.Processors; 6 | 7 | namespace MongoFramework.Tests.Infrastructure.Mapping.Processors 8 | { 9 | [TestClass] 10 | public class SkipMappingProcessorTests : MappingTestBase 11 | { 12 | [NotMapped] 13 | public class SkippedMappingModel 14 | { 15 | } 16 | 17 | public class DefaultModel 18 | { 19 | } 20 | 21 | [TestMethod] 22 | public void ModelSkippedWithAttribute() 23 | { 24 | EntityMapping.AddMappingProcessor(new SkipMappingProcessor()); 25 | 26 | var exception = Assert.ThrowsException(() => EntityMapping.RegisterType(typeof(SkippedMappingModel))); 27 | Assert.IsTrue(exception.Message.Contains("was skipped")); 28 | } 29 | 30 | [TestMethod] 31 | public void ModelNotSkipped() 32 | { 33 | EntityMapping.AddMappingProcessor(new SkipMappingProcessor()); 34 | 35 | var definition = EntityMapping.RegisterType(typeof(DefaultModel)); 36 | Assert.IsNotNull(definition); 37 | } 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /tests/MongoFramework.Tests/Infrastructure/Mapping/PropertyTraversalExtensionTests.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | using System.Linq; 3 | using Microsoft.VisualStudio.TestTools.UnitTesting; 4 | using MongoFramework.Infrastructure.Mapping; 5 | 6 | namespace MongoFramework.Tests.Infrastructure.Mapping; 7 | 8 | [TestClass] 9 | public class PropertyTraversalExtensionTests : TestBase 10 | { 11 | public class TraverseMappingModel 12 | { 13 | public string Id { get; set; } 14 | public NestedTraverseMappingModel NestedModel { get; set; } 15 | public NestedTraverseMappingModel RepeatedType { get; set; } 16 | public TraverseMappingModel RecursionType { get; set; } 17 | 18 | public NestedTraverseMappingModel[] ArrayModel { get; set; } 19 | public IEnumerable EnumerableModel { get; set; } 20 | public List ListModel { get; set; } 21 | 22 | } 23 | public class NestedTraverseMappingModel 24 | { 25 | public string PropertyOne { get; set; } 26 | public int PropertyTwo { get; set; } 27 | public InnerNestedTraverseMappingModel InnerModel { get; set; } 28 | } 29 | public class InnerNestedTraverseMappingModel 30 | { 31 | public string InnerMostProperty { get; set; } 32 | public TraverseMappingModel NestedRecursionType { get; set; } 33 | } 34 | 35 | [TestMethod] 36 | public void TraverseProperties() 37 | { 38 | var definition = EntityMapping.RegisterType(typeof(TraverseMappingModel)); 39 | var result = definition.TraverseProperties().ToArray(); 40 | 41 | Assert.AreEqual(32, result.Length); 42 | 43 | Assert.IsTrue(result.Any(m => m.GetPath() == "RecursionType" && m.Depth == 0)); 44 | 45 | Assert.IsTrue(result.Any(m => m.GetPath() == "NestedModel.PropertyOne" && m.Depth == 1)); 46 | Assert.IsTrue(result.Any(m => m.GetPath() == "NestedModel.InnerModel" && m.Depth == 1)); 47 | Assert.IsTrue(result.Any(m => m.GetPath() == "NestedModel.InnerModel.InnerMostProperty" && m.Depth == 2)); 48 | Assert.IsTrue(result.Any(m => m.GetPath() == "NestedModel.InnerModel.NestedRecursionType" && m.Depth == 2)); 49 | 50 | Assert.IsTrue(result.Any(m => m.GetPath() == "RepeatedType.PropertyOne" && m.Depth == 1)); 51 | Assert.IsTrue(result.Any(m => m.GetPath() == "RepeatedType.InnerModel" && m.Depth == 1)); 52 | Assert.IsTrue(result.Any(m => m.GetPath() == "RepeatedType.InnerModel.InnerMostProperty" && m.Depth == 2)); 53 | Assert.IsTrue(result.Any(m => m.GetPath() == "RepeatedType.InnerModel.NestedRecursionType" && m.Depth == 2)); 54 | 55 | Assert.IsTrue(result.Any(m => m.GetPath() == "ArrayModel" && m.Depth == 0)); 56 | Assert.IsTrue(result.Any(m => m.GetPath() == "ArrayModel.InnerModel" && m.Depth == 1)); 57 | Assert.IsTrue(result.Any(m => m.GetPath() == "ArrayModel.InnerModel.InnerMostProperty" && m.Depth == 2)); 58 | Assert.IsTrue(result.Any(m => m.GetPath() == "ArrayModel.InnerModel.NestedRecursionType" && m.Depth == 2)); 59 | 60 | Assert.IsTrue(result.Any(m => m.GetPath() == "EnumerableModel" && m.Depth == 0)); 61 | Assert.IsTrue(result.Any(m => m.GetPath() == "EnumerableModel.InnerModel" && m.Depth == 1)); 62 | Assert.IsTrue(result.Any(m => m.GetPath() == "EnumerableModel.InnerModel.InnerMostProperty" && m.Depth == 2)); 63 | Assert.IsTrue(result.Any(m => m.GetPath() == "EnumerableModel.InnerModel.NestedRecursionType" && m.Depth == 2)); 64 | 65 | Assert.IsTrue(result.Any(m => m.GetPath() == "ListModel" && m.Depth == 0)); 66 | Assert.IsTrue(result.Any(m => m.GetPath() == "ListModel.InnerModel" && m.Depth == 1)); 67 | Assert.IsTrue(result.Any(m => m.GetPath() == "ListModel.InnerModel.InnerMostProperty" && m.Depth == 2)); 68 | Assert.IsTrue(result.Any(m => m.GetPath() == "ListModel.InnerModel.NestedRecursionType" && m.Depth == 2)); 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /tests/MongoFramework.Tests/Infrastructure/Serialization/TypeDiscoveryIntegrationTests.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | using System.Linq; 3 | using Microsoft.VisualStudio.TestTools.UnitTesting; 4 | using MongoFramework.Attributes; 5 | 6 | namespace MongoFramework.Tests.Infrastructure.Serialization 7 | { 8 | [TestClass] 9 | public class TypeDiscoveryIntegrationTests : TestBase 10 | { 11 | [RuntimeTypeDiscovery] 12 | public class RootKnownBaseModel 13 | { 14 | public string Id { get; set; } 15 | public string Description { get; set; } 16 | } 17 | 18 | public class UnknownChildToRootModel : RootKnownBaseModel 19 | { 20 | public string AdditionProperty { get; set; } 21 | } 22 | 23 | public class UnknownPropertyTypeModel 24 | { 25 | public string Id { get; set; } 26 | public object UnknownItem { get; set; } 27 | } 28 | 29 | public class UnknownPropertyTypeChildModel 30 | { 31 | public string Description { get; set; } 32 | } 33 | 34 | [TestMethod] 35 | public void ReadAndWriteRootEntity() 36 | { 37 | var connection = TestConfiguration.GetConnection(); 38 | var context = new MongoDbContext(connection); 39 | var dbSet = new MongoDbSet(context); 40 | 41 | var rootEntity = new RootKnownBaseModel 42 | { 43 | Description = "ReadAndWriteRootEntity-RootKnownBaseModel" 44 | }; 45 | dbSet.Add(rootEntity); 46 | 47 | var childEntity = new UnknownChildToRootModel 48 | { 49 | Description = "ReadAndWriteRootEntity-UnknownChildToRootModel" 50 | }; 51 | dbSet.Add(childEntity); 52 | 53 | context.SaveChanges(); 54 | 55 | ResetMongoDb(); 56 | dbSet = new MongoDbSet(context); 57 | 58 | var dbRootEntity = dbSet.Where(e => e.Id == rootEntity.Id).FirstOrDefault(); 59 | Assert.IsNotNull(dbRootEntity); 60 | Assert.IsInstanceOfType(dbRootEntity, typeof(RootKnownBaseModel)); 61 | 62 | var dbChildEntity = dbSet.Where(e => e.Id == childEntity.Id).FirstOrDefault(); 63 | Assert.IsNotNull(dbChildEntity); 64 | Assert.IsInstanceOfType(dbChildEntity, typeof(UnknownChildToRootModel)); 65 | } 66 | 67 | [TestMethod] 68 | public void ReadAndWriteUnknownPropertyTypeEntity() 69 | { 70 | var connection = TestConfiguration.GetConnection(); 71 | var context = new MongoDbContext(connection); 72 | var dbSet = new MongoDbSet(context); 73 | 74 | var entities = new[] 75 | { 76 | new UnknownPropertyTypeModel(), 77 | new UnknownPropertyTypeModel 78 | { 79 | UnknownItem = new UnknownPropertyTypeChildModel 80 | { 81 | Description = "UnknownPropertyTypeChildModel" 82 | } 83 | }, 84 | new UnknownPropertyTypeModel 85 | { 86 | UnknownItem = new Dictionary 87 | { 88 | { "Age", 1 } 89 | } 90 | } 91 | }; 92 | 93 | dbSet.AddRange(entities); 94 | context.SaveChanges(); 95 | 96 | ResetMongoDb(); 97 | dbSet = new MongoDbSet(context); 98 | 99 | var dbEntities = dbSet.ToArray(); 100 | Assert.IsNull(dbEntities[0].UnknownItem); 101 | Assert.IsInstanceOfType(dbEntities[1].UnknownItem, typeof(UnknownPropertyTypeChildModel)); 102 | Assert.IsInstanceOfType(dbEntities[2].UnknownItem, typeof(Dictionary)); 103 | } 104 | } 105 | } 106 | -------------------------------------------------------------------------------- /tests/MongoFramework.Tests/MongoDbConnectionTests.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using Microsoft.VisualStudio.TestTools.UnitTesting; 3 | 4 | namespace MongoFramework.Tests 5 | { 6 | [TestClass] 7 | public class MongoDbConnectionTests 8 | { 9 | [TestMethod] 10 | public void ConnectionFromConnectionString() 11 | { 12 | var connection = MongoDbConnection.FromConnectionString("mongodb://localhost:27017/MongoFrameworkTests"); 13 | Assert.IsNotNull(connection); 14 | } 15 | 16 | [TestMethod, ExpectedException(typeof(ArgumentNullException))] 17 | public void NullUrlThrowsException() 18 | { 19 | MongoDbConnection.FromUrl(null); 20 | } 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /tests/MongoFramework.Tests/MongoDbDriverHelper.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Concurrent; 3 | using System.Collections.Generic; 4 | using System.Reflection; 5 | using MongoDB.Bson; 6 | using MongoDB.Bson.Serialization; 7 | 8 | namespace MongoFramework.Tests 9 | { 10 | public static class MongoDbDriverHelper 11 | { 12 | public static void ResetDriver() 13 | { 14 | //Primarily introduced to better test TypeDiscoverySerializer, this is designed to reset the MongoDB driver 15 | //as if the assembly just loaded. It is likely incomplete and would be easily subject to breaking in future 16 | //driver updates. If someone knows a better way to reset the MongoDB driver, please open a pull request! 17 | 18 | var classMapField = typeof(BsonClassMap).GetField("__classMaps", BindingFlags.NonPublic | BindingFlags.Static); 19 | if (classMapField.GetValue(null) is Dictionary classMaps) 20 | { 21 | classMaps.Clear(); 22 | } 23 | 24 | var knownTypesField = typeof(BsonSerializer).GetField("__typesWithRegisteredKnownTypes", BindingFlags.NonPublic | BindingFlags.Static); 25 | if (knownTypesField.GetValue(null) is HashSet knownTypes) 26 | { 27 | knownTypes.Clear(); 28 | } 29 | 30 | var discriminatorTypesField = typeof(BsonSerializer).GetField("__discriminatedTypes", BindingFlags.NonPublic | BindingFlags.Static); 31 | if (discriminatorTypesField.GetValue(null) is HashSet discriminatorTypes) 32 | { 33 | discriminatorTypes.Clear(); 34 | } 35 | 36 | var discriminatorsField = typeof(BsonSerializer).GetField("__discriminators", BindingFlags.NonPublic | BindingFlags.Static); 37 | if (discriminatorsField.GetValue(null) is Dictionary> discriminators) 38 | { 39 | discriminators.Clear(); 40 | } 41 | 42 | var serializerRegistryField = typeof(BsonSerializer).GetField("__serializerRegistry", BindingFlags.NonPublic | BindingFlags.Static); 43 | if (serializerRegistryField.GetValue(null) is BsonSerializerRegistry registry) 44 | { 45 | var cacheField = typeof(BsonSerializerRegistry).GetField("_cache", BindingFlags.NonPublic | BindingFlags.Instance); 46 | var registryCache = cacheField.GetValue(registry) as ConcurrentDictionary; 47 | registryCache.Clear(); 48 | } 49 | } 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /tests/MongoFramework.Tests/MongoDbUtilityTests.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.VisualStudio.TestTools.UnitTesting; 2 | using MongoFramework.Infrastructure; 3 | 4 | namespace MongoFramework.Tests.Infrastructure 5 | { 6 | [TestClass] 7 | public class MongoDbUtilityTests : TestBase 8 | { 9 | public class MongoDbUtilityModel 10 | { 11 | public string Id { get; set; } 12 | } 13 | 14 | [TestMethod] 15 | public void ValidObjectId() 16 | { 17 | var connection = TestConfiguration.GetConnection(); 18 | var context = new MongoDbContext(connection); 19 | 20 | var entity = new MongoDbUtilityModel(); 21 | context.ChangeTracker.SetEntityState(entity, EntityEntryState.Added); 22 | context.SaveChanges(); 23 | 24 | Assert.IsTrue(MongoDbUtility.IsValidObjectId(entity.Id)); 25 | } 26 | 27 | [TestMethod] 28 | public void InvalidObjectId() 29 | { 30 | Assert.IsFalse(MongoDbUtility.IsValidObjectId(string.Empty)); 31 | Assert.IsFalse(MongoDbUtility.IsValidObjectId("0")); 32 | Assert.IsFalse(MongoDbUtility.IsValidObjectId("a")); 33 | Assert.IsFalse(MongoDbUtility.IsValidObjectId("0123456789ABCDEFGHIJKLMN")); 34 | Assert.IsFalse(MongoDbUtility.IsValidObjectId(null)); 35 | } 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /tests/MongoFramework.Tests/MongoFramework.Tests.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | MongoFramework.Tests 5 | MongoFramework.Tests 6 | net461;net48;net6.0;net7.0 7 | false 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | all 17 | runtime; build; native; contentfiles; analyzers; buildtransitive 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | -------------------------------------------------------------------------------- /tests/MongoFramework.Tests/TestBase.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.VisualStudio.TestTools.UnitTesting; 2 | using MongoDB.Driver; 3 | using MongoFramework.Infrastructure; 4 | using MongoFramework.Infrastructure.Indexing; 5 | using MongoFramework.Infrastructure.Mapping; 6 | using MongoFramework.Infrastructure.Serialization; 7 | 8 | namespace MongoFramework.Tests 9 | { 10 | [TestClass] 11 | public abstract class TestBase 12 | { 13 | protected static void ResetMongoDb() 14 | { 15 | MongoDbDriverHelper.ResetDriver(); 16 | EntityMapping.RemoveAllDefinitions(); 17 | 18 | EntityMapping.RemoveAllMappingProcessors(); 19 | EntityMapping.AddMappingProcessors(DefaultMappingProcessors.Processors); 20 | 21 | TypeDiscovery.ClearCache(); 22 | EntityIndexWriter.ClearCache(); 23 | 24 | DriverAbstractionRules.ApplyRules(); 25 | } 26 | 27 | protected static void ClearDatabase() 28 | { 29 | //Removing the database created for the tests 30 | var client = new MongoClient(TestConfiguration.ConnectionString); 31 | client.DropDatabase(TestConfiguration.GetDatabaseName()); 32 | } 33 | 34 | [TestInitialize] 35 | public void Initialise() 36 | { 37 | ResetMongoDb(); 38 | ClearDatabase(); 39 | } 40 | 41 | [AssemblyCleanup] 42 | public static void AssemblyCleanup() 43 | { 44 | ClearDatabase(); 45 | } 46 | } 47 | } -------------------------------------------------------------------------------- /tests/MongoFramework.Tests/TestConfiguration.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using MongoDB.Driver; 3 | 4 | namespace MongoFramework.Tests 5 | { 6 | static class TestConfiguration 7 | { 8 | public static string ConnectionString => Environment.GetEnvironmentVariable("MONGODB_URI") ?? "mongodb://localhost"; 9 | 10 | public static string GetTenantId() 11 | { 12 | return "testkey1"; 13 | } 14 | 15 | public static string GetDatabaseName() 16 | { 17 | return "MongoFrameworkTests"; 18 | } 19 | 20 | public static IMongoDbConnection GetConnection() 21 | { 22 | var urlBuilder = new MongoUrlBuilder(ConnectionString) 23 | { 24 | DatabaseName = GetDatabaseName() 25 | }; 26 | return MongoDbConnection.FromUrl(urlBuilder.ToMongoUrl()); 27 | } 28 | } 29 | } -------------------------------------------------------------------------------- /tests/MongoFramework.Tests/Utilities/CheckTests.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) .NET Foundation. All rights reserved. 2 | // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. 3 | // https://github.com/dotnet/efcore/blob/main/test/EFCore.Tests/Utilities/CheckTest.cs 4 | 5 | using System; 6 | using Microsoft.VisualStudio.TestTools.UnitTesting; 7 | using MongoFramework.Utilities; 8 | 9 | namespace MongoFramework.Tests.Utilities 10 | { 11 | [TestClass] 12 | public class CheckTest 13 | { 14 | [TestMethod] 15 | public void Not_null_throws_when_arg_is_null() 16 | { 17 | Assert.ThrowsException(() => Check.NotNull(null, "foo")); 18 | } 19 | 20 | [TestMethod] 21 | public void Not_null_throws_when_arg_name_empty() 22 | { 23 | Assert.ThrowsException(() => Check.NotNull(null as object, string.Empty)); 24 | } 25 | 26 | [TestMethod] 27 | public void Not_empty_throws_when_empty() 28 | { 29 | Assert.ThrowsException(() => Check.NotEmpty("", string.Empty)); 30 | } 31 | 32 | [TestMethod] 33 | public void Not_empty_throws_when_whitespace() 34 | { 35 | Assert.ThrowsException(() => Check.NotEmpty(" ", string.Empty)); 36 | } 37 | 38 | [TestMethod] 39 | public void Not_empty_throws_when_parameter_name_null() 40 | { 41 | Assert.ThrowsException(() => Check.NotEmpty(null, null)); 42 | } 43 | 44 | [TestMethod] 45 | public void Generic_Not_empty_throws_when_arg_is_empty() 46 | { 47 | Assert.ThrowsException(() => Check.NotEmpty(Array.Empty(), "foo")); 48 | } 49 | 50 | [TestMethod] 51 | public void Generic_Not_empty_throws_when_arg_is_null() 52 | { 53 | Assert.ThrowsException(() => Check.NotEmpty(null, "foo")); 54 | } 55 | 56 | [TestMethod] 57 | public void Generic_Not_empty_throws_when_arg_name_empty() 58 | { 59 | Assert.ThrowsException(() => Check.NotEmpty(null, string.Empty)); 60 | } 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /tests/MongoFramework.Tests/app.config: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | 5 | 6 | --------------------------------------------------------------------------------