├── .editorconfig ├── .github ├── FUNDING.yml ├── dependabot.yml └── workflows │ └── build.yml ├── .gitignore ├── .idea └── .idea.Foundatio.Repositories │ └── .idea │ ├── indexLayout.xml │ ├── projectSettingsUpdater.xml │ └── vcs.xml ├── .vscode ├── launch.json ├── settings.json └── tasks.json ├── Foundatio.Repositories.sln ├── LICENSE.txt ├── NuGet.config ├── README.md ├── build ├── Foundatio.snk ├── common.props └── foundatio-icon.png ├── docker-compose.yml ├── global.json ├── samples ├── Directory.Build.props └── Foundatio.SampleApp │ ├── Client │ ├── App.razor │ ├── Foundatio.SampleApp.Client.csproj │ ├── Pages │ │ ├── Counter.razor │ │ └── Index.razor │ ├── Program.cs │ ├── Properties │ │ └── launchSettings.json │ ├── Shared │ │ ├── MainLayout.razor │ │ ├── MainLayout.razor.css │ │ ├── NavMenu.razor │ │ ├── NavMenu.razor.css │ │ └── SurveyPrompt.razor │ ├── _Imports.razor │ └── wwwroot │ │ ├── css │ │ └── app.css │ │ ├── favicon.png │ │ ├── icon-192.png │ │ └── index.html │ ├── Server │ ├── Foundatio.SampleApp.Server.csproj │ ├── Pages │ │ ├── Error.cshtml │ │ └── Error.cshtml.cs │ ├── Program.cs │ ├── Properties │ │ └── launchSettings.json │ ├── Repositories │ │ ├── Configuration │ │ │ ├── ElasticExtensions.cs │ │ │ ├── Indexes │ │ │ │ └── GameReviewIndex.cs │ │ │ └── SampleAppElasticConfiguration.cs │ │ └── GameReviewRepository.cs │ ├── appsettings.Development.json │ └── appsettings.json │ └── Shared │ ├── Foundatio.SampleApp.Shared.csproj │ └── GameReview.cs ├── src ├── Directory.Build.props ├── Foundatio.Repositories.Elasticsearch │ ├── Configuration │ │ ├── DailyIndex.cs │ │ ├── DynamicIndex.cs │ │ ├── ElasticConfiguration.cs │ │ ├── IElasticConfiguration.cs │ │ ├── IIndex.cs │ │ ├── Index.cs │ │ ├── MonthlyIndex.cs │ │ └── VersionedIndex.cs │ ├── CustomFields │ │ ├── CustomFieldDefinition.cs │ │ ├── CustomFieldDefinitionRepository.cs │ │ ├── ICustomFieldType.cs │ │ └── StandardFieldTypes │ │ │ ├── BooleanFieldType.cs │ │ │ ├── DateFieldType.cs │ │ │ ├── DoubleFieldType.cs │ │ │ ├── FloatFieldType.cs │ │ │ ├── IntegerFieldType.cs │ │ │ ├── KeywordFieldType.cs │ │ │ ├── LongFieldType.cs │ │ │ └── StringFieldType.cs │ ├── ElasticUtility.cs │ ├── Extensions │ │ ├── CollectionExtensions.cs │ │ ├── ElasticIndexExtensions.cs │ │ ├── ElasticLazyDocument.cs │ │ ├── FindHitExtensions.cs │ │ ├── FindResultsExtensions.cs │ │ ├── IBodyWithApiCallDetailsExtensions.cs │ │ ├── LoggerExtensions.cs │ │ └── ResolverExtensions.cs │ ├── Foundatio.Repositories.Elasticsearch.csproj │ ├── Jobs │ │ ├── CleanupIndexesJob.cs │ │ ├── CleanupSnapshotJob.cs │ │ ├── ElasticMigrationJob.cs │ │ ├── MaintainIndexesJob.cs │ │ ├── ReindexWorkItem.cs │ │ ├── ReindexWorkItemHandler.cs │ │ └── SnapshotJob.cs │ ├── Options │ │ └── ElasticCommandOptions.cs │ ├── Queries │ │ ├── Builders │ │ │ ├── AggregationsQueryBuilder.cs │ │ │ ├── ChildQueryBuilder.cs │ │ │ ├── DateRangeQueryBuilder.cs │ │ │ ├── ElasticFilterQueryBuilder.cs │ │ │ ├── ElasticQueryBuilder.cs │ │ │ ├── ExpressionQueryBuilder.cs │ │ │ ├── FieldConditionsQueryBuilder.cs │ │ │ ├── FieldIncludesQueryBuilder.cs │ │ │ ├── IElasticQueryBuilder.cs │ │ │ ├── IdentityQueryBuilder.cs │ │ │ ├── PageableQueryBuilder.cs │ │ │ ├── ParentQueryBuilder.cs │ │ │ ├── RuntimeFieldsQueryBuilder.cs │ │ │ ├── SearchAfterQueryBuilder.cs │ │ │ ├── SoftDeletesQueryBuilder.cs │ │ │ └── SortQueryBuilder.cs │ │ └── QueryParts │ │ │ └── ElasticIndexesQuery.cs │ ├── Repositories │ │ ├── ElasticDocumentVersion.cs │ │ ├── ElasticReadOnlyRepositoryBase.cs │ │ ├── ElasticReindexer.cs │ │ ├── ElasticRepositoryBase.cs │ │ ├── IParentChildDocument.cs │ │ └── MigrationStateRepository.cs │ └── Utility │ │ └── FieldIncludeParser.cs └── Foundatio.Repositories │ ├── Exceptions │ ├── AsyncQueryNotFoundException.cs │ ├── DocumentException.cs │ ├── DocumentNotFoundException.cs │ ├── DocumentValidationException.cs │ ├── DuplicateDocumentException.cs │ ├── RepositoryException.cs │ └── VersionConflictDocumentException.cs │ ├── Extensions │ ├── AggregationsExtensions.cs │ ├── DictionaryExtensions.cs │ ├── EnumerableExtensions.cs │ ├── FindResultsExtensions.cs │ ├── NumberExtensions.cs │ ├── StringExtensions.cs │ └── TaskExtensions.cs │ ├── Foundatio.Repositories.csproj │ ├── IReadOnlyRepository.cs │ ├── IRepository.cs │ ├── ISearchableReadOnlyRepository.cs │ ├── ISearchableRepository.cs │ ├── Id.cs │ ├── JsonPatch │ ├── AbstractPatcher.cs │ ├── AddOperation.cs │ ├── CopyOperation.cs │ ├── JsonDiffer.cs │ ├── JsonPatcher.cs │ ├── MoveOperation.cs │ ├── Operation.cs │ ├── PatchDocument.cs │ ├── PatchDocumentConverter.cs │ ├── RemoveOperation.cs │ ├── ReplaceOperation.cs │ └── TestOperation.cs │ ├── Migration │ ├── IMigration.cs │ ├── IMigrationStateRepository.cs │ ├── MigrationBase.cs │ ├── MigrationManager.cs │ └── MigrationState.cs │ ├── Models │ ├── Aggregations │ │ ├── AggregationsHelper.cs │ │ ├── BucketAggregate.cs │ │ ├── BucketAggregateBase.cs │ │ ├── BucketBase.cs │ │ ├── BucketedAggregation.cs │ │ ├── DateHistogramBucket.cs │ │ ├── ExtendedStatsAggregate.cs │ │ ├── IAggregate.cs │ │ ├── IBucket.cs │ │ ├── KeyedAggregation.cs │ │ ├── KeyedBucket.cs │ │ ├── MetricAggregateBase.cs │ │ ├── MultiBucketAggregate.cs │ │ ├── ObjectValueAggregate.cs │ │ ├── PercentilesAggregate.cs │ │ ├── RangeBucket.cs │ │ ├── SingleBucketAggregate.cs │ │ ├── StandardDeviationBounds.cs │ │ ├── StatsAggregate.cs │ │ ├── TermsAggregate.cs │ │ ├── TopHitsAggregate.cs │ │ └── ValueAggregate.cs │ ├── BeforeGetEventArgs.cs │ ├── BeforePublishEntityChangedEventArgs.cs │ ├── BeforeQueryEventArgs.cs │ ├── ChangeType.cs │ ├── DocumentChangeEventArgs.cs │ ├── EmptyReadOnly.cs │ ├── EntityChanged.cs │ ├── FindResults.cs │ ├── IHaveDates.cs │ ├── IIdentity.cs │ ├── IPatchOperation.cs │ ├── ISupportSoftDeletes.cs │ ├── IVersioned.cs │ └── LazyDocument.cs │ ├── Options │ ├── AsyncQueryOptions.cs │ ├── CacheOptions.cs │ ├── CommandOptionsDescriptor.cs │ ├── ConsistencyOptions.cs │ ├── ICommandOptions.cs │ ├── IOptions.cs │ ├── NotificationOptions.cs │ ├── OriginalOptions.cs │ ├── PagingOptions.cs │ ├── QueryLogLevelOptions.cs │ ├── SoftDeletesOptions.cs │ ├── TimeProviderOptions.cs │ ├── TimeoutOptions.cs │ ├── UpdatedIdsCallbackOptions.cs │ ├── ValidationOptions.cs │ └── VersionOptions.cs │ ├── Queries │ ├── IRepositoryQuery.cs │ ├── ISystemFilter.cs │ ├── IdentityQuery.cs │ └── RepositoryQueryDescriptor.cs │ └── Utility │ ├── AggregationsNewtonsoftJsonConverter.cs │ ├── AggregationsSystemTextJsonConverter.cs │ ├── BucketsNewtonsoftJsonConverter.cs │ ├── BucketsSystemTextJsonConverter.cs │ ├── DoubleSystemTextJsonConverter.cs │ ├── ObjectId.cs │ ├── ObjectToInferredTypesConverter.cs │ └── TypeHelper.cs └── tests ├── Directory.Build.props ├── Foundatio.Repositories.Elasticsearch.Tests ├── AggregationQueryTests.cs ├── CustomFieldTests.cs ├── DailyRepositoryTests.cs ├── ElasticRepositoryTestBase.cs ├── Extensions │ └── ElasticsearchExtensions.cs ├── FieldIncludeParserTests.cs ├── Foundatio.Repositories.Elasticsearch.Tests.csproj ├── IndexTests.cs ├── MigrationTests.cs ├── MonthlyRepositoryTests.cs ├── ParentChildTests.cs ├── PipelineTests.cs ├── Properties │ └── AssemblyInfo.cs ├── QueryBuilderTests.cs ├── QueryTests.cs ├── QueryableReadOnlyRepositoryTests.cs ├── QueryableRepositoryTests.cs ├── ReadOnlyRepositoryTests.cs ├── ReindexTests.cs ├── Repositories │ ├── ChildRepository.cs │ ├── Configuration │ │ ├── ElasticsearchJsonNetSerializer.cs │ │ ├── Indexes │ │ │ ├── CalculatedIntegerFieldType.cs │ │ │ ├── DailyFileAccessHistoryIndex.cs │ │ │ ├── DailyLogEventIndex.cs │ │ │ ├── EmployeeIndex.cs │ │ │ ├── EmployeeWithCustomFieldsIndex.cs │ │ │ ├── IdentityIndex.cs │ │ │ ├── MonthlyFileAccessHistoryIndex.cs │ │ │ ├── MonthlyLogEventIndex.cs │ │ │ └── ParentChildIndex.cs │ │ └── MyAppElasticConfiguration.cs │ ├── EmployeeRepository.cs │ ├── EmployeeWithCustomFieldsRepository.cs │ ├── FileAccessHistoryRepository.cs │ ├── IdentityRepository.cs │ ├── LogEventRepository.cs │ ├── Models │ │ ├── Child.cs │ │ ├── Employee.cs │ │ ├── FileAccessHistory.cs │ │ ├── Identity.cs │ │ ├── LogEvent.cs │ │ └── Parent.cs │ ├── ParentRepository.cs │ └── Queries │ │ ├── AgeQuery.cs │ │ ├── CompanyQuery.cs │ │ └── EmailAddressQuery.cs ├── RepositoryTests.cs └── VersionedTests.cs └── Foundatio.Repositories.Tests ├── Foundatio.Repositories.Tests.csproj ├── JsonPatch └── JsonPatchTests.cs ├── ObjectIdTests.cs └── Properties └── AssemblyInfo.cs /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | github: exceptionless 2 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | 4 | - package-ecosystem: nuget 5 | directory: "/" 6 | schedule: 7 | interval: weekly 8 | 9 | - package-ecosystem: "docker-compose" 10 | directory: "/" 11 | schedule: 12 | interval: quarterly 13 | ignore: 14 | - dependency-name: "elasticsearch/elasticsearch" 15 | versions: 16 | - "<8.0.0" 17 | - ">=9.0.0" 18 | - dependency-name: "kibana/kibana" 19 | versions: 20 | - "<8.0.0" 21 | - ">=9.0.0" 22 | -------------------------------------------------------------------------------- /.github/workflows/build.yml: -------------------------------------------------------------------------------- 1 | name: Build 2 | on: [push, pull_request] 3 | 4 | jobs: 5 | build: 6 | uses: FoundatioFx/Foundatio/.github/workflows/build-workflow.yml@main 7 | secrets: inherit 8 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # User-specific files 2 | *.suo 3 | *.user 4 | 5 | # Build results 6 | [Bb]in/ 7 | [Oo]bj/ 8 | artifacts 9 | .vs/ 10 | 11 | # MSTest test Results 12 | [Tt]est[Rr]esult*/ 13 | 14 | # ReSharper is a .NET coding add-in 15 | _ReSharper*/ 16 | *.[Rr]e[Ss]harper 17 | *.DotSettings.user 18 | 19 | # JustCode is a .NET coding addin-in 20 | .JustCode 21 | 22 | # DotCover is a Code Coverage Tool 23 | *.dotCover 24 | 25 | # NCrunch 26 | _NCrunch_* 27 | .*crunch*.local.xml 28 | 29 | # NuGet Packages 30 | *.nupkg 31 | 32 | .DS_Store 33 | 34 | # Rider 35 | 36 | # User specific 37 | **/.idea/**/workspace.xml 38 | **/.idea/**/tasks.xml 39 | **/.idea/shelf/* 40 | **/.idea/dictionaries 41 | 42 | # Sensitive or high-churn files 43 | **/.idea/**/dataSources/ 44 | **/.idea/**/dataSources.ids 45 | **/.idea/**/dataSources.xml 46 | **/.idea/**/dataSources.local.xml 47 | **/.idea/**/sqlDataSources.xml 48 | **/.idea/**/dynamic.xml 49 | 50 | # Rider 51 | # Rider auto-generates .iml files, and contentModel.xml 52 | **/.idea/**/*.iml 53 | **/.idea/**/contentModel.xml 54 | **/.idea/**/modules.xml -------------------------------------------------------------------------------- /.idea/.idea.Foundatio.Repositories/.idea/indexLayout.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /.idea/.idea.Foundatio.Repositories/.idea/projectSettingsUpdater.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 7 | -------------------------------------------------------------------------------- /.idea/.idea.Foundatio.Repositories/.idea/vcs.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | // Use IntelliSense to find out which attributes exist for C# debugging 3 | // Use hover for the description of the existing attributes 4 | // For further information visit https://github.com/OmniSharp/omnisharp-vscode/blob/master/debugger-launchjson.md 5 | "version": "0.2.0", 6 | "configurations": [ 7 | { 8 | "name": ".NET Core Launch (console)", 9 | "type": "coreclr", 10 | "request": "launch", 11 | "preLaunchTask": "build", 12 | // If you have changed target frameworks, make sure to update the program path. 13 | "program": "${workspaceFolder}/Tests/Foundatio.Repositories.Elasticsearch.Tests/bin/Debug/net8.0/Foundatio.Repositories.Elasticsearch.Tests.dll", 14 | "args": [], 15 | "cwd": "${workspaceFolder}/Tests/Foundatio.Repositories.Elasticsearch.Tests", 16 | // For more information about the 'console' field, see https://github.com/OmniSharp/omnisharp-vscode/blob/master/debugger-launchjson.md#console-terminal-window 17 | "console": "internalConsole", 18 | "stopAtEntry": false, 19 | "internalConsoleOptions": "openOnSessionStart" 20 | }, 21 | { 22 | "name": ".NET Core Attach", 23 | "type": "coreclr", 24 | "request": "attach", 25 | "processId": "${command:pickProcess}" 26 | } 27 | ,] 28 | } -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "cSpell.words": [ 3 | "Reindex", 4 | "Reindexing", 5 | "reindexer" 6 | ] 7 | } -------------------------------------------------------------------------------- /.vscode/tasks.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "2.0.0", 3 | "tasks": [ 4 | { 5 | "label": "build", 6 | "command": "dotnet", 7 | "type": "process", 8 | "group": { 9 | "kind": "build", 10 | "isDefault": true 11 | }, 12 | "args": [ 13 | "build", 14 | "${workspaceFolder}/Foundatio.Repositories.sln", 15 | "/p:GenerateFullPaths=true" 16 | ], 17 | "problemMatcher": "$msCompile" 18 | }, 19 | { 20 | "label": "test: shared", 21 | "command": "dotnet", 22 | "type": "process", 23 | "group": { 24 | "kind": "test", 25 | "isDefault": true 26 | }, 27 | "args": [ 28 | "test", 29 | "${workspaceFolder}/tests/Foundatio.Repositories.Tests/Foundatio.Repositories.Tests.csproj", 30 | "/p:GenerateFullPaths=true" 31 | ], 32 | "problemMatcher": "$msCompile" 33 | }, 34 | { 35 | "label": "test: elasticsearch", 36 | "command": "dotnet", 37 | "type": "process", 38 | "group": { 39 | "kind": "test", 40 | "isDefault": true 41 | }, 42 | "args": [ 43 | "test", 44 | "${workspaceFolder}/tests/Foundatio.Repositories.Elasticsearch.Tests/Foundatio.Repositories.Elasticsearch.Tests.csproj", 45 | "/p:GenerateFullPaths=true" 46 | ], 47 | "problemMatcher": "$msCompile" 48 | }, 49 | { 50 | "label": "pack", 51 | "command": "dotnet pack -c Release -o ${workspaceFolder}/artifacts", 52 | "type": "shell", 53 | "problemMatcher": [] 54 | }, 55 | { 56 | "label": "docker: elasticsearch", 57 | "command": "docker compose up", 58 | "type": "shell", 59 | "isBackground": true, 60 | "group": "test", 61 | "problemMatcher": [] 62 | } 63 | ] 64 | } -------------------------------------------------------------------------------- /NuGet.config: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ![Foundatio](https://raw.githubusercontent.com/FoundatioFx/Foundatio/master/media/foundatio-dark-bg.svg#gh-dark-mode-only "Foundatio")![Foundatio](https://raw.githubusercontent.com/FoundatioFx/Foundatio/master/media/foundatio.svg#gh-light-mode-only "Foundatio") 2 | 3 | [![Build status](https://github.com/FoundatioFx/Foundatio.Repositories/workflows/Build/badge.svg)](https://github.com/FoundatioFx/Foundatio.Repositories/actions) 4 | [![NuGet Version](http://img.shields.io/nuget/v/Foundatio.Repositories.svg?style=flat)](https://www.nuget.org/packages/Foundatio.Repositories/) 5 | [![feedz.io](https://img.shields.io/badge/endpoint.svg?url=https%3A%2F%2Ff.feedz.io%2Ffoundatio%2Ffoundatio%2Fshield%2FFoundatio.Repositories%2Flatest)](https://f.feedz.io/foundatio/foundatio/packages/Foundatio.Repositories/latest/download) 6 | [![Discord](https://img.shields.io/discord/715744504891703319)](https://discord.gg/6HxgFCx) 7 | 8 | Generic repository contract and implementations. Currently only implemented for Elasticsearch, but there are plans for other implementations. 9 | 10 | # Features 11 | 12 | - Simple document repository pattern 13 | - CRUD operations: Add, Save, Remove, Get 14 | - Supports patch operations 15 | - JSON patch 16 | - Partial document patch 17 | - Painless script patch (Elasticsearch) 18 | - Can be applied in bulk using queries 19 | - Async events that can be wired up and listened to outside of the repos 20 | - Caching (real-time invalidated before save and stored in distributed cache) 21 | - Message bus support (enables real-time apps sends messages like doc updated up to the client so they know to update the UI) 22 | - Searchable that works with Foundatio.Parsers lib for dynamic querying, filtering, sorting and aggregations 23 | - Document validation 24 | - Document versioning 25 | - Soft deletes 26 | - Auto document created and updated dates 27 | - Document migrations 28 | - Elasticsearch implementation 29 | - Plan to add additional implementations (Postgres with Marten would be a good fit) 30 | - Elasticsearch index configuration allows simpler and more organized configuration 31 | - Schema versioning 32 | - Parent child queries 33 | - Daily and monthly index strategies 34 | - Supports different consistency models (immediate, wait or eventual) 35 | - Can be configured at the index type or individual query level 36 | - Query builders used to make common ways of querying data easier and more portable between repo implementations 37 | - Can still use raw Elasticsearch queries 38 | - Field includes and excludes to make the response size smaller 39 | - Field conditions query builder 40 | - Paging including snapshot paging support 41 | - Dynamic field resolution for using friendly names of dynamically generated fields 42 | - Jobs for index maintenance, snapshots, reindex 43 | - Strongly typed field access (using lambda expressions) to enable refactoring 44 | -------------------------------------------------------------------------------- /build/Foundatio.snk: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/FoundatioFx/Foundatio.Repositories/77e67f095c84cc63e7633f4b860c288c605132d0/build/Foundatio.snk -------------------------------------------------------------------------------- /build/common.props: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | netstandard2.0;net8.0 5 | Foundatio Elasticsearch Repositories 6 | Generic Repository implementations for Elasticsearch. 7 | https://github.com/FoundatioFx/Foundatio.Repositories 8 | https://github.com/FoundatioFx/Foundatio.Repositories/releases 9 | true 10 | v 11 | true 12 | false 13 | $(ProjectDir)..\..\..\ 14 | 15 | Copyright (c) 2025 Foundatio. All rights reserved. 16 | FoundatioFx 17 | $(NoWarn);CS1591 18 | true 19 | latest 20 | true 21 | $(SolutionDir)artifacts 22 | foundatio-icon.png 23 | README.md 24 | Apache-2.0 25 | $(PackageProjectUrl) 26 | true 27 | true 28 | embedded 29 | 30 | 31 | 32 | true 33 | 34 | 35 | 36 | true 37 | $(MSBuildThisFileDirectory)Foundatio.snk 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | -------------------------------------------------------------------------------- /build/foundatio-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/FoundatioFx/Foundatio.Repositories/77e67f095c84cc63e7633f4b860c288c605132d0/build/foundatio-icon.png -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | services: 2 | elasticsearch: 3 | image: docker.elastic.co/elasticsearch/elasticsearch:8.18.1 4 | environment: 5 | discovery.type: single-node 6 | xpack.security.enabled: "false" 7 | action.destructive_requires_name: false 8 | ES_JAVA_OPTS: -Xms512m -Xmx512m 9 | ports: 10 | - 9200:9200 11 | - 9300:9300 12 | networks: 13 | - foundatio 14 | healthcheck: 15 | interval: 2s 16 | retries: 10 17 | test: curl -s http://localhost:9200/_cluster/health | grep -vq '"status":"red"' 18 | 19 | kibana: 20 | depends_on: 21 | elasticsearch: 22 | condition: service_healthy 23 | image: docker.elastic.co/kibana/kibana:8.18.1 24 | environment: 25 | xpack.security.enabled: "false" 26 | ports: 27 | - 5601:5601 28 | networks: 29 | - foundatio 30 | healthcheck: 31 | interval: 2s 32 | retries: 20 33 | test: curl --write-out 'HTTP %{http_code}' --fail --silent --output /dev/null http://localhost:5601/api/status 34 | 35 | ready: 36 | image: andrewlock/wait-for-dependencies 37 | command: elasticsearch:9200 38 | depends_on: 39 | - elasticsearch 40 | networks: 41 | - foundatio 42 | 43 | networks: 44 | foundatio: 45 | driver: bridge 46 | name: foundatio 47 | -------------------------------------------------------------------------------- /global.json: -------------------------------------------------------------------------------- 1 | { 2 | "sdk": { 3 | "version": "8.0.100", 4 | "rollForward": "latestMinor", 5 | "allowPrerelease": false 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /samples/Directory.Build.props: -------------------------------------------------------------------------------- 1 | 2 | 3 | False 4 | 5 | -------------------------------------------------------------------------------- /samples/Foundatio.SampleApp/Client/App.razor: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | 5 | 6 | 7 | Not found 8 | 9 |

Sorry, there's nothing at this address.

10 |
11 |
12 |
13 | -------------------------------------------------------------------------------- /samples/Foundatio.SampleApp/Client/Foundatio.SampleApp.Client.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | net8.0 5 | enable 6 | enable 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | -------------------------------------------------------------------------------- /samples/Foundatio.SampleApp/Client/Pages/Counter.razor: -------------------------------------------------------------------------------- 1 | @page "/counter" 2 | 3 | Counter 4 | 5 |

Counter

6 | 7 |

Current count: @currentCount

