├── .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