diff --git a/Datasync.Toolkit.sln b/Datasync.Toolkit.sln index ecba367..c823206 100644 --- a/Datasync.Toolkit.sln +++ b/Datasync.Toolkit.sln @@ -64,6 +64,10 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "samples", "samples", "{75F7 EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Sample.Datasync.Server", "samples\datasync-server\src\Sample.Datasync.Server\Sample.Datasync.Server.csproj", "{A9967817-2A2C-4C6D-A133-967A6062E9B3}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "CommunityToolkit.Datasync.Server.MongoDB", "src\CommunityToolkit.Datasync.Server.MongoDB\CommunityToolkit.Datasync.Server.MongoDB.csproj", "{DC20ACF9-12E9-41D9-B672-CB5FD85548E9}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "CommunityToolkit.Datasync.Server.MongoDB.Test", "tests\CommunityToolkit.Datasync.Server.MongoDB.Test\CommunityToolkit.Datasync.Server.MongoDB.Test.csproj", "{4FC45D20-0BA9-484B-9040-641687659AF6}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -154,6 +158,14 @@ Global {A9967817-2A2C-4C6D-A133-967A6062E9B3}.Debug|Any CPU.Build.0 = Debug|Any CPU {A9967817-2A2C-4C6D-A133-967A6062E9B3}.Release|Any CPU.ActiveCfg = Release|Any CPU {A9967817-2A2C-4C6D-A133-967A6062E9B3}.Release|Any CPU.Build.0 = Release|Any CPU + {DC20ACF9-12E9-41D9-B672-CB5FD85548E9}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {DC20ACF9-12E9-41D9-B672-CB5FD85548E9}.Debug|Any CPU.Build.0 = Debug|Any CPU + {DC20ACF9-12E9-41D9-B672-CB5FD85548E9}.Release|Any CPU.ActiveCfg = Release|Any CPU + {DC20ACF9-12E9-41D9-B672-CB5FD85548E9}.Release|Any CPU.Build.0 = Release|Any CPU + {4FC45D20-0BA9-484B-9040-641687659AF6}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {4FC45D20-0BA9-484B-9040-641687659AF6}.Debug|Any CPU.Build.0 = Debug|Any CPU + {4FC45D20-0BA9-484B-9040-641687659AF6}.Release|Any CPU.ActiveCfg = Release|Any CPU + {4FC45D20-0BA9-484B-9040-641687659AF6}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -180,6 +192,8 @@ Global {D3B72031-D4BD-44D3-973C-2752AB1570F6} = {84AD662A-4B9E-4E64-834D-72529FB7FCE5} {2889E6B2-9CD1-437C-A43C-98CFAFF68B99} = {D59F1489-5D74-4F52-B78B-88037EAB2838} {A9967817-2A2C-4C6D-A133-967A6062E9B3} = {75F709FD-8CC2-4558-A802-FE57086167C2} + {DC20ACF9-12E9-41D9-B672-CB5FD85548E9} = {84AD662A-4B9E-4E64-834D-72529FB7FCE5} + {4FC45D20-0BA9-484B-9040-641687659AF6} = {D59F1489-5D74-4F52-B78B-88037EAB2838} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {78A935E9-8F14-448A-BEDF-360FB742F14E} diff --git a/Directory.Packages.props b/Directory.Packages.props index e316022..f759986 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -24,6 +24,7 @@ + diff --git a/infra/modules/cosmos-mongodb.bicep b/infra/modules/cosmos-mongodb.bicep index fa44521..2b546cf 100644 --- a/infra/modules/cosmos-mongodb.bicep +++ b/infra/modules/cosmos-mongodb.bicep @@ -1,182 +1,75 @@ targetScope = 'resourceGroup' -@minLength(1) -@description('The name of the test container to create') -param containerName string = 'Movies' +@secure() +@description('The password for the administrator') +param administratorPassword string -@minLength(1) -@description('The name of the test database to create') -param databaseName string = 'unittests' +@description('The username for the administrator') +param administratorUsername string = 'tester' + +@description('The list of firewall rules to install') +param firewallRules FirewallRule[] = [ + { startIpAddress: '0.0.0.0', endIpAddress: '0.0.0.0' } +] @minLength(1) @description('Primary location for all resources') param location string = resourceGroup().location -@allowed(['3.2', '3.6', '4.0', '4.2']) -@description('Specifies the MongoDB server version to use.') -param mongoVersion string = '4.2' - @description('The name of the Mongo Server to create.') param serverName string @description('The list of tags to apply to all resources.') param tags object = {} -/*********************************************************************************/ - -var compositeIndices = [ - [ - { path: '/BestPictureWinner', order: 'ascending' } - { path: '/id', order: 'ascending' } - ] - [ - { path: '/BestPictureWinner', order: 'descending' } - { path: '/id', order: 'ascending' } - ] - [ - { path: '/Duration', order: 'ascending' } - { path: '/id', order: 'ascending' } - ] - [ - { path: '/Duration', order: 'descending' } - { path: '/id', order: 'ascending' } - ] - [ - { path: '/Rating', order: 'ascending' } - { path: '/id', order: 'ascending' } - ] - [ - { path: '/Rating', order: 'descending' } - { path: '/id', order: 'ascending' } - ] - [ - { path: '/ReleaseDate', order: 'ascending' } - { path: '/id', order: 'ascending' } - ] - [ - { path: '/ReleaseDate', order: 'descending' } - { path: '/id', order: 'ascending' } - ] - [ - { path: '/Title', order: 'ascending' } - { path: '/id', order: 'ascending' } - ] - [ - { path: '/Title', order: 'descending' } - { path: '/id', order: 'ascending' } - ] - [ - { path: '/UpdatedAt', order: 'ascending' } - { path: '/id', order: 'ascending' } - ] - [ - { path: '/UpdatedAt', order: 'descending' } - { path: '/id', order: 'ascending' } - ] - [ - { path: '/Year', order: 'ascending' } - { path: '/id', order: 'ascending' } - ] - [ - { path: '/Year', order: 'descending' } - { path: '/id', order: 'ascending' } - ] - [ - { path: '/Year', order: 'ascending' } - { path: '/Title', order: 'ascending' } - { path: '/id', order: 'ascending' } - ] - [ - { path: '/Year', order: 'descending' } - { path: '/Title', order: 'ascending' } - { path: '/id', order: 'ascending' } - ] - [ - { path: '/Year', order: 'ascending' } - { path: '/Title', order: 'descending' } - { path: '/id', order: 'ascending' } - ] - [ - { path: '/Year', order: 'descending' } - { path: '/Title', order: 'descending' } - { path: '/id', order: 'ascending' } - ] -] +@description('The tier to use for compute') +@allowed([ 'Free', 'M10', 'M20', 'M25', 'M30', 'M40', 'M50', 'M60', 'M80', 'M200', 'M200-Autoscale']) +param tier string = 'M10' /*********************************************************************************/ -resource account 'Microsoft.DocumentDB/databaseAccounts@2022-05-15' = { +resource cluster 'Microsoft.DocumentDB/mongoClusters@2024-07-01' = { name: toLower(serverName) location: location - kind: 'MongoDB' tags: tags properties: { - apiProperties: { - serverVersion: mongoVersion + administrator: { + userName: administratorUsername + password: administratorPassword } - capabilities: [ - { - name: 'DisableRateLimitingResponses' - } - ] - consistencyPolicy: { - defaultConsistencyLevel: 'Session' + compute: { tier: tier } + highAvailability: { + targetMode: 'Disabled' } - databaseAccountOfferType: 'Standard' - disableLocalAuth: false - locations: [ - { - locationName: location - isZoneRedundant: false - } - ] - } -} - -resource database 'Microsoft.DocumentDB/databaseAccounts/mongodbDatabases@2022-05-15' = { - parent: account - name: databaseName - tags: tags - properties: { - resource: { - id: databaseName - } - options: { - throughput: 400 + publicNetworkAccess: 'Enabled' + serverVersion: '7.0' + sharding: { + shardCount: 1 } + storage: { sizeGb: 32 } } } -resource collection 'Microsoft.DocumentDb/databaseAccounts/mongodbDatabases/collections@2022-05-15' = { - parent: database - name: containerName - tags: tags - properties: { - resource: { - id: containerName - shardKey: { - _id: 'Hash' - } - indexes: [ - { - key: { - keys: [ - '_id' - ] - } - } - { - key: { - keys: [ - '$**' - ] - } - } - ] +resource mongoFirewallRule 'Microsoft.DocumentDB/mongoClusters/firewallRules@2024-07-01' = [ + for (fwRule, index) in firewallRules: { + name: fwRule.?name ?? 'rule-${index}' + parent: cluster + properties: { + startIpAddress: fwRule.startIpAddress + endIpAddress: fwRule.endIpAddress } } -} +] + /*********************************************************************************/ #disable-next-line outputs-should-not-contain-secrets -output MONGODB_CONNECTIONSTRING string = account.listConnectionStrings().connectionStrings[1].connectionString +output MONGO_CONNECTIONSTRING string = replace(replace(cluster.listConnectionStrings().connectionStrings[0].connectionString, '', administratorUsername), '', administratorPassword) + +/*********************************************************************************/ + +type FirewallRule = { + name: string? + startIpAddress: string + endIpAddress: string +} diff --git a/infra/resources.bicep b/infra/resources.bicep index 7aa8242..3be157e 100644 --- a/infra/resources.bicep +++ b/infra/resources.bicep @@ -110,9 +110,9 @@ module mongodb './modules/cosmos-mongodb.bicep' = { params: { location: location tags: tags - databaseName: testDatabaseName - containerName: cosmosContainerName serverName: mongoServerName + administratorPassword: sqlAdminPassword + administratorUsername: sqlAdminUsername } } @@ -148,7 +148,7 @@ module app_service './modules/appservice.bicep' = { output AZSQL_CONNECTIONSTRING string = azuresql.outputs.AZSQL_CONNECTIONSTRING output COSMOS_CONNECTIONSTRING string = cosmos.outputs.COSMOS_CONNECTIONSTRING -output MONGO_CONNECTIONSTRING string = mongodb.outputs.MONGODB_CONNECTIONSTRING +output MONGO_CONNECTIONSTRING string = mongodb.outputs.MONGO_CONNECTIONSTRING output MONGOACI_CONNECTIONSTRING string = mongoaci.outputs.MONGO_CONNECTIONSTRING output MYSQL_CONNECTIONSTRING string = mysql.outputs.MYSQL_CONNECTIONSTRING output PGSQL_CONNECTIONSTRING string = pgsql.outputs.PGSQL_CONNECTIONSTRING diff --git a/src/CommunityToolkit.Datasync.Server.MongoDB/CommunityToolkit.Datasync.Server.MongoDB.csproj b/src/CommunityToolkit.Datasync.Server.MongoDB/CommunityToolkit.Datasync.Server.MongoDB.csproj new file mode 100644 index 0000000..92b855e --- /dev/null +++ b/src/CommunityToolkit.Datasync.Server.MongoDB/CommunityToolkit.Datasync.Server.MongoDB.csproj @@ -0,0 +1,18 @@ + + + A repository for the server-side of the Datasync Toolkit that uses MongoDB for storage. + + + + + + + + + + + + + + + diff --git a/src/CommunityToolkit.Datasync.Server.MongoDB/MongoDBRepository.cs b/src/CommunityToolkit.Datasync.Server.MongoDB/MongoDBRepository.cs new file mode 100644 index 0000000..331ea80 --- /dev/null +++ b/src/CommunityToolkit.Datasync.Server.MongoDB/MongoDBRepository.cs @@ -0,0 +1,166 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using CommunityToolkit.Datasync.Server.Abstractions.Http; +using MongoDB.Bson; +using MongoDB.Driver; + +namespace CommunityToolkit.Datasync.Server.MongoDB; + +/// +/// A repository implementation that stored data in a LiteDB database. +/// +/// The entity type to store in the database. +public class MongoDBRepository : IRepository where TEntity : MongoTableData +{ + /// + /// Creates a new using the provided MongoDB database. + /// + /// + /// The collection name is based on the entity type. + /// + /// The to use for storing entities. + public MongoDBRepository(IMongoDatabase database) : this(database.GetCollection(typeof(TEntity).Name.ToLowerInvariant() + "s")) + { + } + + /// + /// Creates a new using the provided database connection + /// and collection name. + /// + /// The to use for storing entities. + public MongoDBRepository(IMongoCollection collection) + { + Collection = collection; + // TODO: Ensure that there is an index on the right properties. + } + + /// + /// The collection within the LiteDb database that stores the entities. + /// + public virtual IMongoCollection Collection { get; } + + /// + /// The mechanism by which an Id is generated when one is not provided. + /// + public Func IdGenerator { get; set; } = _ => Guid.NewGuid().ToString("N"); + + /// + /// The mechanism by which a new version byte array is generated. + /// + public Func VersionGenerator { get; set; } = () => Guid.NewGuid().ToByteArray(); + + /// + /// Updates the system properties for the provided entity on write. + /// + /// The entity to update. + protected void UpdateEntity(TEntity entity) + { + entity.UpdatedAt = DateTimeOffset.UtcNow; + entity.Version = VersionGenerator.Invoke(); + } + + /// + /// Checks that the provided ID is valid. + /// + /// The ID of the entity to check. + /// Thrown if the ID is not valid. + protected static void CheckIdIsValid(string id) + { + if (string.IsNullOrEmpty(id)) + { + throw new HttpException(HttpStatusCodes.Status400BadRequest); + } + } + + /// + /// Returns a filter definition for finding a single document. + /// + /// The ID of the document to find. + /// The filter definition to find the document. + protected FilterDefinition GetFilterById(string id) + => Builders.Filter.Eq(x => x.Id, id); + + /// + /// Returns the document with the provided ID, or null if it doesn't exist. + /// + /// The ID of the document to find. + /// A to observe. + /// The document, or null if not found. + protected async ValueTask FindDocumentByIdAsync(string id, CancellationToken cancellationToken = default) + { + return await Collection.Find(GetFilterById(id)).FirstOrDefaultAsync(cancellationToken); + } + + /// + public virtual ValueTask> AsQueryableAsync(CancellationToken cancellationToken = default) + => ValueTask.FromResult(Collection.AsQueryable()); + + /// + public virtual async ValueTask CreateAsync(TEntity entity, CancellationToken cancellationToken = default) + { + if (string.IsNullOrEmpty(entity.Id)) + { + entity.Id = IdGenerator.Invoke(entity); + } + + TEntity? existingEntity = await FindDocumentByIdAsync(entity.Id, cancellationToken).ConfigureAwait(false); + if (existingEntity is not null) + { + throw new HttpException(HttpStatusCodes.Status409Conflict) { Payload = existingEntity }; + } + + UpdateEntity(entity); + await Collection.InsertOneAsync(entity, options: null, cancellationToken); + } + + /// + public virtual async ValueTask DeleteAsync(string id, byte[]? version = null, CancellationToken cancellationToken = default) + { + CheckIdIsValid(id); + + TEntity storedEntity = await FindDocumentByIdAsync(id, cancellationToken).ConfigureAwait(false) + ?? throw new HttpException(HttpStatusCodes.Status404NotFound); + if (version?.Length > 0 && !storedEntity.Version.SequenceEqual(version)) + { + throw new HttpException(HttpStatusCodes.Status412PreconditionFailed) { Payload = storedEntity }; + } + + DeleteResult result = await Collection.DeleteOneAsync(GetFilterById(id), cancellationToken); + if (result.DeletedCount == 0) + { + throw new HttpException(HttpStatusCodes.Status404NotFound); + } + } + + /// + public virtual async ValueTask ReadAsync(string id, CancellationToken cancellationToken = default) + { + CheckIdIsValid(id); + + return await FindDocumentByIdAsync(id, cancellationToken).ConfigureAwait(false) + ?? throw new HttpException(HttpStatusCodes.Status404NotFound); + } + + /// + public virtual async ValueTask ReplaceAsync(TEntity entity, byte[]? version = null, CancellationToken cancellationToken = default) + { + CheckIdIsValid(entity.Id); + + TEntity storedEntity = await FindDocumentByIdAsync(entity.Id, cancellationToken).ConfigureAwait(false) + ?? throw new HttpException(HttpStatusCodes.Status404NotFound); + if (version?.Length > 0 && !storedEntity.Version.SequenceEqual(version)) + { + throw new HttpException(HttpStatusCodes.Status412PreconditionFailed) { Payload = storedEntity }; + } + + UpdateEntity(entity); + ReplaceOptions options = new() { IsUpsert = false }; + ReplaceOneResult result = await Collection.ReplaceOneAsync(GetFilterById(entity.Id), entity, options, cancellationToken); + if (result.IsModifiedCountAvailable && result.ModifiedCount == 0) + { + throw new HttpException(HttpStatusCodes.Status404NotFound); + } + } +} diff --git a/src/CommunityToolkit.Datasync.Server.MongoDB/MongoTableData.cs b/src/CommunityToolkit.Datasync.Server.MongoDB/MongoTableData.cs new file mode 100644 index 0000000..818f7e0 --- /dev/null +++ b/src/CommunityToolkit.Datasync.Server.MongoDB/MongoTableData.cs @@ -0,0 +1,32 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using MongoDB.Bson.Serialization.Attributes; + +namespace CommunityToolkit.Datasync.Server.MongoDB; + +/// +/// An implementation of the interface for +/// handling entities in a MongoDB database. +/// +public class MongoTableData : ITableData +{ + /// + [BsonId] + public string Id { get; set; } = string.Empty; + + /// + public bool Deleted { get; set; } = false; + + /// + public DateTimeOffset? UpdatedAt { get; set; } = DateTimeOffset.UnixEpoch; + + /// + public byte[] Version { get; set; } = []; + + /// + public bool Equals(ITableData? other) + => other != null && Id == other.Id && Version.SequenceEqual(other.Version); +} + diff --git a/tests/CommunityToolkit.Datasync.Server.MongoDB.Test/CommunityToolkit.Datasync.Server.MongoDB.Test.csproj b/tests/CommunityToolkit.Datasync.Server.MongoDB.Test/CommunityToolkit.Datasync.Server.MongoDB.Test.csproj new file mode 100644 index 0000000..fbfe531 --- /dev/null +++ b/tests/CommunityToolkit.Datasync.Server.MongoDB.Test/CommunityToolkit.Datasync.Server.MongoDB.Test.csproj @@ -0,0 +1,20 @@ + + + + + + + + runtime; build; native; contentfiles; analyzers; buildtransitive + all + + + runtime; build; native; contentfiles; analyzers; buildtransitive + all + + + runtime; build; native; contentfiles; analyzers; buildtransitive + all + + + diff --git a/tests/CommunityToolkit.Datasync.Server.MongoDB.Test/CosmosMongoRepository_Tests.cs b/tests/CommunityToolkit.Datasync.Server.MongoDB.Test/CosmosMongoRepository_Tests.cs new file mode 100644 index 0000000..cf84b07 --- /dev/null +++ b/tests/CommunityToolkit.Datasync.Server.MongoDB.Test/CosmosMongoRepository_Tests.cs @@ -0,0 +1,55 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using CommunityToolkit.Datasync.TestCommon; +using CommunityToolkit.Datasync.TestCommon.Databases; +using Microsoft.EntityFrameworkCore; +using MongoDB.Bson; +using MongoDB.Driver; +using Xunit; +using Xunit.Abstractions; + +namespace CommunityToolkit.Datasync.Server.MongoDB.Test; + +[ExcludeFromCodeCoverage] +public class CosmosMongoRepository_Tests(ITestOutputHelper output) : RepositoryTests(), IAsyncLifetime +{ + #region Setup + private readonly Random random = new(); + private List movies = []; + + public async Task InitializeAsync() + { + if (!string.IsNullOrEmpty(ConnectionStrings.CosmosMongo)) + { + Context = await MongoDBContext.CreateContextAsync(ConnectionStrings.CosmosMongo, output); + this.movies = await Context.Movies.Find(new BsonDocument()).ToListAsync(); + } + } + + public async Task DisposeAsync() + { + if (Context is not null) + { + await Context.DisposeAsync(); + } + } + + public MongoDBContext Context { get; set; } + + protected override bool CanRunLiveTests() => !string.IsNullOrEmpty(ConnectionStrings.CosmosMongo); + + protected override async Task GetEntityAsync(string id) + => await Context.Movies.Find(Builders.Filter.Eq(x => x.Id, id)).FirstOrDefaultAsync(); + + protected override async Task GetEntityCountAsync() + => (int)(await Context.Movies.CountDocumentsAsync(Builders.Filter.Empty)); + + protected override Task> GetPopulatedRepositoryAsync() + => Task.FromResult>(new MongoDBRepository(Context.Movies)); + + protected override Task GetRandomEntityIdAsync(bool exists) + => Task.FromResult(exists ? this.movies[this.random.Next(this.movies.Count)].Id : Guid.NewGuid().ToString()); + #endregion +} diff --git a/tests/CommunityToolkit.Datasync.Server.MongoDB.Test/MongoDBRepository_Tests.cs b/tests/CommunityToolkit.Datasync.Server.MongoDB.Test/MongoDBRepository_Tests.cs new file mode 100644 index 0000000..6bb294b --- /dev/null +++ b/tests/CommunityToolkit.Datasync.Server.MongoDB.Test/MongoDBRepository_Tests.cs @@ -0,0 +1,55 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using CommunityToolkit.Datasync.TestCommon; +using CommunityToolkit.Datasync.TestCommon.Databases; +using Microsoft.EntityFrameworkCore; +using MongoDB.Bson; +using MongoDB.Driver; +using Xunit; +using Xunit.Abstractions; + +namespace CommunityToolkit.Datasync.Server.MongoDB.Test; + +[ExcludeFromCodeCoverage] +public class MongoDBRepository_Tests(ITestOutputHelper output) : RepositoryTests(), IAsyncLifetime +{ + #region Setup + private readonly Random random = new(); + private List movies = []; + + public async Task InitializeAsync() + { + if (!string.IsNullOrEmpty(ConnectionStrings.MongoCommunity)) + { + Context = await MongoDBContext.CreateContextAsync(ConnectionStrings.MongoCommunity, output); + this.movies = await Context.Movies.Find(new BsonDocument()).ToListAsync(); + } + } + + public async Task DisposeAsync() + { + if (Context is not null) + { + await Context.DisposeAsync(); + } + } + + public MongoDBContext Context { get; set; } + + protected override bool CanRunLiveTests() => !string.IsNullOrEmpty(ConnectionStrings.CosmosDb); + + protected override async Task GetEntityAsync(string id) + => await Context.Movies.Find(Builders.Filter.Eq(x => x.Id, id)).FirstOrDefaultAsync(); + + protected override async Task GetEntityCountAsync() + => (int)(await Context.Movies.CountDocumentsAsync(Builders.Filter.Empty)); + + protected override Task> GetPopulatedRepositoryAsync() + => Task.FromResult>(new MongoDBRepository(Context.Movies)); + + protected override Task GetRandomEntityIdAsync(bool exists) + => Task.FromResult(exists ? this.movies[this.random.Next(this.movies.Count)].Id : Guid.NewGuid().ToString()); + #endregion +} \ No newline at end of file diff --git a/tests/CommunityToolkit.Datasync.Server.MongoDB.Test/MongoTableData_Tests.cs b/tests/CommunityToolkit.Datasync.Server.MongoDB.Test/MongoTableData_Tests.cs new file mode 100644 index 0000000..ca81a34 --- /dev/null +++ b/tests/CommunityToolkit.Datasync.Server.MongoDB.Test/MongoTableData_Tests.cs @@ -0,0 +1,27 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using CommunityToolkit.Datasync.Common.Test; +using CommunityToolkit.Datasync.TestCommon; +using FluentAssertions; +using Xunit; + +namespace CommunityToolkit.Datasync.Server.MongoDB.Test; + +[ExcludeFromCodeCoverage] +public class MongoTableData_Tests +{ + [Theory, ClassData(typeof(ITableData_TestData))] + public void Equals_Works(ITableData a, ITableData b, bool expected) + { + MongoTableData entity_a = a.ToTableEntity(); + MongoTableData entity_b = b.ToTableEntity(); + + entity_a.Equals(entity_b).Should().Be(expected); + entity_b.Equals(entity_a).Should().Be(expected); + + entity_a.Equals(null).Should().BeFalse(); + entity_b.Equals(null).Should().BeFalse(); + } +} diff --git a/tests/CommunityToolkit.Datasync.Server.Test/Helpers/LiveControllerTests.cs b/tests/CommunityToolkit.Datasync.Server.Test/Helpers/LiveControllerTests.cs index cd434b4..102a137 100644 --- a/tests/CommunityToolkit.Datasync.Server.Test/Helpers/LiveControllerTests.cs +++ b/tests/CommunityToolkit.Datasync.Server.Test/Helpers/LiveControllerTests.cs @@ -174,6 +174,7 @@ public async Task Query_Test_003() { Skip.IfNot(CanRunLiveTests()); Skip.If(DriverName == "Cosmos", "Complex math is not supported"); + Skip.If(DriverName == "MongoDB", "Not supported by MongoDB"); await MovieQueryTest( $"{MovieEndpoint}?$count=true&$filter=((year div 1000.5) eq 2) and (rating eq 'R')", @@ -203,6 +204,7 @@ public async Task Query_Test_005() { Skip.IfNot(CanRunLiveTests()); Skip.If(DriverName == "Cosmos", "Complex math is not supported"); + Skip.If(DriverName == "MongoDB", "Not supported by MongoDB"); await MovieQueryTest( $"{MovieEndpoint}?$count=true&$filter=(year div 1000.5) eq 2", @@ -260,6 +262,7 @@ public async Task Query_Test_009() { Skip.IfNot(CanRunLiveTests()); Skip.If(DriverName == "Cosmos", "Complex math is not supported"); + Skip.If(DriverName == "MongoDB", "Not supported by MongoDB"); await MovieQueryTest( $"{MovieEndpoint}?$count=true&$filter=bestPictureWinner eq true and ceiling(duration div 60.0) eq 2", @@ -275,6 +278,7 @@ public async Task Query_Test_010() { Skip.IfNot(CanRunLiveTests()); Skip.If(DriverName == "Cosmos", "Complex math is not supported"); + Skip.If(DriverName == "MongoDB", "Not supported by MongoDB"); await MovieQueryTest( $"{MovieEndpoint}?$count=true&$filter=bestPictureWinner eq true and floor(duration div 60.0) eq 2", @@ -290,6 +294,7 @@ public async Task Query_Test_011() { Skip.IfNot(CanRunLiveTests()); Skip.If(DriverName == "Cosmos", "Complex math is not supported"); + Skip.If(DriverName == "MongoDB", "Not supported by MongoDB"); await MovieQueryTest( $"{MovieEndpoint}?$count=true&$filter=bestPictureWinner eq true and round(duration div 60.0) eq 2", @@ -347,6 +352,7 @@ public async Task Query_Test_015() { Skip.IfNot(CanRunLiveTests()); Skip.If(DriverName == "Cosmos", "Complex math is not supported"); + Skip.If(DriverName == "MongoDB", "Not supported by MongoDB"); await MovieQueryTest( $"{MovieEndpoint}?$count=true&$filter=ceiling(duration div 60.0) eq 2", @@ -362,6 +368,7 @@ public async Task Query_Test_016() { Skip.IfNot(CanRunLiveTests()); Skip.If(DriverName == "Cosmos", "Date components are not supported"); + Skip.If(DriverName == "MongoDB", "Not supported by MongoDB"); await MovieQueryTest( $"{MovieEndpoint}?$count=true&$filter=day(releaseDate) eq 1", @@ -433,6 +440,7 @@ public async Task Query_Test_021() { Skip.IfNot(CanRunLiveTests()); Skip.If(DriverName == "Cosmos", "Complex math is not supported"); + Skip.If(DriverName == "MongoDB", "Not supported by MongoDB"); await MovieQueryTest( $"{MovieEndpoint}?$count=true&$filter=floor(duration div 60.0) eq 2", @@ -448,6 +456,7 @@ public async Task Query_Test_022() { Skip.IfNot(CanRunLiveTests()); Skip.If(DriverName == "Cosmos", "Date components is not supported"); + Skip.If(DriverName == "MongoDB", "Not supported by MongoDB"); await MovieQueryTest( $"{MovieEndpoint}?$count=true&$filter=month(releaseDate) eq 11", @@ -561,6 +570,7 @@ public async Task Query_Test_030() { Skip.IfNot(CanRunLiveTests()); Skip.If(DriverName == "Cosmos", "Date components are not supported"); + Skip.If(DriverName == "MongoDB", "Not supported by MongoDB"); await MovieQueryTest( $"{MovieEndpoint}?$count=true&$filter=releaseDate eq cast(1994-10-14,Edm.Date)", @@ -576,6 +586,7 @@ public async Task Query_Test_031() { Skip.IfNot(CanRunLiveTests()); Skip.If(DriverName == "Cosmos", "Date components are not supported"); + Skip.If(DriverName == "MongoDB", "Not supported by MongoDB"); await MovieQueryTest( $"{MovieEndpoint}?$count=true&$filter=releaseDate ge cast(1999-12-31,Edm.Date)", @@ -591,6 +602,7 @@ public async Task Query_Test_032() { Skip.IfNot(CanRunLiveTests()); Skip.If(DriverName == "Cosmos", "Date components are not supported"); + Skip.If(DriverName == "MongoDB", "Not supported by MongoDB"); await MovieQueryTest( $"{MovieEndpoint}?$count=true&$filter=releaseDate gt cast(1999-12-31,Edm.Date)", @@ -606,6 +618,7 @@ public async Task Query_Test_033() { Skip.IfNot(CanRunLiveTests()); Skip.If(DriverName == "Cosmos", "Date components are not supported"); + Skip.If(DriverName == "MongoDB", "Not supported by MongoDB"); await MovieQueryTest( $"{MovieEndpoint}?$count=true&$filter=releaseDate le cast(2000-01-01,Edm.Date)", @@ -621,6 +634,7 @@ public async Task Query_Test_034() { Skip.IfNot(CanRunLiveTests()); Skip.If(DriverName == "Cosmos", "Date components are not supported"); + Skip.If(DriverName == "MongoDB", "Not supported by MongoDB"); await MovieQueryTest( $"{MovieEndpoint}?$count=true&$filter=releaseDate lt cast(2000-01-01,Edm.Date)", @@ -636,6 +650,7 @@ public async Task Query_Test_035() { Skip.IfNot(CanRunLiveTests()); Skip.If(DriverName == "Cosmos", "Date components are not supported"); + Skip.If(DriverName == "MongoDB", "Not supported by MongoDB"); await MovieQueryTest( $"{MovieEndpoint}?$count=true&$filter=round(duration div 60.0) eq 2", @@ -777,6 +792,7 @@ public async Task Query_Test_046() { Skip.IfNot(CanRunLiveTests()); Skip.If(DriverName == "Cosmos", "Date components are not supported"); + Skip.If(DriverName == "MongoDB", "Not supported by MongoDB"); await MovieQueryTest( $"{MovieEndpoint}?$count=true&$filter=year(releaseDate) eq 1994", @@ -1016,6 +1032,7 @@ public async Task Query_Test_063() { Skip.IfNot(CanRunLiveTests()); Skip.If(DriverName == "Cosmos", "Complex math is not supported"); + Skip.If(DriverName == "MongoDB", "Not supported by MongoDB"); await MovieQueryTest( $"{MovieEndpoint}?$count=true&$top=125&$filter=((year div 1000.5) eq 2) and (rating eq 'R')", @@ -1045,6 +1062,7 @@ public async Task Query_Test_065() { Skip.IfNot(CanRunLiveTests()); Skip.If(DriverName == "Cosmos", "Complex math is not supported"); + Skip.If(DriverName == "MongoDB", "Not supported by MongoDB"); await MovieQueryTest( $"{MovieEndpoint}?$count=true&$top=125&$filter=(year div 1000.5) eq 2", @@ -1102,6 +1120,7 @@ public async Task Query_Test_069() { Skip.IfNot(CanRunLiveTests()); Skip.If(DriverName == "Cosmos", "Complex math is not supported"); + Skip.If(DriverName == "MongoDB", "Not supported by MongoDB"); await MovieQueryTest( $"{MovieEndpoint}?$count=true&$top=125&$filter=bestPictureWinner eq true and ceiling(duration div 60.0) eq 2", @@ -1117,6 +1136,7 @@ public async Task Query_Test_070() { Skip.IfNot(CanRunLiveTests()); Skip.If(DriverName == "Cosmos", "Complex math is not supported"); + Skip.If(DriverName == "MongoDB", "Not supported by MongoDB"); await MovieQueryTest( $"{MovieEndpoint}?$count=true&$top=125&$filter=bestPictureWinner eq true and floor(duration div 60.0) eq 2", @@ -1132,6 +1152,7 @@ public async Task Query_Test_071() { Skip.IfNot(CanRunLiveTests()); Skip.If(DriverName == "Cosmos", "Complex math is not supported"); + Skip.If(DriverName == "MongoDB", "Not supported by MongoDB"); await MovieQueryTest( $"{MovieEndpoint}?$count=true&$top=125&$filter=bestPictureWinner eq true and round(duration div 60.0) eq 2", @@ -1189,6 +1210,7 @@ public async Task Query_Test_075() { Skip.IfNot(CanRunLiveTests()); Skip.If(DriverName == "Cosmos", "Complex math is not supported"); + Skip.If(DriverName == "MongoDB", "Not supported by MongoDB"); await MovieQueryTest( $"{MovieEndpoint}?$count=true&$top=125&$filter=ceiling(duration div 60.0) eq 2", @@ -1204,6 +1226,7 @@ public async Task Query_Test_076() { Skip.IfNot(CanRunLiveTests()); Skip.If(DriverName == "Cosmos", "Date components are not supported"); + Skip.If(DriverName == "MongoDB", "Not supported by MongoDB"); await MovieQueryTest( $"{MovieEndpoint}?$count=true&$top=125&$filter=day(releaseDate) eq 1", @@ -1275,6 +1298,7 @@ public async Task Query_Test_081() { Skip.IfNot(CanRunLiveTests()); Skip.If(DriverName == "Cosmos", "Complex math is not supported"); + Skip.If(DriverName == "MongoDB", "Not supported by MongoDB"); await MovieQueryTest( $"{MovieEndpoint}?$count=true&$top=125&$filter=floor(duration div 60.0) eq 2", @@ -1290,6 +1314,7 @@ public async Task Query_Test_082() { Skip.IfNot(CanRunLiveTests()); Skip.If(DriverName == "Cosmos", "Complex math is not supported"); + Skip.If(DriverName == "MongoDB", "Not supported by MongoDB"); await MovieQueryTest( $"{MovieEndpoint}?$count=true&$top=125&$filter=month(releaseDate) eq 11", @@ -1403,6 +1428,7 @@ public async Task Query_Test_090() { Skip.IfNot(CanRunLiveTests()); Skip.If(DriverName == "Cosmos", "Date components are not supported"); + Skip.If(DriverName == "MongoDB", "Not supported by MongoDB"); await MovieQueryTest( $"{MovieEndpoint}?$count=true&$top=125&$filter=releaseDate eq cast(1994-10-14,Edm.Date)", @@ -1418,6 +1444,7 @@ public async Task Query_Test_091() { Skip.IfNot(CanRunLiveTests()); Skip.If(DriverName == "Cosmos", "Date components are not supported"); + Skip.If(DriverName == "MongoDB", "Not supported by MongoDB"); await MovieQueryTest( $"{MovieEndpoint}?$count=true&$top=125&$filter=releaseDate ge cast(1999-12-31,Edm.Date)", @@ -1433,6 +1460,7 @@ public async Task Query_Test_092() { Skip.IfNot(CanRunLiveTests()); Skip.If(DriverName == "Cosmos", "Date components are not supported"); + Skip.If(DriverName == "MongoDB", "Not supported by MongoDB"); await MovieQueryTest( $"{MovieEndpoint}?$count=true&$top=125&$filter=releaseDate gt cast(1999-12-31,Edm.Date)", @@ -1448,6 +1476,7 @@ public async Task Query_Test_093() { Skip.IfNot(CanRunLiveTests()); Skip.If(DriverName == "Cosmos", "Date components are not supported"); + Skip.If(DriverName == "MongoDB", "Not supported by MongoDB"); await MovieQueryTest( $"{MovieEndpoint}?$count=true&$top=125&$filter=releaseDate le cast(2000-01-01,Edm.Date)", @@ -1463,6 +1492,7 @@ public async Task Query_Test_094() { Skip.IfNot(CanRunLiveTests()); Skip.If(DriverName == "Cosmos", "Date components are not supported"); + Skip.If(DriverName == "MongoDB", "Not supported by MongoDB"); await MovieQueryTest( $"{MovieEndpoint}?$count=true&$top=125&$filter=releaseDate lt cast(2000-01-01,Edm.Date)", @@ -1478,6 +1508,7 @@ public async Task Query_Test_095() { Skip.IfNot(CanRunLiveTests()); Skip.If(DriverName == "Cosmos", "Complex math is not supported"); + Skip.If(DriverName == "MongoDB", "Not supported by MongoDB"); await MovieQueryTest( $"{MovieEndpoint}?$count=true&$top=125&$filter=round(duration div 60.0) eq 2", @@ -1619,6 +1650,7 @@ public async Task Query_Test_106() { Skip.IfNot(CanRunLiveTests()); Skip.If(DriverName == "Cosmos", "Date components are not supported"); + Skip.If(DriverName == "MongoDB", "Not supported by MongoDB"); await MovieQueryTest( $"{MovieEndpoint}?$count=true&$top=125&$filter=year(releaseDate) eq 1994", @@ -1858,6 +1890,7 @@ public async Task Query_Test_123() { Skip.IfNot(CanRunLiveTests()); Skip.If(DriverName == "Cosmos", "Complex math is not supported"); + Skip.If(DriverName == "MongoDB", "Not supported by MongoDB"); await MovieQueryTest( $"{MovieEndpoint}?$filter=((year div 1000.5) eq 2) and (rating eq 'R')", @@ -1887,6 +1920,7 @@ public async Task Query_Test_125() { Skip.IfNot(CanRunLiveTests()); Skip.If(DriverName == "Cosmos", "Complex math is not supported"); + Skip.If(DriverName == "MongoDB", "Not supported by MongoDB"); await MovieQueryTest( $"{MovieEndpoint}?$filter=(year div 1000.5) eq 2", @@ -1944,6 +1978,7 @@ public async Task Query_Test_129() { Skip.IfNot(CanRunLiveTests()); Skip.If(DriverName == "Cosmos", "Complex math is not supported"); + Skip.If(DriverName == "MongoDB", "Not supported by MongoDB"); await MovieQueryTest( $"{MovieEndpoint}?$filter=bestPictureWinner eq true and ceiling(duration div 60.0) eq 2", @@ -1959,6 +1994,7 @@ public async Task Query_Test_130() { Skip.IfNot(CanRunLiveTests()); Skip.If(DriverName == "Cosmos", "Complex math is not supported"); + Skip.If(DriverName == "MongoDB", "Not supported by MongoDB"); await MovieQueryTest( $"{MovieEndpoint}?$filter=bestPictureWinner eq true and floor(duration div 60.0) eq 2", @@ -1974,6 +2010,7 @@ public async Task Query_Test_131() { Skip.IfNot(CanRunLiveTests()); Skip.If(DriverName == "Cosmos", "Complex math is not supported"); + Skip.If(DriverName == "MongoDB", "Not supported by MongoDB"); await MovieQueryTest( $"{MovieEndpoint}?$filter=bestPictureWinner eq true and round(duration div 60.0) eq 2", @@ -2031,6 +2068,7 @@ public async Task Query_Test_135() { Skip.IfNot(CanRunLiveTests()); Skip.If(DriverName == "Cosmos", "Complex math is not supported"); + Skip.If(DriverName == "MongoDB", "Not supported by MongoDB"); await MovieQueryTest( $"{MovieEndpoint}?$filter=ceiling(duration div 60.0) eq 2", @@ -2046,6 +2084,7 @@ public async Task Query_Test_136() { Skip.IfNot(CanRunLiveTests()); Skip.If(DriverName == "Cosmos", "Date components are not supported"); + Skip.If(DriverName == "MongoDB", "Not supported by MongoDB"); await MovieQueryTest( $"{MovieEndpoint}?$filter=day(releaseDate) eq 1", @@ -2117,6 +2156,7 @@ public async Task Query_Test_141() { Skip.IfNot(CanRunLiveTests()); Skip.If(DriverName == "Cosmos", "Complex math is not supported"); + Skip.If(DriverName == "MongoDB", "Not supported by MongoDB"); await MovieQueryTest( $"{MovieEndpoint}?$filter=floor(duration div 60.0) eq 2", @@ -2132,6 +2172,7 @@ public async Task Query_Test_142() { Skip.IfNot(CanRunLiveTests()); Skip.If(DriverName == "Cosmos", "Date components are not supported"); + Skip.If(DriverName == "MongoDB", "Not supported by MongoDB"); await MovieQueryTest( $"{MovieEndpoint}?$filter=month(releaseDate) eq 11", @@ -2245,6 +2286,7 @@ public async Task Query_Test_150() { Skip.IfNot(CanRunLiveTests()); Skip.If(DriverName == "Cosmos", "Date components are not supported"); + Skip.If(DriverName == "MongoDB", "Not supported by MongoDB"); await MovieQueryTest( $"{MovieEndpoint}?$filter=releaseDate eq cast(1994-10-14,Edm.Date)", @@ -2260,6 +2302,7 @@ public async Task Query_Test_151() { Skip.IfNot(CanRunLiveTests()); Skip.If(DriverName == "Cosmos", "Date components are not supported"); + Skip.If(DriverName == "MongoDB", "Not supported by MongoDB"); await MovieQueryTest( $"{MovieEndpoint}?$filter=releaseDate ge cast(1999-12-31,Edm.Date)", @@ -2275,6 +2318,7 @@ public async Task Query_Test_152() { Skip.IfNot(CanRunLiveTests()); Skip.If(DriverName == "Cosmos", "Date components are not supported"); + Skip.If(DriverName == "MongoDB", "Not supported by MongoDB"); await MovieQueryTest( $"{MovieEndpoint}?$filter=releaseDate gt cast(1999-12-31,Edm.Date)", @@ -2290,6 +2334,7 @@ public async Task Query_Test_153() { Skip.IfNot(CanRunLiveTests()); Skip.If(DriverName == "Cosmos", "Date components are not supported"); + Skip.If(DriverName == "MongoDB", "Not supported by MongoDB"); await MovieQueryTest( $"{MovieEndpoint}?$filter=releaseDate le cast(2000-01-01,Edm.Date)", @@ -2305,6 +2350,7 @@ public async Task Query_Test_154() { Skip.IfNot(CanRunLiveTests()); Skip.If(DriverName == "Cosmos", "Date components are not supported"); + Skip.If(DriverName == "MongoDB", "Not supported by MongoDB"); await MovieQueryTest( $"{MovieEndpoint}?$filter=releaseDate lt cast(2000-01-01,Edm.Date)", @@ -2320,6 +2366,7 @@ public async Task Query_Test_155() { Skip.IfNot(CanRunLiveTests()); Skip.If(DriverName == "Cosmos", "Complex math is not supported"); + Skip.If(DriverName == "MongoDB", "Not supported by MongoDB"); await MovieQueryTest( $"{MovieEndpoint}?$filter=round(duration div 60.0) eq 2", @@ -2461,6 +2508,7 @@ public async Task Query_Test_166() { Skip.IfNot(CanRunLiveTests()); Skip.If(DriverName == "Cosmos", "Date components are not supported"); + Skip.If(DriverName == "MongoDB", "Not supported by MongoDB"); await MovieQueryTest( $"{MovieEndpoint}?$filter=year(releaseDate) eq 1994", @@ -2700,6 +2748,7 @@ public async Task Query_Test_183() { Skip.IfNot(CanRunLiveTests()); Skip.If(DriverName == "Cosmos", "Complex math is not supported"); + Skip.If(DriverName == "MongoDB", "Not supported by MongoDB"); await MovieQueryTest( $"{MovieEndpoint}?$filter=((year div 1000.5) eq 2) and (rating eq 'R')&$skip=5", @@ -2728,6 +2777,7 @@ public async Task Query_Test_185() { Skip.IfNot(CanRunLiveTests()); Skip.If(DriverName == "Cosmos", "Complex math is not supported"); + Skip.If(DriverName == "MongoDB", "Not supported by MongoDB"); await MovieQueryTest( $"{MovieEndpoint}?$filter=(year div 1000.5) eq 2&$skip=5", @@ -2785,6 +2835,7 @@ public async Task Query_Test_189() { Skip.IfNot(CanRunLiveTests()); Skip.If(DriverName == "Cosmos", "Complex math is not supported"); + Skip.If(DriverName == "MongoDB", "Not supported by MongoDB"); await MovieQueryTest( $"{MovieEndpoint}?$filter=bestPictureWinner eq true and ceiling(duration div 60.0) eq 2&$skip=5", @@ -2800,6 +2851,7 @@ public async Task Query_Test_190() { Skip.IfNot(CanRunLiveTests()); Skip.If(DriverName == "Cosmos", "Complex math is not supported"); + Skip.If(DriverName == "MongoDB", "Not supported by MongoDB"); await MovieQueryTest( $"{MovieEndpoint}?$filter=bestPictureWinner eq true and floor(duration div 60.0) eq 2&$skip=5", @@ -2815,6 +2867,7 @@ public async Task Query_Test_191() { Skip.IfNot(CanRunLiveTests()); Skip.If(DriverName == "Cosmos", "Complex math is not supported"); + Skip.If(DriverName == "MongoDB", "Not supported by MongoDB"); await MovieQueryTest( $"{MovieEndpoint}?$filter=bestPictureWinner eq true and round(duration div 60.0) eq 2&$skip=5", @@ -2872,6 +2925,7 @@ public async Task Query_Test_195() { Skip.IfNot(CanRunLiveTests()); Skip.If(DriverName == "Cosmos", "Complex math is not supported"); + Skip.If(DriverName == "MongoDB", "Not supported by MongoDB"); await MovieQueryTest( $"{MovieEndpoint}?$filter=ceiling(duration div 60.0) eq 2&$skip=5", @@ -2887,6 +2941,7 @@ public async Task Query_Test_196() { Skip.IfNot(CanRunLiveTests()); Skip.If(DriverName == "Cosmos", "Date components are not supported"); + Skip.If(DriverName == "MongoDB", "Not supported by MongoDB"); await MovieQueryTest( $"{MovieEndpoint}?$filter=day(releaseDate) eq 1&$skip=5", @@ -2958,6 +3013,7 @@ public async Task Query_Test_201() { Skip.IfNot(CanRunLiveTests()); Skip.If(DriverName == "Cosmos", "Complex math is not supported"); + Skip.If(DriverName == "MongoDB", "Not supported by MongoDB"); await MovieQueryTest( $"{MovieEndpoint}?$filter=floor(duration div 60.0) eq 2&$skip=5", @@ -2973,6 +3029,7 @@ public async Task Query_Test_202() { Skip.IfNot(CanRunLiveTests()); Skip.If(DriverName == "Cosmos", "Date components are not supported"); + Skip.If(DriverName == "MongoDB", "Not supported by MongoDB"); await MovieQueryTest( $"{MovieEndpoint}?$filter=month(releaseDate) eq 11&$skip=5", @@ -3086,6 +3143,7 @@ public async Task Query_Test_210() { Skip.IfNot(CanRunLiveTests()); Skip.If(DriverName == "Cosmos", "Date components are not supported"); + Skip.If(DriverName == "MongoDB", "Not supported by MongoDB"); await MovieQueryTest( $"{MovieEndpoint}?$filter=releaseDate eq cast(1994-10-14,Edm.Date)&$skip=5", @@ -3100,6 +3158,7 @@ public async Task Query_Test_211() { Skip.IfNot(CanRunLiveTests()); Skip.If(DriverName == "Cosmos", "Date components are not supported"); + Skip.If(DriverName == "MongoDB", "Not supported by MongoDB"); await MovieQueryTest( $"{MovieEndpoint}?$filter=releaseDate ge cast(1999-12-31,Edm.Date)&$skip=5", @@ -3115,6 +3174,7 @@ public async Task Query_Test_212() { Skip.IfNot(CanRunLiveTests()); Skip.If(DriverName == "Cosmos", "Date components are not supported"); + Skip.If(DriverName == "MongoDB", "Not supported by MongoDB"); await MovieQueryTest( $"{MovieEndpoint}?$filter=releaseDate gt cast(1999-12-31,Edm.Date)&$skip=5", @@ -3130,6 +3190,7 @@ public async Task Query_Test_213() { Skip.IfNot(CanRunLiveTests()); Skip.If(DriverName == "Cosmos", "Date components are not supported"); + Skip.If(DriverName == "MongoDB", "Not supported by MongoDB"); await MovieQueryTest( $"{MovieEndpoint}?$filter=releaseDate le cast(2000-01-01,Edm.Date)&$skip=5", @@ -3145,6 +3206,7 @@ public async Task Query_Test_214() { Skip.IfNot(CanRunLiveTests()); Skip.If(DriverName == "Cosmos", "Date components are not supported"); + Skip.If(DriverName == "MongoDB", "Not supported by MongoDB"); await MovieQueryTest( $"{MovieEndpoint}?$filter=releaseDate lt cast(2000-01-01,Edm.Date)&$skip=5", @@ -3160,6 +3222,7 @@ public async Task Query_Test_215() { Skip.IfNot(CanRunLiveTests()); Skip.If(DriverName == "Cosmos", "Complex math is not supported"); + Skip.If(DriverName == "MongoDB", "Not supported by MongoDB"); await MovieQueryTest( $"{MovieEndpoint}?$filter=round(duration div 60.0) eq 2&$skip=5", @@ -3300,6 +3363,7 @@ public async Task Query_Test_226() { Skip.IfNot(CanRunLiveTests()); Skip.If(DriverName == "Cosmos", "Date components are not supported"); + Skip.If(DriverName == "MongoDB", "Not supported by MongoDB"); await MovieQueryTest( $"{MovieEndpoint}?$filter=year(releaseDate) eq 1994&$skip=5", @@ -3594,6 +3658,7 @@ public async Task Query_Test_247() { Skip.IfNot(CanRunLiveTests()); Skip.If(DriverName == "Cosmos", "Complex math is not supported"); + Skip.If(DriverName == "MongoDB", "Not supported by MongoDB"); await MovieQueryTest( $"{MovieEndpoint}?$top=5&$filter=((year div 1000.5) eq 2) and (rating eq 'R')", @@ -3623,6 +3688,7 @@ public async Task Query_Test_249() { Skip.IfNot(CanRunLiveTests()); Skip.If(DriverName == "Cosmos", "Date components are not supported"); + Skip.If(DriverName == "MongoDB", "Not supported by MongoDB"); await MovieQueryTest( $"{MovieEndpoint}?$top=5&$filter=(year div 1000.5) eq 2", @@ -3680,6 +3746,7 @@ public async Task Query_Test_253() { Skip.IfNot(CanRunLiveTests()); Skip.If(DriverName == "Cosmos", "Date components are not supported"); + Skip.If(DriverName == "MongoDB", "Not supported by MongoDB"); await MovieQueryTest( $"{MovieEndpoint}?$top=5&$filter=bestPictureWinner eq true and ceiling(duration div 60.0) eq 2", @@ -3695,6 +3762,7 @@ public async Task Query_Test_254() { Skip.IfNot(CanRunLiveTests()); Skip.If(DriverName == "Cosmos", "Date components are not supported"); + Skip.If(DriverName == "MongoDB", "Not supported by MongoDB"); await MovieQueryTest( $"{MovieEndpoint}?$top=5&$filter=bestPictureWinner eq true and floor(duration div 60.0) eq 2", @@ -3710,6 +3778,7 @@ public async Task Query_Test_255() { Skip.IfNot(CanRunLiveTests()); Skip.If(DriverName == "Cosmos", "Date components are not supported"); + Skip.If(DriverName == "MongoDB", "Not supported by MongoDB"); await MovieQueryTest( $"{MovieEndpoint}?$top=5&$filter=bestPictureWinner eq true and round(duration div 60.0) eq 2", @@ -3767,6 +3836,7 @@ public async Task Query_Test_259() { Skip.IfNot(CanRunLiveTests()); Skip.If(DriverName == "Cosmos", "Complex math is not supported"); + Skip.If(DriverName == "MongoDB", "Not supported by MongoDB"); await MovieQueryTest( $"{MovieEndpoint}?$top=5&$filter=ceiling(duration div 60.0) eq 2", @@ -3782,6 +3852,7 @@ public async Task Query_Test_260() { Skip.IfNot(CanRunLiveTests()); Skip.If(DriverName == "Cosmos", "Date components are not supported"); + Skip.If(DriverName == "MongoDB", "Not supported by MongoDB"); await MovieQueryTest( $"{MovieEndpoint}?$top=5&$filter=day(releaseDate) eq 1", @@ -3853,6 +3924,7 @@ public async Task Query_Test_265() { Skip.IfNot(CanRunLiveTests()); Skip.If(DriverName == "Cosmos", "Complex math is not supported"); + Skip.If(DriverName == "MongoDB", "Not supported by MongoDB"); await MovieQueryTest( $"{MovieEndpoint}?$top=5&$filter=floor(duration div 60.0) eq 2", @@ -3868,6 +3940,7 @@ public async Task Query_Test_266() { Skip.IfNot(CanRunLiveTests()); Skip.If(DriverName == "Cosmos", "Date components are not supported"); + Skip.If(DriverName == "MongoDB", "Not supported by MongoDB"); await MovieQueryTest( $"{MovieEndpoint}?$top=5&$filter=month(releaseDate) eq 11", @@ -3981,6 +4054,7 @@ public async Task Query_Test_274() { Skip.IfNot(CanRunLiveTests()); Skip.If(DriverName == "Cosmos", "Date components are not supported"); + Skip.If(DriverName == "MongoDB", "Not supported by MongoDB"); await MovieQueryTest( $"{MovieEndpoint}?$top=5&$filter=releaseDate eq cast(1994-10-14,Edm.Date)", @@ -3996,6 +4070,7 @@ public async Task Query_Test_275() { Skip.IfNot(CanRunLiveTests()); Skip.If(DriverName == "Cosmos", "Date components are not supported"); + Skip.If(DriverName == "MongoDB", "Not supported by MongoDB"); await MovieQueryTest( $"{MovieEndpoint}?$top=5&$filter=releaseDate ge cast(1999-12-31,Edm.Date)", @@ -4011,6 +4086,7 @@ public async Task Query_Test_276() { Skip.IfNot(CanRunLiveTests()); Skip.If(DriverName == "Cosmos", "Date components are not supported"); + Skip.If(DriverName == "MongoDB", "Not supported by MongoDB"); await MovieQueryTest( $"{MovieEndpoint}?$top=5&$filter=releaseDate gt cast(1999-12-31,Edm.Date)", @@ -4026,6 +4102,7 @@ public async Task Query_Test_277() { Skip.IfNot(CanRunLiveTests()); Skip.If(DriverName == "Cosmos", "Date components are not supported"); + Skip.If(DriverName == "MongoDB", "Not supported by MongoDB"); await MovieQueryTest( $"{MovieEndpoint}?$top=5&$filter=releaseDate le cast(2000-01-01,Edm.Date)", @@ -4041,6 +4118,7 @@ public async Task Query_Test_278() { Skip.IfNot(CanRunLiveTests()); Skip.If(DriverName == "Cosmos", "Date components are not supported"); + Skip.If(DriverName == "MongoDB", "Not supported by MongoDB"); await MovieQueryTest( $"{MovieEndpoint}?$top=5&$filter=releaseDate lt cast(2000-01-01,Edm.Date)", @@ -4056,6 +4134,7 @@ public async Task Query_Test_279() { Skip.IfNot(CanRunLiveTests()); Skip.If(DriverName == "Cosmos", "Date components are not supported"); + Skip.If(DriverName == "MongoDB", "Not supported by MongoDB"); await MovieQueryTest( $"{MovieEndpoint}?$top=5&$filter=round(duration div 60.0) eq 2", @@ -4197,6 +4276,7 @@ public async Task Query_Test_290() { Skip.IfNot(CanRunLiveTests()); Skip.If(DriverName == "Cosmos", "Date components are not supported"); + Skip.If(DriverName == "MongoDB", "Not supported by MongoDB"); await MovieQueryTest( $"{MovieEndpoint}?$top=5&$filter=year(releaseDate) eq 1994", diff --git a/tests/CommunityToolkit.Datasync.Server.Test/Helpers/LiveTestsCollection.cs b/tests/CommunityToolkit.Datasync.Server.Test/Helpers/LiveTestsCollection.cs index d2c8f79..7273f90 100644 --- a/tests/CommunityToolkit.Datasync.Server.Test/Helpers/LiveTestsCollection.cs +++ b/tests/CommunityToolkit.Datasync.Server.Test/Helpers/LiveTestsCollection.cs @@ -12,6 +12,8 @@ public class DatabaseFixture { public bool AzureSqlIsInitialized { get; set; } = false; public bool CosmosIsInitialized { get; set; } = false; + public bool CosmosMongoIsInitialized { get; set; } = false; + public bool MongoCommunityIsInitialized { get; set; } = false; public bool MysqlIsInitialized { get; set; } = false; public bool PgIsInitialized { get; set; } = false; } diff --git a/tests/CommunityToolkit.Datasync.Server.Test/Live/CosmosMongo_Controller_Tests.cs b/tests/CommunityToolkit.Datasync.Server.Test/Live/CosmosMongo_Controller_Tests.cs new file mode 100644 index 0000000..29d2eb2 --- /dev/null +++ b/tests/CommunityToolkit.Datasync.Server.Test/Live/CosmosMongo_Controller_Tests.cs @@ -0,0 +1,62 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using CommunityToolkit.Datasync.Server.MongoDB; +using CommunityToolkit.Datasync.Server.Test.Helpers; +using CommunityToolkit.Datasync.TestCommon.Databases; +using MongoDB.Bson; +using MongoDB.Driver; +using Xunit.Abstractions; + +namespace CommunityToolkit.Datasync.Server.Test.Live; + +[ExcludeFromCodeCoverage] +[Collection("LiveTestsCollection")] +public class CosmosMongo_Controller_Tests(DatabaseFixture fixture, ITestOutputHelper output) : LiveControllerTests(), IAsyncLifetime +{ + #region Setup + private readonly Random random = new(); + private List movies = []; + + public async Task InitializeAsync() + { + if (!string.IsNullOrEmpty(ConnectionStrings.CosmosMongo)) + { + // Note: we don't clear entities on every run to speed up the test runs. This can only be done because + // the tests are read-only (associated with the query and get capabilities). If the test being run writes + // to the database then change clearEntities to true. + output.WriteLine($"CosmosMongoIsInitialized = {fixture.CosmosMongoIsInitialized}"); + Context = await MongoDBContext.CreateContextAsync(ConnectionStrings.CosmosMongo, output, clearEntities: !fixture.CosmosMongoIsInitialized); + this.movies = await Context.Movies.Find(new BsonDocument()).ToListAsync(); + fixture.CosmosMongoIsInitialized = true; + } + } + + public async Task DisposeAsync() + { + if (Context is not null) + { + await Context.DisposeAsync(); + } + } + + private MongoDBContext Context { get; set; } + + protected override string DriverName { get; } = "MongoDB"; + + protected override bool CanRunLiveTests() => !string.IsNullOrEmpty(ConnectionStrings.CosmosMongo); + + protected override async Task GetEntityAsync(string id) + => await Context.Movies.Find(Builders.Filter.Eq(x => x.Id, id)).FirstOrDefaultAsync(); + + protected override async Task GetEntityCountAsync() + => (int)(await Context.Movies.CountDocumentsAsync(Builders.Filter.Empty)); + + protected override Task> GetPopulatedRepositoryAsync() + => Task.FromResult>(new MongoDBRepository(Context.Movies)); + + protected override Task GetRandomEntityIdAsync(bool exists) + => Task.FromResult(exists ? this.movies[this.random.Next(this.movies.Count)].Id : Guid.NewGuid().ToString()); + #endregion +} diff --git a/tests/CommunityToolkit.Datasync.Server.Test/Live/MongoDB_Controller_Tests.cs b/tests/CommunityToolkit.Datasync.Server.Test/Live/MongoDB_Controller_Tests.cs new file mode 100644 index 0000000..69d9b09 --- /dev/null +++ b/tests/CommunityToolkit.Datasync.Server.Test/Live/MongoDB_Controller_Tests.cs @@ -0,0 +1,62 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using CommunityToolkit.Datasync.Server.MongoDB; +using CommunityToolkit.Datasync.Server.Test.Helpers; +using CommunityToolkit.Datasync.TestCommon.Databases; +using MongoDB.Bson; +using MongoDB.Driver; +using Xunit.Abstractions; + +namespace CommunityToolkit.Datasync.Server.Test.Live; + +[ExcludeFromCodeCoverage] +[Collection("LiveTestsCollection")] +public class MongoDB_Controller_Tests(DatabaseFixture fixture, ITestOutputHelper output) : LiveControllerTests(), IAsyncLifetime +{ + #region Setup + private readonly Random random = new(); + private List movies = []; + + public async Task InitializeAsync() + { + if (!string.IsNullOrEmpty(ConnectionStrings.MongoCommunity)) + { + // Note: we don't clear entities on every run to speed up the test runs. This can only be done because + // the tests are read-only (associated with the query and get capabilities). If the test being run writes + // to the database then change clearEntities to true. + output.WriteLine($"MongoCommunityIsInitialized = {fixture.MongoCommunityIsInitialized}"); + Context = await MongoDBContext.CreateContextAsync(ConnectionStrings.MongoCommunity, output, clearEntities: !fixture.MongoCommunityIsInitialized); + this.movies = await Context.Movies.Find(new BsonDocument()).ToListAsync(); + fixture.MongoCommunityIsInitialized = true; + } + } + + public async Task DisposeAsync() + { + if (Context is not null) + { + await Context.DisposeAsync(); + } + } + + private MongoDBContext Context { get; set; } + + protected override string DriverName { get; } = "MongoDB"; + + protected override bool CanRunLiveTests() => !string.IsNullOrEmpty(ConnectionStrings.MongoCommunity); + + protected override async Task GetEntityAsync(string id) + => await Context.Movies.Find(Builders.Filter.Eq(x => x.Id, id)).FirstOrDefaultAsync(); + + protected override async Task GetEntityCountAsync() + => (int)(await Context.Movies.CountDocumentsAsync(Builders.Filter.Empty)); + + protected override Task> GetPopulatedRepositoryAsync() + => Task.FromResult>(new MongoDBRepository(Context.Movies)); + + protected override Task GetRandomEntityIdAsync(bool exists) + => Task.FromResult(exists ? this.movies[this.random.Next(this.movies.Count)].Id : Guid.NewGuid().ToString()); + #endregion +} diff --git a/tests/CommunityToolkit.Datasync.TestCommon/CommunityToolkit.Datasync.TestCommon.csproj b/tests/CommunityToolkit.Datasync.TestCommon/CommunityToolkit.Datasync.TestCommon.csproj index ed10564..6c0154d 100644 --- a/tests/CommunityToolkit.Datasync.TestCommon/CommunityToolkit.Datasync.TestCommon.csproj +++ b/tests/CommunityToolkit.Datasync.TestCommon/CommunityToolkit.Datasync.TestCommon.csproj @@ -27,5 +27,6 @@ + diff --git a/tests/CommunityToolkit.Datasync.TestCommon/Databases/MongoDB/MongoDBContext.cs b/tests/CommunityToolkit.Datasync.TestCommon/Databases/MongoDB/MongoDBContext.cs new file mode 100644 index 0000000..2acd0c8 --- /dev/null +++ b/tests/CommunityToolkit.Datasync.TestCommon/Databases/MongoDB/MongoDBContext.cs @@ -0,0 +1,148 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using MongoDB.Bson; +using MongoDB.Driver; +using MongoDB.Driver.Core.Events; +using Xunit.Abstractions; + +namespace CommunityToolkit.Datasync.TestCommon.Databases; + +[ExcludeFromCodeCoverage] +public class MongoDBContext(MongoClient client, IMongoDatabase database) : IAsyncDisposable +{ + public static async Task CreateContextAsync(string connectionString, ITestOutputHelper output, bool clearEntities = true) + { + MongoClientSettings settings = MongoClientSettings.FromConnectionString(connectionString); + if (output is not null) + { + settings.ClusterConfigurator = cb => cb.Subscribe(e => output.WriteLine($"{e.CommandName} - {e.Command.ToJson()}")); + } + + MongoClient client = new(settings); + IMongoDatabase database = client.GetDatabase("unittests"); + MongoDBContext context = new(client, database); + + await context.InitializeDatabaseAsync(clearEntities); + await context.PopulateDatabaseAsync(); + + return context; + } + + public ValueTask DisposeAsync() + { + client.Dispose(); + GC.SuppressFinalize(this); + return ValueTask.CompletedTask; + } + + public MongoClient Client => client; + public IMongoDatabase Database => database; + public IMongoCollection Movies => database.GetCollection("movies"); + + public async Task InitializeDatabaseAsync(bool clearEntities) + { + if (clearEntities) + { + FilterDefinition filter = Builders.Filter.Empty; + await Movies.DeleteManyAsync(filter); + } + } + + public async Task PopulateDatabaseAsync() + { + FilterDefinition filter = Builders.Filter.Empty; + bool hasEntities = await Movies.CountDocumentsAsync(filter) > 0; + if (hasEntities) + { + return; + } + + // Create the indices required for all the tests. + //List> indices = []; + //string[] props = ["BestPictureWinner", "Duration", "Rating", "ReleaseDate", "Title", "Year", "UpdatedAt", "Deleted"]; + //foreach (string prop in props) + //{ + // indices.AddRange(GetCompoundIndexDefinitions(prop)); + //} + + //indices.AddRange(GetCompoundIndexDefinitions("UpdatedAt", "Deleted", includeId: false)); + //indices.AddRange(GetCompoundIndexDefinitions("Title", "Year")); + //indices.AddRange(GetCompoundIndexDefinitions("Year", "Title")); + //await Movies.Indexes.CreateManyAsync(indices); + + // Now populate the database with the test data, after the indices are defined. + foreach (MongoDBMovie movie in TestData.Movies.OfType()) + { + movie.UpdatedAt = DateTimeOffset.UtcNow; + movie.Version = Guid.NewGuid().ToByteArray(); + InsertOneOptions options = new(); + await Movies.InsertOneAsync(movie, options); + } + } + + private static IEnumerable> GetCompoundIndexDefinitions(string field) + { + return [ + new CreateIndexModel(Builders.IndexKeys.Combine( + Builders.IndexKeys.Ascending(field), + Builders.IndexKeys.Ascending("_id") + )), + new CreateIndexModel(Builders.IndexKeys.Combine( + Builders.IndexKeys.Descending(field), + Builders.IndexKeys.Ascending("_id") + )) + ]; + } + + private static IEnumerable> GetCompoundIndexDefinitions(string field1, string field2, bool includeId = true) + { + if (includeId) + { + return [ + new CreateIndexModel(Builders.IndexKeys.Combine( + Builders.IndexKeys.Ascending(field1), + Builders.IndexKeys.Ascending(field2), + Builders.IndexKeys.Ascending("_id") + )), + new CreateIndexModel(Builders.IndexKeys.Combine( + Builders.IndexKeys.Ascending(field1), + Builders.IndexKeys.Descending(field2), + Builders.IndexKeys.Ascending("_id") + )), + new CreateIndexModel(Builders.IndexKeys.Combine( + Builders.IndexKeys.Descending(field1), + Builders.IndexKeys.Ascending(field2), + Builders.IndexKeys.Ascending("_id") + )), + new CreateIndexModel(Builders.IndexKeys.Combine( + Builders.IndexKeys.Descending(field1), + Builders.IndexKeys.Descending(field2), + Builders.IndexKeys.Ascending("_id") + )), + ]; + } + else + { + return [ + new CreateIndexModel(Builders.IndexKeys.Combine( + Builders.IndexKeys.Ascending(field1), + Builders.IndexKeys.Ascending(field2) + )), + new CreateIndexModel(Builders.IndexKeys.Combine( + Builders.IndexKeys.Ascending(field1), + Builders.IndexKeys.Descending(field2) + )), + new CreateIndexModel(Builders.IndexKeys.Combine( + Builders.IndexKeys.Descending(field1), + Builders.IndexKeys.Ascending(field2) + )), + new CreateIndexModel(Builders.IndexKeys.Combine( + Builders.IndexKeys.Descending(field1), + Builders.IndexKeys.Descending(field2) + )), + ]; + } + } +} diff --git a/tests/CommunityToolkit.Datasync.TestCommon/Databases/MongoDB/MongoDBMovie.cs b/tests/CommunityToolkit.Datasync.TestCommon/Databases/MongoDB/MongoDBMovie.cs new file mode 100644 index 0000000..5b38841 --- /dev/null +++ b/tests/CommunityToolkit.Datasync.TestCommon/Databases/MongoDB/MongoDBMovie.cs @@ -0,0 +1,64 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using CommunityToolkit.Datasync.Server.MongoDB; +using CommunityToolkit.Datasync.TestCommon.Models; +using System.ComponentModel.DataAnnotations; + +namespace CommunityToolkit.Datasync.TestCommon.Databases; + +[ExcludeFromCodeCoverage] +public class MongoDBMovie : MongoTableData, IMovie, IEquatable +{ + /// + /// True if the movie won the oscar for Best Picture + /// + public bool BestPictureWinner { get; set; } + + /// + /// The running time of the movie + /// + [Required] + [Range(60, 360)] + public int Duration { get; set; } + + /// + /// The MPAA rating for the movie, if available. + /// + public MovieRating Rating { get; set; } = MovieRating.Unrated; + + /// + /// The release date of the movie. + /// + [Required] + public DateOnly ReleaseDate { get; set; } + + /// + /// The title of the movie. + /// + [Required] + [StringLength(128, MinimumLength = 2)] + public string Title { get; set; } = string.Empty; + + /// + /// The year that the movie was released. + /// + [Required] + [Range(1920, 2030)] + public int Year { get; set; } + + /// + /// Determines if this movie has the same content as another movie. + /// + /// The other movie + /// true if the content is the same + public bool Equals(IMovie other) + => other != null + && other.BestPictureWinner == BestPictureWinner + && other.Duration == Duration + && other.Rating == Rating + && other.ReleaseDate == ReleaseDate + && other.Title == Title + && other.Year == Year; +} diff --git a/tests/CommunityToolkit.Datasync.TestCommon/RepositoryTests.cs b/tests/CommunityToolkit.Datasync.TestCommon/RepositoryTests.cs index ded1444..8aa0917 100644 --- a/tests/CommunityToolkit.Datasync.TestCommon/RepositoryTests.cs +++ b/tests/CommunityToolkit.Datasync.TestCommon/RepositoryTests.cs @@ -73,7 +73,28 @@ public async Task AsQueryableAsync_CanRetrieveFilteredLists() IRepository Repository = await GetPopulatedRepositoryAsync(); int expected = TestData.Movies.Count(m => m.Rating == MovieRating.R); IQueryable queryable = await Repository.AsQueryableAsync(); - IList actual = await Repository.ToListAsync(queryable.Where(m => m.Rating == MovieRating.R)); + IQueryable sut = queryable + .Where(m => m.Rating == MovieRating.R); + IList actual = await Repository.ToListAsync(sut); + + actual.Should().HaveCount(expected); + } + + [SkippableFact] + public async Task AsQueryableAsync_CanRetrieveOrderedLists() + { + Skip.IfNot(CanRunLiveTests()); + + IRepository Repository = await GetPopulatedRepositoryAsync(); + int expected = TestData.Movies.Count(); + IQueryable queryable = await Repository.AsQueryableAsync(); + + // We pick this set of orderings because we create a CosmosDB composite index for these already. + IQueryable sut = queryable + .OrderBy(m => m.ReleaseDate) + .ThenBy(m => m.Id); + + IList actual = await Repository.ToListAsync(sut); actual.Should().HaveCount(expected); } @@ -85,7 +106,11 @@ public async Task AsQueryableAsync_CanUseTopAndSkip() IRepository Repository = await GetPopulatedRepositoryAsync(); IQueryable queryable = await Repository.AsQueryableAsync(); - IList actual = await Repository.ToListAsync(queryable.Where(m => m.Rating == MovieRating.R).Skip(5).Take(20)); + IQueryable sut = queryable + .Where(m => m.Rating == MovieRating.R) + .Skip(5) + .Take(20); + IList actual = await Repository.ToListAsync(sut); actual.Should().HaveCount(20); } @@ -100,7 +125,12 @@ public async Task AsQueryableAsync_CanRetrievePagedDatasyncQuery() IRepository Repository = await GetPopulatedRepositoryAsync(); IQueryable queryable = await Repository.AsQueryableAsync(); - IList actual = await Repository.ToListAsync(queryable.Where(m => m.UpdatedAt > DateTimeOffset.UnixEpoch && !m.Deleted).OrderBy(m => m.UpdatedAt).Skip(10).Take(10)); + IQueryable sut = queryable + .Where(m => m.UpdatedAt > DateTimeOffset.UnixEpoch && !m.Deleted) + .OrderBy(m => m.UpdatedAt) + .Skip(10) + .Take(10); + IList actual = await Repository.ToListAsync(sut); actual.Should().HaveCount(10); }