diff --git a/.github/workflows/post-integration.yml b/.github/workflows/post-integration.yml index 088efb0..749c8a5 100644 --- a/.github/workflows/post-integration.yml +++ b/.github/workflows/post-integration.yml @@ -11,6 +11,7 @@ env: ATC_EMAIL: 'atcnet.org@gmail.com' ATC_NAME: 'Atc-Net' NUGET_REPO_URL: 'https://nuget.pkg.github.com/atc-net/index.json' + VERSION: 1.1.0.${{ github.run_number }} jobs: merge-to-stable: @@ -43,14 +44,14 @@ jobs: - name: ๐Ÿงน Clean run: dotnet clean -c Release && dotnet nuget locals all --clear - - name: ๐Ÿ” Restore packages - run: dotnet restore + - name: ๐Ÿ› ๏ธ Building library in release mode + run: dotnet pack -c Release -o packages -p:UseSourceLink=true -p:Version=${{ env.VERSION }} -p:PackageVersion=${{ env.VERSION }} - - name: ๐Ÿ› ๏ธ Build - run: dotnet build -c Release --no-restore /p:UseSourceLink=true + - name: ๐Ÿ› ๏ธ Building preview library in release mode + run: dotnet pack -c Release -o packages -p:UseSourceLink=true -p:Version=${{ env.VERSION }} -p:PackageVersion=${{ env.VERSION }}-preview -p:DefineConstants=PREVIEW - name: ๐Ÿงช Run unit tests - run: dotnet test -c Release --no-build + run: dotnet test -c Release -p:DefineConstants=PREVIEW - name: ๐ŸŒฉ๏ธ SonarCloud install scanner run: dotnet tool install --global dotnet-sonarscanner @@ -60,6 +61,7 @@ jobs: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }} shell: pwsh + continue-on-error: true run: | dotnet sonarscanner begin /k:"atc-cosmos" /o:"atc-net" /d:sonar.login="${{ secrets.SONAR_TOKEN }}" /d:sonar.host.url="https://sonarcloud.io" dotnet build -c Release /p:UseSourceLink=true --no-restore @@ -73,8 +75,14 @@ jobs: git merge --ff-only main git push origin stable - - name: ๐Ÿ—ณ๏ธ Creating library package for pre-release - run: dotnet pack -c Release --no-restore -o ${GITHUB_WORKSPACE}/packages -p:RepositoryBranch=$BRANCH_NAME - - name: ๐Ÿ“ฆ Push packages to GitHub Package Registry - run: dotnet nuget push ${GITHUB_WORKSPACE}/packages/'Atc.Cosmos.'${NBGV_NuGetPackageVersion}'.nupkg' -k ${{ secrets.GITHUB_TOKEN }} -s ${{ env.NUGET_REPO_URL }} --skip-duplicate --no-symbols \ No newline at end of file + continue-on-error: true + run: dotnet nuget push **/*.nupkg -k ${{ secrets.GITHUB_TOKEN }} -s ${{ env.NUGET_REPO_URL }} --skip-duplicate --no-symbols + + - name: ๐Ÿ—ณ๏ธ Upload build artifacts + uses: actions/upload-artifact@v3 + with: + name: Packages + path: | + packages/*.nupkg + README.md \ No newline at end of file diff --git a/.github/workflows/pre-integration.yml b/.github/workflows/pre-integration.yml index 57d7b09..d19df7d 100644 --- a/.github/workflows/pre-integration.yml +++ b/.github/workflows/pre-integration.yml @@ -1,18 +1,15 @@ name: "Pre-Integration" on: - pull_request: - types: - - opened - - synchronize - - reopened + push: + workflow_dispatch: + +env: + VERSION: 1.1.0.${{ github.run_number }} jobs: - dotnet5-build: - strategy: - matrix: - os: [ubuntu-latest, macos-latest, windows-latest] - runs-on: ${{ matrix.os }} + build: + runs-on: ubuntu-latest steps: - name: ๐Ÿ›’ Checkout repository uses: actions/checkout@v2 @@ -27,16 +24,24 @@ jobs: - name: ๐Ÿงน Clean run: dotnet clean -c Release && dotnet nuget locals all --clear - - name: ๐Ÿ” Restore packages - run: dotnet restore - - name: ๐Ÿ› ๏ธ Building library in release mode - run: dotnet build -c Release --no-restore + run: dotnet pack -c Release -o packages -p:UseSourceLink=true -p:Version=${{ env.VERSION }} -p:PackageVersion=${{ env.VERSION }} - dotnet-test: + - name: ๐Ÿ› ๏ธ Building preview library in release mode + run: dotnet pack -c Release -o packages -p:UseSourceLink=true -p:Version=${{ env.VERSION }} -p:PackageVersion=${{ env.VERSION }}-preview -p:DefineConstants=PREVIEW + + - name: ๐Ÿ—ณ๏ธ Upload build artifacts + uses: actions/upload-artifact@v3 + with: + name: Packages + path: | + packages/*.nupkg + README.md + + test: runs-on: ubuntu-latest needs: - - dotnet5-build + - build steps: - name: ๐Ÿ›’ Checkout repository uses: actions/checkout@v2 @@ -51,8 +56,5 @@ jobs: - name: ๐Ÿ” Restore packages run: dotnet restore - - name: ๐Ÿ› ๏ธ Build - run: dotnet build -c Release --no-restore /p:UseSourceLink=true - - name: ๐Ÿงช Run unit tests - run: dotnet test -c Release --no-build \ No newline at end of file + run: dotnet test -c Release -p:DefineConstants=PREVIEW \ No newline at end of file diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index bb4912b..01708b4 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -7,6 +7,7 @@ env: ATC_EMAIL: 'atcnet.org@gmail.com' ATC_NAME: 'Atc-Net' NUGET_REPO_URL: 'https://api.nuget.org/v3/index.json' + VERSION: 1.1.${{ github.run_number }} jobs: release: @@ -19,14 +20,6 @@ jobs: fetch-depth: 0 token: ${{ secrets.PAT_WORKFLOWS }} - - name: โš›๏ธ Sets environment variables - branch-name - uses: nelonoel/branch-name@v1.0.1 - - - name: โš›๏ธ Sets environment variables - Nerdbank.GitVersioning - uses: dotnet/nbgv@master - with: - setAllVars: true - - name: โš™๏ธ Setup dotnet 6.0.x uses: actions/setup-dotnet@v1 with: @@ -35,11 +28,19 @@ jobs: - name: ๐Ÿงน Clean run: dotnet clean -c Release && dotnet nuget locals all --clear - - name: ๐Ÿ” Restore packages - run: dotnet restore - - name: ๐Ÿ› ๏ธ Building library in release mode - run: dotnet build -c Release --no-restore /p:UseSourceLink=true + run: dotnet pack -c Release -o packages -p:UseSourceLink=true -p:Version=${{ env.VERSION }} -p:PackageVersion=${{ env.VERSION }} + + - name: ๐Ÿ› ๏ธ Building preview library in release mode + run: dotnet pack -c Release -o packages -p:UseSourceLink=true -p:Version=${{ env.VERSION }} -p:PackageVersion=${{ env.VERSION }}-preview -p:DefineConstants=PREVIEW + + - name: ๐Ÿ—ณ๏ธ Upload build artifacts + uses: actions/upload-artifact@v3 + with: + name: Packages + path: | + packages/*.nupkg + README.md - name: โฉ Merge to release-branch run: | @@ -49,8 +50,5 @@ jobs: git merge --ff-only stable git push origin release - - name: ๐Ÿ—ณ๏ธ Creating library package for release - run: dotnet pack -c Release --no-restore -o ${GITHUB_WORKSPACE}/packages -p:RepositoryBranch=$BRANCH_NAME /p:PublicRelease=true - - name: ๐Ÿ“ฆ Push packages to NuGet - run: dotnet nuget push ${GITHUB_WORKSPACE}/packages/'Atc.Cosmos.'${NBGV_NuGetPackageVersion}'.nupkg' -k ${{ secrets.NUGET_KEY }} -s ${{ env.NUGET_REPO_URL }} --skip-duplicate --no-symbols \ No newline at end of file + run: dotnet nuget push **/*.nupkg -k ${{ secrets.NUGET_KEY }} -s ${{ env.NUGET_REPO_URL }} --skip-duplicate --no-symbols \ No newline at end of file diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 0000000..8226ed0 --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,3 @@ +{ + "dotnet.defaultSolution": "Atc.Cosmos.sln" +} \ No newline at end of file diff --git a/Atc.Cosmos.sln b/Atc.Cosmos.sln index 13aa2dd..98a44c0 100644 --- a/Atc.Cosmos.sln +++ b/Atc.Cosmos.sln @@ -11,16 +11,26 @@ Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU Release|Any CPU = Release|Any CPU + Debug Preview|Any CPU = Debug Preview|Any CPU + Release Preview|Any CPU = Release Preview|Any CPU EndGlobalSection GlobalSection(ProjectConfigurationPlatforms) = postSolution {AD8BA566-1E47-4E9E-BEBA-985DCA7A0DB5}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {AD8BA566-1E47-4E9E-BEBA-985DCA7A0DB5}.Debug|Any CPU.Build.0 = Debug|Any CPU {AD8BA566-1E47-4E9E-BEBA-985DCA7A0DB5}.Release|Any CPU.ActiveCfg = Release|Any CPU {AD8BA566-1E47-4E9E-BEBA-985DCA7A0DB5}.Release|Any CPU.Build.0 = Release|Any CPU + {AD8BA566-1E47-4E9E-BEBA-985DCA7A0DB5}.Debug Preview|Any CPU.ActiveCfg = Debug Preview|Any CPU + {AD8BA566-1E47-4E9E-BEBA-985DCA7A0DB5}.Debug Preview|Any CPU.Build.0 = Debug Preview|Any CPU + {AD8BA566-1E47-4E9E-BEBA-985DCA7A0DB5}.Release Preview|Any CPU.ActiveCfg = Release Preview|Any CPU + {AD8BA566-1E47-4E9E-BEBA-985DCA7A0DB5}.Release Preview|Any CPU.Build.0 = Release Preview|Any CPU {16FE83BC-DF0D-493D-8EE0-A78006A07EFF}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {16FE83BC-DF0D-493D-8EE0-A78006A07EFF}.Debug|Any CPU.Build.0 = Debug|Any CPU {16FE83BC-DF0D-493D-8EE0-A78006A07EFF}.Release|Any CPU.ActiveCfg = Release|Any CPU {16FE83BC-DF0D-493D-8EE0-A78006A07EFF}.Release|Any CPU.Build.0 = Release|Any CPU + {16FE83BC-DF0D-493D-8EE0-A78006A07EFF}.Debug Preview|Any CPU.ActiveCfg = Debug Preview|Any CPU + {16FE83BC-DF0D-493D-8EE0-A78006A07EFF}.Debug Preview|Any CPU.Build.0 = Debug Preview|Any CPU + {16FE83BC-DF0D-493D-8EE0-A78006A07EFF}.Release Preview|Any CPU.ActiveCfg = Release Preview|Any CPU + {16FE83BC-DF0D-493D-8EE0-A78006A07EFF}.Release Preview|Any CPU.Build.0 = Release Preview|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE diff --git a/Directory.Build.props b/Directory.Build.props index 558e2c4..c52e122 100644 --- a/Directory.Build.props +++ b/Directory.Build.props @@ -1,9 +1,25 @@ - + + + + Debug;Release;Debug Preview;Release Preview + AnyCPU + + + + TRACE;PREVIEW; + + + + TRACE;PREVIEW; + true + true + atc-net atc-cosmos + $(DefineConstants.Contains('PREVIEW')) diff --git a/src/Atc.Cosmos/Atc.Cosmos.csproj b/src/Atc.Cosmos/Atc.Cosmos.csproj index 2181ab9..33396f1 100644 --- a/src/Atc.Cosmos/Atc.Cosmos.csproj +++ b/src/Atc.Cosmos/Atc.Cosmos.csproj @@ -4,15 +4,17 @@ Atc.Cosmos cosmos;cosmos-sql;netcore;repository Library for configuring containers in Cosmos and providing an easy way to read and write document resources. + preview - + + @@ -22,8 +24,4 @@ - - - - \ No newline at end of file diff --git a/src/Atc.Cosmos/DependencyInjection/ServiceCollectionExtensions.cs b/src/Atc.Cosmos/DependencyInjection/ServiceCollectionExtensions.cs index f495053..dd5cfdc 100644 --- a/src/Atc.Cosmos/DependencyInjection/ServiceCollectionExtensions.cs +++ b/src/Atc.Cosmos/DependencyInjection/ServiceCollectionExtensions.cs @@ -61,12 +61,21 @@ public static IServiceCollection ConfigureCosmos( services.AddSingleton(); services.AddSingleton(typeof(ICosmosReader<>), typeof(CosmosReader<>)); services.AddSingleton(typeof(ICosmosWriter<>), typeof(CosmosWriter<>)); + services.AddSingleton(typeof(ICosmosBulkReader<>), typeof(CosmosBulkReader<>)); services.AddSingleton(typeof(ICosmosBulkWriter<>), typeof(CosmosBulkWriter<>)); services.AddSingleton(); services.AddSingleton(); services.AddSingleton(); services.AddSingleton(); services.AddSingleton(); +#if PREVIEW + services.AddSingleton(typeof(ILowPriorityCosmosReader<>), typeof(LowPriorityCosmosReader<>)); + services.AddSingleton(typeof(ILowPriorityCosmosWriter<>), typeof(LowPriorityCosmosWriter<>)); + services.AddSingleton(typeof(ILowPriorityCosmosBulkReader<>), typeof(LowPriorityCosmosBulkReader<>)); + services.AddSingleton(typeof(ILowPriorityCosmosBulkWriter<>), typeof(LowPriorityCosmosBulkWriter<>)); + services.AddSingleton(); + services.AddSingleton(); +#endif builder(new CosmosBuilder(services, registry, null)); return services; diff --git a/src/Atc.Cosmos/ILowPriorityCosmosBulkReader.cs b/src/Atc.Cosmos/ILowPriorityCosmosBulkReader.cs new file mode 100644 index 0000000..1efde5f --- /dev/null +++ b/src/Atc.Cosmos/ILowPriorityCosmosBulkReader.cs @@ -0,0 +1,17 @@ +#if PREVIEW +namespace Atc.Cosmos +{ + /// + /// Represents a reader that can perform bulk reads on Cosmos resources using the PriorityLevel Low. + /// + /// + /// The type of + /// to be read by this reader. + /// + public interface ILowPriorityCosmosBulkReader + : ICosmosBulkReader + where T : class, ICosmosResource + { + } +} +#endif \ No newline at end of file diff --git a/src/Atc.Cosmos/ILowPriorityCosmosBulkWriter.cs b/src/Atc.Cosmos/ILowPriorityCosmosBulkWriter.cs new file mode 100644 index 0000000..9970998 --- /dev/null +++ b/src/Atc.Cosmos/ILowPriorityCosmosBulkWriter.cs @@ -0,0 +1,17 @@ +#if PREVIEW +namespace Atc.Cosmos +{ + /// + /// Represents a reader that can perform bulk reads on Cosmos resources using the PriorityLevel Low. + /// + /// + /// The type of + /// to be read by this reader. + /// + public interface ILowPriorityCosmosBulkWriter + : ICosmosBulkWriter + where T : class, ICosmosResource + { + } +} +#endif \ No newline at end of file diff --git a/src/Atc.Cosmos/ILowPriorityCosmosReader.cs b/src/Atc.Cosmos/ILowPriorityCosmosReader.cs new file mode 100644 index 0000000..d94ce1a --- /dev/null +++ b/src/Atc.Cosmos/ILowPriorityCosmosReader.cs @@ -0,0 +1,17 @@ +#if PREVIEW +namespace Atc.Cosmos +{ + /// + /// Represents a reader that can read Cosmos resources using the PriorityLevel Low. + /// + /// + /// The type of + /// to be read by this reader. + /// + public interface ILowPriorityCosmosReader + : ICosmosReader + where T : class, ICosmosResource + { + } +} +#endif \ No newline at end of file diff --git a/src/Atc.Cosmos/ILowPriorityCosmosReaderFactory.cs b/src/Atc.Cosmos/ILowPriorityCosmosReaderFactory.cs new file mode 100644 index 0000000..1e898f6 --- /dev/null +++ b/src/Atc.Cosmos/ILowPriorityCosmosReaderFactory.cs @@ -0,0 +1,26 @@ +#if PREVIEW +namespace Atc.Cosmos +{ + /// + /// Represents a factory for creating instances. + /// + public interface ILowPriorityCosmosReaderFactory + { + /// + /// Create a . + /// + /// The for the . + /// A . + ILowPriorityCosmosReader CreateReader() + where TResource : class, ICosmosResource; + + /// + /// Create a . + /// + /// The for the . + /// A . + ILowPriorityCosmosBulkReader CreateBulkReader() + where TResource : class, ICosmosResource; + } +} +#endif \ No newline at end of file diff --git a/src/Atc.Cosmos/ILowPriorityCosmosWriter.cs b/src/Atc.Cosmos/ILowPriorityCosmosWriter.cs new file mode 100644 index 0000000..8e2805b --- /dev/null +++ b/src/Atc.Cosmos/ILowPriorityCosmosWriter.cs @@ -0,0 +1,17 @@ +#if PREVIEW +namespace Atc.Cosmos +{ + /// + /// Represents a writer that can write Cosmos resources using the PriorityLevel Low. + /// + /// + /// The type of + /// to be read by this reader. + /// + public interface ILowPriorityCosmosWriter + : ICosmosWriter + where T : class, ICosmosResource + { + } +} +#endif \ No newline at end of file diff --git a/src/Atc.Cosmos/ILowPriorityCosmosWriterFactory.cs b/src/Atc.Cosmos/ILowPriorityCosmosWriterFactory.cs new file mode 100644 index 0000000..ca05a44 --- /dev/null +++ b/src/Atc.Cosmos/ILowPriorityCosmosWriterFactory.cs @@ -0,0 +1,26 @@ +#if PREVIEW +namespace Atc.Cosmos +{ + /// + /// Represents a factory for creating instances. + /// + public interface ILowPriorityCosmosWriterFactory + { + /// + /// Create a . + /// + /// The for the . + /// A . + ILowPriorityCosmosWriter CreateWriter() + where TResource : class, ICosmosResource; + + /// + /// Create a . + /// + /// The for the . + /// A . + ILowPriorityCosmosBulkWriter CreateBulkWriter() + where TResource : class, ICosmosResource; + } +} +#endif \ No newline at end of file diff --git a/src/Atc.Cosmos/Internal/CosmosBulkReader.cs b/src/Atc.Cosmos/Internal/CosmosBulkReader.cs index d4f03f2..492139f 100644 --- a/src/Atc.Cosmos/Internal/CosmosBulkReader.cs +++ b/src/Atc.Cosmos/Internal/CosmosBulkReader.cs @@ -18,6 +18,10 @@ public CosmosBulkReader(ICosmosContainerProvider containerProvider) this.container = containerProvider.GetContainer(allowBulk: true); } +#if PREVIEW + protected virtual PriorityLevel PriorityLevel => PriorityLevel.High; +#endif + public async Task ReadAsync( string documentId, string partitionKey, @@ -27,6 +31,12 @@ public async Task ReadAsync( .ReadItemAsync( documentId, new PartitionKey(partitionKey), + new ItemRequestOptions + { +#if PREVIEW + PriorityLevel = PriorityLevel, +#endif + }, cancellationToken: cancellationToken) .ConfigureAwait(false); @@ -62,6 +72,9 @@ public async IAsyncEnumerable ReadAllAsync( requestOptions: new QueryRequestOptions { PartitionKey = new PartitionKey(partitionKey), +#if PREVIEW + PriorityLevel = PriorityLevel, +#endif }); while (reader.HasMoreResults && !cancellationToken.IsCancellationRequested) @@ -92,6 +105,9 @@ public async IAsyncEnumerable QueryAsync( requestOptions: new QueryRequestOptions { PartitionKey = new PartitionKey(partitionKey), +#if PREVIEW + PriorityLevel = PriorityLevel, +#endif }); while (reader.HasMoreResults && !cancellationToken.IsCancellationRequested) @@ -133,6 +149,9 @@ public async Task> PagedQueryAsync( { PartitionKey = new PartitionKey(partitionKey), MaxItemCount = pageSize, +#if PREVIEW + PriorityLevel = PriorityLevel, +#endif }); if (!reader.HasMoreResults) @@ -193,6 +212,9 @@ public async Task> CrossPartitionPagedQueryAsync( requestOptions: new QueryRequestOptions { MaxItemCount = pageSize, +#if PREVIEW + PriorityLevel = PriorityLevel, +#endif }); if (!reader.HasMoreResults) @@ -220,6 +242,9 @@ public async IAsyncEnumerable> BatchReadAllAsync( requestOptions: new QueryRequestOptions { PartitionKey = new PartitionKey(partitionKey), +#if PREVIEW + PriorityLevel = PriorityLevel, +#endif }); while (reader.HasMoreResults && !cancellationToken.IsCancellationRequested) @@ -248,6 +273,9 @@ public async IAsyncEnumerable> BatchQueryAsync( requestOptions: new QueryRequestOptions { PartitionKey = new PartitionKey(partitionKey), +#if PREVIEW + PriorityLevel = PriorityLevel, +#endif }); while (reader.HasMoreResults && !cancellationToken.IsCancellationRequested) diff --git a/src/Atc.Cosmos/Internal/CosmosBulkWriter.cs b/src/Atc.Cosmos/Internal/CosmosBulkWriter.cs index 8195b65..b90f9b9 100644 --- a/src/Atc.Cosmos/Internal/CosmosBulkWriter.cs +++ b/src/Atc.Cosmos/Internal/CosmosBulkWriter.cs @@ -1,6 +1,5 @@ using System.Threading; using System.Threading.Tasks; -using Atc.Cosmos.Serialization; using Microsoft.Azure.Cosmos; namespace Atc.Cosmos.Internal @@ -9,16 +8,17 @@ public class CosmosBulkWriter : ICosmosBulkWriter where T : class, ICosmosResource { private readonly Container container; - private readonly IJsonCosmosSerializer serializer; public CosmosBulkWriter( - ICosmosContainerProvider containerProvider, - IJsonCosmosSerializer serializer) + ICosmosContainerProvider containerProvider) { this.container = containerProvider.GetContainer(allowBulk: true); - this.serializer = serializer; } +#if PREVIEW + protected virtual PriorityLevel PriorityLevel => PriorityLevel.High; +#endif + public Task CreateAsync( T document, CancellationToken cancellationToken = default) @@ -26,7 +26,13 @@ public Task CreateAsync( .CreateItemAsync( document, new PartitionKey(document.PartitionKey), - new ItemRequestOptions { EnableContentResponseOnWrite = false }, + new ItemRequestOptions + { + EnableContentResponseOnWrite = false, +#if PREVIEW + PriorityLevel = PriorityLevel, +#endif + }, cancellationToken); public Task WriteAsync( @@ -36,7 +42,13 @@ public Task WriteAsync( .UpsertItemAsync( document, new PartitionKey(document.PartitionKey), - new ItemRequestOptions { EnableContentResponseOnWrite = false }, + new ItemRequestOptions + { + EnableContentResponseOnWrite = false, +#if PREVIEW + PriorityLevel = PriorityLevel, +#endif + }, cancellationToken); public Task ReplaceAsync( @@ -51,6 +63,9 @@ public Task ReplaceAsync( { IfMatchEtag = document.ETag, EnableContentResponseOnWrite = false, +#if PREVIEW + PriorityLevel = PriorityLevel, +#endif }, cancellationToken); @@ -62,7 +77,13 @@ public Task DeleteAsync( .DeleteItemAsync( documentId, new PartitionKey(partitionKey), - new ItemRequestOptions { EnableContentResponseOnWrite = false }, + new ItemRequestOptions + { + EnableContentResponseOnWrite = false, +#if PREVIEW + PriorityLevel = PriorityLevel, +#endif + }, cancellationToken: cancellationToken); } } \ No newline at end of file diff --git a/src/Atc.Cosmos/Internal/CosmosReader.cs b/src/Atc.Cosmos/Internal/CosmosReader.cs index 0bfb4c4..a4a3fe9 100644 --- a/src/Atc.Cosmos/Internal/CosmosReader.cs +++ b/src/Atc.Cosmos/Internal/CosmosReader.cs @@ -6,7 +6,6 @@ using System.Threading.Tasks; using Microsoft.Azure.Cosmos; using Microsoft.Azure.Cosmos.Linq; -using Microsoft.Extensions.Options; namespace Atc.Cosmos.Internal { @@ -24,6 +23,10 @@ public CosmosReader( this.options = containerProvider.GetCosmosOptions(); } +#if PREVIEW + protected virtual PriorityLevel PriorityLevel => PriorityLevel.High; +#endif + public async Task ReadAsync( string documentId, string partitionKey, @@ -33,6 +36,12 @@ public async Task ReadAsync( .ReadItemAsync( documentId, new PartitionKey(partitionKey), + new ItemRequestOptions + { +#if PREVIEW + PriorityLevel = PriorityLevel, +#endif + }, cancellationToken: cancellationToken) .ConfigureAwait(false); @@ -68,6 +77,9 @@ public async IAsyncEnumerable ReadAllAsync( requestOptions: new QueryRequestOptions { PartitionKey = new PartitionKey(partitionKey), +#if PREVIEW + PriorityLevel = PriorityLevel, +#endif }); while (reader.HasMoreResults && !cancellationToken.IsCancellationRequested) @@ -107,6 +119,9 @@ public async IAsyncEnumerable QueryAsync( requestOptions: new QueryRequestOptions { PartitionKey = new PartitionKey(partitionKey), +#if PREVIEW + PriorityLevel = PriorityLevel, +#endif }); while (reader.HasMoreResults && !cancellationToken.IsCancellationRequested) @@ -149,6 +164,9 @@ public async Task> PagedQueryAsync( PartitionKey = new PartitionKey(partitionKey), MaxItemCount = pageSize, ResponseContinuationTokenLimitInKb = options.ContinuationTokenLimitInKb, +#if PREVIEW + PriorityLevel = PriorityLevel, +#endif }); if (!reader.HasMoreResults) @@ -230,6 +248,9 @@ public async Task> CrossPartitionPagedQueryAsync( { MaxItemCount = pageSize, ResponseContinuationTokenLimitInKb = options.ContinuationTokenLimitInKb, +#if PREVIEW + PriorityLevel = PriorityLevel, +#endif }); if (!reader.HasMoreResults) @@ -268,6 +289,9 @@ public async IAsyncEnumerable> BatchReadAllAsync( requestOptions: new QueryRequestOptions { PartitionKey = new PartitionKey(partitionKey), +#if PREVIEW + PriorityLevel = PriorityLevel, +#endif }); while (reader.HasMoreResults && !cancellationToken.IsCancellationRequested) @@ -296,6 +320,9 @@ public async IAsyncEnumerable> BatchQueryAsync( requestOptions: new QueryRequestOptions { PartitionKey = new PartitionKey(partitionKey), +#if PREVIEW + PriorityLevel = PriorityLevel, +#endif }); while (reader.HasMoreResults && !cancellationToken.IsCancellationRequested) @@ -326,7 +353,14 @@ public async IAsyncEnumerable> BatchCrossPartitionQueryAsyn QueryDefinition query, [EnumeratorCancellation] CancellationToken cancellationToken = default) { - var reader = container.GetItemQueryIterator(query); + var reader = container.GetItemQueryIterator( + query, + requestOptions: new QueryRequestOptions + { +#if PREVIEW + PriorityLevel = PriorityLevel, +#endif + }); while (reader.HasMoreResults && !cancellationToken.IsCancellationRequested) { @@ -347,6 +381,14 @@ public IAsyncEnumerable> BatchCrossPartitionQueryAsync( Func, IQueryable> queryBuilder) - => queryBuilder(container.GetItemLinqQueryable()).ToQueryDefinition(); + => queryBuilder( + container.GetItemLinqQueryable( + requestOptions: new QueryRequestOptions + { +#if PREVIEW + PriorityLevel = PriorityLevel, +#endif + })) + .ToQueryDefinition(); } } \ No newline at end of file diff --git a/src/Atc.Cosmos/Internal/CosmosWriter.cs b/src/Atc.Cosmos/Internal/CosmosWriter.cs index f1b2dbc..e64e6b1 100644 --- a/src/Atc.Cosmos/Internal/CosmosWriter.cs +++ b/src/Atc.Cosmos/Internal/CosmosWriter.cs @@ -27,6 +27,10 @@ public CosmosWriter( this.serializer = serializer; } +#if PREVIEW + protected virtual PriorityLevel PriorityLevel => PriorityLevel.High; +#endif + public Task CreateAsync( T document, CancellationToken cancellationToken = default) @@ -34,7 +38,12 @@ public Task CreateAsync( .CreateItemAsync( document, new PartitionKey(document.PartitionKey), - new ItemRequestOptions { }, + new ItemRequestOptions + { +#if PREVIEW + PriorityLevel = PriorityLevel, +#endif + }, cancellationToken) .GetResourceWithEtag(serializer); @@ -45,7 +54,13 @@ public Task CreateWithNoResponseAsync( .CreateItemAsync( document, new PartitionKey(document.PartitionKey), - new ItemRequestOptions { EnableContentResponseOnWrite = false }, + new ItemRequestOptions + { + EnableContentResponseOnWrite = false, +#if PREVIEW + PriorityLevel = PriorityLevel, +#endif + }, cancellationToken); public Task WriteAsync( @@ -55,7 +70,12 @@ public Task WriteAsync( .UpsertItemAsync( document, new PartitionKey(document.PartitionKey), - new ItemRequestOptions { }, + new ItemRequestOptions + { +#if PREVIEW + PriorityLevel = PriorityLevel, +#endif + }, cancellationToken) .GetResourceWithEtag(serializer); @@ -66,7 +86,13 @@ public Task WriteWithNoResponseAsync( .UpsertItemAsync( document, new PartitionKey(document.PartitionKey), - new ItemRequestOptions { EnableContentResponseOnWrite = false }, + new ItemRequestOptions + { + EnableContentResponseOnWrite = false, +#if PREVIEW + PriorityLevel = PriorityLevel, +#endif + }, cancellationToken); public Task ReplaceAsync( @@ -80,6 +106,9 @@ public Task ReplaceAsync( new ItemRequestOptions { IfMatchEtag = document.ETag, +#if PREVIEW + PriorityLevel = PriorityLevel, +#endif }, cancellationToken) .GetResourceWithEtag(serializer); @@ -96,6 +125,9 @@ public Task ReplaceWithNoResponseAsync( { IfMatchEtag = document.ETag, EnableContentResponseOnWrite = false, +#if PREVIEW + PriorityLevel = PriorityLevel, +#endif }, cancellationToken); @@ -107,6 +139,12 @@ public Task DeleteAsync( .DeleteItemAsync( documentId, new PartitionKey(partitionKey), + new ItemRequestOptions + { +#if PREVIEW + PriorityLevel = PriorityLevel, +#endif + }, cancellationToken: cancellationToken); public async Task TryDeleteAsync( @@ -120,6 +158,12 @@ await container .DeleteItemAsync( documentId, new PartitionKey(partitionKey), + new ItemRequestOptions + { +#if PREVIEW + PriorityLevel = PriorityLevel, +#endif + }, cancellationToken: cancellationToken) .ConfigureAwait(false); } @@ -270,6 +314,9 @@ public Task PatchAsync( new PatchItemRequestOptions { FilterPredicate = filterPredicate, +#if PREVIEW + PriorityLevel = PriorityLevel, +#endif }, cancellationToken) .GetResourceWithEtag(serializer); @@ -289,6 +336,9 @@ public Task PatchWithNoResponseAsync( { FilterPredicate = filterPredicate, EnableContentResponseOnWrite = false, +#if PREVIEW + PriorityLevel = PriorityLevel, +#endif }, cancellationToken); diff --git a/src/Atc.Cosmos/Internal/CosmosWriterFactory.cs b/src/Atc.Cosmos/Internal/CosmosWriterFactory.cs index cd1dda5..032756f 100644 --- a/src/Atc.Cosmos/Internal/CosmosWriterFactory.cs +++ b/src/Atc.Cosmos/Internal/CosmosWriterFactory.cs @@ -28,7 +28,6 @@ public ICosmosWriter CreateWriter() public ICosmosBulkWriter CreateBulkWriter() where TResource : class, ICosmosResource => new CosmosBulkWriter( - provider, - serializer); + provider); } } diff --git a/src/Atc.Cosmos/Internal/LowPriorityCosmosBulkReader.cs b/src/Atc.Cosmos/Internal/LowPriorityCosmosBulkReader.cs new file mode 100644 index 0000000..8a5afd2 --- /dev/null +++ b/src/Atc.Cosmos/Internal/LowPriorityCosmosBulkReader.cs @@ -0,0 +1,19 @@ +#if PREVIEW +using Atc.Cosmos.Internal; +using Microsoft.Azure.Cosmos; + +namespace Atc.Cosmos +{ + public class LowPriorityCosmosBulkReader + : CosmosBulkReader, ILowPriorityCosmosBulkReader + where T : class, ICosmosResource + { + public LowPriorityCosmosBulkReader(ICosmosContainerProvider containerProvider) + : base(containerProvider) + { + } + + protected override PriorityLevel PriorityLevel => PriorityLevel.Low; + } +} +#endif \ No newline at end of file diff --git a/src/Atc.Cosmos/Internal/LowPriorityCosmosBulkWriter.cs b/src/Atc.Cosmos/Internal/LowPriorityCosmosBulkWriter.cs new file mode 100644 index 0000000..ebe79db --- /dev/null +++ b/src/Atc.Cosmos/Internal/LowPriorityCosmosBulkWriter.cs @@ -0,0 +1,22 @@ +#if PREVIEW +using Atc.Cosmos.Internal; +using Atc.Cosmos.Serialization; +using Microsoft.Azure.Cosmos; + +namespace Atc.Cosmos +{ + public class LowPriorityCosmosBulkWriter + : CosmosBulkWriter, ILowPriorityCosmosBulkWriter + where T : class, ICosmosResource + { + public LowPriorityCosmosBulkWriter( + ICosmosContainerProvider containerProvider, + IJsonCosmosSerializer serializer) + : base(containerProvider) + { + } + + protected override PriorityLevel PriorityLevel => PriorityLevel.Low; + } +} +#endif \ No newline at end of file diff --git a/src/Atc.Cosmos/Internal/LowPriorityCosmosReader.cs b/src/Atc.Cosmos/Internal/LowPriorityCosmosReader.cs new file mode 100644 index 0000000..0b26e19 --- /dev/null +++ b/src/Atc.Cosmos/Internal/LowPriorityCosmosReader.cs @@ -0,0 +1,19 @@ +#if PREVIEW +using Atc.Cosmos.Internal; +using Microsoft.Azure.Cosmos; + +namespace Atc.Cosmos +{ + public class LowPriorityCosmosReader + : CosmosReader, ILowPriorityCosmosReader + where T : class, ICosmosResource + { + public LowPriorityCosmosReader(ICosmosContainerProvider containerProvider) + : base(containerProvider) + { + } + + protected override PriorityLevel PriorityLevel => PriorityLevel.Low; + } +} +#endif \ No newline at end of file diff --git a/src/Atc.Cosmos/Internal/LowPriorityCosmosReaderFactory.cs b/src/Atc.Cosmos/Internal/LowPriorityCosmosReaderFactory.cs new file mode 100644 index 0000000..7227102 --- /dev/null +++ b/src/Atc.Cosmos/Internal/LowPriorityCosmosReaderFactory.cs @@ -0,0 +1,25 @@ +#if PREVIEW +using Atc.Cosmos.Internal; + +namespace Atc.Cosmos +{ + public class LowPriorityCosmosReaderFactory : ILowPriorityCosmosReaderFactory + { + private readonly ICosmosContainerProvider provider; + + public LowPriorityCosmosReaderFactory( + ICosmosContainerProvider provider) + { + this.provider = provider; + } + + public ILowPriorityCosmosReader CreateReader() + where TResource : class, ICosmosResource + => new LowPriorityCosmosReader(provider); + + public ILowPriorityCosmosBulkReader CreateBulkReader() + where TResource : class, ICosmosResource + => new LowPriorityCosmosBulkReader(provider); + } +} +#endif \ No newline at end of file diff --git a/src/Atc.Cosmos/Internal/LowPriorityCosmosWriter.cs b/src/Atc.Cosmos/Internal/LowPriorityCosmosWriter.cs new file mode 100644 index 0000000..c9da785 --- /dev/null +++ b/src/Atc.Cosmos/Internal/LowPriorityCosmosWriter.cs @@ -0,0 +1,23 @@ +#if PREVIEW +using Atc.Cosmos.Internal; +using Atc.Cosmos.Serialization; +using Microsoft.Azure.Cosmos; + +namespace Atc.Cosmos +{ + public class LowPriorityCosmosWriter + : CosmosWriter, ILowPriorityCosmosWriter + where T : class, ICosmosResource + { + public LowPriorityCosmosWriter( + ICosmosContainerProvider containerProvider, + ILowPriorityCosmosReader reader, + IJsonCosmosSerializer serializer) + : base(containerProvider, reader, serializer) + { + } + + protected override PriorityLevel PriorityLevel => PriorityLevel.Low; + } +} +#endif \ No newline at end of file diff --git a/src/Atc.Cosmos/Internal/LowPriorityCosmosWriterFactory.cs b/src/Atc.Cosmos/Internal/LowPriorityCosmosWriterFactory.cs new file mode 100644 index 0000000..b006274 --- /dev/null +++ b/src/Atc.Cosmos/Internal/LowPriorityCosmosWriterFactory.cs @@ -0,0 +1,35 @@ +#if PREVIEW +using Atc.Cosmos.Internal; +using Atc.Cosmos.Serialization; + +namespace Atc.Cosmos +{ + public class LowPriorityCosmosWriterFactory : ILowPriorityCosmosWriterFactory + { + private readonly ICosmosContainerProvider provider; + private readonly ILowPriorityCosmosReaderFactory factory; + private readonly IJsonCosmosSerializer serializer; + + public LowPriorityCosmosWriterFactory( + ICosmosContainerProvider provider, + ILowPriorityCosmosReaderFactory factory, + IJsonCosmosSerializer serializer) + { + this.provider = provider; + this.factory = factory; + this.serializer = serializer; + } + + public ILowPriorityCosmosWriter CreateWriter() + where TResource : class, ICosmosResource + => new LowPriorityCosmosWriter( + provider, + factory.CreateReader(), + serializer); + + public ILowPriorityCosmosBulkWriter CreateBulkWriter() + where TResource : class, ICosmosResource + => new LowPriorityCosmosBulkWriter(provider, serializer); + } +} +#endif \ No newline at end of file diff --git a/src/Directory.Build.props b/src/Directory.Build.props index 5908235..c7b406a 100644 --- a/src/Directory.Build.props +++ b/src/Directory.Build.props @@ -55,7 +55,6 @@ - diff --git a/test/Atc.Cosmos.Tests/CosmosBulkReaderTests.cs b/test/Atc.Cosmos.Tests/CosmosBulkReaderTests.cs index f50e879..62c1a68 100644 --- a/test/Atc.Cosmos.Tests/CosmosBulkReaderTests.cs +++ b/test/Atc.Cosmos.Tests/CosmosBulkReaderTests.cs @@ -87,7 +87,11 @@ public async Task ReadAsync_Reads_Item_In_Container( .ReadItemAsync( documentId, new PartitionKey(partitionKey), +#if PREVIEW + Arg.Is(c => c.PriorityLevel == PriorityLevel.High), +#else Arg.Any(), +#endif cancellationToken); } diff --git a/test/Atc.Cosmos.Tests/CosmosBulkWriterTests.cs b/test/Atc.Cosmos.Tests/CosmosBulkWriterTests.cs index a4c8365..0025b98 100644 --- a/test/Atc.Cosmos.Tests/CosmosBulkWriterTests.cs +++ b/test/Atc.Cosmos.Tests/CosmosBulkWriterTests.cs @@ -47,7 +47,7 @@ public CosmosBulkWriterTests() .FromString(default) .ReturnsForAnyArgs(new Fixture().Create()); - sut = new CosmosBulkWriter(containerProvider, serializer); + sut = new CosmosBulkWriter(containerProvider); } [Fact] diff --git a/test/Atc.Cosmos.Tests/CosmosReaderBatchTests.cs b/test/Atc.Cosmos.Tests/CosmosReaderBatchTests.cs index cc14c09..5e86e73 100644 --- a/test/Atc.Cosmos.Tests/CosmosReaderBatchTests.cs +++ b/test/Atc.Cosmos.Tests/CosmosReaderBatchTests.cs @@ -184,7 +184,8 @@ public async Task QueryAsync_Returns_Empty_No_More_Result( { feedIterator.HasMoreResults.Returns(false); - var response = await sut.BatchQueryAsync(query, partitionKey, cancellationToken).ToListAsync(cancellationToken); + var response = await sut.BatchQueryAsync(query, partitionKey, cancellationToken) + .ToListAsync(cancellationToken); _ = feedIterator .Received(1) @@ -207,7 +208,8 @@ public async Task QueryAsync_Returns_Empty_When_Query_Matches_Non( { feedIterator.HasMoreResults.Returns(true, false); - var response = await sut.BatchQueryAsync(query, partitionKey, cancellationToken).ToListAsync(cancellationToken); + var response = await sut.BatchQueryAsync(query, partitionKey, cancellationToken) + .ToListAsync(cancellationToken); _ = feedIterator .Received(2) @@ -237,7 +239,8 @@ public async Task QueryAsync_Returns_Items_When_Query_Matches( .GetEnumerator() .Returns(new List { record }.GetEnumerator()); - var response = await sut.BatchQueryAsync(query, partitionKey, cancellationToken).ToListAsync(cancellationToken); + var response = await sut.BatchQueryAsync(query, partitionKey, cancellationToken) + .ToListAsync(cancellationToken); _ = feedIterator .Received(2) @@ -294,7 +297,8 @@ public async Task QueryAsync_With_Custom_Returns_Empty_No_More_Result( { feedIterator.HasMoreResults.Returns(false); - var response = await sut.BatchQueryAsync(query, partitionKey, cancellationToken).ToListAsync(cancellationToken); + var response = await sut.BatchQueryAsync(query, partitionKey, cancellationToken) + .ToListAsync(cancellationToken); _ = feedIterator .Received(1) @@ -317,7 +321,8 @@ public async Task QueryAsync_With_Custom_Returns_Empty_When_Query_Matches_Non( { feedIterator.HasMoreResults.Returns(true, false); - var response = await sut.BatchQueryAsync(query, partitionKey, cancellationToken).ToListAsync(cancellationToken); + var response = await sut.BatchQueryAsync(query, partitionKey, cancellationToken) + .ToListAsync(cancellationToken); _ = feedIterator .Received(2) @@ -347,7 +352,8 @@ public async Task QueryAsync_With_Custom_Returns_Items_When_Query_Matches( .GetEnumerator() .Returns(new List { record }.GetEnumerator()); - var response = await sut.BatchQueryAsync(query, partitionKey, cancellationToken).ToListAsync(cancellationToken); + var response = await sut.BatchQueryAsync(query, partitionKey, cancellationToken) + .ToListAsync(cancellationToken); _ = feedIterator .Received(2) @@ -380,7 +386,7 @@ public void CrossPartitionQueryAsync_Uses_The_Right_Container( } [Theory, AutoNSubstituteData] - public void CrossPartitionQueryAsync_Does_Not_Specify_QueryRequestOptions( + public void CrossPartitionQueryAsync_Uses_QueryRequestOptions_With_PriorityLevel_High( QueryDefinition query, CancellationToken cancellationToken) { @@ -388,7 +394,13 @@ public void CrossPartitionQueryAsync_Does_Not_Specify_QueryRequestOptions( container .Received(1) - .GetItemQueryIterator(query, requestOptions: null); + .GetItemQueryIterator( + query, +#if PREVIEW + requestOptions: Arg.Is(c => c.PriorityLevel == PriorityLevel.High)); +#else + requestOptions: Arg.Any()); +#endif } [Theory, AutoNSubstituteData] @@ -398,7 +410,8 @@ public async Task CrossPartitionQueryAsync_Returns_Empty_No_More_Result( { feedIterator.HasMoreResults.Returns(false); - var response = await sut.BatchCrossPartitionQueryAsync(query, cancellationToken).ToListAsync(cancellationToken); + var response = await sut.BatchCrossPartitionQueryAsync(query, cancellationToken) + .ToListAsync(cancellationToken); _ = feedIterator .Received(1) @@ -420,7 +433,8 @@ public async Task CrossPartitionQueryAsync_Returns_Empty_When_Query_Matches_Non( { feedIterator.HasMoreResults.Returns(true, false); - var response = await sut.BatchCrossPartitionQueryAsync(query, cancellationToken).ToListAsync(cancellationToken); + var response = await sut.BatchCrossPartitionQueryAsync(query, cancellationToken) + .ToListAsync(cancellationToken); _ = feedIterator .Received(2) @@ -449,7 +463,8 @@ public async Task CrossPartitionQueryAsync_Returns_Items_When_Query_Matches( .GetEnumerator() .Returns(new List { record }.GetEnumerator()); - var response = await sut.BatchCrossPartitionQueryAsync(query, cancellationToken).ToListAsync(cancellationToken); + var response = await sut.BatchCrossPartitionQueryAsync(query, cancellationToken) + .ToListAsync(cancellationToken); _ = feedIterator .Received(2) diff --git a/test/Atc.Cosmos.Tests/CosmosWriterTests.cs b/test/Atc.Cosmos.Tests/CosmosWriterTests.cs index 8011071..2f4b432 100644 --- a/test/Atc.Cosmos.Tests/CosmosWriterTests.cs +++ b/test/Atc.Cosmos.Tests/CosmosWriterTests.cs @@ -92,7 +92,11 @@ await container .UpsertItemAsync( record, new PartitionKey(record.Pk), +#if PREVIEW + Arg.Is(o => o.PriorityLevel == PriorityLevel.High), +#else Arg.Any(), +#endif cancellationToken); } @@ -110,13 +114,17 @@ await container .UpsertItemAsync( record, new PartitionKey(record.Pk), +#if PREVIEW + Arg.Is(p => p.EnableContentResponseOnWrite == false && p.PriorityLevel == PriorityLevel.High), +#else Arg.Is(p => p.EnableContentResponseOnWrite == false), +#endif cancellationToken); } [Theory, AutoNSubstituteData] public async Task CreateAsync_Calls_CreateItem_On_Container( - CancellationToken cancellationToken) + CancellationToken cancellationToken) { await sut.CreateAsync(record, cancellationToken); _ = container @@ -124,13 +132,17 @@ public async Task CreateAsync_Calls_CreateItem_On_Container( .CreateItemAsync( record, new PartitionKey(record.Pk), +#if PREVIEW + Arg.Is(o => o.PriorityLevel == PriorityLevel.High), +#else Arg.Any(), +#endif cancellationToken); } [Theory, AutoNSubstituteData] public async Task CreateWithNoResponseAsync_Calls_CreateItem_On_Container( - CancellationToken cancellationToken) + CancellationToken cancellationToken) { await sut.CreateWithNoResponseAsync(record, cancellationToken); _ = container @@ -138,13 +150,17 @@ public async Task CreateWithNoResponseAsync_Calls_CreateItem_On_Container( .CreateItemAsync( record, new PartitionKey(record.Pk), +#if PREVIEW + Arg.Is(p => p.EnableContentResponseOnWrite == false && p.PriorityLevel == PriorityLevel.High), +#else Arg.Is(p => p.EnableContentResponseOnWrite == false), +#endif cancellationToken); } [Theory, AutoNSubstituteData] public async Task ReplaceAsync_Calls_ReplaceItemAsync_On_Container( - CancellationToken cancellationToken) + CancellationToken cancellationToken) { await sut.ReplaceAsync(record, cancellationToken); _ = container @@ -153,13 +169,17 @@ public async Task ReplaceAsync_Calls_ReplaceItemAsync_On_Container( record, record.Id, new PartitionKey(record.Pk), - Arg.Is(o => o.IfMatchEtag == ((ICosmosResource)record).ETag), +#if PREVIEW + Arg.Is(o => o.IfMatchEtag == record.ETag && o.PriorityLevel == PriorityLevel.High), +#else + Arg.Is(o => o.IfMatchEtag == record.ETag), +#endif cancellationToken); } [Theory, AutoNSubstituteData] public async Task ReplaceWithNoResponseAsync_Calls_ReplaceItemAsync_On_Container( - CancellationToken cancellationToken) + CancellationToken cancellationToken) { await sut.ReplaceWithNoResponseAsync(record, cancellationToken); _ = container @@ -168,8 +188,12 @@ public async Task ReplaceWithNoResponseAsync_Calls_ReplaceItemAsync_On_Container record, record.Id, new PartitionKey(record.Pk), - Arg.Is(o => o.IfMatchEtag == ((ICosmosResource)record).ETag - && o.EnableContentResponseOnWrite == false), + Arg.Is( + o => o.IfMatchEtag == record.ETag +#if PREVIEW + && o.PriorityLevel == PriorityLevel.High +#endif + && o.EnableContentResponseOnWrite == false), cancellationToken); } @@ -192,7 +216,7 @@ public void Multiple_Operations_Uses_Same_Container( [Theory, AutoNSubstituteData] public async Task DeleteAsync_Calls_DeleteItemAsync_On_Container( - CancellationToken cancellationToken) + CancellationToken cancellationToken) { await sut.DeleteAsync(record.Id, record.Pk, cancellationToken); _ = container @@ -200,13 +224,17 @@ public async Task DeleteAsync_Calls_DeleteItemAsync_On_Container( .DeleteItemAsync( record.Id, new PartitionKey(record.Pk), +#if PREVIEW + Arg.Is(o => o.PriorityLevel == PriorityLevel.High), +#else Arg.Any(), +#endif cancellationToken: cancellationToken); } [Theory, AutoNSubstituteData] public async Task Should_Return_True_When_Trying_To_Delete_Existing_Resource( - CancellationToken cancellationToken) + CancellationToken cancellationToken) { var deleted = await sut.TryDeleteAsync( record.Id, @@ -222,13 +250,17 @@ public async Task Should_Return_True_When_Trying_To_Delete_Existing_Resource( .DeleteItemAsync( record.Id, new PartitionKey(record.Pk), +#if PREVIEW + Arg.Is(o => o.PriorityLevel == PriorityLevel.High), +#else Arg.Any(), +#endif cancellationToken: cancellationToken); } [Theory, AutoNSubstituteData] public async Task Should_Return_False_When_Trying_To_Delete_NonExisting_Resource( - CancellationToken cancellationToken) + CancellationToken cancellationToken) { container .DeleteItemAsync(default, default, default, default) @@ -249,7 +281,11 @@ public async Task Should_Return_False_When_Trying_To_Delete_NonExisting_Resource .DeleteItemAsync( record.Id, new PartitionKey(record.Pk), +#if PREVIEW + Arg.Is(o => o.PriorityLevel == PriorityLevel.High), +#else Arg.Any(), +#endif cancellationToken: cancellationToken); } @@ -317,16 +353,21 @@ await sut.UpdateAsync( record, record.Id, new PartitionKey(record.Pk), - Arg.Is(o => o.IfMatchEtag == ((ICosmosResource)record).ETag), +#if PREVIEW + Arg.Is( + o => o.IfMatchEtag == record.ETag && o.PriorityLevel == PriorityLevel.High), +#else + Arg.Is(o => o.IfMatchEtag == record.ETag), +#endif cancellationToken); } [Theory, AutoNSubstituteData] public async Task UpdateOrCreateAsync_Finds_The_Resource( - Action updateDocument, - int retries, - Record defaultDocument, - CancellationToken cancellationToken) + Action updateDocument, + int retries, + Record defaultDocument, + CancellationToken cancellationToken) { await sut.UpdateOrCreateAsync( () => defaultDocument, @@ -432,7 +473,11 @@ await sut.UpdateOrCreateAsync( .CreateItemAsync( defaultDocument, new PartitionKey(defaultDocument.Pk), +#if PREVIEW + Arg.Is(o => o.PriorityLevel == PriorityLevel.High), +#else Arg.Any(), +#endif cancellationToken); } @@ -455,7 +500,11 @@ await sut.PatchAsync( record.Id, new PartitionKey(record.Pk), patchOperations, +#if PREVIEW + Arg.Is(o => o.PriorityLevel == PriorityLevel.High), +#else Arg.Any(), +#endif cancellationToken); } @@ -478,7 +527,11 @@ await sut.PatchWithNoResponseAsync( record.Id, new PartitionKey(record.Pk), patchOperations, +#if PREVIEW + Arg.Is(o => o.PriorityLevel == PriorityLevel.High), +#else Arg.Any(), +#endif cancellationToken); } } diff --git a/test/Atc.Cosmos.Tests/LowPriorityCosmosBulkReaderTests.cs b/test/Atc.Cosmos.Tests/LowPriorityCosmosBulkReaderTests.cs new file mode 100644 index 0000000..0b78205 --- /dev/null +++ b/test/Atc.Cosmos.Tests/LowPriorityCosmosBulkReaderTests.cs @@ -0,0 +1,805 @@ +#if PREVIEW +using System.Collections.Generic; +using System.Threading; +using System.Threading.Tasks; +using Atc.Cosmos.Internal; +using Atc.Test; +using AutoFixture; +using Dasync.Collections; +using FluentAssertions; +using Microsoft.Azure.Cosmos; +using NSubstitute; +using NSubstitute.ExceptionExtensions; +using Xunit; + +namespace Atc.Cosmos.Tests +{ + public class LowPriorityCosmosBulkReaderTests + { + private readonly ItemResponse itemResponse; + private readonly FeedIterator feedIterator; + private readonly FeedResponse feedResponse; + private readonly Record record; + private readonly Container container; + private readonly ICosmosContainerProvider containerProvider; + private readonly LowPriorityCosmosBulkReader sut; + + public LowPriorityCosmosBulkReaderTests() + { + record = new Fixture().Create(); + itemResponse = Substitute.For>(); + itemResponse + .Resource + .Returns(record); + + feedResponse = Substitute.For>(); + feedIterator = Substitute.For>(); + feedIterator + .ReadNextAsync(default) + .ReturnsForAnyArgs(feedResponse); + + container = Substitute.For(); + container + .ReadItemAsync(default, default, default) + .ReturnsForAnyArgs(itemResponse); + + container + .GetItemQueryIterator(default(QueryDefinition), default) + .ReturnsForAnyArgs(feedIterator); + + container + .GetItemQueryIterator(default(string), default) + .ReturnsForAnyArgs(feedIterator); + + containerProvider = Substitute.For(); + containerProvider + .GetContainer(allowBulk: true) + .Returns(container, null); + sut = new LowPriorityCosmosBulkReader(containerProvider); + } + + [Fact] + public void Implements_Interface() + => sut.Should().BeAssignableTo>(); + + [Theory, AutoNSubstituteData] + public async Task ReadAsync_Uses_The_Right_Container( + string documentId, + string partitionKey, + CancellationToken cancellationToken) + { + await sut.ReadAsync(documentId, partitionKey, cancellationToken); + + containerProvider + .Received(1) + .GetContainer(allowBulk: true); + } + + [Theory, AutoNSubstituteData] + public async Task ReadAsync_Reads_Item_In_Container( + string documentId, + string partitionKey, + CancellationToken cancellationToken) + { + await sut.ReadAsync(documentId, partitionKey, cancellationToken); + + _ = container + .Received(1) + .ReadItemAsync( + documentId, + new PartitionKey(partitionKey), + Arg.Is(c => c.PriorityLevel == PriorityLevel.Low), + cancellationToken); + } + + [Theory, AutoNSubstituteData] + public async Task ReadAsync_Returns_Item_Read_From_Container( + string documentId, + string partitionKey, + CancellationToken cancellationToken) + { + var result = await sut.ReadAsync(documentId, partitionKey, cancellationToken); + result + .Should() + .Be(itemResponse.Resource); + } + + [Theory, AutoNSubstituteData] + public void ReadAsync_Throws_Expection_When_Record_IsNot_Found( + CosmosException exception, + string documentId, + string partitionKey, + CancellationToken cancellationToken) + { + container + .ReadItemAsync(default, default, default, default) + .ThrowsForAnyArgs(exception); + + FluentActions + .Awaiting(() => sut.ReadAsync(documentId, partitionKey, cancellationToken)) + .Should() + .ThrowAsync(); + } + + [Theory, AutoNSubstituteData] + public async Task FindAsync_Uses_The_Right_Container( + string documentId, + string partitionKey, + CancellationToken cancellationToken) + { + await sut.FindAsync(documentId, partitionKey, cancellationToken); + + containerProvider + .Received(1) + .GetContainer(allowBulk: true); + } + + [Theory, AutoNSubstituteData] + public async Task FindAsync_Return_Default_When_Record_IsNot_Found( + CosmosException exception, + string documentId, + string partitionKey, + CancellationToken cancellationToken) + { + container + .ReadItemAsync(default, default, default, default) + .ThrowsForAnyArgs(exception); + + var response = await sut.FindAsync(documentId, partitionKey, cancellationToken); + + response + .Should() + .BeNull(); + } + + [Theory, AutoNSubstituteData] + public async Task FindAsync_Returns_Record_When_Successful( + string partitionKey, + string documentId, + CancellationToken cancellationToken) + { + var result = await sut.FindAsync(documentId, partitionKey, cancellationToken); + result + .Should() + .Be(record); + } + + [Theory, AutoNSubstituteData] + public void ReadAllAsync_Uses_The_Right_Container( + string partitionKey, + CancellationToken cancellationToken) + { + _ = sut.ReadAllAsync(partitionKey, cancellationToken); + + containerProvider + .Received(1) + .GetContainer(allowBulk: true); + } + + [Theory, AutoNSubstituteData] + public async Task ReadAllAsync_Returns_Empty_No_More_Result( + string partitionKey, + CancellationToken cancellationToken) + { + feedIterator.HasMoreResults.Returns(false); + + var response = await sut + .ReadAllAsync(partitionKey, cancellationToken) + .ToListAsync(cancellationToken); + + _ = feedIterator + .Received(1) + .HasMoreResults; + + _ = feedIterator + .Received(0) + .ReadNextAsync(default); + + response + .Should() + .BeEmpty(); + } + + [Theory, AutoNSubstituteData] + public async Task ReadAllAsync_Returns_Empty_When_Query_Matches_Non( + string partitionKey, + CancellationToken cancellationToken) + { + feedIterator.HasMoreResults.Returns(true, false); + + var response = await sut + .ReadAllAsync(partitionKey, cancellationToken) + .ToListAsync(cancellationToken); + + _ = feedIterator + .Received(2) + .HasMoreResults; + + _ = feedIterator + .Received(1) + .ReadNextAsync(default); + + response + .Should() + .BeEmpty(); + } + + [Theory, AutoNSubstituteData] + public async Task ReadAllAsync_Returns_All_Items( + string partitionKey, + CancellationToken cancellationToken) + { + feedIterator + .HasMoreResults + .Returns(true, false); + + feedResponse + .GetEnumerator() + .Returns(new List { record }.GetEnumerator()); + + var response = await sut + .ReadAllAsync(partitionKey, cancellationToken) + .ToListAsync(cancellationToken); + + _ = feedIterator + .Received(2) + .HasMoreResults; + + _ = feedIterator + .Received(1) + .ReadNextAsync(default); + + response + .Should() + .NotBeEmpty(); + + response[0] + .Should() + .Be(record); + } + + [Theory, AutoNSubstituteData] + public void QueryAsync_Uses_The_Right_Container( + QueryDefinition query, + string partitionKey, + CancellationToken cancellationToken) + { + _ = sut.QueryAsync(query, partitionKey, cancellationToken); + + containerProvider + .Received(1) + .GetContainer(allowBulk: true); + } + + [Theory, AutoNSubstituteData] + public async Task QueryAsync_Returns_Empty_No_More_Result( + QueryDefinition query, + string partitionKey, + CancellationToken cancellationToken) + { + feedIterator.HasMoreResults.Returns(false); + + var response = await sut.QueryAsync(query, partitionKey, cancellationToken).ToListAsync(cancellationToken); + + _ = feedIterator + .Received(1) + .HasMoreResults; + + _ = feedIterator + .Received(0) + .ReadNextAsync(default); + + response + .Should() + .BeEmpty(); + } + + [Theory, AutoNSubstituteData] + public async Task QueryAsync_Returns_Empty_When_Query_Matches_Non( + QueryDefinition query, + string partitionKey, + CancellationToken cancellationToken) + { + feedIterator.HasMoreResults.Returns(true, false); + + var response = await sut.QueryAsync(query, partitionKey, cancellationToken).ToListAsync(cancellationToken); + + _ = feedIterator + .Received(2) + .HasMoreResults; + + _ = feedIterator + .Received(1) + .ReadNextAsync(default); + + response + .Should() + .BeEmpty(); + } + + [Theory, AutoNSubstituteData] + public async Task QueryAsync_Returns_Items_When_Query_Matches( + QueryDefinition query, + string partitionKey, + CancellationToken cancellationToken) + { + feedIterator + .HasMoreResults + .Returns(true, false); + + feedResponse + .GetEnumerator() + .Returns(new List { record }.GetEnumerator()); + + var response = await sut.QueryAsync(query, partitionKey, cancellationToken).ToListAsync(cancellationToken); + + _ = feedIterator + .Received(2) + .HasMoreResults; + + _ = feedIterator + .Received(1) + .ReadNextAsync(default); + + response + .Should() + .NotBeEmpty(); + + response[0] + .Should() + .Be(record); + } + + [Theory, AutoNSubstituteData] + public async Task Should_Have_ETag_From_ItemResponse( + string etag, + string partitionKey, + string documentId, + CancellationToken cancellationToken) + { + itemResponse + .ETag + .Returns(etag); + itemResponse + .Resource + .Returns(record); + + var result = await sut.FindAsync(documentId, partitionKey, cancellationToken); + + var resource = result as ICosmosResource; + resource + .Should() + .NotBeNull(); + + resource + .ETag + .Should() + .NotBeNullOrWhiteSpace(); + + resource + .ETag + .Should() + .Be(etag); + } + + [Theory, AutoNSubstituteData] + public void Multiple_Operations_Uses_Same_Container( + QueryDefinition query, + string documentId, + string partitionKey, + CancellationToken cancellationToken) + { + _ = sut.ReadAsync(documentId, partitionKey, cancellationToken); + _ = sut.ReadAsync(documentId, partitionKey, cancellationToken); + _ = sut.FindAsync(documentId, partitionKey, cancellationToken); + _ = sut.FindAsync(documentId, partitionKey, cancellationToken); + _ = sut.QueryAsync(query, partitionKey, cancellationToken).ToListAsync(cancellationToken); + _ = sut.QueryAsync(query, partitionKey, cancellationToken).ToListAsync(cancellationToken); + + container + .ReceivedCalls() + .Should() + .HaveCount(6); + } + + [Theory, AutoNSubstituteData] + public void QueryAsync_With_Custom_Result_Uses_The_Right_Container( + QueryDefinition query, + string partitionKey, + CancellationToken cancellationToken) + { + _ = sut.QueryAsync(query, partitionKey, cancellationToken); + + containerProvider + .Received(1) + .GetContainer(allowBulk: true); + } + + [Theory, AutoNSubstituteData] + public async Task QueryAsync_With_Custom_Returns_Empty_No_More_Result( + QueryDefinition query, + string partitionKey, + CancellationToken cancellationToken) + { + feedIterator.HasMoreResults.Returns(false); + + var response = await sut.QueryAsync(query, partitionKey, cancellationToken).ToListAsync(cancellationToken); + + _ = feedIterator + .Received(1) + .HasMoreResults; + + _ = feedIterator + .Received(0) + .ReadNextAsync(default); + + response + .Should() + .BeEmpty(); + } + + [Theory, AutoNSubstituteData] + public async Task QueryAsync_With_Custom_Returns_Empty_When_Query_Matches_Non( + QueryDefinition query, + string partitionKey, + CancellationToken cancellationToken) + { + feedIterator.HasMoreResults.Returns(true, false); + + var response = await sut.QueryAsync(query, partitionKey, cancellationToken).ToListAsync(cancellationToken); + + _ = feedIterator + .Received(2) + .HasMoreResults; + + _ = feedIterator + .Received(1) + .ReadNextAsync(default); + + response + .Should() + .BeEmpty(); + } + + [Theory, AutoNSubstituteData] + public async Task QueryAsync_With_Custom_Returns_Items_When_Query_Matches( + QueryDefinition query, + string partitionKey, + CancellationToken cancellationToken) + { + feedIterator + .HasMoreResults + .Returns(true, false); + + feedResponse + .GetEnumerator() + .Returns(new List { record }.GetEnumerator()); + + var response = await sut.QueryAsync(query, partitionKey, cancellationToken).ToListAsync(cancellationToken); + + _ = feedIterator + .Received(2) + .HasMoreResults; + + _ = feedIterator + .Received(1) + .ReadNextAsync(default); + + response + .Should() + .NotBeEmpty(); + + response[0] + .Should() + .Be(record); + } + + [Theory, AutoNSubstituteData] + public void CrossPartitionQueryAsync_Uses_The_Right_Container( + QueryDefinition query, + CancellationToken cancellationToken) + { + _ = sut.CrossPartitionQueryAsync(query, cancellationToken); + + containerProvider + .Received(1) + .GetContainer(allowBulk: true); + } + + [Theory, AutoNSubstituteData] + public void CrossPartitionQueryAsync_Does_Not_Specify_QueryRequestOptions( + QueryDefinition query, + CancellationToken cancellationToken) + { + _ = sut.CrossPartitionQueryAsync(query, cancellationToken).ToArrayAsync(cancellationToken); + + container + .Received(1) + .GetItemQueryIterator(query, requestOptions: null); + } + + [Theory, AutoNSubstituteData] + public async Task CrossPartitionQueryAsync_Returns_Empty_No_More_Result( + QueryDefinition query, + CancellationToken cancellationToken) + { + feedIterator.HasMoreResults.Returns(false); + + var response = await sut.CrossPartitionQueryAsync(query, cancellationToken).ToListAsync(cancellationToken); + + _ = feedIterator + .Received(1) + .HasMoreResults; + + _ = feedIterator + .Received(0) + .ReadNextAsync(default); + + response + .Should() + .BeEmpty(); + } + + [Theory, AutoNSubstituteData] + public async Task CrossPartitionQueryAsync_Returns_Empty_When_Query_Matches_Non( + QueryDefinition query, + CancellationToken cancellationToken) + { + feedIterator.HasMoreResults.Returns(true, false); + + var response = await sut.CrossPartitionQueryAsync(query, cancellationToken).ToListAsync(cancellationToken); + + _ = feedIterator + .Received(2) + .HasMoreResults; + + _ = feedIterator + .Received(1) + .ReadNextAsync(default); + + response + .Should() + .BeEmpty(); + } + + [Theory, AutoNSubstituteData] + public async Task CrossPartitionQueryAsync_Returns_Items_When_Query_Matches( + QueryDefinition query, + CancellationToken cancellationToken) + { + feedIterator + .HasMoreResults + .Returns(true, false); + + feedResponse + .GetEnumerator() + .Returns(new List { record }.GetEnumerator()); + + var response = await sut.CrossPartitionQueryAsync(query, cancellationToken).ToListAsync(cancellationToken); + + _ = feedIterator + .Received(2) + .HasMoreResults; + + _ = feedIterator + .Received(1) + .ReadNextAsync(default); + + response + .Should() + .NotBeEmpty(); + + response[0] + .Should() + .Be(record); + } + + [Theory, AutoNSubstituteData] + public void CrossPartitionPagedQueryAsync_Uses_The_Right_Container( + QueryDefinition query, + int pageSize, + string continuationToken, + CancellationToken cancellationToken) + { + _ = sut.CrossPartitionPagedQueryAsync( + query, + pageSize, + continuationToken, + cancellationToken); + + containerProvider + .Received(1) + .GetContainer(allowBulk: true); + } + + [Theory, AutoNSubstituteData] + public void CrossPartitionPagedQueryAsync_Gets_ItemQueryIterator( + QueryDefinition query, + int pageSize, + string continuationToken, + CancellationToken cancellationToken) + { + _ = sut.CrossPartitionPagedQueryAsync( + query, + pageSize, + continuationToken, + cancellationToken); + + container + .Received(1) + .GetItemQueryIterator( + query, + continuationToken, + requestOptions: Arg.Is(o + => o.PartitionKey == null + && o.MaxItemCount == pageSize)); + } + + [Theory, AutoNSubstituteData] + public async Task CrossPartitionPagedQueryAsync_Returns_Empty_When_No_More_Result( + QueryDefinition query, + int pageSize, + string continuationToken, + CancellationToken cancellationToken) + { + feedIterator.HasMoreResults.Returns(false); + + var response = await sut + .CrossPartitionPagedQueryAsync( + query, + pageSize, + continuationToken, + cancellationToken); + + _ = feedIterator + .Received(1) + .HasMoreResults; + + _ = feedIterator + .Received(0) + .ReadNextAsync(default); + + response.Items + .Should() + .BeEmpty(); + response.ContinuationToken + .Should() + .BeNull(); + } + + [Theory, AutoNSubstituteData] + public async Task CrossPartitionPagedQueryAsync_Returns_Items_When_Query_Matches( + QueryDefinition query, + int pageSize, + string continuationToken, + List records, + CancellationToken cancellationToken) + { + feedIterator + .HasMoreResults + .Returns(true); + feedResponse + .ContinuationToken + .Returns(continuationToken); + feedResponse + .GetEnumerator() + .Returns(records.GetEnumerator()); + + var response = await sut + .CrossPartitionPagedQueryAsync( + query, + pageSize, + null, + cancellationToken); + + _ = feedIterator + .Received(1) + .HasMoreResults; + + _ = feedIterator + .Received(1) + .ReadNextAsync(default); + + response.Items + .Should() + .BeEquivalentTo(records); + + response.ContinuationToken + .Should() + .Be(continuationToken); + } + + [Theory, AutoNSubstituteData] + public void CrossPartitionPagedQueryAsync_With_Custom_Uses_The_Right_Container( + QueryDefinition query, + int pageSize, + string continuationToken, + CancellationToken cancellationToken) + { + _ = sut.CrossPartitionPagedQueryAsync( + query, + pageSize, + continuationToken, + cancellationToken); + + containerProvider + .Received(1) + .GetContainer(allowBulk: true); + } + + [Theory, AutoNSubstituteData] + public async Task CrossPartitionPagedQueryAsync_With_Custom_Returns_Empty_No_More_Result( + QueryDefinition query, + int pageSize, + string continuationToken, + CancellationToken cancellationToken) + { + feedIterator.HasMoreResults.Returns(false); + + var response = await sut + .CrossPartitionPagedQueryAsync( + query, + pageSize, + continuationToken, + cancellationToken); + + _ = feedIterator + .Received(1) + .HasMoreResults; + + _ = feedIterator + .Received(0) + .ReadNextAsync(default); + + response.Items + .Should() + .BeEmpty(); + response.ContinuationToken + .Should() + .BeNull(); + } + + [Theory, AutoNSubstituteData] + public async Task CrossPartitionPagedQueryAsync_With_Custom_Returns_Items_When_Query_Matches( + QueryDefinition query, + int pageSize, + string continuationToken, + List records, + CancellationToken cancellationToken) + { + feedIterator + .HasMoreResults + .Returns(true); + feedResponse + .ContinuationToken + .Returns(continuationToken); + feedResponse + .GetEnumerator() + .Returns(records.GetEnumerator()); + + var response = await sut + .CrossPartitionPagedQueryAsync( + query, + pageSize, + null, + cancellationToken); + + _ = feedIterator + .Received(1) + .HasMoreResults; + + _ = feedIterator + .Received(1) + .ReadNextAsync(default); + + response.Items + .Should() + .BeEquivalentTo(records); + + response.ContinuationToken + .Should() + .Be(continuationToken); + } + } +} +#endif \ No newline at end of file diff --git a/test/Atc.Cosmos.Tests/LowPriorityCosmosReaderBatchTests.cs b/test/Atc.Cosmos.Tests/LowPriorityCosmosReaderBatchTests.cs new file mode 100644 index 0000000..a98c0e4 --- /dev/null +++ b/test/Atc.Cosmos.Tests/LowPriorityCosmosReaderBatchTests.cs @@ -0,0 +1,475 @@ +#if PREVIEW +using System.Collections.Generic; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using Atc.Cosmos.Internal; +using Atc.Test; +using AutoFixture; +using Dasync.Collections; +using FluentAssertions; +using Microsoft.Azure.Cosmos; +using NSubstitute; +using Xunit; + +namespace Atc.Cosmos.Tests +{ + public class LowPriorityCosmosReaderBatchTests + { + private readonly CosmosOptions options; + private readonly ItemResponse itemResponse; + private readonly FeedIterator feedIterator; + private readonly FeedResponse feedResponse; + private readonly Record record; + private readonly Container container; + private readonly ICosmosContainerProvider containerProvider; + private readonly LowPriorityCosmosReader sut; + + public LowPriorityCosmosReaderBatchTests() + { + var fixture = FixtureFactory.Create(); + options = fixture.Create(); + record = fixture.Create(); + itemResponse = Substitute.For>(); + itemResponse + .Resource + .Returns(record); + + feedResponse = Substitute.For>(); + feedIterator = Substitute.For>(); + feedIterator + .ReadNextAsync(default) + .ReturnsForAnyArgs(feedResponse); + + container = Substitute.For(); + container + .ReadItemAsync(default, default, default) + .ReturnsForAnyArgs(itemResponse); + + container + .GetItemQueryIterator(default(QueryDefinition), default) + .ReturnsForAnyArgs(feedIterator); + + container + .GetItemQueryIterator(default(string), default) + .ReturnsForAnyArgs(feedIterator); + + containerProvider = Substitute.For(); + containerProvider + .GetContainer() + .Returns(container, null); + + sut = new LowPriorityCosmosReader(containerProvider); + } + + [Fact] + public void Implements_Interface() + => sut.Should().BeAssignableTo>(); + + [Theory, AutoNSubstituteData] + public void ReadAllAsync_Uses_The_Right_Container( + string partitionKey, + CancellationToken cancellationToken) + { + _ = sut.BatchReadAllAsync(partitionKey, cancellationToken); + + containerProvider + .Received(1) + .GetContainer(); + } + + [Theory, AutoNSubstituteData] + public async Task ReadAllAsync_Returns_Empty_No_More_Result( + string partitionKey, + CancellationToken cancellationToken) + { + feedIterator.HasMoreResults.Returns(false); + + var response = await sut + .BatchReadAllAsync(partitionKey, cancellationToken) + .ToListAsync(cancellationToken); + + _ = feedIterator + .Received(1) + .HasMoreResults; + + _ = feedIterator + .Received(0) + .ReadNextAsync(default); + + response + .Should() + .BeEmpty(); + } + + [Theory, AutoNSubstituteData] + public async Task ReadAllAsync_Returns_Empty_When_Query_Matches_Non( + string partitionKey, + CancellationToken cancellationToken) + { + feedIterator.HasMoreResults.Returns(true, false); + + var response = await sut + .BatchReadAllAsync(partitionKey, cancellationToken) + .ToListAsync(cancellationToken); + + _ = feedIterator + .Received(2) + .HasMoreResults; + + _ = feedIterator + .Received(1) + .ReadNextAsync(default); + + response + .First() + .Should() + .BeEmpty(); + } + + [Theory, AutoNSubstituteData] + public async Task ReadAllAsync_Returns_All_Items( + string partitionKey, + CancellationToken cancellationToken) + { + feedIterator + .HasMoreResults + .Returns(true, false); + + feedResponse + .GetEnumerator() + .Returns(new List { record }.GetEnumerator()); + + var response = await sut + .BatchReadAllAsync(partitionKey, cancellationToken) + .ToListAsync(cancellationToken); + + _ = feedIterator + .Received(2) + .HasMoreResults; + + _ = feedIterator + .Received(1) + .ReadNextAsync(default); + + response + .Should() + .NotBeEmpty(); + + response[0] + .First() + .Should() + .Be(record); + } + + [Theory, AutoNSubstituteData] + public void QueryAsync_Uses_The_Right_Container( + QueryDefinition query, + string partitionKey, + CancellationToken cancellationToken) + { + _ = sut.BatchQueryAsync(query, partitionKey, cancellationToken); + + containerProvider + .Received(1) + .GetContainer(); + } + + [Theory, AutoNSubstituteData] + public async Task QueryAsync_Returns_Empty_No_More_Result( + QueryDefinition query, + string partitionKey, + CancellationToken cancellationToken) + { + feedIterator.HasMoreResults.Returns(false); + + var response = await sut.BatchQueryAsync(query, partitionKey, cancellationToken).ToListAsync(cancellationToken); + + _ = feedIterator + .Received(1) + .HasMoreResults; + + _ = feedIterator + .Received(0) + .ReadNextAsync(default); + + response + .Should() + .BeEmpty(); + } + + [Theory, AutoNSubstituteData] + public async Task QueryAsync_Returns_Empty_When_Query_Matches_Non( + QueryDefinition query, + string partitionKey, + CancellationToken cancellationToken) + { + feedIterator.HasMoreResults.Returns(true, false); + + var response = await sut.BatchQueryAsync(query, partitionKey, cancellationToken).ToListAsync(cancellationToken); + + _ = feedIterator + .Received(2) + .HasMoreResults; + + _ = feedIterator + .Received(1) + .ReadNextAsync(default); + + response + .First() + .Should() + .BeEmpty(); + } + + [Theory, AutoNSubstituteData] + public async Task QueryAsync_Returns_Items_When_Query_Matches( + QueryDefinition query, + string partitionKey, + CancellationToken cancellationToken) + { + feedIterator + .HasMoreResults + .Returns(true, false); + + feedResponse + .GetEnumerator() + .Returns(new List { record }.GetEnumerator()); + + var response = await sut.BatchQueryAsync(query, partitionKey, cancellationToken).ToListAsync(cancellationToken); + + _ = feedIterator + .Received(2) + .HasMoreResults; + + _ = feedIterator + .Received(1) + .ReadNextAsync(default); + + response + .Should() + .NotBeEmpty(); + + response[0] + .First() + .Should() + .Be(record); + } + + [Theory, AutoNSubstituteData] + public void Multiple_Operations_Uses_Same_Container( + QueryDefinition query, + string partitionKey, + CancellationToken cancellationToken) + { + _ = sut.BatchReadAllAsync(partitionKey, cancellationToken).ToArrayAsync(cancellationToken); + _ = sut.BatchQueryAsync(query, partitionKey, cancellationToken).ToListAsync(cancellationToken); + _ = sut.BatchCrossPartitionQueryAsync(query, cancellationToken).ToListAsync(cancellationToken); + + container + .ReceivedCalls() + .Should() + .HaveCount(3); + } + + [Theory, AutoNSubstituteData] + public void QueryAsync_With_Custom_Result_Uses_The_Right_Container( + QueryDefinition query, + string partitionKey, + CancellationToken cancellationToken) + { + _ = sut.BatchQueryAsync(query, partitionKey, cancellationToken); + + containerProvider + .Received(1) + .GetContainer(); + } + + [Theory, AutoNSubstituteData] + public async Task QueryAsync_With_Custom_Returns_Empty_No_More_Result( + QueryDefinition query, + string partitionKey, + CancellationToken cancellationToken) + { + feedIterator.HasMoreResults.Returns(false); + + var response = await sut.BatchQueryAsync(query, partitionKey, cancellationToken).ToListAsync(cancellationToken); + + _ = feedIterator + .Received(1) + .HasMoreResults; + + _ = feedIterator + .Received(0) + .ReadNextAsync(default); + + response + .Should() + .BeEmpty(); + } + + [Theory, AutoNSubstituteData] + public async Task QueryAsync_With_Custom_Returns_Empty_When_Query_Matches_Non( + QueryDefinition query, + string partitionKey, + CancellationToken cancellationToken) + { + feedIterator.HasMoreResults.Returns(true, false); + + var response = await sut.BatchQueryAsync(query, partitionKey, cancellationToken).ToListAsync(cancellationToken); + + _ = feedIterator + .Received(2) + .HasMoreResults; + + _ = feedIterator + .Received(1) + .ReadNextAsync(default); + + response + .First() + .Should() + .BeEmpty(); + } + + [Theory, AutoNSubstituteData] + public async Task QueryAsync_With_Custom_Returns_Items_When_Query_Matches( + QueryDefinition query, + string partitionKey, + CancellationToken cancellationToken) + { + feedIterator + .HasMoreResults + .Returns(true, false); + + feedResponse + .GetEnumerator() + .Returns(new List { record }.GetEnumerator()); + + var response = await sut.BatchQueryAsync(query, partitionKey, cancellationToken).ToListAsync(cancellationToken); + + _ = feedIterator + .Received(2) + .HasMoreResults; + + _ = feedIterator + .Received(1) + .ReadNextAsync(default); + + response + .Should() + .NotBeEmpty(); + + response[0] + .First() + .Should() + .Be(record); + } + + [Theory, AutoNSubstituteData] + public void CrossPartitionQueryAsync_Uses_The_Right_Container( + QueryDefinition query, + CancellationToken cancellationToken) + { + _ = sut.BatchCrossPartitionQueryAsync(query, cancellationToken); + + containerProvider + .Received(1) + .GetContainer(); + } + + [Theory, AutoNSubstituteData] + public void CrossPartitionQueryAsync_Uses_QueryRequestOptions_With_PriorityLevel_Low( + QueryDefinition query, + CancellationToken cancellationToken) + { + _ = sut.BatchCrossPartitionQueryAsync(query, cancellationToken).ToArrayAsync(cancellationToken); + + container + .Received(1) + .GetItemQueryIterator( + query, + requestOptions: Arg.Is( + c => c.PriorityLevel == PriorityLevel.Low)); + } + + [Theory, AutoNSubstituteData] + public async Task CrossPartitionQueryAsync_Returns_Empty_No_More_Result( + QueryDefinition query, + CancellationToken cancellationToken) + { + feedIterator.HasMoreResults.Returns(false); + + var response = await sut.BatchCrossPartitionQueryAsync(query, cancellationToken).ToListAsync(cancellationToken); + + _ = feedIterator + .Received(1) + .HasMoreResults; + + _ = feedIterator + .Received(0) + .ReadNextAsync(default); + + response + .Should() + .BeEmpty(); + } + + [Theory, AutoNSubstituteData] + public async Task CrossPartitionQueryAsync_Returns_Empty_When_Query_Matches_Non( + QueryDefinition query, + CancellationToken cancellationToken) + { + feedIterator.HasMoreResults.Returns(true, false); + + var response = await sut.BatchCrossPartitionQueryAsync(query, cancellationToken).ToListAsync(cancellationToken); + + _ = feedIterator + .Received(2) + .HasMoreResults; + + _ = feedIterator + .Received(1) + .ReadNextAsync(default); + + response + .First() + .Should() + .BeEmpty(); + } + + [Theory, AutoNSubstituteData] + public async Task CrossPartitionQueryAsync_Returns_Items_When_Query_Matches( + QueryDefinition query, + CancellationToken cancellationToken) + { + feedIterator + .HasMoreResults + .Returns(true, false); + + feedResponse + .GetEnumerator() + .Returns(new List { record }.GetEnumerator()); + + var response = await sut.BatchCrossPartitionQueryAsync(query, cancellationToken).ToListAsync(cancellationToken); + + _ = feedIterator + .Received(2) + .HasMoreResults; + + _ = feedIterator + .Received(1) + .ReadNextAsync(default); + + response + .Should() + .NotBeEmpty(); + + response[0] + .First() + .Should() + .Be(record); + } + } +} +#endif \ No newline at end of file diff --git a/test/Atc.Cosmos.Tests/LowPriorityCosmosReaderTests.cs b/test/Atc.Cosmos.Tests/LowPriorityCosmosReaderTests.cs new file mode 100644 index 0000000..5878fc0 --- /dev/null +++ b/test/Atc.Cosmos.Tests/LowPriorityCosmosReaderTests.cs @@ -0,0 +1,1035 @@ +#if PREVIEW +using System.Collections.Generic; +using System.Threading; +using System.Threading.Tasks; +using Atc.Cosmos.Internal; +using Atc.Test; +using AutoFixture; +using Dasync.Collections; +using FluentAssertions; +using Microsoft.Azure.Cosmos; +using NSubstitute; +using NSubstitute.ExceptionExtensions; +using Xunit; + +namespace Atc.Cosmos.Tests +{ + public class LowPriorityCosmosReaderTests + { + private readonly CosmosOptions options; + private readonly ItemResponse itemResponse; + private readonly FeedIterator feedIterator; + private readonly FeedResponse feedResponse; + private readonly Record record; + private readonly Container container; + private readonly ICosmosContainerProvider containerProvider; + private readonly LowPriorityCosmosReader sut; + + public LowPriorityCosmosReaderTests() + { + var fixture = FixtureFactory.Create(); + options = fixture.Create(); + record = fixture.Create(); + itemResponse = Substitute.For>(); + itemResponse + .Resource + .Returns(record); + + feedResponse = Substitute.For>(); + feedIterator = Substitute.For>(); + feedIterator + .ReadNextAsync(default) + .ReturnsForAnyArgs(feedResponse); + + container = Substitute.For(); + container + .ReadItemAsync(default, default, default) + .ReturnsForAnyArgs(itemResponse); + + container + .GetItemQueryIterator(default(QueryDefinition), default) + .ReturnsForAnyArgs(feedIterator); + + container + .GetItemQueryIterator(default(string), default) + .ReturnsForAnyArgs(feedIterator); + + containerProvider = Substitute.For(); + containerProvider + .GetContainer() + .Returns(container, null); + containerProvider + .GetCosmosOptions() + .Returns(options); + + sut = new LowPriorityCosmosReader(containerProvider); + } + + [Fact] + public void Implements_Interface() + => sut.Should().BeAssignableTo>(); + + [Theory, AutoNSubstituteData] + public async Task ReadAsync_Uses_The_Right_Container( + string documentId, + string partitionKey, + CancellationToken cancellationToken) + { + await sut.ReadAsync(documentId, partitionKey, cancellationToken); + + containerProvider + .Received(1) + .GetContainer(); + } + + [Theory, AutoNSubstituteData] + public async Task ReadAsync_Reads_Item_In_Container_Using_PriorityLevel_Low( + string documentId, + string partitionKey, + CancellationToken cancellationToken) + { + await sut.ReadAsync(documentId, partitionKey, cancellationToken); + + _ = container + .Received(1) + .ReadItemAsync( + documentId, + new PartitionKey(partitionKey), + Arg.Is(c => c.PriorityLevel == PriorityLevel.Low), + cancellationToken); + } + + [Theory, AutoNSubstituteData] + public async Task ReadAsync_Returns_Item_Read_From_Container( + string documentId, + string partitionKey, + CancellationToken cancellationToken) + { + var result = await sut.ReadAsync(documentId, partitionKey, cancellationToken); + result + .Should() + .Be(itemResponse.Resource); + } + + [Theory, AutoNSubstituteData] + public void ReadAsync_Throws_Expection_When_Record_IsNot_Found( + CosmosException exception, + string documentId, + string partitionKey, + CancellationToken cancellationToken) + { + container + .ReadItemAsync(default, default, default, default) + .ThrowsForAnyArgs(exception); + + FluentActions + .Awaiting(() => sut.ReadAsync(documentId, partitionKey, cancellationToken)) + .Should() + .ThrowAsync(); + } + + [Theory, AutoNSubstituteData] + public async Task FindAsync_Uses_The_Right_Container( + string documentId, + string partitionKey, + CancellationToken cancellationToken) + { + await sut.FindAsync(documentId, partitionKey, cancellationToken); + + containerProvider + .Received(1) + .GetContainer(); + } + + [Theory, AutoNSubstituteData] + public async Task FindAsync_Return_Default_When_Record_IsNot_Found( + CosmosException exception, + string documentId, + string partitionKey, + CancellationToken cancellationToken) + { + container + .ReadItemAsync(default, default, default, default) + .ThrowsForAnyArgs(exception); + + var response = await sut.FindAsync(documentId, partitionKey, cancellationToken); + + response + .Should() + .BeNull(); + } + + [Theory, AutoNSubstituteData] + public async Task FindAsync_Returns_Record_When_Successful( + string partitionKey, + string documentId, + CancellationToken cancellationToken) + { + var result = await sut.FindAsync(documentId, partitionKey, cancellationToken); + result + .Should() + .Be(record); + } + + [Theory, AutoNSubstituteData] + public void ReadAllAsync_Uses_The_Right_Container( + string partitionKey, + CancellationToken cancellationToken) + { + _ = sut.ReadAllAsync(partitionKey, cancellationToken); + + containerProvider + .Received(1) + .GetContainer(); + } + + [Theory, AutoNSubstituteData] + public async Task ReadAllAsync_Returns_Empty_No_More_Result( + string partitionKey, + CancellationToken cancellationToken) + { + feedIterator.HasMoreResults.Returns(false); + + var response = await sut + .ReadAllAsync(partitionKey, cancellationToken) + .ToListAsync(cancellationToken); + + _ = feedIterator + .Received(1) + .HasMoreResults; + + _ = feedIterator + .Received(0) + .ReadNextAsync(default); + + response + .Should() + .BeEmpty(); + } + + [Theory, AutoNSubstituteData] + public async Task ReadAllAsync_Returns_Empty_When_Query_Matches_Non( + string partitionKey, + CancellationToken cancellationToken) + { + feedIterator.HasMoreResults.Returns(true, false); + + var response = await sut + .ReadAllAsync(partitionKey, cancellationToken) + .ToListAsync(cancellationToken); + + _ = feedIterator + .Received(2) + .HasMoreResults; + + _ = feedIterator + .Received(1) + .ReadNextAsync(default); + + response + .Should() + .BeEmpty(); + } + + [Theory, AutoNSubstituteData] + public async Task ReadAllAsync_Returns_All_Items( + string partitionKey, + CancellationToken cancellationToken) + { + feedIterator + .HasMoreResults + .Returns(true, false); + + feedResponse + .GetEnumerator() + .Returns(new List { record }.GetEnumerator()); + + var response = await sut + .ReadAllAsync(partitionKey, cancellationToken) + .ToListAsync(cancellationToken); + + _ = feedIterator + .Received(2) + .HasMoreResults; + + _ = feedIterator + .Received(1) + .ReadNextAsync(default); + + response + .Should() + .NotBeEmpty(); + + response[0] + .Should() + .Be(record); + } + + [Theory, AutoNSubstituteData] + public void QueryAsync_Uses_The_Right_Container( + QueryDefinition query, + string partitionKey, + CancellationToken cancellationToken) + { + _ = sut.QueryAsync(query, partitionKey, cancellationToken); + + containerProvider + .Received(1) + .GetContainer(); + } + + [Theory, AutoNSubstituteData] + public async Task QueryAsync_Returns_Empty_No_More_Result( + QueryDefinition query, + string partitionKey, + CancellationToken cancellationToken) + { + feedIterator.HasMoreResults.Returns(false); + + var response = await sut.QueryAsync(query, partitionKey, cancellationToken).ToListAsync(cancellationToken); + + _ = feedIterator + .Received(1) + .HasMoreResults; + + _ = feedIterator + .Received(0) + .ReadNextAsync(default); + + response + .Should() + .BeEmpty(); + } + + [Theory, AutoNSubstituteData] + public async Task QueryAsync_Returns_Empty_When_Query_Matches_Non( + QueryDefinition query, + string partitionKey, + CancellationToken cancellationToken) + { + feedIterator.HasMoreResults.Returns(true, false); + + var response = await sut.QueryAsync(query, partitionKey, cancellationToken).ToListAsync(cancellationToken); + + _ = feedIterator + .Received(2) + .HasMoreResults; + + _ = feedIterator + .Received(1) + .ReadNextAsync(default); + + response + .Should() + .BeEmpty(); + } + + [Theory, AutoNSubstituteData] + public async Task QueryAsync_Returns_Items_When_Query_Matches( + QueryDefinition query, + string partitionKey, + CancellationToken cancellationToken) + { + feedIterator + .HasMoreResults + .Returns(true, false); + + feedResponse + .GetEnumerator() + .Returns(new List { record }.GetEnumerator()); + + var response = await sut.QueryAsync(query, partitionKey, cancellationToken).ToListAsync(cancellationToken); + + _ = feedIterator + .Received(2) + .HasMoreResults; + + _ = feedIterator + .Received(1) + .ReadNextAsync(default); + + response + .Should() + .NotBeEmpty(); + + response[0] + .Should() + .Be(record); + } + + [Theory, AutoNSubstituteData] + public async Task Should_Have_ETag_From_ItemResponse( + string etag, + string partitionKey, + string documentId, + CancellationToken cancellationToken) + { + itemResponse + .ETag + .Returns(etag); + itemResponse + .Resource + .Returns(record); + + var result = await sut.FindAsync(documentId, partitionKey, cancellationToken); + + var resource = result as ICosmosResource; + resource + .Should() + .NotBeNull(); + + resource + .ETag + .Should() + .NotBeNullOrWhiteSpace(); + + resource + .ETag + .Should() + .Be(etag); + } + + [Theory, AutoNSubstituteData] + public void Multiple_Operations_Uses_Same_Container( + QueryDefinition query, + string documentId, + string partitionKey, + CancellationToken cancellationToken) + { + _ = sut.ReadAsync(documentId, partitionKey, cancellationToken); + _ = sut.ReadAsync(documentId, partitionKey, cancellationToken); + _ = sut.FindAsync(documentId, partitionKey, cancellationToken); + _ = sut.FindAsync(documentId, partitionKey, cancellationToken); + _ = sut.QueryAsync(query, partitionKey, cancellationToken).ToListAsync(cancellationToken); + _ = sut.QueryAsync(query, partitionKey, cancellationToken).ToListAsync(cancellationToken); + + container + .ReceivedCalls() + .Should() + .HaveCount(6); + } + + [Theory, AutoNSubstituteData] + public void QueryAsync_With_Custom_Result_Uses_The_Right_Container( + QueryDefinition query, + string partitionKey, + CancellationToken cancellationToken) + { + _ = sut.QueryAsync(query, partitionKey, cancellationToken); + + containerProvider + .Received(1) + .GetContainer(); + } + + [Theory, AutoNSubstituteData] + public async Task QueryAsync_With_Custom_Returns_Empty_No_More_Result( + QueryDefinition query, + string partitionKey, + CancellationToken cancellationToken) + { + feedIterator.HasMoreResults.Returns(false); + + var response = await sut.QueryAsync(query, partitionKey, cancellationToken).ToListAsync(cancellationToken); + + _ = feedIterator + .Received(1) + .HasMoreResults; + + _ = feedIterator + .Received(0) + .ReadNextAsync(default); + + response + .Should() + .BeEmpty(); + } + + [Theory, AutoNSubstituteData] + public async Task QueryAsync_With_Custom_Returns_Empty_When_Query_Matches_Non( + QueryDefinition query, + string partitionKey, + CancellationToken cancellationToken) + { + feedIterator.HasMoreResults.Returns(true, false); + + var response = await sut.QueryAsync(query, partitionKey, cancellationToken).ToListAsync(cancellationToken); + + _ = feedIterator + .Received(2) + .HasMoreResults; + + _ = feedIterator + .Received(1) + .ReadNextAsync(default); + + response + .Should() + .BeEmpty(); + } + + [Theory, AutoNSubstituteData] + public async Task QueryAsync_With_Custom_Returns_Items_When_Query_Matches( + QueryDefinition query, + string partitionKey, + CancellationToken cancellationToken) + { + feedIterator + .HasMoreResults + .Returns(true, false); + + feedResponse + .GetEnumerator() + .Returns(new List { record }.GetEnumerator()); + + var response = await sut.QueryAsync(query, partitionKey, cancellationToken).ToListAsync(cancellationToken); + + _ = feedIterator + .Received(2) + .HasMoreResults; + + _ = feedIterator + .Received(1) + .ReadNextAsync(default); + + response + .Should() + .NotBeEmpty(); + + response[0] + .Should() + .Be(record); + } + + [Theory, AutoNSubstituteData] + public void PagedQueryAsync_Uses_The_Right_Container( + QueryDefinition query, + string partitionKey, + int pageSize, + string continuationToken, + CancellationToken cancellationToken) + { + _ = sut.PagedQueryAsync( + query, + partitionKey, + pageSize, + continuationToken, + cancellationToken); + + containerProvider + .Received(1) + .GetContainer(); + } + + [Theory, AutoNSubstituteData] + public void PagedQueryAsync_Gets_ItemQueryIterator( + QueryDefinition query, + string partitionKey, + int pageSize, + string continuationToken, + CancellationToken cancellationToken) + { + _ = sut.PagedQueryAsync( + query, + partitionKey, + pageSize, + continuationToken, + cancellationToken); + + container + .Received(1) + .GetItemQueryIterator( + query, + continuationToken, + requestOptions: Arg.Is(o + => o.PartitionKey == new PartitionKey(partitionKey) + && o.MaxItemCount == pageSize + && o.ResponseContinuationTokenLimitInKb == options.ContinuationTokenLimitInKb)); + } + + [Theory, AutoNSubstituteData] + public async Task PagedQueryAsync_Returns_Empty_When_No_More_Result( + QueryDefinition query, + string partitionKey, + int pageSize, + string continuationToken, + CancellationToken cancellationToken) + { + feedIterator.HasMoreResults.Returns(false); + + var response = await sut + .PagedQueryAsync( + query, + partitionKey, + pageSize, + continuationToken, + cancellationToken); + + _ = feedIterator + .Received(1) + .HasMoreResults; + + _ = feedIterator + .Received(0) + .ReadNextAsync(default); + + response.Items + .Should() + .BeEmpty(); + response.ContinuationToken + .Should() + .BeNull(); + } + + [Theory, AutoNSubstituteData] + public async Task PagedQueryAsync_Returns_Items_When_Query_Matches( + QueryDefinition query, + string partitionKey, + int pageSize, + string continuationToken, + List records, + CancellationToken cancellationToken) + { + feedIterator + .HasMoreResults + .Returns(true); + feedResponse + .ContinuationToken + .Returns(continuationToken); + feedResponse + .GetEnumerator() + .Returns(records.GetEnumerator()); + + var response = await sut + .PagedQueryAsync( + query, + partitionKey, + pageSize, + null, + cancellationToken); + + _ = feedIterator + .Received(1) + .HasMoreResults; + + _ = feedIterator + .Received(1) + .ReadNextAsync(default); + + response.Items + .Should() + .BeEquivalentTo(records); + + response.ContinuationToken + .Should() + .Be(continuationToken); + } + + [Theory, AutoNSubstituteData] + public void PagedQueryAsync_With_Custom_Uses_The_Right_Container( + QueryDefinition query, + string partitionKey, + int pageSize, + string continuationToken, + CancellationToken cancellationToken) + { + _ = sut.PagedQueryAsync( + query, + partitionKey, + pageSize, + continuationToken, + cancellationToken); + + containerProvider + .Received(1) + .GetContainer(); + } + + [Theory, AutoNSubstituteData] + public async Task PagedQueryAsync_With_Custom_Returns_Empty_No_More_Result( + QueryDefinition query, + string partitionKey, + int pageSize, + string continuationToken, + CancellationToken cancellationToken) + { + feedIterator.HasMoreResults.Returns(false); + + var response = await sut + .PagedQueryAsync( + query, + partitionKey, + pageSize, + continuationToken, + cancellationToken); + + _ = feedIterator + .Received(1) + .HasMoreResults; + + _ = feedIterator + .Received(0) + .ReadNextAsync(default); + + response.Items + .Should() + .BeEmpty(); + response.ContinuationToken + .Should() + .BeNull(); + } + + [Theory, AutoNSubstituteData] + public async Task PagedQueryAsync_With_Custom_Returns_Items_When_Query_Matches( + QueryDefinition query, + string partitionKey, + int pageSize, + string continuationToken, + List records, + CancellationToken cancellationToken) + { + feedIterator + .HasMoreResults + .Returns(true); + feedResponse + .ContinuationToken + .Returns(continuationToken); + feedResponse + .GetEnumerator() + .Returns(records.GetEnumerator()); + + var response = await sut + .PagedQueryAsync( + query, + partitionKey, + pageSize, + null, + cancellationToken); + + _ = feedIterator + .Received(1) + .HasMoreResults; + + _ = feedIterator + .Received(1) + .ReadNextAsync(default); + + response.Items + .Should() + .BeEquivalentTo(records); + + response.ContinuationToken + .Should() + .Be(continuationToken); + } + + [Theory, AutoNSubstituteData] + public void CrossPartitionQueryAsync_Uses_The_Right_Container( + QueryDefinition query, + CancellationToken cancellationToken) + { + _ = sut.CrossPartitionQueryAsync(query, cancellationToken); + + containerProvider + .Received(1) + .GetContainer(); + } + + [Theory, AutoNSubstituteData] + public void CrossPartitionQueryAsync_Does_Not_Specify_QueryRequestOptions( + QueryDefinition query, + CancellationToken cancellationToken) + { + _ = sut.CrossPartitionQueryAsync(query, cancellationToken).ToArrayAsync(cancellationToken); + + container + .Received(1) + .GetItemQueryIterator(query, requestOptions: null); + } + + [Theory, AutoNSubstituteData] + public async Task CrossPartitionQueryAsync_Returns_Empty_No_More_Result( + QueryDefinition query, + CancellationToken cancellationToken) + { + feedIterator.HasMoreResults.Returns(false); + + var response = await sut.CrossPartitionQueryAsync(query, cancellationToken).ToListAsync(cancellationToken); + + _ = feedIterator + .Received(1) + .HasMoreResults; + + _ = feedIterator + .Received(0) + .ReadNextAsync(default); + + response + .Should() + .BeEmpty(); + } + + [Theory, AutoNSubstituteData] + public async Task CrossPartitionQueryAsync_Returns_Empty_When_Query_Matches_Non( + QueryDefinition query, + CancellationToken cancellationToken) + { + feedIterator.HasMoreResults.Returns(true, false); + + var response = await sut.CrossPartitionQueryAsync(query, cancellationToken).ToListAsync(cancellationToken); + + _ = feedIterator + .Received(2) + .HasMoreResults; + + _ = feedIterator + .Received(1) + .ReadNextAsync(default); + + response + .Should() + .BeEmpty(); + } + + [Theory, AutoNSubstituteData] + public async Task CrossPartitionQueryAsync_Returns_Items_When_Query_Matches( + QueryDefinition query, + CancellationToken cancellationToken) + { + feedIterator + .HasMoreResults + .Returns(true, false); + + feedResponse + .GetEnumerator() + .Returns(new List { record }.GetEnumerator()); + + var response = await sut.CrossPartitionQueryAsync(query, cancellationToken).ToListAsync(cancellationToken); + + _ = feedIterator + .Received(2) + .HasMoreResults; + + _ = feedIterator + .Received(1) + .ReadNextAsync(default); + + response + .Should() + .NotBeEmpty(); + + response[0] + .Should() + .Be(record); + } + + [Theory, AutoNSubstituteData] + public void CrossPartitionPagedQueryAsync_Uses_The_Right_Container( + QueryDefinition query, + int pageSize, + string continuationToken, + CancellationToken cancellationToken) + { + _ = sut.CrossPartitionPagedQueryAsync( + query, + pageSize, + continuationToken, + cancellationToken); + + containerProvider + .Received(1) + .GetContainer(); + } + + [Theory, AutoNSubstituteData] + public void CrossPartitionPagedQueryAsync_Gets_ItemQueryIterator( + QueryDefinition query, + int pageSize, + string continuationToken, + CancellationToken cancellationToken) + { + _ = sut.CrossPartitionPagedQueryAsync( + query, + pageSize, + continuationToken, + cancellationToken); + + container + .Received(1) + .GetItemQueryIterator( + query, + continuationToken, + requestOptions: Arg.Is(o + => o.PartitionKey == null + && o.MaxItemCount == pageSize + && o.ResponseContinuationTokenLimitInKb == options.ContinuationTokenLimitInKb)); + } + + [Theory, AutoNSubstituteData] + public async Task CrossPartitionPagedQueryAsync_Returns_Empty_When_No_More_Result( + QueryDefinition query, + int pageSize, + string continuationToken, + CancellationToken cancellationToken) + { + feedIterator.HasMoreResults.Returns(false); + + var response = await sut + .CrossPartitionPagedQueryAsync( + query, + pageSize, + continuationToken, + cancellationToken); + + _ = feedIterator + .Received(1) + .HasMoreResults; + + _ = feedIterator + .Received(0) + .ReadNextAsync(default); + + response.Items + .Should() + .BeEmpty(); + response.ContinuationToken + .Should() + .BeNull(); + } + + [Theory, AutoNSubstituteData] + public async Task CrossPartitionPagedQueryAsync_Returns_Items_When_Query_Matches( + QueryDefinition query, + int pageSize, + string continuationToken, + List records, + CancellationToken cancellationToken) + { + feedIterator + .HasMoreResults + .Returns(true); + feedResponse + .ContinuationToken + .Returns(continuationToken); + feedResponse + .GetEnumerator() + .Returns(records.GetEnumerator()); + + var response = await sut + .CrossPartitionPagedQueryAsync( + query, + pageSize, + null, + cancellationToken); + + _ = feedIterator + .Received(1) + .HasMoreResults; + + _ = feedIterator + .Received(1) + .ReadNextAsync(default); + + response.Items + .Should() + .BeEquivalentTo(records); + + response.ContinuationToken + .Should() + .Be(continuationToken); + } + + [Theory, AutoNSubstituteData] + public void CrossPartitionPagedQueryAsync_With_Custom_Uses_The_Right_Container( + QueryDefinition query, + int pageSize, + string continuationToken, + CancellationToken cancellationToken) + { + _ = sut.CrossPartitionPagedQueryAsync( + query, + pageSize, + continuationToken, + cancellationToken); + + containerProvider + .Received(1) + .GetContainer(); + } + + [Theory, AutoNSubstituteData] + public async Task CrossPartitionPagedQueryAsync_With_Custom_Returns_Empty_No_More_Result( + QueryDefinition query, + int pageSize, + string continuationToken, + CancellationToken cancellationToken) + { + feedIterator.HasMoreResults.Returns(false); + + var response = await sut + .CrossPartitionPagedQueryAsync( + query, + pageSize, + continuationToken, + cancellationToken); + + _ = feedIterator + .Received(1) + .HasMoreResults; + + _ = feedIterator + .Received(0) + .ReadNextAsync(default); + + response.Items + .Should() + .BeEmpty(); + response.ContinuationToken + .Should() + .BeNull(); + } + + [Theory, AutoNSubstituteData] + public async Task CrossPartitionPagedQueryAsync_With_Custom_Returns_Items_When_Query_Matches( + QueryDefinition query, + int pageSize, + string continuationToken, + List records, + CancellationToken cancellationToken) + { + feedIterator + .HasMoreResults + .Returns(true); + feedResponse + .ContinuationToken + .Returns(continuationToken); + feedResponse + .GetEnumerator() + .Returns(records.GetEnumerator()); + + var response = await sut + .CrossPartitionPagedQueryAsync( + query, + pageSize, + null, + cancellationToken); + + _ = feedIterator + .Received(1) + .HasMoreResults; + + _ = feedIterator + .Received(1) + .ReadNextAsync(default); + + response.Items + .Should() + .BeEquivalentTo(records); + + response.ContinuationToken + .Should() + .Be(continuationToken); + } + } +} +#endif \ No newline at end of file diff --git a/test/Atc.Cosmos.Tests/LowPriorityCosmosWriterTests.cs b/test/Atc.Cosmos.Tests/LowPriorityCosmosWriterTests.cs new file mode 100644 index 0000000..6d7f70c --- /dev/null +++ b/test/Atc.Cosmos.Tests/LowPriorityCosmosWriterTests.cs @@ -0,0 +1,490 @@ +#if PREVIEW +using System; +using System.Collections.Generic; +using System.Net; +using System.Threading; +using System.Threading.Tasks; +using Atc.Cosmos.Internal; +using Atc.Cosmos.Serialization; +using Atc.Test; +using AutoFixture; +using AutoFixture.AutoNSubstitute; +using FluentAssertions; +using Microsoft.Azure.Cosmos; +using NSubstitute; +using Xunit; + +namespace Atc.Cosmos.Tests +{ + public class LowPriorityCosmosWriterTests + { + private readonly Record record; + private readonly Container container; + private readonly ICosmosContainerProvider containerProvider; + private readonly ILowPriorityCosmosReader reader; + private readonly IJsonCosmosSerializer serializer; + private readonly LowPriorityCosmosWriter sut; + + public LowPriorityCosmosWriterTests() + { + record = new Fixture().Create(); + + container = Substitute.For(); + + containerProvider = Substitute.For(); + containerProvider + .GetContainer() + .ReturnsForAnyArgs(container, null); + + var response = Substitute.For>(); + response.Resource.Returns(new Fixture().Create()); + container + .CreateItemAsync(default, default, default, default) + .ReturnsForAnyArgs(response); + container + .ReplaceItemAsync(default, default, default, default, default) + .ReturnsForAnyArgs(response); + container + .UpsertItemAsync(default, default, default, default) + .ReturnsForAnyArgs(response); + container + .PatchItemAsync(default, default, default, default) + .ReturnsForAnyArgs(response); + + reader = Substitute.For>(); + reader + .ReadAsync(default, default, default) + .ReturnsForAnyArgs(record); + + serializer = Substitute.For(); + serializer + .FromString(default) + .ReturnsForAnyArgs(new Fixture().Create()); + + sut = new LowPriorityCosmosWriter(containerProvider, reader, serializer); + } + + [Fact] + public void Implements_Interface() + => sut.Should().BeAssignableTo>(); + + [Theory, AutoNSubstituteData] + public async Task WriteAsync_Uses_The_Right_Container( + CancellationToken cancellationToken) + { + await sut.WriteAsync(record, cancellationToken); + containerProvider + .Received(1) + .GetContainer( + allowBulk: false); + } + + [Theory, AutoNSubstituteData] + public async Task WriteAsync_UpsertItem_In_Container( + CancellationToken cancellationToken) + { + containerProvider + .GetContainer() + .ReturnsForAnyArgs(container); + + await sut.WriteAsync(record, cancellationToken); + await container + .Received(1) + .UpsertItemAsync( + record, + new PartitionKey(record.Pk), + Arg.Is(c => c.PriorityLevel == PriorityLevel.Low), + cancellationToken); + } + + [Theory, AutoNSubstituteData] + public async Task WriteWithNoResponseAsync_UpsertItem_In_Container( + CancellationToken cancellationToken) + { + containerProvider + .GetContainer() + .ReturnsForAnyArgs(container); + + await sut.WriteWithNoResponseAsync(record, cancellationToken); + await container + .Received(1) + .UpsertItemAsync( + record, + new PartitionKey(record.Pk), + Arg.Is( + p => p.EnableContentResponseOnWrite == false && p.PriorityLevel == PriorityLevel.Low), + cancellationToken); + } + + [Theory, AutoNSubstituteData] + public async Task CreateAsync_Calls_CreateItem_On_Container( + CancellationToken cancellationToken) + { + await sut.CreateAsync(record, cancellationToken); + _ = container + .Received(1) + .CreateItemAsync( + record, + new PartitionKey(record.Pk), + Arg.Is(o => o.PriorityLevel == PriorityLevel.Low), + cancellationToken); + } + + [Theory, AutoNSubstituteData] + public async Task CreateWithNoResponseAsync_Calls_CreateItem_On_Container( + CancellationToken cancellationToken) + { + await sut.CreateWithNoResponseAsync(record, cancellationToken); + _ = container + .Received(1) + .CreateItemAsync( + record, + new PartitionKey(record.Pk), + Arg.Is(p => p.EnableContentResponseOnWrite == false && p.PriorityLevel == PriorityLevel.Low), + cancellationToken); + } + + [Theory, AutoNSubstituteData] + public async Task ReplaceAsync_Calls_ReplaceItemAsync_On_Container( + CancellationToken cancellationToken) + { + await sut.ReplaceAsync(record, cancellationToken); + _ = container + .Received(1) + .ReplaceItemAsync( + record, + record.Id, + new PartitionKey(record.Pk), + Arg.Is( + o => o.IfMatchEtag == record.ETag && o.PriorityLevel == PriorityLevel.Low), + cancellationToken); + } + + [Theory, AutoNSubstituteData] + public async Task ReplaceWithNoResponseAsync_Calls_ReplaceItemAsync_On_Container( + CancellationToken cancellationToken) + { + await sut.ReplaceWithNoResponseAsync(record, cancellationToken); + _ = container + .Received(1) + .ReplaceItemAsync( + record, + record.Id, + new PartitionKey(record.Pk), + Arg.Is(o => o.IfMatchEtag == record.ETag + && o.EnableContentResponseOnWrite == false + && o.PriorityLevel == PriorityLevel.Low), + cancellationToken); + } + + [Theory, AutoNSubstituteData] + public void Multiple_Operations_Uses_Same_Container( + CancellationToken cancellationToken) + { + _ = sut.WriteAsync(record, cancellationToken); + _ = sut.WriteAsync(record, cancellationToken); + _ = sut.CreateAsync(record, cancellationToken); + _ = sut.CreateAsync(record, cancellationToken); + _ = sut.ReplaceAsync(record, cancellationToken); + _ = sut.ReplaceAsync(record, cancellationToken); + + container + .ReceivedCalls() + .Should() + .HaveCount(6); + } + + [Theory, AutoNSubstituteData] + public async Task DeleteAsync_Calls_DeleteItemAsync_On_Container( + CancellationToken cancellationToken) + { + await sut.DeleteAsync(record.Id, record.Pk, cancellationToken); + _ = container + .Received(1) + .DeleteItemAsync( + record.Id, + new PartitionKey(record.Pk), + Arg.Is(o => o.PriorityLevel == PriorityLevel.Low), + cancellationToken: cancellationToken); + } + + [Theory, AutoNSubstituteData] + public async Task Should_Return_True_When_Trying_To_Delete_Existing_Resource( + CancellationToken cancellationToken) + { + var deleted = await sut.TryDeleteAsync( + record.Id, + record.Pk, + cancellationToken); + + deleted + .Should() + .BeTrue(); + + _ = container + .Received(1) + .DeleteItemAsync( + record.Id, + new PartitionKey(record.Pk), + Arg.Is(o => o.PriorityLevel == PriorityLevel.Low), + cancellationToken: cancellationToken); + } + + [Theory, AutoNSubstituteData] + public async Task Should_Return_False_When_Trying_To_Delete_NonExisting_Resource( + CancellationToken cancellationToken) + { + container + .DeleteItemAsync(default, default, default, default) + .ReturnsForAnyArgs>( + r => throw new CosmosException("fake", HttpStatusCode.NotFound, 0, "1", 1)); + + var deleted = await sut.TryDeleteAsync( + record.Id, + record.Pk, + cancellationToken); + + deleted + .Should() + .BeFalse(); + + _ = container + .Received(1) + .DeleteItemAsync( + record.Id, + new PartitionKey(record.Pk), + Arg.Is(o => o.PriorityLevel == PriorityLevel.Low), + cancellationToken: cancellationToken); + } + + [Theory, AutoNSubstituteData] + public async Task UpdateAsync_Reads_The_Resource( + string documentId, + string partitionKey, + Action updateDocument, + int retries, + CancellationToken cancellationToken) + { + await sut.UpdateAsync( + documentId, + partitionKey, + updateDocument, + retries, + cancellationToken); + + _ = reader + .Received(1) + .ReadAsync( + documentId, + partitionKey, + cancellationToken); + } + + [Theory, AutoNSubstituteData] + public async Task UpdateAsync_Calls_UpdateDocument_With_Read_Resource( + string documentId, + string partitionKey, + [Substitute] Action updateDocument, + int retries, + CancellationToken cancellationToken) + { + await sut.UpdateAsync( + documentId, + partitionKey, + updateDocument, + retries, + cancellationToken); + + updateDocument + .Received(1) + .Invoke(record); + } + + [Theory, AutoNSubstituteData] + public async Task UpdateAsync_Calls_ReplaceItem_With_Updated_Resource( + string documentId, + string partitionKey, + [Substitute] Action updateDocument, + int retries, + CancellationToken cancellationToken) + { + await sut.UpdateAsync( + documentId, + partitionKey, + updateDocument, + retries, + cancellationToken); + + _ = container + .Received(1) + .ReplaceItemAsync( + record, + record.Id, + new PartitionKey(record.Pk), + Arg.Is(o => o.IfMatchEtag == record.ETag && o.PriorityLevel == PriorityLevel.Low), + cancellationToken); + } + + [Theory, AutoNSubstituteData] + public async Task UpdateOrCreateAsync_Finds_The_Resource( + Action updateDocument, + int retries, + Record defaultDocument, + CancellationToken cancellationToken) + { + await sut.UpdateOrCreateAsync( + () => defaultDocument, + updateDocument, + retries, + cancellationToken); + + _ = reader + .Received(1) + .FindAsync( + defaultDocument.Id, + defaultDocument.Pk, + cancellationToken); + } + + [Theory, AutoNSubstituteData] + public async Task UpdateAsync_Calls_UpdateDocument_With_Found_Resource( + [Substitute] Action updateDocument, + int retries, + Record defaultDocument, + Record foundResource, + CancellationToken cancellationToken) + { + reader + .FindAsync(default, default, default) + .ReturnsForAnyArgs(foundResource); + + await sut.UpdateOrCreateAsync( + () => defaultDocument, + updateDocument, + retries, + cancellationToken); + + updateDocument + .Received(1) + .Invoke(foundResource); + } + + [Theory, AutoNSubstituteData] + public async Task UpdateAsync_Calls_UpdateDocument_With_Default_Document_If_Not_Found( + [Substitute] Action updateDocument, + int retries, + Record defaultDocument, + CancellationToken cancellationToken) + { + await sut.UpdateOrCreateAsync( + () => defaultDocument, + updateDocument, + retries, + cancellationToken); + + updateDocument + .Received(1) + .Invoke(defaultDocument); + } + + [Theory, AutoNSubstituteData] + public async Task UpdateOrCreateAsync_Calls_ReplaceItem_If_Resource_Has_ETag( + [Substitute] Action updateDocument, + int retries, + Record defaultDocument, + Record foundResource, + string etag, + CancellationToken cancellationToken) + { + ((ICosmosResource)foundResource).ETag = etag; + reader + .FindAsync(default, default, default) + .ReturnsForAnyArgs(foundResource); + + await sut.UpdateOrCreateAsync( + () => defaultDocument, + updateDocument, + retries, + cancellationToken); + + _ = container + .Received(1) + .ReplaceItemAsync( + foundResource, + foundResource.Id, + new PartitionKey(foundResource.Pk), + Arg.Is(o => o.IfMatchEtag == foundResource.ETag && o.PriorityLevel == PriorityLevel.Low), + cancellationToken); + } + + [Theory, AutoNSubstituteData] + public async Task UpdateOrCreateAsync_Calls_CreateItem_If_Resource_Has_No_ETag( + [Substitute] Action updateDocument, + int retries, + Record defaultDocument, + CancellationToken cancellationToken) + { + defaultDocument.ETag = null; + await sut.UpdateOrCreateAsync( + () => defaultDocument, + updateDocument, + retries, + cancellationToken); + + _ = container + .Received(1) + .CreateItemAsync( + defaultDocument, + new PartitionKey(defaultDocument.Pk), + Arg.Is(o => o.PriorityLevel == PriorityLevel.Low), + cancellationToken); + } + + [Theory, AutoNSubstituteData] + public async Task PatchAsync_Calls_PatchItemAsync_On_Container( + IReadOnlyList patchOperations, + string filterPredicate, + CancellationToken cancellationToken) + { + await sut.PatchAsync( + record.Id, + record.Pk, + patchOperations, + filterPredicate, + cancellationToken); + + _ = container + .Received(1) + .PatchItemAsync( + record.Id, + new PartitionKey(record.Pk), + patchOperations, + Arg.Is(o => o.PriorityLevel == PriorityLevel.Low), + cancellationToken); + } + + [Theory, AutoNSubstituteData] + public async Task PatchWithNoResponseAsync_Calls_PatchItemAsync_On_Container( + IReadOnlyList patchOperations, + string filterPredicate, + CancellationToken cancellationToken) + { + await sut.PatchWithNoResponseAsync( + record.Id, + record.Pk, + patchOperations, + filterPredicate, + cancellationToken); + + _ = container + .Received(1) + .PatchItemAsync( + record.Id, + new PartitionKey(record.Pk), + patchOperations, + Arg.Is(o => o.PriorityLevel == PriorityLevel.Low), + cancellationToken); + } + } +} +#endif \ No newline at end of file