8 | 9 | 10 | 11 | @code { 12 | private int currentCount = 0; 13 | 14 | private void IncrementCount() 15 | { 16 | currentCount++; 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /samples/Foundatio.SampleApp/Client/Program.cs: -------------------------------------------------------------------------------- 1 | using Foundatio.SampleApp.Client; 2 | using Microsoft.AspNetCore.Components.Web; 3 | using Microsoft.AspNetCore.Components.WebAssembly.Hosting; 4 | 5 | var builder = WebAssemblyHostBuilder.CreateDefault(args); 6 | builder.RootComponents.Add("#app"); 7 | builder.RootComponents.Add("head::after"); 8 | 9 | builder.Services.AddScoped(sp => new HttpClient { BaseAddress = new Uri(builder.HostEnvironment.BaseAddress) }); 10 | 11 | await builder.Build().RunAsync(); 12 | -------------------------------------------------------------------------------- /samples/Foundatio.SampleApp/Client/Properties/launchSettings.json: -------------------------------------------------------------------------------- 1 | { 2 | "iisSettings": { 3 | "windowsAuthentication": false, 4 | "anonymousAuthentication": true, 5 | "iisExpress": { 6 | "applicationUrl": "http://localhost:22157", 7 | "sslPort": 44393 8 | } 9 | }, 10 | "profiles": { 11 | "http": { 12 | "commandName": "Project", 13 | "dotnetRunMessages": true, 14 | "launchBrowser": true, 15 | "inspectUri": "{wsProtocol}://{url.hostname}:{url.port}/_framework/debug/ws-proxy?browser={browserInspectUri}", 16 | "applicationUrl": "http://localhost:5154", 17 | "environmentVariables": { 18 | "ASPNETCORE_ENVIRONMENT": "Development" 19 | } 20 | }, 21 | "https": { 22 | "commandName": "Project", 23 | "dotnetRunMessages": true, 24 | "launchBrowser": true, 25 | "inspectUri": "{wsProtocol}://{url.hostname}:{url.port}/_framework/debug/ws-proxy?browser={browserInspectUri}", 26 | "applicationUrl": "https://localhost:7188;http://localhost:5154", 27 | "environmentVariables": { 28 | "ASPNETCORE_ENVIRONMENT": "Development" 29 | } 30 | }, 31 | "IIS Express": { 32 | "commandName": "IISExpress", 33 | "launchBrowser": true, 34 | "inspectUri": "{wsProtocol}://{url.hostname}:{url.port}/_framework/debug/ws-proxy?browser={browserInspectUri}", 35 | "environmentVariables": { 36 | "ASPNETCORE_ENVIRONMENT": "Development" 37 | } 38 | } 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /samples/Foundatio.SampleApp/Client/Shared/MainLayout.razor: -------------------------------------------------------------------------------- 1 | @inherits LayoutComponentBase 2 | 3 |
4 | 7 | 8 |
9 |
10 | About 11 |
12 | 13 |
14 | @Body 15 |
16 |
17 |
18 | -------------------------------------------------------------------------------- /samples/Foundatio.SampleApp/Client/Shared/MainLayout.razor.css: -------------------------------------------------------------------------------- 1 | .page { 2 | position: relative; 3 | display: flex; 4 | flex-direction: column; 5 | } 6 | 7 | main { 8 | flex: 1; 9 | } 10 | 11 | .sidebar { 12 | background-image: linear-gradient(180deg, rgb(5, 39, 103) 0%, #3a0647 70%); 13 | } 14 | 15 | .top-row { 16 | background-color: #f7f7f7; 17 | border-bottom: 1px solid #d6d5d5; 18 | justify-content: flex-end; 19 | height: 3.5rem; 20 | display: flex; 21 | align-items: center; 22 | } 23 | 24 | .top-row ::deep a, .top-row ::deep .btn-link { 25 | white-space: nowrap; 26 | margin-left: 1.5rem; 27 | text-decoration: none; 28 | } 29 | 30 | .top-row ::deep a:hover, .top-row ::deep .btn-link:hover { 31 | text-decoration: underline; 32 | } 33 | 34 | .top-row ::deep a:first-child { 35 | overflow: hidden; 36 | text-overflow: ellipsis; 37 | } 38 | 39 | @media (max-width: 640.98px) { 40 | .top-row:not(.auth) { 41 | display: none; 42 | } 43 | 44 | .top-row.auth { 45 | justify-content: space-between; 46 | } 47 | 48 | .top-row ::deep a, .top-row ::deep .btn-link { 49 | margin-left: 0; 50 | } 51 | } 52 | 53 | @media (min-width: 641px) { 54 | .page { 55 | flex-direction: row; 56 | } 57 | 58 | .sidebar { 59 | width: 250px; 60 | height: 100vh; 61 | position: sticky; 62 | top: 0; 63 | } 64 | 65 | .top-row { 66 | position: sticky; 67 | top: 0; 68 | z-index: 1; 69 | } 70 | 71 | .top-row.auth ::deep a:first-child { 72 | flex: 1; 73 | text-align: right; 74 | width: 0; 75 | } 76 | 77 | .top-row, article { 78 | padding-left: 2rem !important; 79 | padding-right: 1.5rem !important; 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /samples/Foundatio.SampleApp/Client/Shared/NavMenu.razor: -------------------------------------------------------------------------------- 1 |  9 | 10 | 24 | 25 | @code { 26 | private bool collapseNavMenu = true; 27 | 28 | private string? NavMenuCssClass => collapseNavMenu ? "collapse" : null; 29 | 30 | private void ToggleNavMenu() 31 | { 32 | collapseNavMenu = !collapseNavMenu; 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /samples/Foundatio.SampleApp/Client/Shared/NavMenu.razor.css: -------------------------------------------------------------------------------- 1 | .navbar-toggler { 2 | background-color: rgba(255, 255, 255, 0.1); 3 | } 4 | 5 | .top-row { 6 | height: 3.5rem; 7 | background-color: rgba(0,0,0,0.4); 8 | } 9 | 10 | .navbar-brand { 11 | font-size: 1.1rem; 12 | } 13 | 14 | .oi { 15 | width: 2rem; 16 | font-size: 1.1rem; 17 | vertical-align: text-top; 18 | top: -2px; 19 | } 20 | 21 | .nav-item { 22 | font-size: 0.9rem; 23 | padding-bottom: 0.5rem; 24 | } 25 | 26 | .nav-item:first-of-type { 27 | padding-top: 1rem; 28 | } 29 | 30 | .nav-item:last-of-type { 31 | padding-bottom: 1rem; 32 | } 33 | 34 | .nav-item ::deep a { 35 | color: #d7d7d7; 36 | border-radius: 4px; 37 | height: 3rem; 38 | display: flex; 39 | align-items: center; 40 | line-height: 3rem; 41 | } 42 | 43 | .nav-item ::deep a.active { 44 | background-color: rgba(255,255,255,0.25); 45 | color: white; 46 | } 47 | 48 | .nav-item ::deep a:hover { 49 | background-color: rgba(255,255,255,0.1); 50 | color: white; 51 | } 52 | 53 | @media (min-width: 641px) { 54 | .navbar-toggler { 55 | display: none; 56 | } 57 | 58 | .collapse { 59 | /* Never collapse the sidebar for wide screens */ 60 | display: block; 61 | } 62 | 63 | .nav-scrollable { 64 | /* Allow sidebar to scroll for tall menus */ 65 | height: calc(100vh - 3.5rem); 66 | overflow-y: auto; 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /samples/Foundatio.SampleApp/Client/Shared/SurveyPrompt.razor: -------------------------------------------------------------------------------- 1 | 
2 | 3 | @Title 4 | 5 | 6 | Please take our 7 | brief survey 8 | 9 | and tell us what you think. 10 |
11 | 12 | @code { 13 | // Demonstrates how a parent component can supply parameters 14 | [Parameter] 15 | public string? Title { get; set; } 16 | } 17 | -------------------------------------------------------------------------------- /samples/Foundatio.SampleApp/Client/_Imports.razor: -------------------------------------------------------------------------------- 1 | @using System.Net.Http 2 | @using System.Net.Http.Json 3 | @using Microsoft.AspNetCore.Components.Forms 4 | @using Microsoft.AspNetCore.Components.Routing 5 | @using Microsoft.AspNetCore.Components.Web 6 | @using Microsoft.AspNetCore.Components.Web.Virtualization 7 | @using Microsoft.AspNetCore.Components.WebAssembly.Http 8 | @using Microsoft.JSInterop 9 | @using Foundatio.SampleApp.Client 10 | @using Foundatio.SampleApp.Client.Shared 11 | -------------------------------------------------------------------------------- /samples/Foundatio.SampleApp/Client/wwwroot/favicon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/FoundatioFx/Foundatio.Repositories/77e67f095c84cc63e7633f4b860c288c605132d0/samples/Foundatio.SampleApp/Client/wwwroot/favicon.png -------------------------------------------------------------------------------- /samples/Foundatio.SampleApp/Client/wwwroot/icon-192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/FoundatioFx/Foundatio.Repositories/77e67f095c84cc63e7633f4b860c288c605132d0/samples/Foundatio.SampleApp/Client/wwwroot/icon-192.png -------------------------------------------------------------------------------- /samples/Foundatio.SampleApp/Client/wwwroot/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Foundatio.SampleApp 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 |
17 | 18 | 19 | 20 | 21 |
22 |
23 | 24 |
25 | An unhandled error has occurred. 26 | Reload 27 | 🗙 28 |
29 | 30 | 31 | 32 | 33 | 34 | -------------------------------------------------------------------------------- /samples/Foundatio.SampleApp/Server/Foundatio.SampleApp.Server.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | net8.0 5 | enable 6 | enable 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | -------------------------------------------------------------------------------- /samples/Foundatio.SampleApp/Server/Pages/Error.cshtml: -------------------------------------------------------------------------------- 1 | @page 2 | @model Foundatio.SampleApp.Server.Pages.ErrorModel 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | Error 11 | 12 | 13 | 14 | 15 | 16 |
17 |
18 |

Error.

19 |

An error occurred while processing your request.

20 | 21 | @if (Model.ShowRequestId) 22 | { 23 |

24 | Request ID: @Model.RequestId 25 |

26 | } 27 | 28 |

Development Mode

29 |

30 | Swapping to the Development environment displays detailed information about the error that occurred. 31 |

32 |

33 | The Development environment shouldn't be enabled for deployed applications. 34 | It can result in displaying sensitive information from exceptions to end users. 35 | For local debugging, enable the Development environment by setting the ASPNETCORE_ENVIRONMENT environment variable to Development 36 | and restarting the app. 37 |

38 |
39 |
40 | 41 | 42 | 43 | -------------------------------------------------------------------------------- /samples/Foundatio.SampleApp/Server/Pages/Error.cshtml.cs: -------------------------------------------------------------------------------- 1 | using System.Diagnostics; 2 | using Microsoft.AspNetCore.Mvc; 3 | using Microsoft.AspNetCore.Mvc.RazorPages; 4 | 5 | namespace Foundatio.SampleApp.Server.Pages; 6 | 7 | [ResponseCache(Duration = 0, Location = ResponseCacheLocation.None, NoStore = true)] 8 | [IgnoreAntiforgeryToken] 9 | public class ErrorModel : PageModel 10 | { 11 | public string? RequestId { get; set; } 12 | 13 | public bool ShowRequestId => !string.IsNullOrEmpty(RequestId); 14 | 15 | private readonly ILogger _logger; 16 | 17 | public ErrorModel(ILogger logger) 18 | { 19 | _logger = logger; 20 | } 21 | 22 | public void OnGet() 23 | { 24 | RequestId = Activity.Current?.Id ?? HttpContext.TraceIdentifier; 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /samples/Foundatio.SampleApp/Server/Program.cs: -------------------------------------------------------------------------------- 1 | using Foundatio.Extensions.Hosting.Startup; 2 | using Foundatio.Repositories; 3 | using Foundatio.SampleApp.Server.Repositories; 4 | using Foundatio.SampleApp.Server.Repositories.Configuration; 5 | using Foundatio.SampleApp.Shared; 6 | 7 | var builder = WebApplication.CreateBuilder(args); 8 | 9 | // Add services to the container. 10 | 11 | builder.Services.AddControllersWithViews(); 12 | builder.Services.AddRazorPages(); 13 | 14 | // add elastic configuration and repository to DI 15 | builder.Services.AddSingleton(); 16 | builder.Services.AddSingleton(); 17 | 18 | // configure the elasticsearch indexes 19 | builder.Services.AddConfigureIndexesStartupAction(); 20 | 21 | // add sample data if there is none 22 | builder.Services.AddSampleDataStartupAction(); 23 | 24 | var app = builder.Build(); 25 | 26 | // Configure the HTTP request pipeline. 27 | if (app.Environment.IsDevelopment()) 28 | { 29 | app.UseWebAssemblyDebugging(); 30 | } 31 | else 32 | { 33 | app.UseExceptionHandler("/Error"); 34 | // The default HSTS value is 30 days. You may want to change this for production scenarios, see https://aka.ms/aspnetcore-hsts. 35 | app.UseHsts(); 36 | } 37 | 38 | app.UseHttpsRedirection(); 39 | 40 | app.UseBlazorFrameworkFiles(); 41 | app.UseStaticFiles(); 42 | 43 | app.UseRouting(); 44 | 45 | app.MapRazorPages(); 46 | 47 | app.UseWaitForStartupActionsBeforeServingRequests(); 48 | 49 | // add endpoint to get game reviews 50 | app.MapGet("GameReviews", async (IGameReviewRepository gameReviewRepository, string? search, string? filter, string? sort, int? page, int? limit, string? fields, string? aggs) => 51 | { 52 | var reviews = await gameReviewRepository.FindAsync(q => q 53 | .FilterExpression(filter) 54 | .SortExpression(sort) 55 | .SearchExpression(search) 56 | .IncludeMask(fields) 57 | .AggregationsExpression(aggs ?? "terms:category terms:tags"), 58 | o => o.PageNumber(page).PageLimit(limit).QueryLogLevel(LogLevel.Warning)); 59 | 60 | return GameReviewSearchResult.From(reviews); 61 | }); 62 | 63 | app.MapFallbackToFile("index.html"); 64 | 65 | app.Run(); 66 | -------------------------------------------------------------------------------- /samples/Foundatio.SampleApp/Server/Properties/launchSettings.json: -------------------------------------------------------------------------------- 1 | { 2 | "iisSettings": { 3 | "windowsAuthentication": false, 4 | "anonymousAuthentication": true, 5 | "iisExpress": { 6 | "applicationUrl": "http://localhost:22157", 7 | "sslPort": 44393 8 | } 9 | }, 10 | "profiles": { 11 | "http": { 12 | "commandName": "Project", 13 | "dotnetRunMessages": true, 14 | "launchBrowser": true, 15 | "inspectUri": "{wsProtocol}://{url.hostname}:{url.port}/_framework/debug/ws-proxy?browser={browserInspectUri}", 16 | "applicationUrl": "http://localhost:5154", 17 | "environmentVariables": { 18 | "ASPNETCORE_ENVIRONMENT": "Development" 19 | } 20 | }, 21 | "https": { 22 | "commandName": "Project", 23 | "dotnetRunMessages": true, 24 | "launchBrowser": true, 25 | "inspectUri": "{wsProtocol}://{url.hostname}:{url.port}/_framework/debug/ws-proxy?browser={browserInspectUri}", 26 | "applicationUrl": "https://localhost:7188;http://localhost:5154", 27 | "environmentVariables": { 28 | "ASPNETCORE_ENVIRONMENT": "Development" 29 | } 30 | }, 31 | "IIS Express": { 32 | "commandName": "IISExpress", 33 | "launchBrowser": true, 34 | "inspectUri": "{wsProtocol}://{url.hostname}:{url.port}/_framework/debug/ws-proxy?browser={browserInspectUri}", 35 | "environmentVariables": { 36 | "ASPNETCORE_ENVIRONMENT": "Development" 37 | } 38 | } 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /samples/Foundatio.SampleApp/Server/Repositories/Configuration/ElasticExtensions.cs: -------------------------------------------------------------------------------- 1 | using Foundatio.Extensions.Hosting.Startup; 2 | using Foundatio.SampleApp.Shared; 3 | 4 | namespace Foundatio.SampleApp.Server.Repositories.Configuration; 5 | 6 | public static class ElasticExtensions 7 | { 8 | public static IServiceCollection AddConfigureIndexesStartupAction(this IServiceCollection services) 9 | { 10 | // configure the elasticsearch indexes 11 | services.AddStartupAction("ConfigureIndexes", async sp => 12 | { 13 | var configuration = sp.GetRequiredService(); 14 | await configuration.ConfigureIndexesAsync(beginReindexingOutdated: false); 15 | }); 16 | 17 | return services; 18 | } 19 | 20 | public static IServiceCollection AddSampleDataStartupAction(this IServiceCollection services) 21 | { 22 | services.AddStartupAction("ConfigureIndexes", async sp => 23 | { 24 | // add some sample data if there is none 25 | var repository = sp.GetRequiredService(); 26 | if (await repository.CountAsync() is { Total: 0 }) 27 | { 28 | await repository.AddAsync(new GameReview 29 | { 30 | Name = "Super Mario Bros", 31 | Description = "Super Mario Bros is a platform video game developed and published by Nintendo.", 32 | Category = "Adventure", 33 | Tags = new[] { "Highly Rated", "Single Player" } 34 | }); 35 | 36 | var categories = new[] { "Action", "Adventure", "Sports", "Racing", "RPG", "Strategy", "Simulation", "Puzzle", "Shooter" }; 37 | var tags = new[] { "Highly Rated", "New", "Multiplayer", "Single Player", "Co-op", "Online", "Local", "Multi-Platform", "VR", "Free to Play" }; 38 | 39 | for (int i = 0; i < 100; i++) 40 | { 41 | var category = categories[Random.Shared.Next(0, categories.Length)]; 42 | 43 | var selectedTags = new List(); 44 | for (int x = 0; x < 5; x++) 45 | selectedTags.Add(tags[Random.Shared.Next(0, tags.Length)]); 46 | 47 | await repository.AddAsync(new GameReview 48 | { 49 | Name = $"Test Game {i}", 50 | Description = $"This is a test game {i} review.", 51 | Category = category, 52 | Tags = selectedTags 53 | }); 54 | } 55 | } 56 | }); 57 | 58 | return services; 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /samples/Foundatio.SampleApp/Server/Repositories/Configuration/Indexes/GameReviewIndex.cs: -------------------------------------------------------------------------------- 1 | using Foundatio.Repositories.Elasticsearch.Configuration; 2 | using Foundatio.Repositories.Elasticsearch.Extensions; 3 | using Foundatio.SampleApp.Shared; 4 | using Nest; 5 | 6 | namespace Foundatio.SampleApp.Server.Repositories.Indexes; 7 | 8 | public sealed class GameReviewIndex : VersionedIndex 9 | { 10 | public GameReviewIndex(IElasticConfiguration configuration) : base(configuration, "gamereview", version: 1) { } 11 | 12 | public override CreateIndexDescriptor ConfigureIndex(CreateIndexDescriptor idx) 13 | { 14 | return base.ConfigureIndex(idx.Settings(s => s 15 | .Analysis(a => a.AddSortNormalizer()) 16 | .NumberOfReplicas(0) 17 | .NumberOfShards(1))); 18 | } 19 | 20 | public override TypeMappingDescriptor ConfigureIndexMapping(TypeMappingDescriptor map) 21 | { 22 | // adding new fields will automatically update the index mapping 23 | // changing existing fields requires a new index version and a reindex 24 | return map 25 | .Dynamic(false) 26 | .Properties(p => p 27 | .SetupDefaults() 28 | .Text(f => f.Name(e => e.Name).AddKeywordAndSortFields()) 29 | .Text(f => f.Name(e => e.Description)) 30 | .Text(f => f.Name(e => e.Category).AddKeywordAndSortFields()) 31 | .Text(f => f.Name(e => e.Tags).AddKeywordAndSortFields()) 32 | ); 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /samples/Foundatio.SampleApp/Server/Repositories/Configuration/SampleAppElasticConfiguration.cs: -------------------------------------------------------------------------------- 1 | using Elasticsearch.Net; 2 | using Foundatio.Repositories.Elasticsearch.Configuration; 3 | using Foundatio.SampleApp.Server.Repositories.Indexes; 4 | using Nest; 5 | 6 | namespace Foundatio.SampleApp.Server.Repositories; 7 | 8 | public class SampleAppElasticConfiguration : ElasticConfiguration 9 | { 10 | private string _connectionString; 11 | private IWebHostEnvironment _env; 12 | 13 | public SampleAppElasticConfiguration(IConfiguration config, IWebHostEnvironment env, ILoggerFactory loggerFactory) : base(loggerFactory: loggerFactory) 14 | { 15 | _connectionString = config.GetConnectionString("ElasticsearchConnectionString") ?? "http://localhost:9200"; 16 | _env = env; 17 | AddIndex(GameReviews = new GameReviewIndex(this)); 18 | } 19 | 20 | protected override IConnectionPool CreateConnectionPool() 21 | { 22 | return new SingleNodeConnectionPool(new Uri(_connectionString)); 23 | } 24 | 25 | protected override void ConfigureSettings(ConnectionSettings settings) 26 | { 27 | // only do this in test and dev mode to enable better debug logging 28 | if (_env.IsDevelopment()) 29 | settings.DisableDirectStreaming().PrettyJson(); 30 | 31 | base.ConfigureSettings(settings); 32 | } 33 | 34 | public GameReviewIndex GameReviews { get; } 35 | } 36 | -------------------------------------------------------------------------------- /samples/Foundatio.SampleApp/Server/Repositories/GameReviewRepository.cs: -------------------------------------------------------------------------------- 1 | using Foundatio.Repositories; 2 | using Foundatio.Repositories.Elasticsearch; 3 | using Foundatio.SampleApp.Shared; 4 | 5 | namespace Foundatio.SampleApp.Server.Repositories; 6 | 7 | public interface IGameReviewRepository : ISearchableRepository { } 8 | 9 | public class GameReviewRepository : ElasticRepositoryBase, IGameReviewRepository 10 | { 11 | public GameReviewRepository(SampleAppElasticConfiguration configuration) : base(configuration.GameReviews) { } 12 | } 13 | -------------------------------------------------------------------------------- /samples/Foundatio.SampleApp/Server/appsettings.Development.json: -------------------------------------------------------------------------------- 1 | { 2 | "Logging": { 3 | "LogLevel": { 4 | "Default": "Information", 5 | "Microsoft.AspNetCore": "Warning" 6 | } 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /samples/Foundatio.SampleApp/Server/appsettings.json: -------------------------------------------------------------------------------- 1 | { 2 | "ConnectionStrings": { 3 | "ElasticsearchConnectionString": "http://localhost:9200/" 4 | }, 5 | "Logging": { 6 | "LogLevel": { 7 | "Default": "Information", 8 | "Microsoft.AspNetCore": "Warning" 9 | } 10 | }, 11 | "AllowedHosts": "*" 12 | } 13 | -------------------------------------------------------------------------------- /samples/Foundatio.SampleApp/Shared/Foundatio.SampleApp.Shared.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | net8.0 5 | enable 6 | enable 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | -------------------------------------------------------------------------------- /samples/Foundatio.SampleApp/Shared/GameReview.cs: -------------------------------------------------------------------------------- 1 | using Foundatio.Repositories.Models; 2 | 3 | namespace Foundatio.SampleApp.Shared; 4 | 5 | public class GameReview : IIdentity, IHaveDates 6 | { 7 | public string Id { get; set; } = String.Empty; 8 | public string Name { get; set; } = String.Empty; 9 | public string? Description { get; set; } 10 | public string Category { get; set; } = "General"; 11 | public ICollection Tags { get; set; } = new List(); 12 | public DateTime UpdatedUtc { get; set; } 13 | public DateTime CreatedUtc { get; set; } 14 | } 15 | 16 | public class GameReviewSearchResult 17 | { 18 | public IReadOnlyCollection Reviews { get; set; } = new List(); 19 | public long Total { get; set; } 20 | public ICollection CategoryCounts { get; set; } = new List(); 21 | public ICollection TagCounts { get; set; } = new List(); 22 | 23 | public static GameReviewSearchResult From(FindResults results) 24 | { 25 | var categoryCounts = results.Aggregations.Terms("terms_category")?.Buckets.Select(t => new AggCount { Name = t.Key, Total = t.Total }).ToList() ?? new List(); 26 | var tagCounts = results.Aggregations.Terms("terms_tags")?.Buckets.Select(t => new AggCount { Name = t.Key, Total = t.Total }).ToList() ?? new List(); 27 | 28 | return new GameReviewSearchResult 29 | { 30 | Reviews = results.Documents, 31 | Total = results.Total, 32 | CategoryCounts = categoryCounts, 33 | TagCounts = tagCounts 34 | }; 35 | } 36 | } 37 | 38 | public class AggCount 39 | { 40 | public string Name { get; set; } = String.Empty; 41 | public long? Total { get; set; } 42 | } 43 | -------------------------------------------------------------------------------- /src/Directory.Build.props: -------------------------------------------------------------------------------- 1 | 2 | 3 | -------------------------------------------------------------------------------- /src/Foundatio.Repositories.Elasticsearch/Configuration/DynamicIndex.cs: -------------------------------------------------------------------------------- 1 | using Foundatio.Parsers.ElasticQueries; 2 | using Foundatio.Repositories.Elasticsearch.Extensions; 3 | using Nest; 4 | 5 | namespace Foundatio.Repositories.Elasticsearch.Configuration; 6 | 7 | public class DynamicIndex : Index where T : class 8 | { 9 | public DynamicIndex(IElasticConfiguration configuration, string name = null) : base(configuration, name) { } 10 | 11 | protected override ElasticMappingResolver CreateMappingResolver() 12 | { 13 | return ElasticMappingResolver.Create(ConfigureIndexMapping, Configuration.Client, Name, _logger); 14 | } 15 | 16 | public override TypeMappingDescriptor ConfigureIndexMapping(TypeMappingDescriptor map) 17 | { 18 | return map.Dynamic().AutoMap().Properties(p => p.SetupDefaults()); 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /src/Foundatio.Repositories.Elasticsearch/Configuration/IElasticConfiguration.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Threading.Tasks; 4 | using Foundatio.Caching; 5 | using Foundatio.Messaging; 6 | using Foundatio.Parsers.ElasticQueries; 7 | using Foundatio.Repositories.Elasticsearch.CustomFields; 8 | using Foundatio.Repositories.Elasticsearch.Queries.Builders; 9 | using Microsoft.Extensions.Logging; 10 | using Nest; 11 | 12 | namespace Foundatio.Repositories.Elasticsearch.Configuration; 13 | 14 | public interface IElasticConfiguration : IDisposable 15 | { 16 | IElasticClient Client { get; } 17 | ICacheClient Cache { get; } 18 | IMessageBus MessageBus { get; } 19 | ILoggerFactory LoggerFactory { get; } 20 | TimeProvider TimeProvider { get; set; } 21 | IReadOnlyCollection Indexes { get; } 22 | ICustomFieldDefinitionRepository CustomFieldDefinitionRepository { get; } 23 | 24 | IIndex GetIndex(string name); 25 | void ConfigureGlobalQueryBuilders(ElasticQueryBuilder builder); 26 | void ConfigureGlobalQueryParsers(ElasticQueryParserConfiguration config); 27 | Task ConfigureIndexesAsync(IEnumerable indexes = null, bool beginReindexingOutdated = true); 28 | Task MaintainIndexesAsync(IEnumerable indexes = null); 29 | Task DeleteIndexesAsync(IEnumerable indexes = null); 30 | Task ReindexAsync(IEnumerable indexes = null, Func progressCallbackAsync = null); 31 | } 32 | -------------------------------------------------------------------------------- /src/Foundatio.Repositories.Elasticsearch/Configuration/IIndex.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq.Expressions; 4 | using System.Threading.Tasks; 5 | using Foundatio.Parsers.ElasticQueries; 6 | using Foundatio.Repositories.Elasticsearch.CustomFields; 7 | using Foundatio.Repositories.Elasticsearch.Queries.Builders; 8 | using Nest; 9 | 10 | namespace Foundatio.Repositories.Elasticsearch.Configuration; 11 | 12 | public interface IIndex : IDisposable 13 | { 14 | string Name { get; } 15 | bool HasMultipleIndexes { get; } 16 | IElasticQueryBuilder QueryBuilder { get; } 17 | ElasticMappingResolver MappingResolver { get; } 18 | ElasticQueryParser QueryParser { get; } 19 | IElasticConfiguration Configuration { get; } 20 | IDictionary CustomFieldTypes { get; } 21 | 22 | void ConfigureSettings(ConnectionSettings settings); 23 | Task ConfigureAsync(); 24 | Task EnsureIndexAsync(object target); 25 | Task MaintainAsync(bool includeOptionalTasks = true); 26 | Task DeleteAsync(); 27 | Task ReindexAsync(Func progressCallbackAsync = null); 28 | string CreateDocumentId(object document); 29 | string[] GetIndexesByQuery(IRepositoryQuery query); 30 | string GetIndex(object target); 31 | } 32 | 33 | public interface IIndex : IIndex where T : class 34 | { 35 | TypeMappingDescriptor ConfigureIndexMapping(TypeMappingDescriptor map); 36 | Inferrer Infer { get; } 37 | string InferField(Expression> objectPath); 38 | string InferPropertyName(Expression> objectPath); 39 | } 40 | -------------------------------------------------------------------------------- /src/Foundatio.Repositories.Elasticsearch/CustomFields/ICustomFieldType.cs: -------------------------------------------------------------------------------- 1 | using System.Threading.Tasks; 2 | using Nest; 3 | 4 | namespace Foundatio.Repositories.Elasticsearch.CustomFields; 5 | 6 | public interface ICustomFieldType 7 | { 8 | string Type { get; } 9 | Task ProcessValueAsync(T document, object value, CustomFieldDefinition fieldDefinition) where T : class; 10 | IProperty ConfigureMapping(SingleMappingSelector map) where T : class; 11 | } 12 | 13 | public class ProcessFieldValueResult 14 | { 15 | public object Value { get; set; } 16 | public object Idx { get; set; } 17 | public bool IsCustomFieldDefinitionModified { get; set; } 18 | } 19 | -------------------------------------------------------------------------------- /src/Foundatio.Repositories.Elasticsearch/CustomFields/StandardFieldTypes/BooleanFieldType.cs: -------------------------------------------------------------------------------- 1 | using System.Threading.Tasks; 2 | using Nest; 3 | 4 | namespace Foundatio.Repositories.Elasticsearch.CustomFields; 5 | 6 | public class BooleanFieldType : ICustomFieldType 7 | { 8 | public static string IndexType = "bool"; 9 | public string Type => "bool"; 10 | 11 | public virtual Task ProcessValueAsync(T document, object value, CustomFieldDefinition fieldDefinition) where T : class 12 | { 13 | return Task.FromResult(new ProcessFieldValueResult { Value = value }); 14 | } 15 | 16 | public virtual IProperty ConfigureMapping(SingleMappingSelector map) where T : class 17 | { 18 | return map.Boolean(mp => mp); 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /src/Foundatio.Repositories.Elasticsearch/CustomFields/StandardFieldTypes/DateFieldType.cs: -------------------------------------------------------------------------------- 1 | using System.Threading.Tasks; 2 | using Nest; 3 | 4 | namespace Foundatio.Repositories.Elasticsearch.CustomFields; 5 | 6 | public class DateFieldType : ICustomFieldType 7 | { 8 | public static string IndexType = "date"; 9 | public string Type => IndexType; 10 | 11 | public virtual Task ProcessValueAsync(T document, object value, CustomFieldDefinition fieldDefinition) where T : class 12 | { 13 | return Task.FromResult(new ProcessFieldValueResult { Value = value }); 14 | } 15 | 16 | public virtual IProperty ConfigureMapping(SingleMappingSelector map) where T : class 17 | { 18 | return map.Date(mp => mp); 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /src/Foundatio.Repositories.Elasticsearch/CustomFields/StandardFieldTypes/DoubleFieldType.cs: -------------------------------------------------------------------------------- 1 | using System.Threading.Tasks; 2 | using Nest; 3 | 4 | namespace Foundatio.Repositories.Elasticsearch.CustomFields; 5 | 6 | public class DoubleFieldType : ICustomFieldType 7 | { 8 | public static string IndexType = "double"; 9 | public string Type => IndexType; 10 | 11 | public virtual Task ProcessValueAsync(T document, object value, CustomFieldDefinition fieldDefinition) where T : class 12 | { 13 | return Task.FromResult(new ProcessFieldValueResult { Value = value }); 14 | } 15 | 16 | public virtual IProperty ConfigureMapping(SingleMappingSelector map) where T : class 17 | { 18 | return map.Number(mp => mp.Type(NumberType.Double)); 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /src/Foundatio.Repositories.Elasticsearch/CustomFields/StandardFieldTypes/FloatFieldType.cs: -------------------------------------------------------------------------------- 1 | using System.Threading.Tasks; 2 | using Nest; 3 | 4 | namespace Foundatio.Repositories.Elasticsearch.CustomFields; 5 | 6 | public class FloatFieldType : ICustomFieldType 7 | { 8 | public static string IndexType = "float"; 9 | public string Type => IndexType; 10 | 11 | public virtual Task ProcessValueAsync(T document, object value, CustomFieldDefinition fieldDefinition) where T : class 12 | { 13 | return Task.FromResult(new ProcessFieldValueResult { Value = value }); 14 | } 15 | 16 | public virtual IProperty ConfigureMapping(SingleMappingSelector map) where T : class 17 | { 18 | return map.Number(mp => mp.Type(NumberType.Float)); 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /src/Foundatio.Repositories.Elasticsearch/CustomFields/StandardFieldTypes/IntegerFieldType.cs: -------------------------------------------------------------------------------- 1 | using System.Threading.Tasks; 2 | using Nest; 3 | 4 | namespace Foundatio.Repositories.Elasticsearch.CustomFields; 5 | 6 | public class IntegerFieldType : ICustomFieldType 7 | { 8 | public static string IndexType = "int"; 9 | public string Type => IndexType; 10 | 11 | public virtual Task ProcessValueAsync(T document, object value, CustomFieldDefinition fieldDefinition) where T : class 12 | { 13 | return Task.FromResult(new ProcessFieldValueResult { Value = value }); 14 | } 15 | 16 | public virtual IProperty ConfigureMapping(SingleMappingSelector map) where T : class 17 | { 18 | return map.Number(mp => mp.Type(NumberType.Integer)); 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /src/Foundatio.Repositories.Elasticsearch/CustomFields/StandardFieldTypes/KeywordFieldType.cs: -------------------------------------------------------------------------------- 1 | using System.Threading.Tasks; 2 | using Nest; 3 | 4 | namespace Foundatio.Repositories.Elasticsearch.CustomFields; 5 | 6 | public class KeywordFieldType : ICustomFieldType 7 | { 8 | public static string IndexType = "keyword"; 9 | public string Type => IndexType; 10 | 11 | public virtual Task ProcessValueAsync(T document, object value, CustomFieldDefinition fieldDefinition) where T : class 12 | { 13 | return Task.FromResult(new ProcessFieldValueResult { Value = value }); 14 | } 15 | 16 | public virtual IProperty ConfigureMapping(SingleMappingSelector map) where T : class 17 | { 18 | return map.Keyword(mp => mp); 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /src/Foundatio.Repositories.Elasticsearch/CustomFields/StandardFieldTypes/LongFieldType.cs: -------------------------------------------------------------------------------- 1 | using System.Threading.Tasks; 2 | using Nest; 3 | 4 | namespace Foundatio.Repositories.Elasticsearch.CustomFields; 5 | 6 | public class LongFieldType : ICustomFieldType 7 | { 8 | public static string IndexType = "long"; 9 | public string Type => IndexType; 10 | 11 | public virtual Task ProcessValueAsync(T document, object value, CustomFieldDefinition fieldDefinition) where T : class 12 | { 13 | return Task.FromResult(new ProcessFieldValueResult { Value = value }); 14 | } 15 | 16 | public virtual IProperty ConfigureMapping(SingleMappingSelector map) where T : class 17 | { 18 | return map.Number(mp => mp.Type(NumberType.Long)); 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /src/Foundatio.Repositories.Elasticsearch/CustomFields/StandardFieldTypes/StringFieldType.cs: -------------------------------------------------------------------------------- 1 | using System.Threading.Tasks; 2 | using Nest; 3 | 4 | namespace Foundatio.Repositories.Elasticsearch.CustomFields; 5 | 6 | public class StringFieldType : ICustomFieldType 7 | { 8 | public static string IndexType = "string"; 9 | public string Type => IndexType; 10 | 11 | public virtual Task ProcessValueAsync(T document, object value, CustomFieldDefinition fieldDefinition) where T : class 12 | { 13 | return Task.FromResult(new ProcessFieldValueResult { Value = value }); 14 | } 15 | 16 | public virtual IProperty ConfigureMapping(SingleMappingSelector map) where T : class 17 | { 18 | return map.Text(mp => mp.AddKeywordField()); 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /src/Foundatio.Repositories.Elasticsearch/Extensions/CollectionExtensions.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | 5 | namespace Foundatio.Repositories.Elasticsearch.Extensions; 6 | 7 | #if NETSTANDARD 8 | internal static class CollectionExtensions 9 | { 10 | public static IEnumerable DistinctBy(this IEnumerable source, Func keySelector) 11 | { 12 | var knownKeys = new HashSet(); 13 | foreach (TSource element in source) 14 | { 15 | if (knownKeys.Add(keySelector(element))) 16 | { 17 | yield return element; 18 | } 19 | } 20 | } 21 | 22 | public static IEnumerable> Chunk(this IEnumerable source, int size) 23 | { 24 | T[] bucket = null; 25 | int count = 0; 26 | 27 | foreach (var item in source) 28 | { 29 | if (bucket == null) 30 | bucket = new T[size]; 31 | 32 | bucket[count++] = item; 33 | 34 | if (count != size) 35 | continue; 36 | 37 | yield return bucket.Select(x => x); 38 | 39 | bucket = null; 40 | count = 0; 41 | } 42 | 43 | // Return the last bucket with all remaining elements 44 | if (bucket != null && count > 0) 45 | { 46 | Array.Resize(ref bucket, count); 47 | yield return bucket.Select(x => x); 48 | } 49 | } 50 | } 51 | #endif 52 | -------------------------------------------------------------------------------- /src/Foundatio.Repositories.Elasticsearch/Extensions/ElasticLazyDocument.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.IO; 3 | using System.Reflection; 4 | using Elasticsearch.Net; 5 | using Nest; 6 | using ILazyDocument = Foundatio.Repositories.Models.ILazyDocument; 7 | 8 | namespace Foundatio.Repositories.Elasticsearch.Extensions; 9 | 10 | public class ElasticLazyDocument : ILazyDocument 11 | { 12 | private readonly Nest.ILazyDocument _inner; 13 | private IElasticsearchSerializer _requestResponseSerializer; 14 | 15 | public ElasticLazyDocument(Nest.ILazyDocument inner) 16 | { 17 | _inner = inner; 18 | } 19 | 20 | private static readonly Lazy> _getSerializer = 21 | new(() => 22 | { 23 | var serializerField = typeof(Nest.LazyDocument).GetField("_requestResponseSerializer", BindingFlags.GetField | BindingFlags.NonPublic | BindingFlags.Instance); 24 | return lazyDocument => 25 | { 26 | var d = lazyDocument as Nest.LazyDocument; 27 | if (d == null) 28 | return null; 29 | 30 | var serializer = serializerField?.GetValue(d) as IElasticsearchSerializer; 31 | return serializer; 32 | }; 33 | }); 34 | 35 | private static readonly Lazy> _getBytes = 36 | new(() => 37 | { 38 | var bytesProperty = typeof(Nest.LazyDocument).GetProperty("Bytes", BindingFlags.GetProperty | BindingFlags.NonPublic | BindingFlags.Instance); 39 | return lazyDocument => 40 | { 41 | var d = lazyDocument as Nest.LazyDocument; 42 | if (d == null) 43 | return null; 44 | 45 | var bytes = bytesProperty?.GetValue(d) as byte[]; 46 | return bytes; 47 | }; 48 | }); 49 | 50 | public T As() where T : class 51 | { 52 | if (_requestResponseSerializer == null) 53 | _requestResponseSerializer = _getSerializer.Value(_inner); 54 | 55 | var bytes = _getBytes.Value(_inner); 56 | var hit = _requestResponseSerializer.Deserialize>(new MemoryStream(bytes)); 57 | return hit?.Source; 58 | } 59 | 60 | public object As(Type objectType) 61 | { 62 | var hitType = typeof(IHit<>).MakeGenericType(objectType); 63 | return _inner.As(hitType); 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /src/Foundatio.Repositories.Elasticsearch/Extensions/FindResultsExtensions.cs: -------------------------------------------------------------------------------- 1 | using Foundatio.Utility; 2 | 3 | namespace Foundatio.Repositories.Elasticsearch.Extensions; 4 | 5 | public static class FindResultsExtensions 6 | { 7 | public static string GetScrollId(this IHaveData results) 8 | { 9 | return results.Data.GetString(ElasticDataKeys.ScrollId, null); 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /src/Foundatio.Repositories.Elasticsearch/Extensions/IBodyWithApiCallDetailsExtensions.cs: -------------------------------------------------------------------------------- 1 | using System.Text; 2 | using System.Text.Json; 3 | using Nest; 4 | 5 | namespace Foundatio.Repositories.Elasticsearch.Extensions; 6 | 7 | internal static class IBodyWithApiCallDetailsExtensions 8 | { 9 | private static readonly JsonSerializerOptions _options = new() { PropertyNameCaseInsensitive = true, }; 10 | 11 | public static T DeserializeRaw(this IResponse call) where T : class, new() 12 | { 13 | if (call?.ApiCall?.ResponseBodyInBytes == null) 14 | return default; 15 | 16 | string rawResponse = Encoding.UTF8.GetString(call.ApiCall.ResponseBodyInBytes); 17 | return JsonSerializer.Deserialize(rawResponse, _options); 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /src/Foundatio.Repositories.Elasticsearch/Extensions/ResolverExtensions.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | using System.Linq; 3 | using Foundatio.Parsers.ElasticQueries; 4 | using Nest; 5 | 6 | namespace Foundatio.Repositories.Elasticsearch.Extensions; 7 | 8 | public static class ResolverExtensions 9 | { 10 | public static ICollection GetResolvedFields(this ElasticMappingResolver resolver, ICollection fields) 11 | { 12 | if (fields.Count == 0) 13 | return fields; 14 | 15 | return fields.Select(field => ResolveFieldName(resolver, field)).ToList(); 16 | } 17 | 18 | public static ICollection GetResolvedFields(this ElasticMappingResolver resolver, ICollection sorts) 19 | { 20 | if (sorts.Count == 0) 21 | return sorts; 22 | 23 | return sorts.Select(sort => ResolveFieldSort(resolver, sort)).ToList(); 24 | } 25 | 26 | public static Field ResolveFieldName(this ElasticMappingResolver resolver, Field field) 27 | { 28 | if (field?.Name == null) 29 | return field; 30 | 31 | return new Field(resolver.GetResolvedField(field.Name), field.Boost, field.Format); 32 | } 33 | 34 | public static IFieldSort ResolveFieldSort(this ElasticMappingResolver resolver, IFieldSort sort) 35 | { 36 | return new FieldSort 37 | { 38 | Field = resolver.GetSortFieldName(sort.SortKey), 39 | IgnoreUnmappedFields = sort.IgnoreUnmappedFields, 40 | Missing = sort.Missing, 41 | Mode = sort.Mode, 42 | Nested = sort.Nested, 43 | NumericType = sort.NumericType, 44 | Order = sort.Order, 45 | UnmappedType = sort.UnmappedType 46 | }; 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /src/Foundatio.Repositories.Elasticsearch/Foundatio.Repositories.Elasticsearch.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /src/Foundatio.Repositories.Elasticsearch/Jobs/ElasticMigrationJob.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Linq; 3 | using System.Threading.Tasks; 4 | using Foundatio.Jobs; 5 | using Foundatio.Repositories.Elasticsearch.Configuration; 6 | using Foundatio.Repositories.Extensions; 7 | using Foundatio.Repositories.Migrations; 8 | using Microsoft.Extensions.Logging; 9 | 10 | namespace Foundatio.Repositories.Elasticsearch.Jobs; 11 | 12 | [Job(Description = "Runs any pending system migrations and reindexing tasks.", IsContinuous = false)] 13 | public abstract class ElasticMigrationJobBase : JobBase 14 | { 15 | protected readonly IElasticConfiguration _configuration; 16 | protected readonly Lazy _migrationManager; 17 | 18 | public ElasticMigrationJobBase(MigrationManager migrationManager, IElasticConfiguration configuration, ILoggerFactory loggerFactory = null) 19 | : base(loggerFactory) 20 | { 21 | _migrationManager = new Lazy(() => 22 | { 23 | Configure(migrationManager); 24 | return migrationManager; 25 | }); 26 | _configuration = configuration; 27 | } 28 | 29 | protected virtual void Configure(MigrationManager manager) { } 30 | 31 | public MigrationManager MigrationManager => _migrationManager.Value; 32 | 33 | protected override async Task RunInternalAsync(JobContext context) 34 | { 35 | await _configuration.ConfigureIndexesAsync(null, false).AnyContext(); 36 | 37 | await _migrationManager.Value.RunMigrationsAsync().AnyContext(); 38 | 39 | var tasks = _configuration.Indexes.OfType().Select(ReindexIfNecessary); 40 | await Task.WhenAll(tasks).AnyContext(); 41 | 42 | return JobResult.Success; 43 | } 44 | 45 | private async Task ReindexIfNecessary(IVersionedIndex index) 46 | { 47 | if (index.Version != await index.GetCurrentVersionAsync().AnyContext()) 48 | await index.ReindexAsync().AnyContext(); 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /src/Foundatio.Repositories.Elasticsearch/Jobs/MaintainIndexesJob.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Diagnostics; 3 | using System.Threading; 4 | using System.Threading.Tasks; 5 | using Foundatio.Jobs; 6 | using Foundatio.Lock; 7 | using Foundatio.Repositories.Elasticsearch.Configuration; 8 | using Foundatio.Repositories.Extensions; 9 | using Microsoft.Extensions.Logging; 10 | using Microsoft.Extensions.Logging.Abstractions; 11 | 12 | namespace Foundatio.Repositories.Elasticsearch.Jobs; 13 | 14 | public class MaintainIndexesJob : IJob 15 | { 16 | private readonly IElasticConfiguration _configuration; 17 | private readonly ILogger _logger; 18 | private readonly ILockProvider _lockProvider; 19 | 20 | public MaintainIndexesJob(IElasticConfiguration configuration, ILockProvider lockProvider, ILoggerFactory loggerFactory) 21 | { 22 | _configuration = configuration; 23 | _lockProvider = lockProvider; 24 | _logger = loggerFactory?.CreateLogger(GetType()) ?? NullLogger.Instance; 25 | } 26 | 27 | public virtual async Task RunAsync(CancellationToken cancellationToken = default) 28 | { 29 | _logger.LogInformation("Starting index maintenance..."); 30 | 31 | var sw = Stopwatch.StartNew(); 32 | try 33 | { 34 | bool success = await _lockProvider.TryUsingAsync("es-maintain-indexes", async t => 35 | { 36 | _logger.LogInformation("Got lock to maintain indexes"); 37 | await _configuration.MaintainIndexesAsync().AnyContext(); 38 | sw.Stop(); 39 | 40 | await OnCompleted(sw.Elapsed).AnyContext(); 41 | }, TimeSpan.FromMinutes(30), cancellationToken).AnyContext(); 42 | 43 | if (!success) 44 | _logger.LogInformation("Unable to acquire index maintenance lock."); 45 | 46 | } 47 | catch (Exception ex) 48 | { 49 | sw.Stop(); 50 | await OnFailure(sw.Elapsed, ex).AnyContext(); 51 | throw; 52 | } 53 | 54 | return JobResult.Success; 55 | } 56 | 57 | public virtual Task OnFailure(TimeSpan duration, Exception ex) 58 | { 59 | _logger.LogError(ex, "Failed to maintain indexes after {Duration:g}: {Message}", duration, ex?.Message); 60 | return Task.FromResult(true); 61 | } 62 | 63 | public virtual Task OnCompleted(TimeSpan duration) 64 | { 65 | _logger.LogInformation("Finished index maintenance in {Duration:g}.", duration); 66 | return Task.CompletedTask; 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /src/Foundatio.Repositories.Elasticsearch/Jobs/ReindexWorkItem.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace Foundatio.Repositories.Elasticsearch.Jobs; 4 | 5 | public class ReindexWorkItem 6 | { 7 | public string OldIndex { get; set; } 8 | public string NewIndex { get; set; } 9 | public string Alias { get; set; } 10 | public string Script { get; set; } 11 | public bool DeleteOld { get; set; } 12 | public string TimestampField { get; set; } 13 | public DateTime? StartUtc { get; set; } 14 | } 15 | -------------------------------------------------------------------------------- /src/Foundatio.Repositories.Elasticsearch/Jobs/ReindexWorkItemHandler.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Threading; 3 | using System.Threading.Tasks; 4 | using Foundatio.Jobs; 5 | using Foundatio.Lock; 6 | using Microsoft.Extensions.Logging; 7 | using Nest; 8 | 9 | namespace Foundatio.Repositories.Elasticsearch.Jobs; 10 | 11 | public class ReindexWorkItemHandler : WorkItemHandlerBase 12 | { 13 | private readonly ElasticReindexer _reindexer; 14 | private readonly ILockProvider _lockProvider; 15 | 16 | public ReindexWorkItemHandler(IElasticClient client, ILockProvider lockProvider, ILoggerFactory loggerFactory = null) 17 | { 18 | _reindexer = new ElasticReindexer(client, loggerFactory.CreateLogger()); 19 | _lockProvider = lockProvider; 20 | AutoRenewLockOnProgress = true; 21 | } 22 | 23 | public override Task GetWorkItemLockAsync(object workItem, CancellationToken cancellationToken = default) 24 | { 25 | if (workItem is not ReindexWorkItem reindexWorkItem) 26 | return null; 27 | 28 | return _lockProvider.AcquireAsync(String.Join(":", "reindex", reindexWorkItem.Alias, reindexWorkItem.OldIndex, reindexWorkItem.NewIndex), TimeSpan.FromMinutes(20), cancellationToken); 29 | } 30 | 31 | public override Task HandleItemAsync(WorkItemContext context) 32 | { 33 | var workItem = context.GetData(); 34 | return _reindexer.ReindexAsync(workItem, context.ReportProgressAsync); 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /src/Foundatio.Repositories.Elasticsearch/Queries/Builders/AggregationsQueryBuilder.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Threading.Tasks; 3 | using Foundatio.Parsers.ElasticQueries.Extensions; 4 | using Foundatio.Repositories.Extensions; 5 | using Foundatio.Repositories.Options; 6 | 7 | namespace Foundatio.Repositories 8 | { 9 | public static class AggregationQueryExtensions 10 | { 11 | internal const string AggregationsKey = "@AggregationsExpressionKey"; 12 | 13 | public static T AggregationsExpression(this T options, string aggregations) where T : IRepositoryQuery 14 | { 15 | return options.BuildOption(AggregationsKey, aggregations); 16 | } 17 | } 18 | } 19 | 20 | namespace Foundatio.Repositories.Options 21 | { 22 | public static class ReadAggregationQueryExtensions 23 | { 24 | public static string GetAggregationsExpression(this IRepositoryQuery query) 25 | { 26 | return query.SafeGetOption(AggregationQueryExtensions.AggregationsKey, null); 27 | } 28 | } 29 | } 30 | 31 | namespace Foundatio.Repositories.Elasticsearch.Queries.Builders 32 | { 33 | public class AggregationsQueryBuilder : IElasticQueryBuilder 34 | { 35 | public async Task BuildAsync(QueryBuilderContext ctx) where T : class, new() 36 | { 37 | var elasticIndex = ctx.Options.GetElasticIndex(); 38 | if (elasticIndex?.QueryParser == null) 39 | return; 40 | 41 | string aggregations = ctx.Source.GetAggregationsExpression(); 42 | if (String.IsNullOrEmpty(aggregations)) 43 | return; 44 | 45 | var result = await elasticIndex.QueryParser.BuildAggregationsAsync(aggregations, ctx).AnyContext(); 46 | ctx.Search.Aggregations(result); 47 | } 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /src/Foundatio.Repositories.Elasticsearch/Queries/Builders/ElasticFilterQueryBuilder.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | using System.Threading.Tasks; 3 | using Foundatio.Repositories.Options; 4 | using Nest; 5 | 6 | namespace Foundatio.Repositories 7 | { 8 | public static class ElasticFilterQueryExtensions 9 | { 10 | internal const string ElasticFiltersKey = "@ElasticFilters"; 11 | 12 | public static T ElasticFilter(this T query, QueryContainer filter) where T : IRepositoryQuery 13 | { 14 | return query.AddCollectionOptionValue(ElasticFiltersKey, filter); 15 | } 16 | } 17 | } 18 | 19 | namespace Foundatio.Repositories.Options 20 | { 21 | public static class ReadElasticFilterQueryExtensions 22 | { 23 | public static ICollection GetElasticFilters(this IRepositoryQuery query) 24 | { 25 | return query.SafeGetCollection(ElasticFilterQueryExtensions.ElasticFiltersKey); 26 | } 27 | } 28 | } 29 | 30 | namespace Foundatio.Repositories.Elasticsearch.Queries.Builders 31 | { 32 | public class ElasticFilterQueryBuilder : IElasticQueryBuilder 33 | { 34 | public Task BuildAsync(QueryBuilderContext ctx) where T : class, new() 35 | { 36 | var elasticFilters = ctx.Source.GetElasticFilters(); 37 | if (elasticFilters.Count == 0) 38 | return Task.CompletedTask; 39 | 40 | foreach (var filter in elasticFilters) 41 | ctx.Filter &= filter; 42 | 43 | return Task.CompletedTask; 44 | } 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /src/Foundatio.Repositories.Elasticsearch/Queries/Builders/IdentityQueryBuilder.cs: -------------------------------------------------------------------------------- 1 | using System.Linq; 2 | using System.Threading.Tasks; 3 | using Foundatio.Repositories.Queries; 4 | using Nest; 5 | 6 | namespace Foundatio.Repositories.Elasticsearch.Queries.Builders; 7 | 8 | public class IdentityQueryBuilder : IElasticQueryBuilder 9 | { 10 | public Task BuildAsync(QueryBuilderContext ctx) where T : class, new() 11 | { 12 | var ids = ctx.Source.GetIds(); 13 | if (ids.Count > 0) 14 | ctx.Filter &= new IdsQuery { Values = ids.Select(id => new Nest.Id(id)) }; 15 | 16 | var excludesIds = ctx.Source.GetExcludedIds(); 17 | if (excludesIds.Count > 0) 18 | ctx.Filter &= !new IdsQuery { Values = excludesIds.Select(id => new Nest.Id(id)) }; 19 | 20 | return Task.CompletedTask; 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /src/Foundatio.Repositories.Elasticsearch/Queries/Builders/PageableQueryBuilder.cs: -------------------------------------------------------------------------------- 1 | using System.Threading.Tasks; 2 | using Foundatio.Repositories.Options; 3 | 4 | namespace Foundatio.Repositories.Elasticsearch.Queries.Builders; 5 | 6 | public class PageableQueryBuilder : IElasticQueryBuilder 7 | { 8 | public Task BuildAsync(QueryBuilderContext ctx) where T : class, new() 9 | { 10 | int limit = ctx.Options.GetLimit(); 11 | if (limit >= ctx.Options.GetMaxLimit() || ctx.Options.ShouldUseSnapshotPaging()) 12 | { 13 | ctx.Search.Size(limit); 14 | } 15 | else 16 | { 17 | // add 1 to limit if not snapshot paging so we can know if we have more results 18 | ctx.Search.Size(limit + 1); 19 | } 20 | 21 | // can only use search_after or skip 22 | if (ctx.Options.HasSearchAfter()) 23 | ctx.Search.SearchAfter(ctx.Options.GetSearchAfter()); 24 | else if (ctx.Options.HasSearchBefore()) 25 | ctx.Search.SearchAfter(ctx.Options.GetSearchBefore()); 26 | else if (ctx.Options.ShouldUseSkip()) 27 | ctx.Search.Skip(ctx.Options.GetSkip()); 28 | 29 | return Task.CompletedTask; 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /src/Foundatio.Repositories.Elasticsearch/Queries/Builders/SoftDeletesQueryBuilder.cs: -------------------------------------------------------------------------------- 1 | using System.Threading.Tasks; 2 | using Foundatio.Repositories.Models; 3 | using Foundatio.Repositories.Options; 4 | using Nest; 5 | 6 | namespace Foundatio.Repositories.Elasticsearch.Queries.Builders; 7 | 8 | public class SoftDeletesQueryBuilder : IElasticQueryBuilder 9 | { 10 | public Task BuildAsync(QueryBuilderContext ctx) where T : class, new() 11 | { 12 | // TODO: Figure out how to automatically add parent filter for soft deletes on queries that have a parent document type 13 | 14 | // dont add filter to child query system filters 15 | if (ctx.Parent != null) 16 | return Task.CompletedTask; 17 | 18 | // get soft delete mode, use parent query as default if it exists 19 | var mode = ctx.Options.GetSoftDeleteMode(ctx.Parent?.Options?.GetSoftDeleteMode() ?? SoftDeleteQueryMode.ActiveOnly); 20 | 21 | // no filter needed if we want all 22 | if (mode == SoftDeleteQueryMode.All) 23 | return Task.CompletedTask; 24 | 25 | // check to see if the model supports soft deletes 26 | if (!ctx.Options.SupportsSoftDeletes()) 27 | return Task.CompletedTask; 28 | 29 | var documentType = ctx.Options.DocumentType(); 30 | var property = documentType.GetProperty(nameof(ISupportSoftDeletes.IsDeleted)); 31 | var index = ctx.Options.GetElasticIndex(); 32 | 33 | string fieldName = property != null ? index?.Configuration.Client.Infer.Field(new Field(property)) ?? "isDeleted" : "isDeleted"; 34 | if (mode == SoftDeleteQueryMode.ActiveOnly) 35 | ctx.Filter &= new TermQuery { Field = fieldName, Value = false }; 36 | else if (mode == SoftDeleteQueryMode.DeletedOnly) 37 | ctx.Filter &= new TermQuery { Field = fieldName, Value = true }; 38 | 39 | var parentType = ctx.Options.ParentDocumentType(); 40 | if (parentType != null && parentType != typeof(object)) 41 | ctx.Filter &= new HasParentQuery 42 | { 43 | ParentType = parentType, 44 | Query = new BoolQuery 45 | { 46 | Filter = new[] { new QueryContainer(new TermQuery { Field = fieldName, Value = false }) } 47 | } 48 | }; 49 | 50 | return Task.CompletedTask; 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /src/Foundatio.Repositories.Elasticsearch/Queries/Builders/SortQueryBuilder.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq.Expressions; 4 | using System.Threading.Tasks; 5 | using Foundatio.Parsers.ElasticQueries.Extensions; 6 | using Foundatio.Repositories.Elasticsearch.Extensions; 7 | using Foundatio.Repositories.Options; 8 | using Nest; 9 | 10 | namespace Foundatio.Repositories 11 | { 12 | public static class SortQueryExtensions 13 | { 14 | internal const string SortsKey = "@SortsKey"; 15 | 16 | public static T Sort(this T query, Field field, SortOrder? order = null) where T : IRepositoryQuery 17 | { 18 | return query.AddCollectionOptionValue(SortsKey, new FieldSort { Field = field, Order = order }); 19 | } 20 | 21 | public static T SortDescending(this T query, Field field) where T : IRepositoryQuery 22 | { 23 | return query.Sort(field, SortOrder.Descending); 24 | } 25 | 26 | public static T SortAscending(this T query, Field field) where T : IRepositoryQuery 27 | { 28 | return query.Sort(field, SortOrder.Ascending); 29 | } 30 | 31 | public static IRepositoryQuery Sort(this IRepositoryQuery query, Expression> objectPath, SortOrder? order = null) where T : class 32 | { 33 | return query.AddCollectionOptionValue, IFieldSort>(SortsKey, new FieldSort { Field = objectPath, Order = order }); 34 | } 35 | 36 | public static IRepositoryQuery SortDescending(this IRepositoryQuery query, Expression> objectPath) where T : class 37 | { 38 | return query.Sort(objectPath, SortOrder.Descending); 39 | } 40 | 41 | public static IRepositoryQuery SortAscending(this IRepositoryQuery query, Expression> objectPath) where T : class 42 | { 43 | return query.Sort(objectPath, SortOrder.Ascending); 44 | } 45 | } 46 | } 47 | 48 | namespace Foundatio.Repositories.Options 49 | { 50 | public static class ReadSortQueryExtensions 51 | { 52 | public static ICollection GetSorts(this IRepositoryQuery query) 53 | { 54 | return query.SafeGetCollection(SortQueryExtensions.SortsKey); 55 | } 56 | } 57 | } 58 | 59 | namespace Foundatio.Repositories.Elasticsearch.Queries.Builders 60 | { 61 | public class SortQueryBuilder : IElasticQueryBuilder 62 | { 63 | public Task BuildAsync(QueryBuilderContext ctx) where T : class, new() 64 | { 65 | var sortFields = ctx.Source.GetSorts(); 66 | if (sortFields.Count <= 0) 67 | return Task.CompletedTask; 68 | 69 | var resolver = ctx.GetMappingResolver(); 70 | sortFields = resolver.GetResolvedFields(sortFields); 71 | 72 | ctx.Search.Sort(sortFields); 73 | return Task.CompletedTask; 74 | } 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /src/Foundatio.Repositories.Elasticsearch/Queries/QueryParts/ElasticIndexesQuery.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using Foundatio.Repositories.Options; 4 | 5 | namespace Foundatio.Repositories 6 | { 7 | public static class ElasticIndexesQueryExtensions 8 | { 9 | internal const string ElasticIndexesKey = "@ElasticIndexes"; 10 | public static T Index(this T query, string field) where T : IRepositoryQuery 11 | { 12 | return query.AddCollectionOptionValue(ElasticIndexesKey, field); 13 | } 14 | 15 | public static T Index(this T query, IEnumerable fields) where T : IRepositoryQuery 16 | { 17 | return query.AddCollectionOptionValue(ElasticIndexesKey, fields); 18 | } 19 | 20 | public static T Index(this T query, params string[] fields) where T : IRepositoryQuery 21 | { 22 | return query.AddCollectionOptionValue(ElasticIndexesKey, fields); 23 | } 24 | 25 | internal const string ElasticIndexesStartUtcKey = "@ElasticIndexesStartUtc"; 26 | internal const string ElasticIndexesEndUtcKey = "@ElasticIndexesEndUtc"; 27 | public static T Index(this T query, DateTime? utcStart, DateTime? utcEnd) where T : IRepositoryQuery 28 | { 29 | if (utcStart.HasValue) 30 | query.Values.Set(ElasticIndexesStartUtcKey, utcStart); 31 | 32 | if (utcEnd.HasValue) 33 | query.Values.Set(ElasticIndexesEndUtcKey, utcEnd); 34 | 35 | return query; 36 | } 37 | } 38 | } 39 | 40 | namespace Foundatio.Repositories.Options 41 | { 42 | public static class ReadElasticIndexesQueryExtensions 43 | { 44 | public static ICollection GetElasticIndexes(this IRepositoryQuery options) 45 | { 46 | return options.SafeGetCollection(ElasticIndexesQueryExtensions.ElasticIndexesKey); 47 | } 48 | 49 | public static DateTime? GetElasticIndexesStartUtc(this IRepositoryQuery options) 50 | { 51 | return options.SafeGetOption(ElasticIndexesQueryExtensions.ElasticIndexesStartUtcKey); 52 | } 53 | 54 | public static DateTime? GetElasticIndexesEndUtc(this IRepositoryQuery options) 55 | { 56 | return options.SafeGetOption(ElasticIndexesQueryExtensions.ElasticIndexesEndUtcKey); 57 | } 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /src/Foundatio.Repositories.Elasticsearch/Repositories/IParentChildDocument.cs: -------------------------------------------------------------------------------- 1 | using Foundatio.Repositories.Models; 2 | using Nest; 3 | 4 | namespace Foundatio.Repositories.Elasticsearch; 5 | 6 | public interface IParentChildDocument : IIdentity 7 | { 8 | string ParentId { get; set; } 9 | JoinField Discriminator { get; set; } 10 | } 11 | -------------------------------------------------------------------------------- /src/Foundatio.Repositories.Elasticsearch/Repositories/MigrationStateRepository.cs: -------------------------------------------------------------------------------- 1 | using Foundatio.Repositories.Elasticsearch.Configuration; 2 | using Foundatio.Repositories.Migrations; 3 | using Nest; 4 | 5 | namespace Foundatio.Repositories.Elasticsearch; 6 | 7 | public class MigrationStateRepository : ElasticRepositoryBase, IMigrationStateRepository 8 | { 9 | public MigrationStateRepository(MigrationIndex index) : base(index) 10 | { 11 | DisableCache(); 12 | DefaultConsistency = Consistency.Immediate; 13 | } 14 | } 15 | 16 | public class MigrationIndex : Index 17 | { 18 | private readonly int _replicas; 19 | 20 | public MigrationIndex(IElasticConfiguration configuration, string name = "migration", int replicas = 1) : base(configuration, name) 21 | { 22 | _replicas = replicas; 23 | } 24 | 25 | public override TypeMappingDescriptor ConfigureIndexMapping(TypeMappingDescriptor map) 26 | { 27 | return map 28 | .Dynamic(false) 29 | .Properties(p => p 30 | .Keyword(f => f.Name(e => e.Id)) 31 | .Number(f => f.Name(e => e.Version).Type(NumberType.Integer)) 32 | .Date(f => f.Name(e => e.StartedUtc)) 33 | .Date(f => f.Name(e => e.CompletedUtc)) 34 | ); 35 | } 36 | 37 | public override CreateIndexDescriptor ConfigureIndex(CreateIndexDescriptor idx) 38 | { 39 | return base.ConfigureIndex(idx).Settings(s => s 40 | .NumberOfShards(1) 41 | .NumberOfReplicas(_replicas)); 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /src/Foundatio.Repositories/Exceptions/AsyncQueryNotFoundException.cs: -------------------------------------------------------------------------------- 1 | namespace Foundatio.Repositories.Exceptions; 2 | 3 | public class AsyncQueryNotFoundException : RepositoryException 4 | { 5 | public AsyncQueryNotFoundException() { } 6 | 7 | public AsyncQueryNotFoundException(string id) : base($"Async query \"{id}\" could not be found") 8 | { 9 | Id = id; 10 | } 11 | 12 | public string Id { get; private set; } 13 | } 14 | -------------------------------------------------------------------------------- /src/Foundatio.Repositories/Exceptions/DocumentException.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace Foundatio.Repositories.Exceptions; 4 | 5 | public class DocumentException : RepositoryException 6 | { 7 | public DocumentException() : base() { } 8 | public DocumentException(string message) : base(message) { } 9 | public DocumentException(string message, Exception innerException) : base(message, innerException) { } 10 | } 11 | -------------------------------------------------------------------------------- /src/Foundatio.Repositories/Exceptions/DocumentNotFoundException.cs: -------------------------------------------------------------------------------- 1 | namespace Foundatio.Repositories.Exceptions; 2 | 3 | public class DocumentNotFoundException : DocumentException 4 | { 5 | public DocumentNotFoundException() { } 6 | 7 | public DocumentNotFoundException(string id) : base($"Document \"{id}\" could not be found") 8 | { 9 | Id = id; 10 | } 11 | 12 | public string Id { get; private set; } 13 | } 14 | -------------------------------------------------------------------------------- /src/Foundatio.Repositories/Exceptions/DocumentValidationException.cs: -------------------------------------------------------------------------------- 1 | namespace Foundatio.Repositories.Exceptions; 2 | 3 | public class DocumentValidationException : DocumentException 4 | { 5 | public DocumentValidationException() { } 6 | 7 | public DocumentValidationException(string message) : base(message) { } 8 | } 9 | -------------------------------------------------------------------------------- /src/Foundatio.Repositories/Exceptions/DuplicateDocumentException.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace Foundatio.Repositories.Exceptions; 4 | 5 | public class DuplicateDocumentException : DocumentException 6 | { 7 | public DuplicateDocumentException() : base() { } 8 | public DuplicateDocumentException(string message) : base(message) { } 9 | public DuplicateDocumentException(string message, Exception innerException) : base(message, innerException) { } 10 | } 11 | -------------------------------------------------------------------------------- /src/Foundatio.Repositories/Exceptions/RepositoryException.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace Foundatio.Repositories.Exceptions; 4 | 5 | public class RepositoryException : Exception 6 | { 7 | public RepositoryException() : base() { } 8 | public RepositoryException(string message) : base(message) { } 9 | public RepositoryException(string message, Exception innerException) : base(message, innerException) { } 10 | } 11 | -------------------------------------------------------------------------------- /src/Foundatio.Repositories/Exceptions/VersionConflictDocumentException.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace Foundatio.Repositories.Exceptions; 4 | 5 | public class VersionConflictDocumentException : DocumentException 6 | { 7 | public VersionConflictDocumentException() : base() { } 8 | public VersionConflictDocumentException(string message) : base(message) { } 9 | public VersionConflictDocumentException(string message, Exception innerException) : base(message, innerException) { } 10 | } 11 | -------------------------------------------------------------------------------- /src/Foundatio.Repositories/Extensions/FindResultsExtensions.cs: -------------------------------------------------------------------------------- 1 | using Foundatio.Utility; 2 | 3 | namespace Foundatio.Repositories.Extensions; 4 | 5 | public static class FindResultsExtensions 6 | { 7 | public static string GetAsyncQueryId(this IHaveData results) 8 | { 9 | return results.Data.GetString(AsyncQueryDataKeys.AsyncQueryId, null); 10 | } 11 | 12 | /// 13 | /// Whether the query completed successfully or was interrupted (will also be true while the query is still running) 14 | /// 15 | public static bool IsAsyncQueryPartial(this IHaveData results) 16 | { 17 | return results.Data.GetBoolean(AsyncQueryDataKeys.IsPartial, false); 18 | } 19 | 20 | /// 21 | /// Whether the query is still running 22 | /// 23 | public static bool IsAsyncQueryRunning(this IHaveData results) 24 | { 25 | return results.Data.GetBoolean(AsyncQueryDataKeys.IsRunning, false); 26 | } 27 | } 28 | 29 | public static class AsyncQueryDataKeys 30 | { 31 | public const string AsyncQueryId = "@AsyncQueryId"; 32 | public const string IsRunning = "@IsRunning"; 33 | public const string IsPartial = "@IsPending"; 34 | } 35 | -------------------------------------------------------------------------------- /src/Foundatio.Repositories/Extensions/NumberExtensions.cs: -------------------------------------------------------------------------------- 1 | namespace Foundatio.Repositories.Extensions; 2 | 3 | public static class NumericExtensions 4 | { 5 | public static string ToOrdinal(this int num) 6 | { 7 | switch (num % 100) 8 | { 9 | case 11: 10 | case 12: 11 | case 13: 12 | return num.ToString("#,###0") + "th"; 13 | } 14 | 15 | switch (num % 10) 16 | { 17 | case 1: 18 | return num.ToString("#,###0") + "st"; 19 | case 2: 20 | return num.ToString("#,###0") + "nd"; 21 | case 3: 22 | return num.ToString("#,###0") + "rd"; 23 | default: 24 | return num.ToString("#,###0") + "th"; 25 | } 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /src/Foundatio.Repositories/Extensions/StringExtensions.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Text; 3 | 4 | namespace Foundatio.Repositories.Extensions; 5 | 6 | public static class StringExtensions 7 | { 8 | public static string ToJTokenPath(this string path) 9 | { 10 | if (path.StartsWith("$")) 11 | return path; 12 | 13 | var sb = new StringBuilder(); 14 | string[] parts = path.Split(new[] { '/' }, StringSplitOptions.RemoveEmptyEntries); 15 | for (int i = 0; i < parts.Length; i++) 16 | { 17 | if (parts[i].IsNumeric()) 18 | sb.Append("[").Append(parts[i]).Append("]"); 19 | else 20 | sb.Append(parts[i]); 21 | 22 | if (i < parts.Length - 1 && !parts[i + 1].IsNumeric()) 23 | sb.Append("."); 24 | } 25 | 26 | return sb.ToString(); 27 | } 28 | 29 | public static bool IsNumeric(this string value) 30 | { 31 | if (String.IsNullOrEmpty(value)) 32 | return false; 33 | 34 | for (int i = 0; i < value.Length; i++) 35 | { 36 | if (Char.IsNumber(value[i])) 37 | continue; 38 | 39 | if (i == 0 && value[i] == '-') 40 | continue; 41 | 42 | return false; 43 | } 44 | 45 | return true; 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /src/Foundatio.Repositories/Extensions/TaskExtensions.cs: -------------------------------------------------------------------------------- 1 | using System.Diagnostics; 2 | using System.Runtime.CompilerServices; 3 | using System.Threading.Tasks; 4 | 5 | namespace Foundatio.Repositories.Extensions; 6 | 7 | internal static class TaskHelper 8 | { 9 | [DebuggerStepThrough] 10 | public static ConfiguredTaskAwaitable AnyContext(this Task task) 11 | { 12 | return task.ConfigureAwait(continueOnCapturedContext: false); 13 | } 14 | 15 | [DebuggerStepThrough] 16 | public static ConfiguredTaskAwaitable AnyContext(this Task task) 17 | { 18 | return task.ConfigureAwait(continueOnCapturedContext: false); 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /src/Foundatio.Repositories/Foundatio.Repositories.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /src/Foundatio.Repositories/IReadOnlyRepository.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | using System.Threading.Tasks; 3 | using Foundatio.Repositories.Models; 4 | using Foundatio.Utility; 5 | 6 | namespace Foundatio.Repositories; 7 | 8 | public interface IReadOnlyRepository where T : class, new() 9 | { 10 | Task GetByIdAsync(Id id, CommandOptionsDescriptor options); 11 | Task GetByIdAsync(Id id, ICommandOptions options = null); 12 | 13 | Task> GetByIdsAsync(Ids ids, CommandOptionsDescriptor options); 14 | Task> GetByIdsAsync(Ids ids, ICommandOptions options = null); 15 | 16 | Task> GetAllAsync(CommandOptionsDescriptor options); 17 | Task> GetAllAsync(ICommandOptions options = null); 18 | 19 | Task ExistsAsync(Id id, CommandOptionsDescriptor options); 20 | Task ExistsAsync(Id id, ICommandOptions options = null); 21 | 22 | Task CountAsync(CommandOptionsDescriptor options); 23 | Task CountAsync(ICommandOptions options = null); 24 | 25 | Task InvalidateCacheAsync(T document); 26 | Task InvalidateCacheAsync(IEnumerable documents); 27 | 28 | Task InvalidateCacheAsync(string cacheKey); 29 | Task InvalidateCacheAsync(IEnumerable cacheKeys); 30 | 31 | AsyncEvent> BeforeQuery { get; } 32 | } 33 | -------------------------------------------------------------------------------- /src/Foundatio.Repositories/IRepository.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | using System.Threading.Tasks; 3 | using Foundatio.Repositories.Models; 4 | using Foundatio.Utility; 5 | 6 | namespace Foundatio.Repositories; 7 | 8 | public interface IRepository : IReadOnlyRepository where T : class, IIdentity, new() 9 | { 10 | Task AddAsync(T document, CommandOptionsDescriptor options); 11 | Task AddAsync(T document, ICommandOptions options = null); 12 | 13 | Task AddAsync(IEnumerable documents, CommandOptionsDescriptor options); 14 | Task AddAsync(IEnumerable documents, ICommandOptions options = null); 15 | 16 | Task SaveAsync(T document, CommandOptionsDescriptor options); 17 | Task SaveAsync(T document, ICommandOptions options = null); 18 | 19 | Task SaveAsync(IEnumerable documents, CommandOptionsDescriptor options); 20 | Task SaveAsync(IEnumerable documents, ICommandOptions options = null); 21 | 22 | Task PatchAsync(Id id, IPatchOperation operation, CommandOptionsDescriptor options); 23 | Task PatchAsync(Id id, IPatchOperation operation, ICommandOptions options = null); 24 | 25 | Task PatchAsync(Ids ids, IPatchOperation operation, CommandOptionsDescriptor options); 26 | Task PatchAsync(Ids ids, IPatchOperation operation, ICommandOptions options = null); 27 | 28 | Task RemoveAsync(Id id, CommandOptionsDescriptor options); 29 | Task RemoveAsync(Id id, ICommandOptions options = null); 30 | 31 | Task RemoveAsync(Ids ids, CommandOptionsDescriptor options); 32 | Task RemoveAsync(Ids ids, ICommandOptions options = null); 33 | 34 | Task RemoveAsync(T document, CommandOptionsDescriptor options); 35 | Task RemoveAsync(T document, ICommandOptions options = null); 36 | 37 | Task RemoveAsync(IEnumerable documents, CommandOptionsDescriptor options); 38 | Task RemoveAsync(IEnumerable documents, ICommandOptions options = null); 39 | 40 | Task RemoveAllAsync(CommandOptionsDescriptor options); 41 | Task RemoveAllAsync(ICommandOptions options = null); 42 | 43 | AsyncEvent> DocumentsAdding { get; } 44 | AsyncEvent> DocumentsAdded { get; } 45 | AsyncEvent> DocumentsSaving { get; } 46 | AsyncEvent> DocumentsSaved { get; } 47 | AsyncEvent> DocumentsRemoving { get; } 48 | AsyncEvent> DocumentsRemoved { get; } 49 | AsyncEvent> DocumentsChanging { get; } 50 | AsyncEvent> DocumentsChanged { get; } 51 | } 52 | -------------------------------------------------------------------------------- /src/Foundatio.Repositories/JsonPatch/AbstractPatcher.cs: -------------------------------------------------------------------------------- 1 | namespace Foundatio.Repositories.Utility; 2 | 3 | public abstract class AbstractPatcher where TDoc : class 4 | { 5 | public virtual void Patch(ref TDoc target, PatchDocument document) 6 | { 7 | foreach (var operation in document.Operations) 8 | { 9 | target = ApplyOperation(operation, target); 10 | } 11 | } 12 | 13 | public virtual TDoc ApplyOperation(Operation operation, TDoc target) 14 | { 15 | if (operation is AddOperation) 16 | Add((AddOperation)operation, target); 17 | else if (operation is CopyOperation) 18 | Copy((CopyOperation)operation, target); 19 | else if (operation is MoveOperation) 20 | Move((MoveOperation)operation, target); 21 | else if (operation is RemoveOperation) 22 | Remove((RemoveOperation)operation, target); 23 | else if (operation is ReplaceOperation) 24 | target = Replace((ReplaceOperation)operation, target) ?? target; 25 | else if (operation is TestOperation) 26 | Test((TestOperation)operation, target); 27 | return target; 28 | } 29 | 30 | protected abstract void Add(AddOperation operation, TDoc target); 31 | protected abstract void Copy(CopyOperation operation, TDoc target); 32 | protected abstract void Move(MoveOperation operation, TDoc target); 33 | protected abstract void Remove(RemoveOperation operation, TDoc target); 34 | protected abstract TDoc Replace(ReplaceOperation operation, TDoc target); 35 | protected abstract void Test(TestOperation operation, TDoc target); 36 | } 37 | -------------------------------------------------------------------------------- /src/Foundatio.Repositories/JsonPatch/AddOperation.cs: -------------------------------------------------------------------------------- 1 | using Newtonsoft.Json; 2 | using Newtonsoft.Json.Linq; 3 | 4 | namespace Foundatio.Repositories.Utility; 5 | 6 | public class AddOperation : Operation 7 | { 8 | public JToken Value { get; set; } 9 | 10 | public override void Write(JsonWriter writer) 11 | { 12 | writer.WriteStartObject(); 13 | 14 | WriteOp(writer, "add"); 15 | WritePath(writer, Path); 16 | WriteValue(writer, Value); 17 | 18 | writer.WriteEndObject(); 19 | } 20 | 21 | public override void Read(JObject jOperation) 22 | { 23 | Path = jOperation.Value("path"); 24 | Value = jOperation.GetValue("value"); 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /src/Foundatio.Repositories/JsonPatch/CopyOperation.cs: -------------------------------------------------------------------------------- 1 | using Newtonsoft.Json; 2 | using Newtonsoft.Json.Linq; 3 | 4 | namespace Foundatio.Repositories.Utility; 5 | 6 | public class CopyOperation : Operation 7 | { 8 | public string FromPath { get; set; } 9 | 10 | public override void Write(JsonWriter writer) 11 | { 12 | writer.WriteStartObject(); 13 | 14 | WriteOp(writer, "copy"); 15 | WritePath(writer, Path); 16 | WriteFromPath(writer, FromPath); 17 | 18 | writer.WriteEndObject(); 19 | } 20 | 21 | public override void Read(JObject jOperation) 22 | { 23 | Path = jOperation.Value("path"); 24 | FromPath = jOperation.Value("from"); 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /src/Foundatio.Repositories/JsonPatch/MoveOperation.cs: -------------------------------------------------------------------------------- 1 | using Newtonsoft.Json; 2 | using Newtonsoft.Json.Linq; 3 | 4 | namespace Foundatio.Repositories.Utility; 5 | 6 | public class MoveOperation : Operation 7 | { 8 | public string FromPath { get; set; } 9 | 10 | public override void Write(JsonWriter writer) 11 | { 12 | writer.WriteStartObject(); 13 | 14 | WriteOp(writer, "move"); 15 | WritePath(writer, Path); 16 | WriteFromPath(writer, FromPath); 17 | 18 | writer.WriteEndObject(); 19 | } 20 | 21 | public override void Read(JObject jOperation) 22 | { 23 | Path = jOperation.Value("path"); 24 | FromPath = jOperation.Value("from"); 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /src/Foundatio.Repositories/JsonPatch/Operation.cs: -------------------------------------------------------------------------------- 1 | using Newtonsoft.Json; 2 | using Newtonsoft.Json.Linq; 3 | 4 | namespace Foundatio.Repositories.Utility; 5 | 6 | public abstract class Operation 7 | { 8 | public string Path { get; set; } 9 | 10 | public abstract void Write(JsonWriter writer); 11 | 12 | protected static void WriteOp(JsonWriter writer, string op) 13 | { 14 | writer.WritePropertyName("op"); 15 | writer.WriteValue(op); 16 | } 17 | 18 | protected static void WritePath(JsonWriter writer, string path) 19 | { 20 | writer.WritePropertyName("path"); 21 | writer.WriteValue(path); 22 | } 23 | 24 | protected static void WriteFromPath(JsonWriter writer, string path) 25 | { 26 | writer.WritePropertyName("from"); 27 | writer.WriteValue(path); 28 | } 29 | 30 | protected static void WriteValue(JsonWriter writer, JToken value) 31 | { 32 | writer.WritePropertyName("value"); 33 | value.WriteTo(writer); 34 | } 35 | 36 | public abstract void Read(JObject jOperation); 37 | 38 | public static Operation Parse(string json) 39 | { 40 | return Build(JObject.Parse(json)); 41 | } 42 | 43 | public static Operation Build(JObject jOperation) 44 | { 45 | var op = PatchDocument.CreateOperation((string)jOperation["op"]); 46 | op.Read(jOperation); 47 | return op; 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /src/Foundatio.Repositories/JsonPatch/PatchDocumentConverter.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using Newtonsoft.Json; 3 | using Newtonsoft.Json.Linq; 4 | 5 | namespace Foundatio.Repositories.Utility; 6 | 7 | public class PatchDocumentConverter : JsonConverter 8 | { 9 | public override bool CanConvert(Type objectType) 10 | { 11 | return true; 12 | } 13 | 14 | public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer) 15 | { 16 | if (objectType != typeof(PatchDocument)) 17 | throw new ArgumentException("Object must be of type PatchDocument", nameof(objectType)); 18 | 19 | try 20 | { 21 | if (reader.TokenType == JsonToken.Null) 22 | return null; 23 | 24 | var patch = JArray.Load(reader); 25 | return PatchDocument.Parse(patch.ToString()); 26 | } 27 | catch (Exception ex) 28 | { 29 | throw new ArgumentException("Invalid patch document: " + ex.Message); 30 | } 31 | } 32 | 33 | public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer) 34 | { 35 | if (!(value is PatchDocument)) 36 | return; 37 | 38 | var jsonPatchDoc = (PatchDocument)value; 39 | writer.WriteStartArray(); 40 | foreach (var op in jsonPatchDoc.Operations) 41 | op.Write(writer); 42 | writer.WriteEndArray(); 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /src/Foundatio.Repositories/JsonPatch/RemoveOperation.cs: -------------------------------------------------------------------------------- 1 | using Newtonsoft.Json; 2 | using Newtonsoft.Json.Linq; 3 | 4 | namespace Foundatio.Repositories.Utility; 5 | 6 | public class RemoveOperation : Operation 7 | { 8 | public override void Write(JsonWriter writer) 9 | { 10 | writer.WriteStartObject(); 11 | 12 | WriteOp(writer, "remove"); 13 | WritePath(writer, Path); 14 | 15 | writer.WriteEndObject(); 16 | } 17 | 18 | public override void Read(JObject jOperation) 19 | { 20 | Path = jOperation.Value("path"); 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /src/Foundatio.Repositories/JsonPatch/ReplaceOperation.cs: -------------------------------------------------------------------------------- 1 | using Newtonsoft.Json; 2 | using Newtonsoft.Json.Linq; 3 | 4 | namespace Foundatio.Repositories.Utility; 5 | 6 | public class ReplaceOperation : Operation 7 | { 8 | public JToken Value { get; set; } 9 | 10 | public override void Write(JsonWriter writer) 11 | { 12 | writer.WriteStartObject(); 13 | 14 | WriteOp(writer, "replace"); 15 | WritePath(writer, Path); 16 | WriteValue(writer, Value); 17 | 18 | writer.WriteEndObject(); 19 | } 20 | 21 | public override void Read(JObject jOperation) 22 | { 23 | Path = jOperation.Value("path"); 24 | Value = jOperation.GetValue("value"); 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /src/Foundatio.Repositories/JsonPatch/TestOperation.cs: -------------------------------------------------------------------------------- 1 | using Newtonsoft.Json; 2 | using Newtonsoft.Json.Linq; 3 | 4 | namespace Foundatio.Repositories.Utility; 5 | 6 | public class TestOperation : Operation 7 | { 8 | public JToken Value { get; set; } 9 | 10 | public override void Write(JsonWriter writer) 11 | { 12 | writer.WriteStartObject(); 13 | 14 | WriteOp(writer, "test"); 15 | WritePath(writer, Path); 16 | WriteValue(writer, Value); 17 | 18 | writer.WriteEndObject(); 19 | } 20 | 21 | public override void Read(JObject jOperation) 22 | { 23 | Path = jOperation.Value("path"); 24 | Value = jOperation.GetValue("value"); 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /src/Foundatio.Repositories/Migration/IMigration.cs: -------------------------------------------------------------------------------- 1 | using System.Threading; 2 | using System.Threading.Tasks; 3 | using Foundatio.Lock; 4 | using Microsoft.Extensions.Logging; 5 | 6 | namespace Foundatio.Repositories.Migrations; 7 | 8 | public interface IMigration 9 | { 10 | /// 11 | /// The type of migration. 12 | /// Versioned migrations are run once and they are run sequentially. 13 | /// VersionedAndResumable migrations are the 14 | /// same as Versioned migrations except if they fail to complete, they will 15 | /// automatically be retried. 16 | /// Repeatable migrations will be run after all versioned migrations 17 | /// are run and can be repeatedly run without causing issues. Version is not required on 18 | /// Repeatable migrations, if the Version property 19 | /// is populated, it is treated as a revision for that specific migration and if it has a revision 20 | /// that is higher than from when it was last run, then the migration will be run again. 21 | /// 22 | MigrationType MigrationType { get; } 23 | 24 | /// 25 | /// Versioned and VersionedAndResumable 26 | /// migrations require a version number in order to be run. The version of the migration determines the order in which migrations are run. 27 | /// If a version is not set, they will be treated as disabled and will not run automatically. 28 | /// For Repeatable migrations, if the version is populated then it is considered a 29 | /// revision of that specific migration (not an overall version) and if the revision is higher than the last time the migration was 30 | /// run then the migration will be run again. 31 | /// 32 | int? Version { get; } 33 | 34 | /// 35 | /// If set to true, indicates that this migration needs to be run while the application is offline. 36 | /// 37 | bool RequiresOffline { get; } 38 | 39 | Task RunAsync(MigrationContext context); 40 | } 41 | 42 | public class MigrationContext 43 | { 44 | public MigrationContext(ILock migrationLock, ILogger logger, CancellationToken cancellationToken) 45 | { 46 | Lock = migrationLock; 47 | Logger = logger; 48 | CancellationToken = cancellationToken; 49 | } 50 | 51 | public ILock Lock { get; } 52 | public ILogger Logger { get; } 53 | public CancellationToken CancellationToken { get; } 54 | } 55 | 56 | public enum MigrationType 57 | { 58 | Versioned, 59 | VersionedAndResumable, 60 | Repeatable 61 | } 62 | 63 | public static class MigrationExtensions 64 | { 65 | public static string GetId(this IMigration migration) 66 | { 67 | return migration.MigrationType != MigrationType.Repeatable ? migration.Version.ToString() : migration.GetType().FullName; 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /src/Foundatio.Repositories/Migration/IMigrationStateRepository.cs: -------------------------------------------------------------------------------- 1 | namespace Foundatio.Repositories.Migrations; 2 | 3 | public interface IMigrationStateRepository : IRepository { } 4 | -------------------------------------------------------------------------------- /src/Foundatio.Repositories/Migration/MigrationBase.cs: -------------------------------------------------------------------------------- 1 | using System.Threading.Tasks; 2 | using Microsoft.Extensions.Logging; 3 | using Microsoft.Extensions.Logging.Abstractions; 4 | 5 | namespace Foundatio.Repositories.Migrations; 6 | 7 | public abstract class MigrationBase : IMigration 8 | { 9 | protected ILogger _logger; 10 | 11 | public MigrationBase() { } 12 | 13 | public MigrationBase(ILoggerFactory loggerFactory) 14 | { 15 | loggerFactory = loggerFactory ?? NullLoggerFactory.Instance; 16 | _logger = loggerFactory.CreateLogger(GetType()); 17 | } 18 | 19 | public MigrationType MigrationType { get; protected set; } = MigrationType.Versioned; 20 | 21 | public virtual int? Version { get; protected set; } = null; 22 | 23 | public bool RequiresOffline { get; protected set; } = false; 24 | 25 | public virtual Task RunAsync() 26 | { 27 | return Task.CompletedTask; 28 | } 29 | 30 | public virtual Task RunAsync(MigrationContext context) 31 | { 32 | return RunAsync(); 33 | } 34 | 35 | protected int CalculateProgress(long total, long completed, int startProgress = 0, int endProgress = 100) 36 | { 37 | return startProgress + (int)((100 * (double)completed / total) * (((double)endProgress - startProgress) / 100)); 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /src/Foundatio.Repositories/Migration/MigrationState.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using Foundatio.Repositories.Models; 3 | 4 | namespace Foundatio.Repositories.Migrations; 5 | 6 | public class MigrationState : IIdentity 7 | { 8 | public string Id { get; set; } 9 | public MigrationType MigrationType { get; set; } 10 | public int Version { get; set; } 11 | public DateTime StartedUtc { get; set; } 12 | public DateTime? CompletedUtc { get; set; } 13 | public string ErrorMessage { get; set; } 14 | } 15 | -------------------------------------------------------------------------------- /src/Foundatio.Repositories/Models/Aggregations/AggregationsHelper.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | 3 | namespace Foundatio.Repositories.Models; 4 | 5 | public class AggregationsHelper 6 | { 7 | public IReadOnlyDictionary Aggregations { get; protected internal set; } = EmptyReadOnly.Dictionary; 8 | 9 | public AggregationsHelper() { } 10 | 11 | protected AggregationsHelper(IDictionary aggregations) 12 | { 13 | Aggregations = aggregations != null ? 14 | new Dictionary(aggregations) 15 | : EmptyReadOnly.Dictionary; 16 | } 17 | 18 | public AggregationsHelper(IReadOnlyDictionary aggregations) 19 | { 20 | Aggregations = aggregations ?? EmptyReadOnly.Dictionary; 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /src/Foundatio.Repositories/Models/Aggregations/BucketAggregate.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | 3 | namespace Foundatio.Repositories.Models; 4 | 5 | public class BucketAggregate : IAggregate 6 | { 7 | public IReadOnlyCollection Items { get; set; } = EmptyReadOnly.Collection; 8 | public IReadOnlyDictionary Data { get; set; } = EmptyReadOnly.Dictionary; 9 | public long Total { get; set; } 10 | } 11 | -------------------------------------------------------------------------------- /src/Foundatio.Repositories/Models/Aggregations/BucketAggregateBase.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | 3 | namespace Foundatio.Repositories.Models; 4 | 5 | public abstract class BucketAggregateBase : AggregationsHelper, IAggregate 6 | { 7 | protected BucketAggregateBase() { } 8 | protected BucketAggregateBase(IReadOnlyDictionary aggregations) : base(aggregations) { } 9 | 10 | public IReadOnlyDictionary Data { get; set; } 11 | } 12 | -------------------------------------------------------------------------------- /src/Foundatio.Repositories/Models/Aggregations/BucketBase.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | 3 | namespace Foundatio.Repositories.Models; 4 | 5 | public abstract class BucketBase : AggregationsHelper, IBucket 6 | { 7 | protected BucketBase() { } 8 | protected BucketBase(IReadOnlyDictionary aggregations) : base(aggregations) { } 9 | 10 | public IReadOnlyDictionary Data { get; set; } 11 | } 12 | -------------------------------------------------------------------------------- /src/Foundatio.Repositories/Models/Aggregations/BucketedAggregation.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | 3 | namespace Foundatio.Repositories.Models; 4 | 5 | public class BucketedAggregation 6 | { 7 | public IReadOnlyCollection Buckets { get; set; } = EmptyReadOnly.Collection; 8 | } 9 | -------------------------------------------------------------------------------- /src/Foundatio.Repositories/Models/Aggregations/DateHistogramBucket.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Diagnostics; 4 | 5 | namespace Foundatio.Repositories.Models; 6 | 7 | [DebuggerDisplay("Date: {Date}: Ticks: {Key}")] 8 | public class DateHistogramBucket : KeyedBucket 9 | { 10 | public DateHistogramBucket() 11 | { 12 | } 13 | 14 | [System.Text.Json.Serialization.JsonConstructor] 15 | public DateHistogramBucket(DateTime date, IReadOnlyDictionary aggregations) : base(aggregations) 16 | { 17 | Date = date; 18 | } 19 | 20 | public DateTime Date { get; } 21 | } 22 | -------------------------------------------------------------------------------- /src/Foundatio.Repositories/Models/Aggregations/ExtendedStatsAggregate.cs: -------------------------------------------------------------------------------- 1 | using System.Diagnostics; 2 | 3 | namespace Foundatio.Repositories.Models; 4 | 5 | [DebuggerDisplay("Count: {Count} Min: {Min} Max: {Max} Average: {Average} Sum: {Sum} SumOfSquares: {SumOfSquares} Variance: {Variance} StdDeviation: {StdDeviation}")] 6 | public class ExtendedStatsAggregate : StatsAggregate 7 | { 8 | public double? SumOfSquares { get; set; } 9 | public double? Variance { get; set; } 10 | public double? StdDeviation { get; set; } 11 | public StandardDeviationBounds StdDeviationBounds { get; set; } 12 | } 13 | -------------------------------------------------------------------------------- /src/Foundatio.Repositories/Models/Aggregations/IAggregate.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | using Foundatio.Repositories.Utility; 3 | 4 | namespace Foundatio.Repositories.Models; 5 | 6 | [Newtonsoft.Json.JsonConverter(typeof(AggregationsNewtonsoftJsonConverter))] 7 | [System.Text.Json.Serialization.JsonConverter(typeof(AggregationsSystemTextJsonConverter))] 8 | public interface IAggregate 9 | { 10 | IReadOnlyDictionary Data { get; set; } 11 | } 12 | -------------------------------------------------------------------------------- /src/Foundatio.Repositories/Models/Aggregations/IBucket.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | using Foundatio.Repositories.Utility; 3 | 4 | namespace Foundatio.Repositories.Models; 5 | 6 | [Newtonsoft.Json.JsonConverter(typeof(BucketsNewtonsoftJsonConverter))] 7 | [System.Text.Json.Serialization.JsonConverter(typeof(BucketsSystemTextJsonConverter))] 8 | public interface IBucket 9 | { 10 | IReadOnlyDictionary Data { get; set; } 11 | } 12 | -------------------------------------------------------------------------------- /src/Foundatio.Repositories/Models/Aggregations/KeyedAggregation.cs: -------------------------------------------------------------------------------- 1 | namespace Foundatio.Repositories.Models; 2 | 3 | public class KeyedAggregation : ValueAggregate 4 | { 5 | public T Key { get; set; } 6 | } 7 | -------------------------------------------------------------------------------- /src/Foundatio.Repositories/Models/Aggregations/KeyedBucket.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Diagnostics; 4 | using Foundatio.Repositories.Utility; 5 | 6 | namespace Foundatio.Repositories.Models; 7 | 8 | [DebuggerDisplay("KeyAsString: {KeyAsString} Key: {Key} Total: {Total}")] 9 | public class KeyedBucket : BucketBase 10 | { 11 | public KeyedBucket() 12 | { 13 | } 14 | 15 | [System.Text.Json.Serialization.JsonConstructor] 16 | public KeyedBucket(IReadOnlyDictionary aggregations) : base(aggregations) 17 | { 18 | } 19 | 20 | [System.Text.Json.Serialization.JsonConverter(typeof(ObjectToInferredTypesConverter))] 21 | public T Key { get; set; } 22 | public string KeyAsString { get; set; } 23 | public long? Total { get; set; } 24 | } 25 | -------------------------------------------------------------------------------- /src/Foundatio.Repositories/Models/Aggregations/MetricAggregateBase.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | 3 | namespace Foundatio.Repositories.Models; 4 | 5 | public class MetricAggregateBase : IAggregate 6 | { 7 | public IReadOnlyDictionary Data { get; set; } 8 | } 9 | -------------------------------------------------------------------------------- /src/Foundatio.Repositories/Models/Aggregations/MultiBucketAggregate.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | 3 | namespace Foundatio.Repositories.Models; 4 | 5 | public class MultiBucketAggregate : BucketAggregateBase 6 | where TBucket : IBucket 7 | { 8 | public MultiBucketAggregate() { } 9 | public MultiBucketAggregate(IReadOnlyDictionary aggregations) : base(aggregations) { } 10 | 11 | public IReadOnlyCollection Buckets { get; set; } = EmptyReadOnly.Collection; 12 | } 13 | -------------------------------------------------------------------------------- /src/Foundatio.Repositories/Models/Aggregations/ObjectValueAggregate.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Diagnostics; 3 | using Foundatio.Serializer; 4 | using Newtonsoft.Json.Linq; 5 | 6 | namespace Foundatio.Repositories.Models; 7 | 8 | [DebuggerDisplay("Value: {Value}")] 9 | public class ObjectValueAggregate : MetricAggregateBase 10 | { 11 | public object Value { get; set; } 12 | 13 | public T ValueAs(ITextSerializer serializer = null) 14 | { 15 | if (serializer != null) 16 | { 17 | if (Value is string stringValue) 18 | return serializer.Deserialize(stringValue); 19 | else if (Value is JToken jTokenValue) 20 | return serializer.Deserialize(jTokenValue.ToString()); 21 | } 22 | 23 | return Value is JToken jToken 24 | ? jToken.ToObject() 25 | : (T)Convert.ChangeType(Value, typeof(T)); 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /src/Foundatio.Repositories/Models/Aggregations/PercentilesAggregate.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | using System.Diagnostics; 3 | 4 | namespace Foundatio.Repositories.Models; 5 | 6 | [DebuggerDisplay("Percentile: {Percentile} Value: {Value}")] 7 | public class PercentileItem 8 | { 9 | public double Percentile { get; set; } 10 | public double? Value { get; set; } 11 | } 12 | 13 | public class PercentilesAggregate : MetricAggregateBase 14 | { 15 | public PercentilesAggregate() { } 16 | 17 | public PercentilesAggregate(IEnumerable items) 18 | { 19 | Items = new List(items).AsReadOnly(); 20 | } 21 | 22 | public IReadOnlyCollection Items { get; internal set; } 23 | } 24 | -------------------------------------------------------------------------------- /src/Foundatio.Repositories/Models/Aggregations/RangeBucket.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | using System.Diagnostics; 3 | 4 | namespace Foundatio.Repositories.Models; 5 | 6 | [DebuggerDisplay("Key: {Key} From: {FromAsString} To: {ToAsString} Total: {Total}")] 7 | public class RangeBucket : BucketBase 8 | { 9 | public RangeBucket() { } 10 | 11 | [System.Text.Json.Serialization.JsonConstructor] 12 | public RangeBucket(IReadOnlyDictionary aggregations) : base(aggregations) { } 13 | 14 | public string Key { get; set; } 15 | public double? From { get; set; } 16 | public string FromAsString { get; set; } 17 | public double? To { get; set; } 18 | public string ToAsString { get; set; } 19 | public long Total { get; set; } 20 | } 21 | -------------------------------------------------------------------------------- /src/Foundatio.Repositories/Models/Aggregations/SingleBucketAggregate.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | using System.Diagnostics; 3 | 4 | namespace Foundatio.Repositories.Models; 5 | 6 | [DebuggerDisplay("Total: {Total}")] 7 | public class SingleBucketAggregate : BucketAggregateBase 8 | { 9 | public SingleBucketAggregate() { } 10 | public SingleBucketAggregate(IReadOnlyDictionary aggregations) : base(aggregations) { } 11 | 12 | public long Total { get; set; } 13 | } 14 | -------------------------------------------------------------------------------- /src/Foundatio.Repositories/Models/Aggregations/StandardDeviationBounds.cs: -------------------------------------------------------------------------------- 1 | using System.Diagnostics; 2 | 3 | namespace Foundatio.Repositories.Models; 4 | 5 | [DebuggerDisplay("Lower: {Lower} Upper: {Upper}")] 6 | 7 | public class StandardDeviationBounds 8 | { 9 | public double? Upper { get; set; } 10 | public double? Lower { get; set; } 11 | } 12 | -------------------------------------------------------------------------------- /src/Foundatio.Repositories/Models/Aggregations/StatsAggregate.cs: -------------------------------------------------------------------------------- 1 | using System.Diagnostics; 2 | 3 | namespace Foundatio.Repositories.Models; 4 | 5 | [DebuggerDisplay("Count: {Count} Min: {Min} Max: {Max} Average: {Average} Sum: {Sum}")] 6 | public class StatsAggregate : MetricAggregateBase 7 | { 8 | public long Count { get; set; } 9 | public double? Min { get; set; } 10 | public double? Max { get; set; } 11 | public double? Average { get; set; } 12 | public double? Sum { get; set; } 13 | } 14 | -------------------------------------------------------------------------------- /src/Foundatio.Repositories/Models/Aggregations/TermsAggregate.cs: -------------------------------------------------------------------------------- 1 | namespace Foundatio.Repositories.Models; 2 | 3 | public class TermsAggregate : MultiBucketAggregate> { } 4 | -------------------------------------------------------------------------------- /src/Foundatio.Repositories/Models/Aggregations/TopHitsAggregate.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | using System.Linq; 3 | 4 | namespace Foundatio.Repositories.Models; 5 | 6 | public class TopHitsAggregate : MetricAggregateBase 7 | { 8 | private readonly IList _hits; 9 | 10 | public long Total { get; set; } 11 | public double? MaxScore { get; set; } 12 | 13 | public TopHitsAggregate(IList hits) 14 | { 15 | _hits = hits ?? new List(); 16 | } 17 | 18 | public IReadOnlyCollection Documents() where T : class 19 | { 20 | return _hits.Select(h => h.As()).ToList(); 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /src/Foundatio.Repositories/Models/Aggregations/ValueAggregate.cs: -------------------------------------------------------------------------------- 1 | using System.Diagnostics; 2 | 3 | namespace Foundatio.Repositories.Models; 4 | 5 | [DebuggerDisplay("Value: {Value}")] 6 | public class ValueAggregate : MetricAggregateBase 7 | { 8 | public double? Value { get; set; } 9 | } 10 | 11 | [DebuggerDisplay("Value: {Value}")] 12 | public class ValueAggregate : MetricAggregateBase 13 | { 14 | public T Value { get; set; } 15 | } 16 | -------------------------------------------------------------------------------- /src/Foundatio.Repositories/Models/BeforeGetEventArgs.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace Foundatio.Repositories.Models; 4 | 5 | public class BeforeGetEventArgs : EventArgs where T : class, new() 6 | { 7 | public BeforeGetEventArgs(Ids ids, ICommandOptions options, IReadOnlyRepository repository, Type resultType) 8 | { 9 | Ids = ids; 10 | Options = options; 11 | Repository = repository; 12 | ResultType = resultType; 13 | } 14 | 15 | public Type ResultType { get; private set; } 16 | public Ids Ids { get; private set; } 17 | public ICommandOptions Options { get; private set; } 18 | public IReadOnlyRepository Repository { get; private set; } 19 | } 20 | -------------------------------------------------------------------------------- /src/Foundatio.Repositories/Models/BeforePublishEntityChangedEventArgs.cs: -------------------------------------------------------------------------------- 1 | using System.ComponentModel; 2 | 3 | namespace Foundatio.Repositories.Models; 4 | 5 | public class BeforePublishEntityChangedEventArgs : CancelEventArgs where T : class, IIdentity, new() 6 | { 7 | public BeforePublishEntityChangedEventArgs(IRepository repository, EntityChanged message) 8 | { 9 | Repository = repository; 10 | Message = message; 11 | } 12 | 13 | public EntityChanged Message { get; private set; } 14 | public IReadOnlyRepository Repository { get; private set; } 15 | } 16 | -------------------------------------------------------------------------------- /src/Foundatio.Repositories/Models/BeforeQueryEventArgs.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace Foundatio.Repositories.Models; 4 | 5 | public class BeforeQueryEventArgs : EventArgs where T : class, new() 6 | { 7 | public BeforeQueryEventArgs(IRepositoryQuery query, ICommandOptions options, IReadOnlyRepository repository, Type resultType) 8 | { 9 | Query = query; 10 | Options = options; 11 | Repository = repository; 12 | ResultType = resultType; 13 | } 14 | 15 | public Type ResultType { get; private set; } 16 | public IRepositoryQuery Query { get; private set; } 17 | public ICommandOptions Options { get; private set; } 18 | public IReadOnlyRepository Repository { get; private set; } 19 | } 20 | -------------------------------------------------------------------------------- /src/Foundatio.Repositories/Models/ChangeType.cs: -------------------------------------------------------------------------------- 1 | namespace Foundatio.Repositories.Models; 2 | 3 | public enum ChangeType : byte 4 | { 5 | Added = 0, 6 | Saved = 1, 7 | Removed = 2 8 | } 9 | -------------------------------------------------------------------------------- /src/Foundatio.Repositories/Models/DocumentChangeEventArgs.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | 4 | namespace Foundatio.Repositories.Models; 5 | 6 | public class DocumentsChangeEventArgs : EventArgs where T : class, IIdentity, new() 7 | { 8 | public DocumentsChangeEventArgs(ChangeType changeType, IReadOnlyCollection> documents, IRepository repository, ICommandOptions options) 9 | { 10 | ChangeType = changeType; 11 | Documents = documents ?? EmptyReadOnly>.Collection; 12 | Repository = repository; 13 | Options = options; 14 | } 15 | 16 | public ChangeType ChangeType { get; private set; } 17 | public IReadOnlyCollection> Documents { get; private set; } 18 | public IRepository Repository { get; private set; } 19 | public ICommandOptions Options { get; private set; } 20 | } 21 | 22 | public class DocumentsEventArgs : EventArgs where T : class, IIdentity, new() 23 | { 24 | public DocumentsEventArgs(IReadOnlyCollection documents, IRepository repository, ICommandOptions options) 25 | { 26 | Documents = documents ?? EmptyReadOnly.Collection; 27 | Repository = repository; 28 | Options = options; 29 | } 30 | 31 | public IReadOnlyCollection Documents { get; private set; } 32 | public IRepository Repository { get; private set; } 33 | public ICommandOptions Options { get; private set; } 34 | } 35 | 36 | public class ModifiedDocumentsEventArgs : EventArgs where T : class, IIdentity, new() 37 | { 38 | public ModifiedDocumentsEventArgs(IReadOnlyCollection> documents, IRepository repository, ICommandOptions options) 39 | { 40 | Documents = documents ?? EmptyReadOnly>.Collection; 41 | Repository = repository; 42 | Options = options; 43 | } 44 | 45 | public IReadOnlyCollection> Documents { get; private set; } 46 | public IRepository Repository { get; private set; } 47 | public ICommandOptions Options { get; private set; } 48 | } 49 | 50 | public class ModifiedDocument where T : class, new() 51 | { 52 | public ModifiedDocument(T value, T original) 53 | { 54 | Value = value; 55 | Original = original; 56 | } 57 | 58 | public T Value { get; set; } 59 | public T Original { get; private set; } 60 | } 61 | -------------------------------------------------------------------------------- /src/Foundatio.Repositories/Models/EmptyReadOnly.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | using System.Collections.ObjectModel; 3 | 4 | namespace Foundatio.Repositories.Models; 5 | 6 | public static class EmptyReadOnly 7 | { 8 | public static readonly IReadOnlyCollection Collection = new ReadOnlyCollection(new List()); 9 | } 10 | 11 | public static class EmptyReadOnly 12 | { 13 | public static readonly IReadOnlyDictionary Dictionary = new ReadOnlyDictionary(new Dictionary()); 14 | } 15 | -------------------------------------------------------------------------------- /src/Foundatio.Repositories/Models/EntityChanged.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | using System.Diagnostics; 3 | using Foundatio.Utility; 4 | 5 | namespace Foundatio.Repositories.Models; 6 | 7 | [DebuggerDisplay("{Type} {ChangeType}: Id={Id}")] 8 | public class EntityChanged : IHaveData 9 | { 10 | public EntityChanged() 11 | { 12 | Data = new DataDictionary(); 13 | } 14 | 15 | public string Type { get; set; } 16 | public string Id { get; set; } 17 | public ChangeType ChangeType { get; set; } 18 | public IDictionary Data { get; set; } 19 | } 20 | -------------------------------------------------------------------------------- /src/Foundatio.Repositories/Models/IHaveDates.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace Foundatio.Repositories.Models; 4 | 5 | public interface IHaveDates : IHaveCreatedDate 6 | { 7 | DateTime UpdatedUtc { get; set; } 8 | } 9 | 10 | public interface IHaveCreatedDate 11 | { 12 | DateTime CreatedUtc { get; set; } 13 | } 14 | -------------------------------------------------------------------------------- /src/Foundatio.Repositories/Models/IIdentity.cs: -------------------------------------------------------------------------------- 1 | namespace Foundatio.Repositories.Models; 2 | 3 | public interface IIdentity 4 | { 5 | /// 6 | /// Unique id that identifies a document. 7 | /// 8 | string Id { get; set; } 9 | } 10 | -------------------------------------------------------------------------------- /src/Foundatio.Repositories/Models/IPatchOperation.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Threading.Tasks; 4 | using Foundatio.Repositories.Extensions; 5 | using Foundatio.Repositories.Utility; 6 | 7 | namespace Foundatio.Repositories.Models; 8 | 9 | public interface IPatchOperation { } 10 | 11 | public class PartialPatch : IPatchOperation 12 | { 13 | public PartialPatch(object document) 14 | { 15 | Document = document ?? throw new ArgumentNullException(nameof(document)); 16 | } 17 | 18 | public object Document { get; } 19 | } 20 | 21 | public class ActionPatch : IPatchOperation where T : class 22 | { 23 | public ActionPatch(Action changeAction) 24 | { 25 | if (changeAction == null) 26 | throw new ArgumentNullException(nameof(changeAction)); 27 | 28 | Actions.Add(changeAction); 29 | } 30 | 31 | public ActionPatch(params Action[] changeActions) 32 | { 33 | Actions.AddRange(changeActions); 34 | } 35 | 36 | public ICollection> Actions { get; } = new List>(); 37 | } 38 | 39 | public class JsonPatch : IPatchOperation 40 | { 41 | public JsonPatch(PatchDocument patch) 42 | { 43 | Patch = patch ?? throw new ArgumentNullException(nameof(patch)); 44 | } 45 | 46 | public PatchDocument Patch { get; } 47 | } 48 | 49 | public class ScriptPatch : IPatchOperation 50 | { 51 | public ScriptPatch(string script) 52 | { 53 | if (String.IsNullOrEmpty(script)) 54 | throw new ArgumentNullException(nameof(script)); 55 | 56 | Script = script; 57 | } 58 | 59 | public string Script { get; } 60 | public Dictionary Params { get; set; } 61 | } 62 | 63 | public static class ActionPatchExtensions 64 | { 65 | public static Task PatchAsync(this IRepository repository, Id id, ActionPatch operation, ICommandOptions options = null) where T : class, IIdentity, new() 66 | { 67 | return repository.PatchAsync(id, operation, options); 68 | } 69 | 70 | public static Task PatchAsync(this IRepository repository, Id id, ActionPatch operation, CommandOptionsDescriptor options) where T : class, IIdentity, new() 71 | { 72 | return repository.PatchAsync(id, operation, options); 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /src/Foundatio.Repositories/Models/ISupportSoftDeletes.cs: -------------------------------------------------------------------------------- 1 | namespace Foundatio.Repositories.Models; 2 | 3 | public interface ISupportSoftDeletes 4 | { 5 | bool IsDeleted { get; set; } 6 | } 7 | -------------------------------------------------------------------------------- /src/Foundatio.Repositories/Models/IVersioned.cs: -------------------------------------------------------------------------------- 1 | namespace Foundatio.Repositories.Models; 2 | 3 | public interface IVersioned 4 | { 5 | /// 6 | /// Current modification version for the document. 7 | /// 8 | string Version { get; set; } 9 | } 10 | -------------------------------------------------------------------------------- /src/Foundatio.Repositories/Models/LazyDocument.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using Foundatio.Serializer; 3 | 4 | namespace Foundatio.Repositories.Models; 5 | 6 | public interface ILazyDocument 7 | { 8 | T As() where T : class; 9 | object As(Type objectType); 10 | } 11 | 12 | public class LazyDocument : ILazyDocument 13 | { 14 | private readonly byte[] _data; 15 | private readonly ITextSerializer _serializer; 16 | 17 | public LazyDocument(byte[] data, ITextSerializer serializer = null) 18 | { 19 | _data = data; 20 | _serializer = serializer ?? new JsonNetSerializer(); 21 | } 22 | 23 | public T As() where T : class 24 | { 25 | if (_data == null || _data.Length == 0) 26 | return default; 27 | 28 | return _serializer.Deserialize(_data); 29 | } 30 | 31 | public object As(Type objectType) 32 | { 33 | if (_data == null || _data.Length == 0) 34 | return null; 35 | 36 | return _serializer.Deserialize(_data, objectType); 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /src/Foundatio.Repositories/Options/CommandOptionsDescriptor.cs: -------------------------------------------------------------------------------- 1 | namespace Foundatio.Repositories; 2 | 3 | public static class CommandOptionsDescriptorExtensions 4 | { 5 | public static ICommandOptions Configure(this CommandOptionsDescriptor configure) where T : class 6 | { 7 | ICommandOptions o = new CommandOptions(); 8 | if (configure != null) 9 | o = configure(o); 10 | 11 | return o; 12 | } 13 | 14 | public static ICommandOptions Configure(this CommandOptionsDescriptor configure) 15 | { 16 | ICommandOptions o = new CommandOptions(); 17 | if (configure != null) 18 | o = configure(o); 19 | 20 | return o; 21 | } 22 | } 23 | 24 | public delegate ICommandOptions CommandOptionsDescriptor(ICommandOptions options) where T : class; 25 | public delegate ICommandOptions CommandOptionsDescriptor(ICommandOptions options); 26 | -------------------------------------------------------------------------------- /src/Foundatio.Repositories/Options/ConsistencyOptions.cs: -------------------------------------------------------------------------------- 1 | namespace Foundatio.Repositories 2 | { 3 | public enum Consistency 4 | { 5 | Eventual, 6 | Immediate, 7 | Wait 8 | } 9 | 10 | public static class ConsistencyOptionsExtensions 11 | { 12 | internal const string ConsistencyModeKey = "@ConsistencyMode"; 13 | public static T Consistency(this T options, Consistency mode) where T : ICommandOptions 14 | { 15 | options.Values.Set(ConsistencyModeKey, mode); 16 | return options; 17 | } 18 | 19 | public static T ImmediateConsistency(this T options, bool shouldWait = false) where T : ICommandOptions 20 | { 21 | options.Values.Set(ConsistencyModeKey, shouldWait ? Repositories.Consistency.Wait : Repositories.Consistency.Immediate); 22 | return options; 23 | } 24 | } 25 | } 26 | 27 | namespace Foundatio.Repositories.Options 28 | { 29 | public static class ReadConsistencyOptionsExtensions 30 | { 31 | public static Consistency GetConsistency(this ICommandOptions options, Consistency defaultMode = Consistency.Eventual) 32 | { 33 | return options.SafeGetOption(ConsistencyOptionsExtensions.ConsistencyModeKey, defaultMode); 34 | } 35 | } 36 | } 37 | 38 | -------------------------------------------------------------------------------- /src/Foundatio.Repositories/Options/ICommandOptions.cs: -------------------------------------------------------------------------------- 1 | using Foundatio.Repositories.Options; 2 | 3 | namespace Foundatio.Repositories 4 | { 5 | /// 6 | /// Options that control the behavior of the repositories 7 | /// 8 | public interface ICommandOptions : IOptions { } 9 | public interface ICommandOptions : ICommandOptions where T : class { } 10 | 11 | public class CommandOptions : OptionsBase, ICommandOptions { } 12 | public class CommandOptions : CommandOptions, ICommandOptions where T : class { } 13 | } 14 | 15 | namespace Foundatio.Repositories.Options 16 | { 17 | public static class CommandOptionsExtensions 18 | { 19 | public static ICommandOptions As(this ICommandOptions options) where T : class 20 | { 21 | if (options == null) 22 | return new CommandOptions(); 23 | 24 | if (options is ICommandOptions typedOptions) 25 | return typedOptions; 26 | 27 | return new WrappedCommandOptions(options); 28 | } 29 | } 30 | 31 | internal class WrappedCommandOptions : ICommandOptions where T : class 32 | { 33 | public WrappedCommandOptions(ICommandOptions innerOptions) 34 | { 35 | InnerOptions = innerOptions; 36 | } 37 | 38 | public IOptionsDictionary Values => InnerOptions.Values; 39 | 40 | public ICommandOptions InnerOptions { get; } 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /src/Foundatio.Repositories/Options/NotificationOptions.cs: -------------------------------------------------------------------------------- 1 | using Foundatio.Repositories.Options; 2 | 3 | namespace Foundatio.Repositories 4 | { 5 | public static class SetNotificationOptionsExtensions 6 | { 7 | internal const string NotificationsKey = "@Notifications"; 8 | 9 | public static T Notifications(this T options, bool sendNotifications = true) where T : ICommandOptions 10 | { 11 | return options.BuildOption(NotificationsKey, sendNotifications); 12 | } 13 | } 14 | } 15 | 16 | namespace Foundatio.Repositories.Options 17 | { 18 | public static class ReadNotificationOptionsExtensions 19 | { 20 | public static bool ShouldNotify(this ICommandOptions options) 21 | { 22 | return options.SafeGetOption(SetNotificationOptionsExtensions.NotificationsKey, true); 23 | } 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /src/Foundatio.Repositories/Options/OriginalOptions.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | using Foundatio.Repositories.Options; 3 | 4 | namespace Foundatio.Repositories 5 | { 6 | public static class OriginalOptionsExtensions 7 | { 8 | internal const string OriginalsEnabledKey = "@OriginalsEnabled"; 9 | public static T Originals(this T options, bool enabled = true) where T : ICommandOptions 10 | { 11 | options.Values.Set(OriginalsEnabledKey, enabled); 12 | return options; 13 | } 14 | 15 | internal const string OriginalsValuesKey = "@OriginalsValues"; 16 | public static TOptions AddOriginals(this TOptions options, TValue value) where TOptions : ICommandOptions 17 | { 18 | return options.AddCollectionOptionValue(OriginalsValuesKey, value); 19 | } 20 | 21 | public static TOptions AddOriginals(this TOptions options, IEnumerable values) where TOptions : ICommandOptions 22 | { 23 | return options.AddCollectionOptionValue(OriginalsValuesKey, values); 24 | } 25 | } 26 | } 27 | 28 | namespace Foundatio.Repositories.Options 29 | { 30 | public static class ReadOriginalOptionsExtensions 31 | { 32 | public static ICollection GetOriginals(this ICommandOptions options) 33 | { 34 | return options.SafeGetCollection(OriginalOptionsExtensions.OriginalsValuesKey); 35 | } 36 | 37 | public static bool GetOriginalsEnabled(this ICommandOptions options, bool defaultEnabled = true) 38 | { 39 | return options.SafeGetOption(OriginalOptionsExtensions.OriginalsEnabledKey, defaultEnabled); 40 | } 41 | } 42 | } 43 | 44 | -------------------------------------------------------------------------------- /src/Foundatio.Repositories/Options/PagingOptions.cs: -------------------------------------------------------------------------------- 1 | namespace Foundatio.Repositories 2 | { 3 | public static class SetPagingOptionsExtensions 4 | { 5 | internal const string PageLimitKey = "@PageLimit"; 6 | internal const string PageNumberKey = "@PageNumber"; 7 | 8 | public static T PageNumber(this T options, int? page) where T : ICommandOptions 9 | { 10 | if (page.HasValue) 11 | options.Values.Set(PageNumberKey, page.Value); 12 | 13 | return options; 14 | } 15 | 16 | public static T PageLimit(this T options, int? limit) where T : ICommandOptions 17 | { 18 | if (limit.HasValue) 19 | options.Values.Set(PageLimitKey, limit.Value); 20 | 21 | return options; 22 | } 23 | 24 | internal const string DefaultPageLimitKey = "@DefaultPageLimit"; 25 | public static T DefaultPageLimit(this T options, int limit) where T : ICommandOptions 26 | { 27 | options.Values.Set(DefaultPageLimitKey, limit); 28 | 29 | return options; 30 | } 31 | 32 | internal const string MaxPageLimitKey = "@MaxPageLimit"; 33 | public static T MaxPageLimit(this T options, int limit) where T : ICommandOptions 34 | { 35 | options.Values.Set(MaxPageLimitKey, limit); 36 | 37 | return options; 38 | } 39 | } 40 | } 41 | 42 | namespace Foundatio.Repositories.Options 43 | { 44 | public static class ReadPagingOptionsExtensions 45 | { 46 | public static bool HasPageLimit(this ICommandOptions options) 47 | { 48 | return options.SafeHasOption(SetPagingOptionsExtensions.PageLimitKey); 49 | } 50 | 51 | public static int GetLimit(this ICommandOptions options) 52 | { 53 | int limit = options.SafeGetOption(SetPagingOptionsExtensions.PageLimitKey, options.SafeGetOption(SetPagingOptionsExtensions.DefaultPageLimitKey, 10)); 54 | 55 | int maxLimit = options.SafeGetOption(SetPagingOptionsExtensions.MaxPageLimitKey, 10000); 56 | if (limit > maxLimit) 57 | return maxLimit; 58 | 59 | return limit; 60 | } 61 | 62 | public static int GetMaxLimit(this ICommandOptions options) 63 | { 64 | return options.SafeGetOption(SetPagingOptionsExtensions.MaxPageLimitKey, 10000); 65 | } 66 | 67 | public static bool HasPageNumber(this ICommandOptions options) 68 | { 69 | return options.SafeHasOption(SetPagingOptionsExtensions.PageNumberKey); 70 | } 71 | 72 | public static int GetPage(this ICommandOptions options) 73 | { 74 | return options.SafeGetOption(SetPagingOptionsExtensions.PageNumberKey, 1); 75 | } 76 | 77 | public static bool ShouldUseSkip(this ICommandOptions options) 78 | { 79 | return options.GetPage() > 1; 80 | } 81 | 82 | public static int GetSkip(this ICommandOptions options) 83 | { 84 | if (!options.HasPageLimit() && !options.HasPageNumber()) 85 | return 0; 86 | 87 | int limit = options.GetLimit(); 88 | int page = options.GetPage(); 89 | 90 | int skip = (page - 1) * limit; 91 | if (skip < 0) 92 | skip = 0; 93 | 94 | return skip; 95 | } 96 | } 97 | } 98 | -------------------------------------------------------------------------------- /src/Foundatio.Repositories/Options/QueryLogLevelOptions.cs: -------------------------------------------------------------------------------- 1 | namespace Foundatio.Repositories 2 | { 3 | public static class QueryLogOptionsExtensions 4 | { 5 | internal const string QueryLogLevelKey = "@QueryLogLevel"; 6 | public static T QueryLogLevel(this T options, Microsoft.Extensions.Logging.LogLevel logLevel) where T : ICommandOptions 7 | { 8 | options.Values.Set(QueryLogLevelKey, logLevel); 9 | return options; 10 | } 11 | 12 | internal const string DefaultQueryLogLevelKey = "@DefaultQueryLogLevel"; 13 | public static T DefaultQueryLogLevel(this T options, Microsoft.Extensions.Logging.LogLevel logLevel) where T : ICommandOptions 14 | { 15 | options.Values.Set(DefaultQueryLogLevelKey, logLevel); 16 | 17 | return options; 18 | } 19 | } 20 | } 21 | 22 | namespace Foundatio.Repositories.Options 23 | { 24 | public static class ReadQueryLogOptionsExtensions 25 | { 26 | public static Microsoft.Extensions.Logging.LogLevel GetQueryLogLevel(this ICommandOptions options) 27 | { 28 | return options.SafeGetOption(QueryLogOptionsExtensions.QueryLogLevelKey, options.SafeGetOption(QueryLogOptionsExtensions.DefaultQueryLogLevelKey, Microsoft.Extensions.Logging.LogLevel.Trace)); 29 | } 30 | } 31 | } 32 | 33 | -------------------------------------------------------------------------------- /src/Foundatio.Repositories/Options/SoftDeletesOptions.cs: -------------------------------------------------------------------------------- 1 | using Foundatio.Repositories.Options; 2 | 3 | namespace Foundatio.Repositories 4 | { 5 | public static class SoftDeleteOptionsExtensions 6 | { 7 | internal const string SoftDeleteModeKey = "@SoftDeleteMode"; 8 | 9 | public static T SoftDeleteMode(this T query, SoftDeleteQueryMode mode) where T : ICommandOptions 10 | { 11 | return query.BuildOption(SoftDeleteModeKey, mode); 12 | } 13 | 14 | public static T IncludeSoftDeletes(this T query, bool includeDeleted = true) where T : ICommandOptions 15 | { 16 | return query.BuildOption(SoftDeleteModeKey, includeDeleted ? SoftDeleteQueryMode.All : SoftDeleteQueryMode.ActiveOnly); 17 | } 18 | } 19 | 20 | public enum SoftDeleteQueryMode 21 | { 22 | ActiveOnly, 23 | DeletedOnly, 24 | All 25 | } 26 | } 27 | 28 | namespace Foundatio.Repositories.Options 29 | { 30 | public static class ReadSoftDeleteOptionsExtensions 31 | { 32 | public static SoftDeleteQueryMode GetSoftDeleteMode(this ICommandOptions options, SoftDeleteQueryMode defaultMode = SoftDeleteQueryMode.ActiveOnly) 33 | { 34 | return options.SafeGetOption(SoftDeleteOptionsExtensions.SoftDeleteModeKey, defaultMode); 35 | } 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /src/Foundatio.Repositories/Options/TimeProviderOptions.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using Foundatio.Repositories.Options; 3 | 4 | namespace Foundatio.Repositories 5 | { 6 | public static class SetTimeProviderOptionsExtensions 7 | { 8 | internal const string TimeProviderKey = "@TimeProvider"; 9 | 10 | public static T TimeProvider(this T options, TimeProvider timeProvider) where T : IOptions 11 | { 12 | return options.BuildOption(TimeProviderKey, timeProvider); 13 | } 14 | } 15 | } 16 | 17 | namespace Foundatio.Repositories.Options 18 | { 19 | public static class ReadTimeProviderOptionsExtensions 20 | { 21 | public static TimeProvider GetTimeProvider(this IOptions options) 22 | { 23 | return options.SafeGetOption(SetTimeProviderOptionsExtensions.TimeProviderKey, TimeProvider.System); 24 | } 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /src/Foundatio.Repositories/Options/TimeoutOptions.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using Foundatio.Repositories.Options; 3 | 4 | namespace Foundatio.Repositories 5 | { 6 | public static class TimeoutOptionsExtensions 7 | { 8 | 9 | internal const string QueryTimeoutKey = "@QueryTimeout"; 10 | public static T Timeout(this T options, TimeSpan timeout) where T : ICommandOptions 11 | { 12 | return options.BuildOption(QueryTimeoutKey, timeout); 13 | } 14 | 15 | internal const string RetryCountKey = "@RetryCount"; 16 | public static T Retry(this T options, int retryCount) where T : ICommandOptions 17 | { 18 | return options.BuildOption(RetryCountKey, retryCount); 19 | } 20 | } 21 | } 22 | 23 | namespace Foundatio.Repositories.Options 24 | { 25 | public static class ReadTimeoutOptionsExtensions 26 | { 27 | public static bool HasQueryTimeout(this ICommandOptions options) 28 | { 29 | return options.SafeHasOption(TimeoutOptionsExtensions.QueryTimeoutKey); 30 | } 31 | 32 | public static TimeSpan GetQueryTimeout(this ICommandOptions options) 33 | { 34 | return options.SafeGetOption(TimeoutOptionsExtensions.QueryTimeoutKey, TimeSpan.FromSeconds(1)); 35 | } 36 | 37 | public static int GetRetryCount(this ICommandOptions options, int defaultRetryCount = 10) 38 | { 39 | return options.SafeGetOption(TimeoutOptionsExtensions.RetryCountKey, defaultRetryCount); 40 | } 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /src/Foundatio.Repositories/Options/UpdatedIdsCallbackOptions.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using Foundatio.Repositories.Options; 4 | 5 | namespace Foundatio.Repositories 6 | { 7 | public static class UpdatedIdsCallbackOptionsExtensions 8 | { 9 | internal const string UpdatedIdsCallbackKey = "@UpdatedIdsCallback"; 10 | 11 | public static T UpdateCallback(this T options, Action> callback) where T : ICommandOptions 12 | { 13 | return options.BuildOption(UpdatedIdsCallbackKey, callback); 14 | } 15 | } 16 | } 17 | 18 | namespace Foundatio.Repositories.Options 19 | { 20 | public static class ReadUpdatedIdsCallbackOptionsExtensions 21 | { 22 | public static Action> GetUpdatedIdsCallback(this ICommandOptions options) 23 | { 24 | return options.SafeGetOption>>(UpdatedIdsCallbackOptionsExtensions.UpdatedIdsCallbackKey, null); 25 | } 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /src/Foundatio.Repositories/Options/ValidationOptions.cs: -------------------------------------------------------------------------------- 1 | using Foundatio.Repositories.Options; 2 | 3 | namespace Foundatio.Repositories 4 | { 5 | public static class SetValidationOptionsExtensions 6 | { 7 | internal const string ValidationKey = "@Validation"; 8 | 9 | public static T Validation(this T options, bool shouldValidate) where T : ICommandOptions 10 | { 11 | return options.BuildOption(ValidationKey, shouldValidate); 12 | } 13 | 14 | public static T SkipValidation(this T options) where T : ICommandOptions 15 | { 16 | return options.Validation(false); 17 | } 18 | } 19 | } 20 | 21 | namespace Foundatio.Repositories.Options 22 | { 23 | public static class ReadValidationOptionsExtensions 24 | { 25 | public static bool ShouldValidate(this ICommandOptions options) 26 | { 27 | return options.SafeGetOption(SetValidationOptionsExtensions.ValidationKey, true); 28 | } 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /src/Foundatio.Repositories/Options/VersionOptions.cs: -------------------------------------------------------------------------------- 1 | using Foundatio.Repositories.Options; 2 | 3 | namespace Foundatio.Repositories 4 | { 5 | public static class SetVersionsOptionsExtensions 6 | { 7 | internal const string SkipVersionCheckKey = "@SkipVersionCheck"; 8 | 9 | public static T VersionCheck(this T options, bool shouldCheckVersion) where T : ICommandOptions 10 | { 11 | return options.BuildOption(SkipVersionCheckKey, shouldCheckVersion); 12 | } 13 | 14 | public static T SkipVersionCheck(this T options) where T : ICommandOptions 15 | { 16 | return options.VersionCheck(true); 17 | } 18 | } 19 | } 20 | 21 | namespace Foundatio.Repositories.Options 22 | { 23 | public static class ReadVersionOptionsExtensions 24 | { 25 | public static bool ShouldSkipVersionCheck(this ICommandOptions options) 26 | { 27 | return options.SafeGetOption(SetVersionsOptionsExtensions.SkipVersionCheckKey, false); 28 | } 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /src/Foundatio.Repositories/Queries/IRepositoryQuery.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using Foundatio.Repositories.Options; 3 | 4 | namespace Foundatio.Repositories; 5 | /// 6 | /// Query options that control the result of the repository query operations 7 | /// 8 | public interface IRepositoryQuery : IOptions 9 | { 10 | Type GetDocumentType(); 11 | } 12 | 13 | public interface IRepositoryQuery : IRepositoryQuery where T : class { } 14 | 15 | public interface ISetDocumentType 16 | { 17 | void SetDocumentType(Type documentType); 18 | } 19 | 20 | public class RepositoryQuery : OptionsBase, IRepositoryQuery, ISystemFilter, ISetDocumentType 21 | { 22 | protected Type _documentType; 23 | 24 | public RepositoryQuery() 25 | { 26 | _documentType = typeof(object); 27 | } 28 | 29 | public RepositoryQuery(Type documentType) 30 | { 31 | _documentType = documentType ?? typeof(object); 32 | } 33 | 34 | IRepositoryQuery ISystemFilter.GetQuery() 35 | { 36 | return this; 37 | } 38 | 39 | public Type GetDocumentType() 40 | { 41 | return _documentType; 42 | } 43 | 44 | void ISetDocumentType.SetDocumentType(Type documentType) 45 | { 46 | _documentType = documentType; 47 | } 48 | } 49 | 50 | public class RepositoryQuery : RepositoryQuery, ISetDocumentType, IRepositoryQuery where T : class 51 | { 52 | public RepositoryQuery() 53 | { 54 | _documentType = typeof(T); 55 | } 56 | 57 | void ISetDocumentType.SetDocumentType(Type documentType) { } 58 | } 59 | 60 | public static class RepositoryQueryExtensions 61 | { 62 | public static T DocumentType(this T query, Type documentType) where T : IRepositoryQuery 63 | { 64 | if (query is ISetDocumentType setDocumentType) 65 | setDocumentType.SetDocumentType(documentType); 66 | 67 | return query; 68 | } 69 | 70 | public static IRepositoryQuery As(this IRepositoryQuery query) where T : class 71 | { 72 | if (query == null) 73 | return new RepositoryQuery(); 74 | 75 | if (query is IRepositoryQuery typedQuery) 76 | return typedQuery; 77 | 78 | return new WrappedRepositoryQuery(query); 79 | } 80 | 81 | public static IRepositoryQuery Unwrap(this IRepositoryQuery query) 82 | { 83 | if (query == null) 84 | return new RepositoryQuery(); 85 | 86 | if (query is WrappedRepositoryQuery wrappedQuery) 87 | return wrappedQuery.InnerQuery; 88 | 89 | return query; 90 | } 91 | } 92 | 93 | internal class WrappedRepositoryQuery : WrappedRepositoryQuery, IRepositoryQuery where T : class 94 | { 95 | public WrappedRepositoryQuery(IRepositoryQuery innerQuery) : base(innerQuery) { } 96 | } 97 | 98 | internal class WrappedRepositoryQuery : IRepositoryQuery 99 | { 100 | public WrappedRepositoryQuery(IRepositoryQuery innerQuery) 101 | { 102 | InnerQuery = innerQuery; 103 | } 104 | 105 | public IOptionsDictionary Values => InnerQuery.Values; 106 | 107 | public IRepositoryQuery InnerQuery { get; } 108 | 109 | public Type GetDocumentType() 110 | { 111 | return InnerQuery.GetDocumentType(); 112 | } 113 | } 114 | -------------------------------------------------------------------------------- /src/Foundatio.Repositories/Queries/ISystemFilter.cs: -------------------------------------------------------------------------------- 1 | namespace Foundatio.Repositories; 2 | 3 | public interface ISystemFilter 4 | { 5 | IRepositoryQuery GetQuery(); 6 | } 7 | -------------------------------------------------------------------------------- /src/Foundatio.Repositories/Queries/IdentityQuery.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | using Foundatio.Repositories.Options; 3 | 4 | namespace Foundatio.Repositories 5 | { 6 | public static class IdentityQueryExtensions 7 | { 8 | internal const string IdsKey = "@Ids"; 9 | 10 | public static T Id(this T query, string id) where T : IRepositoryQuery 11 | { 12 | return query.AddSetOptionValue(IdsKey, id); 13 | } 14 | 15 | public static T Id(this T query, params string[] ids) where T : IRepositoryQuery 16 | { 17 | return query.AddSetOptionValue(IdsKey, ids); 18 | } 19 | 20 | public static T Id(this T query, IEnumerable ids) where T : IRepositoryQuery 21 | { 22 | return query.AddSetOptionValue(IdsKey, ids); 23 | } 24 | 25 | internal const string OnlyIdsKey = "@OnlyIds"; 26 | 27 | public static T OnlyIds(this T query) where T : IRepositoryQuery 28 | { 29 | return query.BuildOption(OnlyIdsKey, true); 30 | } 31 | 32 | internal const string ExcludedIdsKey = "@ExcludedIds"; 33 | public static T ExcludedId(this T query, string id) where T : IRepositoryQuery 34 | { 35 | return query.AddSetOptionValue(ExcludedIdsKey, id); 36 | } 37 | 38 | public static T ExcludedId(this T query, params string[] ids) where T : IRepositoryQuery 39 | { 40 | return query.AddSetOptionValue(ExcludedIdsKey, ids); 41 | } 42 | 43 | public static T ExcludedId(this T query, IEnumerable ids) where T : IRepositoryQuery 44 | { 45 | return query.AddSetOptionValue(ExcludedIdsKey, ids); 46 | } 47 | } 48 | } 49 | 50 | namespace Foundatio.Repositories.Queries 51 | { 52 | public static class ReadIdentityQueryExtensions 53 | { 54 | public static ISet GetIds(this T options) where T : IRepositoryQuery 55 | { 56 | return options.SafeGetSet(IdentityQueryExtensions.IdsKey); 57 | } 58 | 59 | public static bool ShouldOnlyHaveIds(this T options) where T : IRepositoryQuery 60 | { 61 | return options.SafeGetOption(IdentityQueryExtensions.OnlyIdsKey); 62 | } 63 | 64 | public static ISet GetExcludedIds(this T options) where T : IRepositoryQuery 65 | { 66 | return options.SafeGetSet(IdentityQueryExtensions.ExcludedIdsKey); 67 | } 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /src/Foundatio.Repositories/Queries/RepositoryQueryDescriptor.cs: -------------------------------------------------------------------------------- 1 | namespace Foundatio.Repositories; 2 | 3 | public static class RepositoryQueryDescriptorExtensions 4 | { 5 | public static IRepositoryQuery Configure(this RepositoryQueryDescriptor configure) where T : class 6 | { 7 | IRepositoryQuery o = new RepositoryQuery(); 8 | if (configure != null) 9 | o = configure(o); 10 | 11 | return o; 12 | } 13 | 14 | public static IRepositoryQuery Configure(this RepositoryQueryDescriptor configure) 15 | { 16 | IRepositoryQuery o = new RepositoryQuery(); 17 | if (configure != null) 18 | o = configure(o); 19 | 20 | return o; 21 | } 22 | } 23 | 24 | public delegate IRepositoryQuery RepositoryQueryDescriptor(IRepositoryQuery query) where T : class; 25 | public delegate IRepositoryQuery RepositoryQueryDescriptor(IRepositoryQuery query); 26 | -------------------------------------------------------------------------------- /src/Foundatio.Repositories/Utility/AggregationsNewtonsoftJsonConverter.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using Foundatio.Repositories.Models; 3 | using Newtonsoft.Json; 4 | using Newtonsoft.Json.Linq; 5 | 6 | namespace Foundatio.Repositories.Utility; 7 | 8 | public class AggregationsNewtonsoftJsonConverter : JsonConverter 9 | { 10 | public override bool CanConvert(Type objectType) 11 | { 12 | return typeof(IAggregate).IsAssignableFrom(objectType); 13 | } 14 | 15 | public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer) 16 | { 17 | var item = JObject.Load(reader); 18 | var typeToken = item.SelectToken("Data.@type") ?? item.SelectToken("data.@type"); 19 | 20 | IAggregate value = null; 21 | if (typeToken != null) 22 | { 23 | string type = typeToken.Value(); 24 | switch (type) 25 | { 26 | case "bucket": 27 | value = new BucketAggregate(); 28 | break; 29 | case "exstats": 30 | value = new ExtendedStatsAggregate(); 31 | break; 32 | case "ovalue": 33 | value = new ObjectValueAggregate(); 34 | break; 35 | case "percentiles": 36 | value = new PercentilesAggregate(); 37 | break; 38 | case "sbucket": 39 | value = new SingleBucketAggregate(); 40 | break; 41 | case "stats": 42 | value = new StatsAggregate(); 43 | break; 44 | case "tophits": 45 | // TODO: Have to get all the docs as JToken and 46 | //value = new TopHitsAggregate(); 47 | break; 48 | case "value": 49 | value = new ValueAggregate(); 50 | break; 51 | case "dvalue": 52 | value = new ValueAggregate(); 53 | break; 54 | } 55 | } 56 | 57 | if (value == null) 58 | value = new ValueAggregate(); 59 | 60 | serializer.Populate(item.CreateReader(), value); 61 | 62 | return value; 63 | } 64 | 65 | public override bool CanWrite => false; 66 | 67 | public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer) 68 | { 69 | throw new NotImplementedException(); 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /src/Foundatio.Repositories/Utility/BucketsNewtonsoftJsonConverter.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using Foundatio.Repositories.Models; 4 | using Newtonsoft.Json; 5 | using Newtonsoft.Json.Linq; 6 | 7 | namespace Foundatio.Repositories.Utility; 8 | 9 | public class BucketsNewtonsoftJsonConverter : JsonConverter 10 | { 11 | private static readonly long _epochTicks = new DateTimeOffset(1970, 1, 1, 0, 0, 0, 0, TimeSpan.Zero).Ticks; 12 | 13 | public override bool CanConvert(Type objectType) 14 | { 15 | return typeof(IBucket).IsAssignableFrom(objectType); 16 | } 17 | 18 | public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer) 19 | { 20 | var item = JObject.Load(reader); 21 | var typeToken = item.SelectToken("Data.@type") ?? item.SelectToken("data.@type"); 22 | 23 | IBucket value = null; 24 | if (typeToken != null) 25 | { 26 | string type = typeToken.Value(); 27 | IReadOnlyDictionary aggregations = null; 28 | var aggregationsToken = item.SelectToken("Aggregations") ?? item.SelectToken("aggregations"); 29 | aggregations = aggregationsToken?.ToObject>(); 30 | 31 | switch (type) 32 | { 33 | case "datehistogram": 34 | var timeZoneToken = item.SelectToken("Data.@timezone") ?? item.SelectToken("data.@timezone"); 35 | var kind = timeZoneToken != null ? DateTimeKind.Unspecified : DateTimeKind.Utc; 36 | var key = item.SelectToken("Key") ?? item.SelectToken("key"); 37 | var date = new DateTime(_epochTicks + ((long)key * TimeSpan.TicksPerMillisecond), kind); 38 | 39 | value = new DateHistogramBucket(date, aggregations); 40 | break; 41 | case "range": 42 | value = new RangeBucket(aggregations); 43 | break; 44 | case "string": 45 | value = new KeyedBucket(aggregations); 46 | break; 47 | case "double": 48 | value = new KeyedBucket(aggregations); 49 | break; 50 | case "object": 51 | value = new KeyedBucket(aggregations); 52 | break; 53 | } 54 | } 55 | 56 | if (value == null) 57 | value = new KeyedBucket(); 58 | 59 | serializer.Populate(item.CreateReader(), value); 60 | 61 | return value; 62 | } 63 | 64 | public override bool CanWrite => false; 65 | 66 | public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer) 67 | { 68 | throw new NotImplementedException(); 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /src/Foundatio.Repositories/Utility/BucketsSystemTextJsonConverter.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Text.Json; 4 | using Foundatio.Repositories.Models; 5 | 6 | namespace Foundatio.Repositories.Utility; 7 | 8 | public class BucketsSystemTextJsonConverter : System.Text.Json.Serialization.JsonConverter 9 | { 10 | private static readonly long _epochTicks = new DateTimeOffset(1970, 1, 1, 0, 0, 0, 0, TimeSpan.Zero).Ticks; 11 | 12 | public override bool CanConvert(Type type) 13 | { 14 | return typeof(IBucket).IsAssignableFrom(type); 15 | } 16 | 17 | public override IBucket Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) 18 | { 19 | IBucket value = null; 20 | 21 | var element = JsonElement.ParseValue(ref reader); 22 | string typeToken = GetDataToken(element, "@type"); 23 | if (typeToken != null) 24 | { 25 | switch (typeToken) 26 | { 27 | case "datehistogram": 28 | string timeZoneToken = GetDataToken(element, "@timezone"); 29 | var kind = timeZoneToken != null ? DateTimeKind.Unspecified : DateTimeKind.Utc; 30 | long key = GetProperty(element, "Key")?.GetInt64() ?? throw new InvalidOperationException(); 31 | var date = new DateTime(_epochTicks + (key * TimeSpan.TicksPerMillisecond), kind); 32 | 33 | var aggregationsElement = GetProperty(element, "Aggregations"); 34 | var aggregations = aggregationsElement?.Deserialize>(options); 35 | 36 | value = new DateHistogramBucket(date, aggregations); 37 | break; 38 | case "range": 39 | value = element.Deserialize(options); 40 | break; 41 | case "string": 42 | value = element.Deserialize>(options); 43 | break; 44 | case "double": 45 | value = element.Deserialize>(options); 46 | break; 47 | case "object": 48 | value = element.Deserialize>(options); 49 | break; 50 | } 51 | } 52 | 53 | if (value is null) 54 | value = element.Deserialize>(options); 55 | 56 | return value; 57 | } 58 | 59 | public override void Write(Utf8JsonWriter writer, IBucket value, JsonSerializerOptions options) 60 | { 61 | JsonSerializer.Serialize(writer, value, value.GetType(), options); 62 | } 63 | 64 | private JsonElement? GetProperty(JsonElement element, string propertyName) 65 | { 66 | if (element.TryGetProperty(propertyName, out var dataElement)) 67 | return dataElement; 68 | 69 | if (element.TryGetProperty(propertyName.ToLower(), out dataElement)) 70 | return dataElement; 71 | 72 | return null; 73 | } 74 | 75 | private string GetDataToken(JsonElement element, string key) 76 | { 77 | var dataPropertyElement = GetProperty(element, "Data"); 78 | if (dataPropertyElement != null && dataPropertyElement.Value.TryGetProperty(key, out var typeElement)) 79 | return typeElement.ToString(); 80 | 81 | return null; 82 | } 83 | } 84 | -------------------------------------------------------------------------------- /src/Foundatio.Repositories/Utility/DoubleSystemTextJsonConverter.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Text.Json; 3 | 4 | namespace Foundatio.Repositories.Utility; 5 | 6 | // NOTE: This fixes an issue where doubles were converted to integers (https://github.com/dotnet/runtime/issues/35195) 7 | public class DoubleSystemTextJsonConverter : System.Text.Json.Serialization.JsonConverter 8 | { 9 | public override double Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) 10 | { 11 | throw new NotImplementedException(); 12 | } 13 | 14 | public override bool CanConvert(Type type) 15 | { 16 | return typeof(double).IsAssignableFrom(type); 17 | } 18 | 19 | public override void Write(Utf8JsonWriter writer, double value, JsonSerializerOptions options) 20 | { 21 | writer.WriteRawValue($"{value:0.0}"); 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /src/Foundatio.Repositories/Utility/ObjectToInferredTypesConverter.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Text.Json; 3 | using System.Text.Json.Serialization; 4 | 5 | namespace Foundatio.Repositories.Utility; 6 | 7 | public class ObjectToInferredTypesConverter : JsonConverterFactory 8 | { 9 | public override bool CanConvert(Type typeToConvert) => true; 10 | 11 | public override JsonConverter CreateConverter(Type typeToConvert, JsonSerializerOptions options) 12 | { 13 | var converterType = typeof(ObjectToInferredTypesConverterInner<>).MakeGenericType(typeToConvert); 14 | return (JsonConverter)Activator.CreateInstance(converterType); 15 | } 16 | 17 | private class ObjectToInferredTypesConverterInner : JsonConverter 18 | { 19 | public override T Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) 20 | { 21 | object result = reader.TokenType switch 22 | { 23 | JsonTokenType.Number when reader.TryGetInt64(out long l) => l, 24 | JsonTokenType.Number => reader.GetDouble(), 25 | JsonTokenType.String when reader.TryGetDateTime(out DateTime datetime) => datetime, 26 | JsonTokenType.String => reader.GetString()!, 27 | JsonTokenType.True => true, 28 | JsonTokenType.False => false, 29 | JsonTokenType.Null => null!, 30 | _ => JsonDocument.ParseValue(ref reader).RootElement.Clone() 31 | }; 32 | 33 | if (result == null) 34 | return default!; 35 | 36 | if (result is T typedResult) 37 | return typedResult; 38 | 39 | try 40 | { 41 | // Special case for JsonElement 42 | if (result is JsonElement element) 43 | { 44 | return JsonSerializer.Deserialize(element.GetRawText(), options)!; 45 | } 46 | 47 | return (T)Convert.ChangeType(result, typeof(T)); 48 | } 49 | catch (Exception ex) 50 | { 51 | throw new JsonException($"Cannot convert {result} to type {typeof(T)}: {ex.Message}", ex); 52 | } 53 | } 54 | 55 | public override void Write(Utf8JsonWriter writer, T value, JsonSerializerOptions options) 56 | { 57 | JsonSerializer.Serialize(writer, value, typeof(T), options); 58 | } 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /tests/Directory.Build.props: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | net8.0 5 | False 6 | $(NoWarn);CS1591;NU1701 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | -------------------------------------------------------------------------------- /tests/Foundatio.Repositories.Elasticsearch.Tests/ElasticRepositoryTestBase.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Diagnostics; 3 | using System.Threading; 4 | using System.Threading.Tasks; 5 | using Foundatio.Caching; 6 | using Foundatio.Jobs; 7 | using Foundatio.Messaging; 8 | using Foundatio.Parsers.ElasticQueries.Extensions; 9 | using Foundatio.Queues; 10 | using Foundatio.Repositories.Elasticsearch.Tests.Repositories.Configuration; 11 | using Foundatio.Utility; 12 | using Foundatio.Xunit; 13 | using Microsoft.Extensions.Logging; 14 | using Nest; 15 | using Xunit.Abstractions; 16 | using IAsyncLifetime = Xunit.IAsyncLifetime; 17 | using LogLevel = Microsoft.Extensions.Logging.LogLevel; 18 | 19 | namespace Foundatio.Repositories.Elasticsearch.Tests; 20 | 21 | public abstract class ElasticRepositoryTestBase : TestWithLoggingBase, IAsyncLifetime 22 | { 23 | protected readonly MyAppElasticConfiguration _configuration; 24 | protected readonly InMemoryCacheClient _cache; 25 | protected readonly IElasticClient _client; 26 | protected readonly IQueue _workItemQueue; 27 | protected readonly InMemoryMessageBus _messageBus; 28 | 29 | public ElasticRepositoryTestBase(ITestOutputHelper output) : base(output) 30 | { 31 | Log.DefaultMinimumLevel = LogLevel.Information; 32 | Log.SetLogLevel(LogLevel.Warning); 33 | 34 | _cache = new InMemoryCacheClient(new InMemoryCacheClientOptions { LoggerFactory = Log }); 35 | _messageBus = new InMemoryMessageBus(new InMemoryMessageBusOptions { LoggerFactory = Log }); 36 | _workItemQueue = new InMemoryQueue(new InMemoryQueueOptions { LoggerFactory = Log }); 37 | _configuration = new MyAppElasticConfiguration(_workItemQueue, _cache, _messageBus, Log); 38 | _client = _configuration.Client; 39 | } 40 | 41 | private static bool _elasticsearchReady; 42 | public virtual async Task InitializeAsync() 43 | { 44 | if (!_elasticsearchReady) 45 | await _client.WaitForReadyAsync(new CancellationTokenSource(TimeSpan.FromMinutes(1)).Token); 46 | 47 | _elasticsearchReady = true; 48 | } 49 | 50 | protected virtual async Task RemoveDataAsync(bool configureIndexes = true) 51 | { 52 | var minimumLevel = Log.DefaultMinimumLevel; 53 | Log.DefaultMinimumLevel = LogLevel.Warning; 54 | 55 | var sw = Stopwatch.StartNew(); 56 | _logger.LogInformation("Starting remove data"); 57 | 58 | await _workItemQueue.DeleteQueueAsync(); 59 | await _configuration.DeleteIndexesAsync(); 60 | await _client.Indices.DeleteAsync(Indices.Parse("employee*")); 61 | if (configureIndexes) 62 | await _configuration.ConfigureIndexesAsync(null, false); 63 | 64 | await _cache.RemoveAllAsync(); 65 | _cache.ResetStats(); 66 | await _client.Indices.RefreshAsync(Indices.All); 67 | _messageBus.ResetMessagesSent(); 68 | sw.Stop(); 69 | _logger.LogInformation("Done removing data {Duration}", sw.Elapsed); 70 | 71 | Log.DefaultMinimumLevel = minimumLevel; 72 | } 73 | 74 | public virtual Task DisposeAsync() => Task.CompletedTask; 75 | } 76 | -------------------------------------------------------------------------------- /tests/Foundatio.Repositories.Elasticsearch.Tests/Extensions/ElasticsearchExtensions.cs: -------------------------------------------------------------------------------- 1 | using System.Threading.Tasks; 2 | using Nest; 3 | using Xunit; 4 | 5 | namespace Foundatio.Repositories.Elasticsearch.Tests; 6 | 7 | public static class ElasticsearchExtensions 8 | { 9 | public static async Task AssertSingleIndexAlias(this IElasticClient client, string indexName, string aliasName) 10 | { 11 | var aliasResponse = await client.Indices.GetAliasAsync(aliasName, a => a.IgnoreUnavailable()); 12 | Assert.True(aliasResponse.IsValid); 13 | Assert.Contains(indexName, aliasResponse.Indices); 14 | Assert.Single(aliasResponse.Indices); 15 | var aliasedIndex = aliasResponse.Indices[indexName]; 16 | Assert.NotNull(aliasedIndex); 17 | Assert.Contains(aliasName, aliasedIndex.Aliases); 18 | Assert.Single(aliasedIndex.Aliases); 19 | } 20 | 21 | public static async Task GetAliasIndexCount(this IElasticClient client, string aliasName) 22 | { 23 | var response = await client.Indices.GetAliasAsync(aliasName, a => a.IgnoreUnavailable()); 24 | // TODO: Fix this properly once https://github.com/elastic/elasticsearch-net/issues/3828 is fixed in beta2 25 | if (!response.IsValid) 26 | return 0; 27 | 28 | if (!response.IsValid && response.ServerError?.Status == 404) 29 | return 0; 30 | 31 | Assert.True(response.IsValid); 32 | return response.Indices.Count; 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /tests/Foundatio.Repositories.Elasticsearch.Tests/FieldIncludeParserTests.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using Foundatio.Repositories.Elasticsearch.Utility; 3 | using Xunit; 4 | 5 | namespace Foundatio.Repositories.Elasticsearch.Tests; 6 | 7 | public class FieldIncludeParserTests 8 | { 9 | [Theory] 10 | [InlineData("property1", "property1", "property1")] 11 | [InlineData("property1.test", "property1.test", "property1.test")] 12 | [InlineData("property1 test", "property1 test", "property1 test")] 13 | [InlineData("property1 test , property2 test ", "property1 test,property2 test", "property1 test,property2 test")] 14 | [InlineData("StringProperty2,Object1(NestedObject1),Object1(NestedObject1(StringProperty1))", "StringProperty2,Object1(NestedObject1(StringProperty1))", "StringProperty2,Object1.NestedObject1.StringProperty1")] 15 | [InlineData("Results(id,applicant,program(name,id,properties),application_id,cas_id,application_status,cas_date_submitted),NextPageToken,Results(applicant(givenName))", "Results(id,applicant(givenName),program(name,id,properties),application_id,cas_id,application_status,cas_date_submitted),NextPageToken", "Results.id,Results.applicant.givenName,Results.program.name,Results.program.id,Results.program.properties,Results.application_id,Results.cas_id,Results.application_status,Results.cas_date_submitted,NextPageToken")] 16 | [InlineData("meTa(sTuFf) , CreaTedUtc", "meTa(sTuFf),CreaTedUtc", "meTa.sTuFf,CreaTedUtc")] 17 | [InlineData("", "", "")] 18 | public void CanParse(string expression, string mask, string fields) 19 | { 20 | var result = FieldIncludeParser.Parse(expression); 21 | Assert.Equal(mask, result.ToString()); 22 | Assert.Equal(fields, string.Join(',', result.ToFieldPaths())); 23 | } 24 | 25 | [Theory] 26 | [InlineData(")", "unexpected")] 27 | [InlineData("blah)", "unexpected")] 28 | [InlineData("(", "missing")] 29 | [InlineData("((", "missing")] 30 | [InlineData("blah(", "missing")] 31 | [InlineData("blah((", "missing")] 32 | [InlineData("blah(()", "missing")] 33 | public void CanHandleInvalid(string expression, string message) 34 | { 35 | var result = FieldIncludeParser.Parse(expression); 36 | Assert.False(result.IsValid); 37 | 38 | if (!String.IsNullOrEmpty(message)) 39 | Assert.Contains(message, result.ValidationMessage, StringComparison.OrdinalIgnoreCase); 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /tests/Foundatio.Repositories.Elasticsearch.Tests/Foundatio.Repositories.Elasticsearch.Tests.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | docker-compose.yml 15 | 16 | 17 | 18 | -------------------------------------------------------------------------------- /tests/Foundatio.Repositories.Elasticsearch.Tests/MonthlyRepositoryTests.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Threading.Tasks; 3 | using Foundatio.Repositories.Elasticsearch.Tests.Repositories; 4 | using Foundatio.Repositories.Elasticsearch.Tests.Repositories.Models; 5 | using Foundatio.Repositories.Models; 6 | using Foundatio.Utility; 7 | using Microsoft.Extensions.Time.Testing; 8 | using Xunit; 9 | using Xunit.Abstractions; 10 | 11 | namespace Foundatio.Repositories.Elasticsearch.Tests; 12 | 13 | public sealed class MonthlyRepositoryTests : ElasticRepositoryTestBase 14 | { 15 | private readonly IFileAccessHistoryRepository _fileAccessHistoryRepository; 16 | 17 | public MonthlyRepositoryTests(ITestOutputHelper output) : base(output) 18 | { 19 | _fileAccessHistoryRepository = new FileAccessHistoryRepository(_configuration.MonthlyFileAccessHistory); 20 | } 21 | 22 | public override async Task InitializeAsync() 23 | { 24 | await base.InitializeAsync(); 25 | await RemoveDataAsync(); 26 | } 27 | 28 | [Fact] 29 | public async Task AddAsyncWithCustomDateIndex() 30 | { 31 | var utcNow = new DateTime(2023, 1, 1, 0, 0, 0, DateTimeKind.Utc); 32 | var history = await _fileAccessHistoryRepository.AddAsync(new FileAccessHistory { Path = "path1", AccessedDateUtc = utcNow }, o => o.ImmediateConsistency()); 33 | Assert.NotNull(history?.Id); 34 | 35 | var result = await _fileAccessHistoryRepository.FindOneAsync(f => f.Id(history.Id)); 36 | Assert.Equal("file-access-history-monthly-v1-2023.01", result.Data.GetString("index")); 37 | } 38 | 39 | [Fact] 40 | public async Task AddAsyncWithCurrentDateViaDocumentsAdding() 41 | { 42 | _configuration.TimeProvider = new FakeTimeProvider(new DateTimeOffset(2023, 02, 1, 0, 0, 0, TimeSpan.Zero)); 43 | 44 | try 45 | { 46 | // NOTE: This has to be async handler as there is no way to remove a sync handler. 47 | _fileAccessHistoryRepository.DocumentsAdding.AddHandler(OnDocumentsAdding); 48 | 49 | var history = await _fileAccessHistoryRepository.AddAsync(new FileAccessHistory { Path = "path2" }, o => o.ImmediateConsistency()); 50 | Assert.NotNull(history?.Id); 51 | 52 | var result = await _fileAccessHistoryRepository.FindOneAsync(f => f.Id(history.Id)); 53 | Assert.Equal("file-access-history-monthly-v1-2023.02", result.Data.GetString("index")); 54 | } 55 | finally 56 | { 57 | _fileAccessHistoryRepository.DocumentsAdding.RemoveHandler(OnDocumentsAdding); 58 | } 59 | } 60 | 61 | private Task OnDocumentsAdding(object sender, DocumentsEventArgs arg) 62 | { 63 | foreach (var document in arg.Documents) 64 | { 65 | if (document.AccessedDateUtc == DateTime.MinValue || document.AccessedDateUtc > _configuration.TimeProvider.GetUtcNow().UtcDateTime) 66 | document.AccessedDateUtc = _configuration.TimeProvider.GetUtcNow().UtcDateTime; 67 | } 68 | 69 | return Task.CompletedTask; 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /tests/Foundatio.Repositories.Elasticsearch.Tests/Properties/AssemblyInfo.cs: -------------------------------------------------------------------------------- 1 | [assembly: Xunit.CollectionBehaviorAttribute(DisableTestParallelization = true, MaxParallelThreads = 1)] 2 | -------------------------------------------------------------------------------- /tests/Foundatio.Repositories.Elasticsearch.Tests/QueryBuilderTests.cs: -------------------------------------------------------------------------------- 1 | using System.Linq; 2 | using System.Threading.Tasks; 3 | using Foundatio.Parsers.ElasticQueries.Visitors; 4 | using Foundatio.Repositories.Elasticsearch.Queries.Builders; 5 | using Foundatio.Repositories.Elasticsearch.Tests.Repositories.Models; 6 | using Foundatio.Xunit; 7 | using Nest; 8 | using Xunit; 9 | using Xunit.Abstractions; 10 | 11 | namespace Foundatio.Repositories.Elasticsearch.Tests; 12 | 13 | public sealed class RuntimeFieldsQueryBuilderTests : TestWithLoggingBase 14 | { 15 | public RuntimeFieldsQueryBuilderTests(ITestOutputHelper output) : base(output) 16 | { 17 | } 18 | 19 | [Fact] 20 | public async Task BuildAsync_MultipleFields() 21 | { 22 | var queryBuilder = new RuntimeFieldsQueryBuilder(); 23 | var query = new RepositoryQuery(); 24 | string runtimeField1 = "One", runtimeField2 = "Two"; 25 | var ctx = new QueryBuilderContext(query, new CommandOptions()); 26 | var ctxElastic = ctx as IElasticQueryVisitorContext; 27 | ctxElastic.RuntimeFields.Add(new Parsers.ElasticRuntimeField() { Name = runtimeField1 }); 28 | ctxElastic.RuntimeFields.Add(new Parsers.ElasticRuntimeField() { Name = runtimeField2 }); 29 | 30 | await queryBuilder.BuildAsync(ctx); 31 | 32 | ISearchRequest request = ctx.Search; 33 | Assert.Equal(2, request.RuntimeFields.Count); 34 | Assert.Equal(runtimeField1, request.RuntimeFields.First().Key); 35 | Assert.Equal(runtimeField2, request.RuntimeFields.Last().Key); 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /tests/Foundatio.Repositories.Elasticsearch.Tests/Repositories/ChildRepository.cs: -------------------------------------------------------------------------------- 1 | using System.Linq; 2 | using System.Threading.Tasks; 3 | using Foundatio.Repositories.Elasticsearch.Tests.Repositories.Configuration; 4 | using Foundatio.Repositories.Elasticsearch.Tests.Repositories.Models; 5 | using Foundatio.Repositories.Models; 6 | using Foundatio.Repositories.Options; 7 | using Nest; 8 | 9 | namespace Foundatio.Repositories.Elasticsearch.Tests.Repositories; 10 | public interface IChildRepository : ISearchableRepository { } 11 | 12 | public class ChildRepository : ElasticRepositoryBase, IChildRepository 13 | { 14 | public ChildRepository(MyAppElasticConfiguration elasticConfiguration) : base(elasticConfiguration.ParentChild) 15 | { 16 | BeforeQuery.AddHandler(OnBeforeQuery); 17 | DocumentsChanging.AddHandler(OnDocumentsChanging); 18 | GetParentIdFunc = d => d.ParentId; 19 | } 20 | 21 | private Task OnDocumentsChanging(object sender, DocumentsChangeEventArgs args) 22 | { 23 | foreach (var doc in args.Documents.Select(d => d.Value).Cast()) 24 | doc.Discriminator = JoinField.Link(doc.ParentId); 25 | 26 | return Task.CompletedTask; 27 | } 28 | 29 | private Task OnBeforeQuery(object sender, BeforeQueryEventArgs args) 30 | { 31 | args.Query.Discriminator("child"); 32 | return Task.CompletedTask; 33 | } 34 | 35 | protected override ICommandOptions ConfigureOptions(ICommandOptions options) 36 | { 37 | return base.ConfigureOptions(options).ParentDocumentType(typeof(Parent)); 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /tests/Foundatio.Repositories.Elasticsearch.Tests/Repositories/Configuration/ElasticsearchJsonNetSerializer.cs: -------------------------------------------------------------------------------- 1 | using Elasticsearch.Net; 2 | using Nest; 3 | using Nest.JsonNetSerializer; 4 | using Newtonsoft.Json; 5 | using Newtonsoft.Json.Serialization; 6 | 7 | namespace Foundatio.Repositories.Elasticsearch.Tests.Repositories.Configuration; 8 | 9 | public class ElasticsearchJsonNetSerializer : ConnectionSettingsAwareSerializerBase 10 | { 11 | public ElasticsearchJsonNetSerializer(IElasticsearchSerializer builtinSerializer, IConnectionSettingsValues connectionSettings) 12 | : base(builtinSerializer, connectionSettings) { } 13 | 14 | protected override JsonSerializerSettings CreateJsonSerializerSettings() => 15 | new JsonSerializerSettings 16 | { 17 | NullValueHandling = NullValueHandling.Include 18 | }; 19 | 20 | protected override void ModifyContractResolver(ConnectionSettingsAwareContractResolver resolver) 21 | { 22 | resolver.NamingStrategy = new CamelCaseNamingStrategy(); 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /tests/Foundatio.Repositories.Elasticsearch.Tests/Repositories/Configuration/Indexes/DailyFileAccessHistoryIndex.cs: -------------------------------------------------------------------------------- 1 | using Foundatio.Repositories.Elasticsearch.Configuration; 2 | using Foundatio.Repositories.Elasticsearch.Tests.Repositories.Models; 3 | using Nest; 4 | 5 | namespace Foundatio.Repositories.Elasticsearch.Tests.Repositories.Configuration.Indexes; 6 | 7 | public sealed class DailyFileAccessHistoryIndex : DailyIndex 8 | { 9 | public DailyFileAccessHistoryIndex(IElasticConfiguration configuration) : base(configuration, "file-access-history-daily", 1, d => ((FileAccessHistory)d).AccessedDateUtc) 10 | { 11 | } 12 | 13 | public override CreateIndexDescriptor ConfigureIndex(CreateIndexDescriptor idx) 14 | { 15 | return base.ConfigureIndex(idx.Settings(s => s.NumberOfReplicas(0).NumberOfShards(1))); 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /tests/Foundatio.Repositories.Elasticsearch.Tests/Repositories/Configuration/Indexes/DailyLogEventIndex.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using Foundatio.Repositories.Elasticsearch.Configuration; 3 | using Foundatio.Repositories.Elasticsearch.Queries.Builders; 4 | using Foundatio.Repositories.Elasticsearch.Tests.Repositories.Models; 5 | using Foundatio.Repositories.Elasticsearch.Tests.Repositories.Queries; 6 | using Nest; 7 | 8 | namespace Foundatio.Repositories.Elasticsearch.Tests.Repositories.Configuration.Indexes; 9 | 10 | public sealed class DailyLogEventIndex : DailyIndex 11 | { 12 | public DailyLogEventIndex(IElasticConfiguration configuration) : base(configuration, "daily-logevents", 1, doc => ((LogEvent)doc).Date.UtcDateTime) 13 | { 14 | AddAlias($"{Name}-today", TimeSpan.FromDays(1)); 15 | AddAlias($"{Name}-last7days", TimeSpan.FromDays(7)); 16 | AddAlias($"{Name}-last30days", TimeSpan.FromDays(30)); 17 | } 18 | 19 | public override TypeMappingDescriptor ConfigureIndexMapping(TypeMappingDescriptor map) 20 | { 21 | return base.ConfigureIndexMapping(map) 22 | .Dynamic(false) 23 | .Properties(p => p 24 | .Keyword(f => f.Name(e => e.Id)) 25 | .Keyword(f => f.Name(e => e.CompanyId)) 26 | .Date(f => f.Name(e => e.Date)) 27 | .Date(f => f.Name(e => e.CreatedUtc)) 28 | ); 29 | } 30 | 31 | protected override void ConfigureQueryBuilder(ElasticQueryBuilder builder) 32 | { 33 | builder.Register(); 34 | } 35 | 36 | public override CreateIndexDescriptor ConfigureIndex(CreateIndexDescriptor idx) 37 | { 38 | return base.ConfigureIndex(idx.Settings(s => s.NumberOfReplicas(0).NumberOfShards(1))); 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /tests/Foundatio.Repositories.Elasticsearch.Tests/Repositories/Configuration/Indexes/IdentityIndex.cs: -------------------------------------------------------------------------------- 1 | using Foundatio.Repositories.Elasticsearch.Configuration; 2 | using Foundatio.Repositories.Elasticsearch.Tests.Repositories.Models; 3 | using Nest; 4 | 5 | namespace Foundatio.Repositories.Elasticsearch.Tests.Repositories.Configuration.Indexes; 6 | 7 | public sealed class IdentityIndex : Index 8 | { 9 | public IdentityIndex(IElasticConfiguration configuration) : base(configuration, "identity") { } 10 | 11 | public override CreateIndexDescriptor ConfigureIndex(CreateIndexDescriptor idx) 12 | { 13 | return base.ConfigureIndex(idx.Settings(s => s.NumberOfReplicas(0).NumberOfShards(1))); 14 | } 15 | 16 | public override TypeMappingDescriptor ConfigureIndexMapping(TypeMappingDescriptor map) 17 | { 18 | return map 19 | .Dynamic(false) 20 | .Properties(p => p 21 | .Keyword(f => f.Name(e => e.Id)) 22 | ); 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /tests/Foundatio.Repositories.Elasticsearch.Tests/Repositories/Configuration/Indexes/MonthlyFileAccessHistoryIndex.cs: -------------------------------------------------------------------------------- 1 | using Foundatio.Repositories.Elasticsearch.Configuration; 2 | using Foundatio.Repositories.Elasticsearch.Tests.Repositories.Models; 3 | using Nest; 4 | 5 | namespace Foundatio.Repositories.Elasticsearch.Tests.Repositories.Configuration.Indexes; 6 | 7 | public sealed class MonthlyFileAccessHistoryIndex : MonthlyIndex 8 | { 9 | public MonthlyFileAccessHistoryIndex(IElasticConfiguration configuration) : base(configuration, "file-access-history-monthly", 1, d => ((FileAccessHistory)d).AccessedDateUtc) 10 | { 11 | } 12 | 13 | public override CreateIndexDescriptor ConfigureIndex(CreateIndexDescriptor idx) 14 | { 15 | return base.ConfigureIndex(idx.Settings(s => s.NumberOfReplicas(0).NumberOfShards(1))); 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /tests/Foundatio.Repositories.Elasticsearch.Tests/Repositories/Configuration/Indexes/MonthlyLogEventIndex.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using Foundatio.Repositories.Elasticsearch.Configuration; 3 | using Foundatio.Repositories.Elasticsearch.Tests.Repositories.Models; 4 | using Nest; 5 | 6 | namespace Foundatio.Repositories.Elasticsearch.Tests.Repositories.Configuration.Indexes; 7 | 8 | public sealed class MonthlyLogEventIndex : MonthlyIndex 9 | { 10 | public MonthlyLogEventIndex(IElasticConfiguration configuration) : base(configuration, "monthly-logevents", 1) 11 | { 12 | AddAlias($"{Name}-thismonth", TimeSpan.FromDays(32)); 13 | AddAlias($"{Name}-last3months", TimeSpan.FromDays(100)); 14 | } 15 | 16 | public override CreateIndexDescriptor ConfigureIndex(CreateIndexDescriptor idx) 17 | { 18 | return base.ConfigureIndex(idx.Settings(s => s.NumberOfReplicas(0).NumberOfShards(1))); 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /tests/Foundatio.Repositories.Elasticsearch.Tests/Repositories/Configuration/Indexes/ParentChildIndex.cs: -------------------------------------------------------------------------------- 1 | using Foundatio.Repositories.Elasticsearch.Configuration; 2 | using Foundatio.Repositories.Elasticsearch.Extensions; 3 | using Foundatio.Repositories.Elasticsearch.Tests.Repositories.Models; 4 | using Nest; 5 | 6 | namespace Foundatio.Repositories.Elasticsearch.Tests.Repositories.Configuration.Indexes; 7 | public sealed class ParentChildIndex : VersionedIndex 8 | { 9 | public ParentChildIndex(IElasticConfiguration configuration) : base(configuration, "parentchild", 1) { } 10 | 11 | public override CreateIndexDescriptor ConfigureIndex(CreateIndexDescriptor idx) 12 | { 13 | return base.ConfigureIndex(idx 14 | .Settings(s => s.NumberOfReplicas(0).NumberOfShards(1)) 15 | .Map(m => m 16 | //.RoutingField(r => r.Required()) 17 | .AutoMap() 18 | .AutoMap() 19 | .Properties(p => p 20 | .SetupDefaults() 21 | .Join(j => j 22 | .Name(n => n.Discriminator) 23 | .Relations(r => r.Join()) 24 | ) 25 | ))); 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /tests/Foundatio.Repositories.Elasticsearch.Tests/Repositories/FileAccessHistoryRepository.cs: -------------------------------------------------------------------------------- 1 | using Foundatio.Repositories.Elasticsearch.Configuration; 2 | using Foundatio.Repositories.Elasticsearch.Tests.Repositories.Models; 3 | 4 | namespace Foundatio.Repositories.Elasticsearch.Tests.Repositories; 5 | 6 | public interface IFileAccessHistoryRepository : ISearchableRepository { } 7 | 8 | public class FileAccessHistoryRepository : ElasticRepositoryBase, IFileAccessHistoryRepository 9 | { 10 | public FileAccessHistoryRepository(DailyIndex dailyIndex) : base(dailyIndex) 11 | { 12 | } 13 | 14 | public FileAccessHistoryRepository(MonthlyIndex monthlyIndex) : base(monthlyIndex) 15 | { 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /tests/Foundatio.Repositories.Elasticsearch.Tests/Repositories/IdentityRepository.cs: -------------------------------------------------------------------------------- 1 | using Foundatio.Repositories.Elasticsearch.Tests.Repositories.Configuration; 2 | using Foundatio.Repositories.Elasticsearch.Tests.Repositories.Models; 3 | 4 | namespace Foundatio.Repositories.Elasticsearch.Tests; 5 | 6 | public interface IIdentityRepository : ISearchableRepository { } 7 | 8 | public class IdentityRepository : ElasticRepositoryBase, IIdentityRepository 9 | { 10 | public IdentityRepository(MyAppElasticConfiguration configuration) : base(configuration.Identities) { } 11 | } 12 | 13 | public class IdentityWithNoCachingRepository : IdentityRepository 14 | { 15 | public IdentityWithNoCachingRepository(MyAppElasticConfiguration configuration) : base(configuration) 16 | { 17 | DisableCache(); 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /tests/Foundatio.Repositories.Elasticsearch.Tests/Repositories/Models/Child.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using Foundatio.Repositories.Models; 3 | using Foundatio.Repositories.Utility; 4 | using Nest; 5 | 6 | namespace Foundatio.Repositories.Elasticsearch.Tests.Repositories.Models; 7 | 8 | public class Child : IParentChildDocument, IHaveDates, ISupportSoftDeletes 9 | { 10 | public string Id { get; set; } 11 | public string ParentId { get; set; } 12 | JoinField IParentChildDocument.Discriminator { get; set; } 13 | public string ChildProperty { get; set; } 14 | public DateTime CreatedUtc { get; set; } 15 | public DateTime UpdatedUtc { get; set; } 16 | public bool IsDeleted { get; set; } 17 | } 18 | 19 | public static class ChildGenerator 20 | { 21 | public static readonly string DefaultId = ObjectId.GenerateNewId().ToString(); 22 | 23 | public static Child Default => new() { Id = DefaultId, ParentId = ParentGenerator.DefaultId }; 24 | 25 | public static Child Generate(string id = null, string parentId = null) 26 | { 27 | return new Child { Id = id, ParentId = parentId }; 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /tests/Foundatio.Repositories.Elasticsearch.Tests/Repositories/Models/FileAccessHistory.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using Foundatio.Repositories.Models; 3 | 4 | namespace Foundatio.Repositories.Elasticsearch.Tests.Repositories.Models; 5 | 6 | public record FileAccessHistory : IIdentity 7 | { 8 | public string Id { get; set; } 9 | public DateTime AccessedDateUtc { get; set; } 10 | public string Path { get; set; } 11 | } 12 | -------------------------------------------------------------------------------- /tests/Foundatio.Repositories.Elasticsearch.Tests/Repositories/Models/Identity.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using Foundatio.Repositories.Models; 4 | using Foundatio.Repositories.Utility; 5 | 6 | namespace Foundatio.Repositories.Elasticsearch.Tests.Repositories.Models; 7 | 8 | public class Identity : IIdentity 9 | { 10 | public string Id { get; set; } 11 | 12 | protected bool Equals(Identity other) 13 | { 14 | return String.Equals(Id, other.Id, StringComparison.InvariantCultureIgnoreCase); 15 | } 16 | 17 | public override bool Equals(object obj) 18 | { 19 | if (ReferenceEquals(null, obj)) 20 | return false; 21 | if (ReferenceEquals(this, obj)) 22 | return true; 23 | if (obj.GetType() != GetType()) 24 | return false; 25 | return Equals((Identity)obj); 26 | } 27 | 28 | public override int GetHashCode() 29 | { 30 | return (Id != null ? StringComparer.InvariantCultureIgnoreCase.GetHashCode(Id) : 0); 31 | } 32 | 33 | public static bool operator ==(Identity left, Identity right) 34 | { 35 | return Equals(left, right); 36 | } 37 | 38 | public static bool operator !=(Identity left, Identity right) 39 | { 40 | return !Equals(left, right); 41 | } 42 | } 43 | 44 | public static class IdentityGenerator 45 | { 46 | public static readonly string DefaultId = ObjectId.GenerateNewId().ToString(); 47 | 48 | public static Identity Default => new() 49 | { 50 | Id = DefaultId 51 | }; 52 | 53 | public static Identity Generate(string id = null) 54 | { 55 | return new Identity { Id = id }; 56 | } 57 | 58 | public static List GenerateIdentities(int count = 10) 59 | { 60 | var results = new List(count); 61 | for (int index = 0; index < count; index++) 62 | results.Add(Generate()); 63 | 64 | return results; 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /tests/Foundatio.Repositories.Elasticsearch.Tests/Repositories/Models/Parent.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using Foundatio.Repositories.Models; 3 | using Foundatio.Repositories.Utility; 4 | using Nest; 5 | 6 | namespace Foundatio.Repositories.Elasticsearch.Tests.Repositories.Models; 7 | 8 | public class Parent : IParentChildDocument, IHaveDates, ISupportSoftDeletes 9 | { 10 | public string Id { get; set; } 11 | string IParentChildDocument.ParentId { get; set; } 12 | JoinField IParentChildDocument.Discriminator { get; set; } 13 | public string ParentProperty { get; set; } 14 | public DateTime CreatedUtc { get; set; } 15 | public DateTime UpdatedUtc { get; set; } 16 | public bool IsDeleted { get; set; } 17 | } 18 | 19 | public static class ParentGenerator 20 | { 21 | public static readonly string DefaultId = ObjectId.GenerateNewId().ToString(); 22 | 23 | public static Parent Default => new() { Id = DefaultId }; 24 | 25 | public static Parent Generate(string id = null) 26 | { 27 | return new Parent { Id = id }; 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /tests/Foundatio.Repositories.Elasticsearch.Tests/Repositories/ParentRepository.cs: -------------------------------------------------------------------------------- 1 | using System.Linq; 2 | using System.Threading.Tasks; 3 | using Foundatio.Repositories.Elasticsearch.Tests.Repositories.Configuration; 4 | using Foundatio.Repositories.Elasticsearch.Tests.Repositories.Models; 5 | using Foundatio.Repositories.Models; 6 | using Nest; 7 | 8 | namespace Foundatio.Repositories.Elasticsearch.Tests.Repositories; 9 | public interface IParentRepository : ISearchableRepository { } 10 | 11 | public class ParentRepository : ElasticRepositoryBase, IParentRepository 12 | { 13 | public ParentRepository(MyAppElasticConfiguration elasticConfiguration) : base(elasticConfiguration.ParentChild) 14 | { 15 | BeforeQuery.AddHandler(OnBeforeQuery); 16 | DocumentsChanging.AddHandler(OnDocumentsChanging); 17 | } 18 | 19 | private Task OnDocumentsChanging(object sender, DocumentsChangeEventArgs args) 20 | { 21 | foreach (var doc in args.Documents.Select(d => d.Value).Cast()) 22 | doc.Discriminator = JoinField.Root(); 23 | 24 | return Task.CompletedTask; 25 | } 26 | 27 | private Task OnBeforeQuery(object sender, BeforeQueryEventArgs args) 28 | { 29 | args.Query.Discriminator("parent"); 30 | return Task.CompletedTask; 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /tests/Foundatio.Repositories.Elasticsearch.Tests/Repositories/Queries/AgeQuery.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | using System.Linq; 3 | using System.Threading.Tasks; 4 | using Foundatio.Repositories.Elasticsearch.Queries.Builders; 5 | using Foundatio.Repositories.Elasticsearch.Tests.Repositories.Models; 6 | using Foundatio.Repositories.Options; 7 | using Nest; 8 | 9 | namespace Foundatio.Repositories 10 | { 11 | public static class AgeQueryExtensions 12 | { 13 | internal const string AgesKey = "@Ages"; 14 | 15 | public static T Age(this T query, int age) where T : IRepositoryQuery 16 | { 17 | return query.AddCollectionOptionValue(AgesKey, age); 18 | } 19 | 20 | public static T AgeRange(this T query, int minAge, int maxAge) where T : IRepositoryQuery 21 | { 22 | foreach (int age in Enumerable.Range(minAge, maxAge - minAge + 1)) 23 | query.AddCollectionOptionValue(AgesKey, age); 24 | 25 | return query; 26 | } 27 | } 28 | } 29 | 30 | namespace Foundatio.Repositories.Options 31 | { 32 | public static class ReadAgeQueryExtensions 33 | { 34 | public static ICollection GetAges(this IRepositoryQuery query) 35 | { 36 | return query.SafeGetCollection(AgeQueryExtensions.AgesKey); 37 | } 38 | } 39 | } 40 | 41 | namespace Foundatio.Repositories.Elasticsearch.Tests.Repositories.Queries 42 | { 43 | public class AgeQueryBuilder : IElasticQueryBuilder 44 | { 45 | public Task BuildAsync(QueryBuilderContext ctx) where T : class, new() 46 | { 47 | var ages = ctx.Source.GetAges(); 48 | if (ages.Count <= 0) 49 | return Task.CompletedTask; 50 | 51 | if (ages.Count == 1) 52 | ctx.Filter &= Query.Term(f => f.Age, ages.First()); 53 | else 54 | ctx.Filter &= Query.Terms(d => d.Field(f => f.Age).Terms(ages)); 55 | 56 | return Task.CompletedTask; 57 | } 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /tests/Foundatio.Repositories.Elasticsearch.Tests/Repositories/Queries/CompanyQuery.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | using System.Linq; 3 | using System.Threading.Tasks; 4 | using Foundatio.Repositories.Elasticsearch.Queries.Builders; 5 | using Foundatio.Repositories.Elasticsearch.Tests.Repositories.Models; 6 | using Foundatio.Repositories.Options; 7 | using Nest; 8 | 9 | namespace Foundatio.Repositories 10 | { 11 | public static class CompanyQueryExtensions 12 | { 13 | internal const string CompaniesKey = "@Companies"; 14 | 15 | public static T Company(this T query, string companyId) where T : IRepositoryQuery 16 | { 17 | return query.AddCollectionOptionValue(CompaniesKey, companyId); 18 | } 19 | } 20 | } 21 | 22 | namespace Foundatio.Repositories.Options 23 | { 24 | public static class ReadCompanyQueryExtensions 25 | { 26 | public static ICollection GetCompanies(this IRepositoryQuery query) 27 | { 28 | return query.SafeGetCollection(CompanyQueryExtensions.CompaniesKey); 29 | } 30 | } 31 | } 32 | 33 | namespace Foundatio.Repositories.Elasticsearch.Tests.Repositories.Queries 34 | { 35 | public class CompanyQueryBuilder : IElasticQueryBuilder 36 | { 37 | public Task BuildAsync(QueryBuilderContext ctx) where T : class, new() 38 | { 39 | var companyIds = ctx.Source.GetCompanies(); 40 | if (companyIds.Count <= 0) 41 | return Task.CompletedTask; 42 | 43 | if (companyIds.Count == 1) 44 | ctx.Filter &= Query.Term(f => f.CompanyId, companyIds.Single()); 45 | else 46 | ctx.Filter &= Query.Terms(d => d.Field(f => f.CompanyId).Terms(companyIds)); 47 | 48 | return Task.CompletedTask; 49 | } 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /tests/Foundatio.Repositories.Elasticsearch.Tests/Repositories/Queries/EmailAddressQuery.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Threading.Tasks; 3 | using Foundatio.Repositories.Elasticsearch.Queries.Builders; 4 | using Foundatio.Repositories.Elasticsearch.Tests.Repositories.Models; 5 | using Foundatio.Repositories.Options; 6 | using Nest; 7 | 8 | namespace Foundatio.Repositories 9 | { 10 | public static class EmailAddressQueryExtensions 11 | { 12 | internal const string EmailAddressKey = "@EmailAddress"; 13 | 14 | public static T EmailAddress(this T query, string emailAddress) where T : IRepositoryQuery 15 | { 16 | return query.BuildOption(EmailAddressKey, emailAddress); 17 | } 18 | } 19 | } 20 | 21 | namespace Foundatio.Repositories.Options 22 | { 23 | public static class ReadEmailAddressQueryExtensions 24 | { 25 | public static string GetEmailAddress(this IRepositoryQuery query) 26 | { 27 | return query.SafeGetOption(EmailAddressQueryExtensions.EmailAddressKey); 28 | } 29 | } 30 | } 31 | 32 | namespace Foundatio.Repositories.Elasticsearch.Tests.Repositories.Queries 33 | { 34 | public class EmailAddressQueryBuilder : IElasticQueryBuilder 35 | { 36 | public Task BuildAsync(QueryBuilderContext ctx) where T : class, new() 37 | { 38 | string emailAddress = ctx.Source.GetEmailAddress(); 39 | if (String.IsNullOrEmpty(emailAddress)) 40 | return Task.CompletedTask; 41 | 42 | ctx.Filter &= Query.Term(f => f.EmailAddress, emailAddress); 43 | 44 | return Task.CompletedTask; 45 | } 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /tests/Foundatio.Repositories.Tests/Foundatio.Repositories.Tests.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /tests/Foundatio.Repositories.Tests/ObjectIdTests.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using Exceptionless.DateTimeExtensions; 3 | using Foundatio.Repositories.Utility; 4 | using Xunit; 5 | 6 | namespace Foundatio.Repositories.Tests; 7 | 8 | public class ObjectIdTests 9 | { 10 | [Fact] 11 | public void CanParseDate() 12 | { 13 | var time = DateTime.UtcNow.Round(TimeSpan.FromSeconds(1)); 14 | var id = ObjectId.GenerateNewId(time); 15 | Assert.Equal(time, id.CreationTime); 16 | 17 | var parsedId = ObjectId.Parse(id.ToString()); 18 | Assert.Equal(id, parsedId); 19 | Assert.Equal(time, parsedId.CreationTime); 20 | } 21 | 22 | [Fact] 23 | public void CanParseOldDate() 24 | { 25 | var time = DateTime.UtcNow.SubtractMonths(1).Round(TimeSpan.FromSeconds(1)); 26 | var id = ObjectId.GenerateNewId(time); 27 | Assert.Equal(time, id.CreationTime); 28 | 29 | var parsedId = ObjectId.Parse(id.ToString()); 30 | Assert.Equal(id, parsedId); 31 | Assert.Equal(time, parsedId.CreationTime); 32 | } 33 | 34 | [Fact] 35 | public void CanCreateUniqueIdsFromSameDateTime() 36 | { 37 | var utcNow = DateTime.UtcNow.Round(TimeSpan.FromSeconds(1)); 38 | var id = ObjectId.GenerateNewId(utcNow); 39 | Assert.Equal(utcNow, id.CreationTime); 40 | 41 | var id2 = ObjectId.GenerateNewId(utcNow); 42 | Assert.Equal(utcNow, id2.CreationTime); 43 | 44 | Assert.NotEqual(id, id2); 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /tests/Foundatio.Repositories.Tests/Properties/AssemblyInfo.cs: -------------------------------------------------------------------------------- 1 | [assembly: Xunit.CollectionBehaviorAttribute(DisableTestParallelization = true, MaxParallelThreads = 1)] 2 | --------------------------------------------------------------------------------