diff --git a/src/Microsoft.Azure.CosmosEventSourcing/Items/EventItem.cs b/src/Microsoft.Azure.CosmosEventSourcing/Items/EventItem.cs index 0b5e3a729..f96c6a51e 100644 --- a/src/Microsoft.Azure.CosmosEventSourcing/Items/EventItem.cs +++ b/src/Microsoft.Azure.CosmosEventSourcing/Items/EventItem.cs @@ -60,6 +60,11 @@ public DomainEvent DomainEvent /// public string PartitionKey { get; set; } = null!; + /// + /// The values used to partition the event. + /// + public IEnumerable PartitionKeys { get; set; } = null!; + /// /// The name of the event stored. /// diff --git a/src/Microsoft.Azure.CosmosEventSourcing/Microsoft.Azure.CosmosEventSourcing.csproj b/src/Microsoft.Azure.CosmosEventSourcing/Microsoft.Azure.CosmosEventSourcing.csproj index ce9c249cc..6f739ca79 100644 --- a/src/Microsoft.Azure.CosmosEventSourcing/Microsoft.Azure.CosmosEventSourcing.csproj +++ b/src/Microsoft.Azure.CosmosEventSourcing/Microsoft.Azure.CosmosEventSourcing.csproj @@ -27,7 +27,7 @@ false false Microsoft.Azure.CosmosEventSourcing - NU5125 + NU5125;NU1507 true https://github.com/IEvangelist/azure-cosmos-dotnet-repository LICENSE diff --git a/src/Microsoft.Azure.CosmosEventSourcing/Stores/DefaultEventStore.Read.cs b/src/Microsoft.Azure.CosmosEventSourcing/Stores/DefaultEventStore.Read.cs index e88f78510..b2fa10c61 100644 --- a/src/Microsoft.Azure.CosmosEventSourcing/Stores/DefaultEventStore.Read.cs +++ b/src/Microsoft.Azure.CosmosEventSourcing/Stores/DefaultEventStore.Read.cs @@ -4,9 +4,9 @@ using System.Linq.Expressions; using System.Reflection; using System.Runtime.CompilerServices; +using Microsoft.Azure.Cosmos; using Microsoft.Azure.CosmosEventSourcing.Aggregates; using Microsoft.Azure.CosmosEventSourcing.Exceptions; -using Microsoft.Azure.CosmosEventSourcing.Extensions; using Microsoft.Azure.CosmosRepository.Paging; namespace Microsoft.Azure.CosmosEventSourcing.Stores; @@ -15,8 +15,7 @@ internal partial class DefaultEventStore { public ValueTask> ReadAsync(string partitionKey, CancellationToken cancellationToken = default) => - readOnlyRepository.GetAsync( - x => x.PartitionKey == partitionKey, + readOnlyRepository.GetAsync(new PartitionKey(partitionKey), cancellationToken); public async ValueTask ReadAggregateAsync( @@ -25,7 +24,7 @@ public async ValueTask ReadAggregateAsync( where TAggregateRoot : IAggregateRoot { IEnumerable events = await readOnlyRepository.GetAsync( - x => x.PartitionKey == partitionKey, + new PartitionKey(partitionKey), cancellationToken); var payloads = events @@ -46,7 +45,7 @@ public async ValueTask ReadAggregateAsync( CancellationToken cancellationToken = default) where TAggregateRoot : IAggregateRoot { IEnumerable events = await readOnlyRepository.GetAsync( - x => x.PartitionKey == partitionKey, + new PartitionKey(partitionKey), cancellationToken); return rootMapper.MapTo(events); @@ -57,9 +56,8 @@ public ValueTask> ReadAsync( Expression> predicate, CancellationToken cancellationToken = default) => readOnlyRepository.GetAsync( - predicate.Compose( - x => x.PartitionKey == partitionKey, - Expression.AndAlso), + new PartitionKey(partitionKey), + predicate, cancellationToken); public async IAsyncEnumerable StreamAsync( @@ -69,13 +67,11 @@ public async IAsyncEnumerable StreamAsync( { string? token = null; - Expression> expression = eventSource => - eventSource.PartitionKey == partitionKey; - do { IPage page = await readOnlyRepository.PageAsync( - expression, + new PartitionKey(partitionKey), + predicate: null, chunkSize, token, cancellationToken: cancellationToken); diff --git a/src/Microsoft.Azure.CosmosRepository.AspNetCore/Microsoft.Azure.CosmosRepository.AspNetCore.csproj b/src/Microsoft.Azure.CosmosRepository.AspNetCore/Microsoft.Azure.CosmosRepository.AspNetCore.csproj index 69def86b0..70a57e377 100644 --- a/src/Microsoft.Azure.CosmosRepository.AspNetCore/Microsoft.Azure.CosmosRepository.AspNetCore.csproj +++ b/src/Microsoft.Azure.CosmosRepository.AspNetCore/Microsoft.Azure.CosmosRepository.AspNetCore.csproj @@ -31,7 +31,7 @@ false false Microsoft.Azure.CosmosRepository.AspNetCore - NU5125 + NU5125;NU1507 true https://github.com/IEvangelist/azure-cosmos-dotnet-repository LICENSE diff --git a/src/Microsoft.Azure.CosmosRepository/Attributes/PartitionKeyPathAttribute.cs b/src/Microsoft.Azure.CosmosRepository/Attributes/PartitionKeyPathAttribute.cs index 97123322e..2877337d1 100644 --- a/src/Microsoft.Azure.CosmosRepository/Attributes/PartitionKeyPathAttribute.cs +++ b/src/Microsoft.Azure.CosmosRepository/Attributes/PartitionKeyPathAttribute.cs @@ -9,20 +9,22 @@ namespace Microsoft.Azure.CosmosRepository.Attributes; /// conjunction with a on the property /// whose value will act as the partition key. Partition key paths should start with "/", /// for example "/partition". For more information, -/// see https://docs.microsoft.com/azure/cosmos-db/partitioning-overview. +/// see https://docs.microsoft.com/azure/cosmos-db/partitioning-overview and +/// https://learn.microsoft.com/en-us/azure/cosmos-db/hierarchical-partition-keys /// /// /// By default, "/id" is used. /// /// -/// Constructor accepting the of the partition key for a given . +/// Constructor accepting the of the partition keys for a given . /// -/// +/// [AttributeUsage(AttributeTargets.Class, AllowMultiple = false, Inherited = true)] -public sealed class PartitionKeyPathAttribute(string path) : Attribute +public sealed class PartitionKeyPathAttribute(params string[] paths) : Attribute { /// - /// Gets the path of the parition key. + /// Gets the path values of the parition key. /// - public string Path { get; } = path ?? throw new ArgumentNullException(nameof(path), "A path is required."); + public string[] Paths { get; } = paths != null && paths.Length >= 1 ? paths : throw new ArgumentNullException(nameof(paths), "At least one path is required."); } + diff --git a/src/Microsoft.Azure.CosmosRepository/Builders/ContainerOptionsBuilder.cs b/src/Microsoft.Azure.CosmosRepository/Builders/ContainerOptionsBuilder.cs index 257162b7b..63d593ffe 100644 --- a/src/Microsoft.Azure.CosmosRepository/Builders/ContainerOptionsBuilder.cs +++ b/src/Microsoft.Azure.CosmosRepository/Builders/ContainerOptionsBuilder.cs @@ -23,9 +23,9 @@ public class ContainerOptionsBuilder(Type type) internal string? Name { get; private set; } /// - /// The partition key for the container. + /// The partition keys for the container. /// - internal string? PartitionKey { get; private set; } + internal IList? PartitionKeys { get; private set; } /// /// The default time to live for a container. @@ -79,7 +79,11 @@ public ContainerOptionsBuilder WithContainer(string name) /// public ContainerOptionsBuilder WithPartitionKey(string partitionKey) { - PartitionKey = partitionKey ?? throw new ArgumentNullException(nameof(partitionKey)); + if (partitionKey is null) throw new ArgumentNullException(nameof(partitionKey)); + + PartitionKeys ??= []; + PartitionKeys.Add(partitionKey); + return this; } diff --git a/src/Microsoft.Azure.CosmosRepository/IItem.cs b/src/Microsoft.Azure.CosmosRepository/IItem.cs index adad0264e..ab0986777 100644 --- a/src/Microsoft.Azure.CosmosRepository/IItem.cs +++ b/src/Microsoft.Azure.CosmosRepository/IItem.cs @@ -18,8 +18,13 @@ public interface IItem /// string Type { get; set; } + ///// + ///// Gets the item's PartitionKey. This string is used to instantiate the Cosmos.PartitionKey struct. + ///// + string PartitionKey { get; } + /// - /// Gets the item's PartitionKey. This string is used to instantiate the Cosmos.PartitionKey struct. + /// Gets the item PartitionKeys. This string array is used to instantiate the Cosmos.PartitionKeys struct. /// - string PartitionKey { get; } + IEnumerable PartitionKeys { get; } } diff --git a/src/Microsoft.Azure.CosmosRepository/Item.cs b/src/Microsoft.Azure.CosmosRepository/Item.cs index a3b08499e..3a751c7ac 100644 --- a/src/Microsoft.Azure.CosmosRepository/Item.cs +++ b/src/Microsoft.Azure.CosmosRepository/Item.cs @@ -44,10 +44,16 @@ public abstract class Item : IItem public string Type { get; set; } /// - /// Gets the PartitionKey based on . + /// Gets the PartitionKey based on last record. /// Implemented explicitly to keep out of Item API /// - string IItem.PartitionKey => GetPartitionKeyValue(); + string IItem.PartitionKey => GetPartitionKeyValues().Last(); + + /// + /// Gets the PartitionKeys based on . + /// Implemented explicitly to keep out of Item API + /// + IEnumerable IItem.PartitionKeys => GetPartitionKeyValues(); /// /// Default constructor, assigns type name to property. @@ -56,10 +62,20 @@ public abstract class Item : IItem /// /// Gets the partition key value for the given type. - /// When overridden, be sure that the value corresponds - /// to the value, i.e.; "/partition" and "partition" + /// When overridden, be sure that the values correspond + /// to the values, i.e.; "/partition" and "partition" /// respectively. If these two values do not correspond an error will occur. /// /// The unless overridden by the subclass. protected virtual string GetPartitionKeyValue() => Id; + + /// + /// Gets the partition key values for the given type. + /// When overridden, be sure that the values correspond + /// to the values, i.e.; "/partition" and "partition" + /// respectively. If all provided key values do not have a matching property with the equivalent name, an error will occur. + /// Make sure to add the latest inner + /// + /// The list with unless overridden by the subclass. + protected virtual IEnumerable GetPartitionKeyValues() => new string[] { GetPartitionKeyValue() }; } diff --git a/src/Microsoft.Azure.CosmosRepository/Logging/LoggerExtensions.cs b/src/Microsoft.Azure.CosmosRepository/Logging/LoggerExtensions.cs index aac8cb234..23425dd10 100644 --- a/src/Microsoft.Azure.CosmosRepository/Logging/LoggerExtensions.cs +++ b/src/Microsoft.Azure.CosmosRepository/Logging/LoggerExtensions.cs @@ -19,6 +19,13 @@ public static void LogPointReadStarted( string partitionKey) where TItem : IItem => LoggerMessageDefinitions.PointReadStarted(logger, typeof(TItem).Name, id, partitionKey, null!); + //Info Logger Extensions + public static void LogPointReadStarted( + this ILogger logger, + string id, + IEnumerable partitionKeys) where TItem : IItem => + LoggerMessageDefinitions.HierarchicalPointReadStarted(logger, typeof(TItem).Name, id, partitionKeys, null!); + public static void LogPointReadExecuted( this ILogger logger, double ruCharge) where TItem : IItem => @@ -41,4 +48,11 @@ public static void LogItemNotFoundHandled( string partitionKey, CosmosException e) where TItem : IItem => LoggerMessageDefinitions.ItemNotFoundHandled(logger, typeof(TItem).Name, id, partitionKey, e); + + public static void LogItemNotFoundHandled( + this ILogger logger, + string id, + IEnumerable partitionKeys, + CosmosException e) where TItem : IItem => + LoggerMessageDefinitions.HierarchicalItemNotFoundHandled(logger, typeof(TItem).Name, id, partitionKeys, e); } \ No newline at end of file diff --git a/src/Microsoft.Azure.CosmosRepository/Logging/LoggerMessageDefinitions.cs b/src/Microsoft.Azure.CosmosRepository/Logging/LoggerMessageDefinitions.cs index 0b5a28616..15beae74d 100644 --- a/src/Microsoft.Azure.CosmosRepository/Logging/LoggerMessageDefinitions.cs +++ b/src/Microsoft.Azure.CosmosRepository/Logging/LoggerMessageDefinitions.cs @@ -20,6 +20,14 @@ internal static class LoggerMessageDefinitions "Point read started for item type {CosmosItemType} with id {CosmosItemId} and partitionKey {CosmosItemPartitionKey}" ); + // Info Definitions + internal static readonly Action, Exception?> HierarchicalPointReadStarted = + LoggerMessage.Define>( + LogLevel.Information, + EventIds.CosmosPointReadStarted, + "Point read started for item type {CosmosItemType} with id {CosmosItemId} and partitionKeys {CosmosItemPartitionKeys}" + ); + internal static readonly Action PointReadExecuted = LoggerMessage.Define( LogLevel.Information, @@ -46,4 +54,11 @@ internal static class LoggerMessageDefinitions EventIds.ItemNotFoundHandled, "CosmosException Status Code 404 handled for item of type {CosmosItemType} with {CosmosItemId} and partition key {CosmosItemPartitionKey}" ); + + internal static readonly Action, Exception?> HierarchicalItemNotFoundHandled = + LoggerMessage.Define>( + LogLevel.Information, + EventIds.ItemNotFoundHandled, + "CosmosException Status Code 404 handled for item of type {CosmosItemType} with {CosmosItemId} and partition keys {CosmosItemPartitionKeys}" + ); } \ No newline at end of file diff --git a/src/Microsoft.Azure.CosmosRepository/Microsoft.Azure.CosmosRepository.csproj b/src/Microsoft.Azure.CosmosRepository/Microsoft.Azure.CosmosRepository.csproj index cff3f7544..8daf36e78 100644 --- a/src/Microsoft.Azure.CosmosRepository/Microsoft.Azure.CosmosRepository.csproj +++ b/src/Microsoft.Azure.CosmosRepository/Microsoft.Azure.CosmosRepository.csproj @@ -34,7 +34,7 @@ false false Microsoft.Azure.CosmosRepository - NU5125 + NU5125;NU1507 true https://github.com/IEvangelist/azure-cosmos-dotnet-repository LICENSE diff --git a/src/Microsoft.Azure.CosmosRepository/Options/ItemConfiguration.cs b/src/Microsoft.Azure.CosmosRepository/Options/ItemConfiguration.cs index 0b52ec62a..05df68c5f 100644 --- a/src/Microsoft.Azure.CosmosRepository/Options/ItemConfiguration.cs +++ b/src/Microsoft.Azure.CosmosRepository/Options/ItemConfiguration.cs @@ -3,32 +3,59 @@ namespace Microsoft.Azure.CosmosRepository.Options; -internal class ItemConfiguration( - Type type, - string containerName, - string partitionKeyPath, - UniqueKeyPolicy? uniqueKeyPolicy, - ThroughputProperties? throughputProperties, - int defaultTimeToLive = -1, - bool syncContainerProperties = false, - ChangeFeedOptions? changeFeedOptions = null, - bool useStrictTypeChecking = true) +internal class ItemConfiguration { - public Type Type { get; } = type; - - public string ContainerName { get; } = containerName; - - public string PartitionKeyPath { get; } = partitionKeyPath; - - public UniqueKeyPolicy? UniqueKeyPolicy { get; } = uniqueKeyPolicy; - - public ThroughputProperties? ThroughputProperties { get; } = throughputProperties; - - public int DefaultTimeToLive { get; } = defaultTimeToLive; - - public bool SyncContainerProperties { get; } = syncContainerProperties; - - public ChangeFeedOptions? ChangeFeedOptions { get; } = changeFeedOptions; - - public bool UseStrictTypeChecking { get; } = useStrictTypeChecking; -} \ No newline at end of file + public ItemConfiguration( + Type type, + string containerName, + string partitionKeyPath, + UniqueKeyPolicy? uniqueKeyPolicy = null, + ThroughputProperties? throughputProperties = null, + int defaultTimeToLive = -1, + bool syncContainerProperties = false, + ChangeFeedOptions? changeFeedOptions = null, + bool useStrictTypeChecking = true) + : this(type, containerName, new[] { partitionKeyPath }, uniqueKeyPolicy, throughputProperties, defaultTimeToLive, syncContainerProperties, changeFeedOptions, useStrictTypeChecking) + { + } + + public ItemConfiguration( + Type type, + string containerName, + IEnumerable partitionKeyPaths, + UniqueKeyPolicy? uniqueKeyPolicy = null, + ThroughputProperties? throughputProperties = null, + int defaultTimeToLive = -1, + bool syncContainerProperties = false, + ChangeFeedOptions? changeFeedOptions = null, + bool useStrictTypeChecking = true) + { + Type = type; + ContainerName = containerName; + PartitionKeyPaths = partitionKeyPaths; + UniqueKeyPolicy = uniqueKeyPolicy; + ThroughputProperties = throughputProperties; + DefaultTimeToLive = defaultTimeToLive; + SyncContainerProperties = syncContainerProperties; + ChangeFeedOptions = changeFeedOptions; + UseStrictTypeChecking = useStrictTypeChecking; + } + + public Type Type { get; } + + public string ContainerName { get; } + + public IEnumerable PartitionKeyPaths { get; } + + public UniqueKeyPolicy? UniqueKeyPolicy { get; } + + public ThroughputProperties? ThroughputProperties { get; } + + public int DefaultTimeToLive { get; } + + public bool SyncContainerProperties { get; } + + public ChangeFeedOptions? ChangeFeedOptions { get; } + + public bool UseStrictTypeChecking { get; } +} diff --git a/src/Microsoft.Azure.CosmosRepository/Providers/DefaultCosmosItemConfigurationProvider.cs b/src/Microsoft.Azure.CosmosRepository/Providers/DefaultCosmosItemConfigurationProvider.cs index da4590f18..3704f29de 100644 --- a/src/Microsoft.Azure.CosmosRepository/Providers/DefaultCosmosItemConfigurationProvider.cs +++ b/src/Microsoft.Azure.CosmosRepository/Providers/DefaultCosmosItemConfigurationProvider.cs @@ -39,7 +39,7 @@ private ItemConfiguration AddOptions(Type itemType) itemType.IsItem(); var containerName = containerNameProvider.GetContainerName(itemType); - var partitionKeyPath = cosmosPartitionKeyPathProvider.GetPartitionKeyPath(itemType); + var partitionKeyPaths = cosmosPartitionKeyPathProvider.GetPartitionKeyPaths(itemType); UniqueKeyPolicy? uniqueKeyPolicy = cosmosUniqueKeyPolicyProvider.GetUniqueKeyPolicy(itemType); var timeToLive = containerDefaultTimeToLiveProvider.GetDefaultTimeToLive(itemType); var sync = syncContainerPropertiesProvider.GetWhetherToSyncContainerProperties(itemType); @@ -49,11 +49,12 @@ private ItemConfiguration AddOptions(Type itemType) return new( itemType, containerName, - partitionKeyPath, + partitionKeyPaths, uniqueKeyPolicy, throughputProperties, timeToLive, sync, useStrictTypeChecking: useStrictTypeChecking); + } } \ No newline at end of file diff --git a/src/Microsoft.Azure.CosmosRepository/Providers/DefaultCosmosPartitionKeyPathProvider.cs b/src/Microsoft.Azure.CosmosRepository/Providers/DefaultCosmosPartitionKeyPathProvider.cs index d3e2fca60..31e4fa7a9 100644 --- a/src/Microsoft.Azure.CosmosRepository/Providers/DefaultCosmosPartitionKeyPathProvider.cs +++ b/src/Microsoft.Azure.CosmosRepository/Providers/DefaultCosmosPartitionKeyPathProvider.cs @@ -1,6 +1,7 @@ // Copyright (c) David Pine. All rights reserved. // Licensed under the MIT License. + namespace Microsoft.Azure.CosmosRepository.Providers; /// @@ -10,23 +11,27 @@ class DefaultCosmosPartitionKeyPathProvider(IOptions options) private readonly IOptions _options = options ?? throw new ArgumentNullException(nameof(options)); /// - public string GetPartitionKeyPath() where TItem : IItem => - GetPartitionKeyPath(typeof(TItem)); + public IEnumerable GetPartitionKeyPaths() where TItem : IItem => + GetPartitionKeyPaths(typeof(TItem)); - public string GetPartitionKeyPath(Type itemType) + public IEnumerable GetPartitionKeyPaths(Type itemType) { Type attributeType = typeof(PartitionKeyPathAttribute); ContainerOptionsBuilder? optionsBuilder = _options.Value.GetContainerOptions(itemType); - if (optionsBuilder is { } && string.IsNullOrWhiteSpace(optionsBuilder.PartitionKey) is false) + if (optionsBuilder is { PartitionKeys: { } } && + optionsBuilder.PartitionKeys.Any() && + optionsBuilder.PartitionKeys.All(x => !string.IsNullOrWhiteSpace(x))) { - return optionsBuilder.PartitionKey!; + return optionsBuilder.PartitionKeys; } return Attribute.GetCustomAttribute( itemType, attributeType) is PartitionKeyPathAttribute partitionKeyPathAttribute - ? partitionKeyPathAttribute.Path - : "/id"; + ? partitionKeyPathAttribute.Paths + : ["/id"]; } + + } diff --git a/src/Microsoft.Azure.CosmosRepository/Providers/ICosmosPartitionKeyPathProvider.cs b/src/Microsoft.Azure.CosmosRepository/Providers/ICosmosPartitionKeyPathProvider.cs index 618a47bbc..041176832 100644 --- a/src/Microsoft.Azure.CosmosRepository/Providers/ICosmosPartitionKeyPathProvider.cs +++ b/src/Microsoft.Azure.CosmosRepository/Providers/ICosmosPartitionKeyPathProvider.cs @@ -10,11 +10,11 @@ namespace Microsoft.Azure.CosmosRepository.Providers; interface ICosmosPartitionKeyPathProvider { /// - /// Gets the partition key path for a given type. + /// Gets the partition key paths for a given type. /// - /// The item for which the partition key path corresponds. - /// A string value representing the partition key path, i.e.; "/partion" - string GetPartitionKeyPath() where TItem : IItem; - - string GetPartitionKeyPath(Type itemType); + /// The item for which the partition keys paths corresponds. + /// A string array representing the partition key paths, i.e.; "/partion" + IEnumerable GetPartitionKeyPaths() where TItem : IItem; + + IEnumerable GetPartitionKeyPaths(Type itemType); } diff --git a/src/Microsoft.Azure.CosmosRepository/Repositories/DefaultRepository.Batch.cs b/src/Microsoft.Azure.CosmosRepository/Repositories/DefaultRepository.Batch.cs index 467c34ca2..08c103bf7 100644 --- a/src/Microsoft.Azure.CosmosRepository/Repositories/DefaultRepository.Batch.cs +++ b/src/Microsoft.Azure.CosmosRepository/Repositories/DefaultRepository.Batch.cs @@ -13,11 +13,22 @@ public async ValueTask UpdateAsBatchAsync( { var list = items.ToList(); - var partitionKey = GetPartitionKeyValue(list); + PartitionKey partitionKey = BuildPartitionKey(list); + + await UpdateAsBatchAsync(items, partitionKey, cancellationToken); + } + + /// + public async ValueTask UpdateAsBatchAsync( + IEnumerable items, + PartitionKey partitionKey, + CancellationToken cancellationToken = default) + { + var list = items.ToList(); Container container = await containerProvider.GetContainerAsync(); - TransactionalBatch batch = container.CreateTransactionalBatch(new PartitionKey(partitionKey)); + TransactionalBatch batch = container.CreateTransactionalBatch(partitionKey); foreach (TItem item in list) { @@ -46,11 +57,22 @@ public async ValueTask CreateAsBatchAsync( { var list = items.ToList(); - var partitionKey = GetPartitionKeyValue(list); + PartitionKey partitionKey = BuildPartitionKey(list); + + await CreateAsBatchAsync(items, partitionKey, cancellationToken); + } + + /// + public async ValueTask CreateAsBatchAsync( + IEnumerable items, + PartitionKey partitionKey, + CancellationToken cancellationToken = default) + { + var list = items.ToList(); Container container = await containerProvider.GetContainerAsync(); - TransactionalBatch batch = container.CreateTransactionalBatch(new PartitionKey(partitionKey)); + TransactionalBatch batch = container.CreateTransactionalBatch(partitionKey); foreach (TItem item in list) { @@ -65,17 +87,30 @@ public async ValueTask CreateAsBatchAsync( } } + /// public async ValueTask DeleteAsBatchAsync( IEnumerable items, CancellationToken cancellationToken = default) { var list = items.ToList(); - var partitionKey = GetPartitionKeyValue(list); + PartitionKey partitionKey = BuildPartitionKey(list); + + await DeleteAsBatchAsync(items, partitionKey, cancellationToken); + + } + + /// + public async ValueTask DeleteAsBatchAsync( + IEnumerable items, + PartitionKey partitionKey, + CancellationToken cancellationToken = default) + { + var list = items.ToList(); Container container = await containerProvider.GetContainerAsync(); - TransactionalBatch batch = container.CreateTransactionalBatch(new PartitionKey(partitionKey)); + TransactionalBatch batch = container.CreateTransactionalBatch(partitionKey); foreach (TItem item in list) { @@ -89,16 +124,4 @@ public async ValueTask DeleteAsBatchAsync( throw new BatchOperationException(response); } } - - private static string GetPartitionKeyValue(List items) - { - if (!items.Any()) - { - throw new ArgumentException( - "Unable to perform batch operation with no items", - nameof(items)); - } - - return items[0].PartitionKey; - } } \ No newline at end of file diff --git a/src/Microsoft.Azure.CosmosRepository/Repositories/DefaultRepository.Count.cs b/src/Microsoft.Azure.CosmosRepository/Repositories/DefaultRepository.Count.cs index 74c1b9aef..18cef5721 100644 --- a/src/Microsoft.Azure.CosmosRepository/Repositories/DefaultRepository.Count.cs +++ b/src/Microsoft.Azure.CosmosRepository/Repositories/DefaultRepository.Count.cs @@ -11,16 +11,30 @@ internal sealed partial class DefaultRepository public async ValueTask CountAsync( CancellationToken cancellationToken = default) { - Container container = - await containerProvider.GetContainerAsync() - .ConfigureAwait(false); + return await InternalCountAsync(cancellationToken: cancellationToken); + } - IQueryable query = container.GetItemLinqQueryable( - linqSerializerOptions: optionsMonitor.CurrentValue.SerializationOptions); + /// + public async ValueTask CountAsync( + Expression> predicate, + CancellationToken cancellationToken = default) + { + return await InternalCountAsync(predicate, default, cancellationToken); + } - TryLogDebugDetails(logger, () => $"Read: {query}"); + public async ValueTask CountAsync( + Expression> predicate, + PartitionKey partitionKey, + CancellationToken cancellationToken = default) + { + return await InternalCountAsync(predicate, partitionKey, cancellationToken); + } - return await cosmosQueryableProcessor.CountAsync(query, cancellationToken); + public async ValueTask CountAsync( + PartitionKey partitionKey, + CancellationToken cancellationToken = default) + { + return await InternalCountAsync(null, partitionKey, cancellationToken); } private async ValueTask> CountAsync( @@ -32,7 +46,15 @@ private async ValueTask> CountAsync( await containerProvider.GetContainerAsync() .ConfigureAwait(false); + QueryRequestOptions options = new(); + + if (specification.PartitionKey is not null) + { + options.PartitionKey = specification.PartitionKey; + } + IQueryable query = container.GetItemLinqQueryable( + requestOptions: options, linqSerializerOptions: optionsMonitor.CurrentValue.SerializationOptions); query = specificationEvaluator.GetQuery(query, specification, evaluateCriteriaOnly: true); @@ -41,23 +63,41 @@ await containerProvider.GetContainerAsync() return await query.CountAsync(cancellationToken); } - /// - public async ValueTask CountAsync( - Expression> predicate, + private async ValueTask InternalCountAsync( + Expression>? predicate = null, + PartitionKey partitionKey = default, CancellationToken cancellationToken = default) { Container container = await containerProvider.GetContainerAsync() .ConfigureAwait(false); + QueryRequestOptions requestOptions = new (); + + if (partitionKey != default) + { + requestOptions.PartitionKey = partitionKey; + } + IQueryable query = container.GetItemLinqQueryable( - linqSerializerOptions: optionsMonitor.CurrentValue.SerializationOptions) - .Where(repositoryExpressionProvider.Build(predicate)); + requestOptions: requestOptions, + linqSerializerOptions: optionsMonitor.CurrentValue.SerializationOptions); + + if (predicate is not null) + { + query = query.Where(repositoryExpressionProvider.Build(predicate)); + } TryLogDebugDetails(logger, () => $"Read: {query}"); return await cosmosQueryableProcessor.CountAsync( query, cancellationToken); } + + //TODO: Write docs + public async ValueTask CountAsync(Expression> predicate, IEnumerable partitionKeyValues, CancellationToken cancellationToken = default) + { + return await CountAsync(predicate, BuildPartitionKey(partitionKeyValues), cancellationToken); + } } \ No newline at end of file diff --git a/src/Microsoft.Azure.CosmosRepository/Repositories/DefaultRepository.Create.cs b/src/Microsoft.Azure.CosmosRepository/Repositories/DefaultRepository.Create.cs index 3c1e0671d..599de1887 100644 --- a/src/Microsoft.Azure.CosmosRepository/Repositories/DefaultRepository.Create.cs +++ b/src/Microsoft.Azure.CosmosRepository/Repositories/DefaultRepository.Create.cs @@ -18,10 +18,12 @@ public async ValueTask CreateAsync( if (value is IItemWithTimeStamps { CreatedTimeUtc: null } valueWithTimestamps) { valueWithTimestamps.CreatedTimeUtc = DateTime.UtcNow; - } + } + + PartitionKey partitionKey = BuildPartitionKey(value); ItemResponse response = - await container.CreateItemAsync(value, new PartitionKey(value.PartitionKey), + await container.CreateItemAsync(value, partitionKey, cancellationToken: cancellationToken) .ConfigureAwait(false); diff --git a/src/Microsoft.Azure.CosmosRepository/Repositories/DefaultRepository.Delete.cs b/src/Microsoft.Azure.CosmosRepository/Repositories/DefaultRepository.Delete.cs index f5c8bc3bc..70cb3fe72 100644 --- a/src/Microsoft.Azure.CosmosRepository/Repositories/DefaultRepository.Delete.cs +++ b/src/Microsoft.Azure.CosmosRepository/Repositories/DefaultRepository.Delete.cs @@ -3,6 +3,7 @@ // ReSharper disable once CheckNamespace + namespace Microsoft.Azure.CosmosRepository; internal sealed partial class DefaultRepository @@ -11,14 +12,20 @@ internal sealed partial class DefaultRepository public ValueTask DeleteAsync( TItem value, CancellationToken cancellationToken = default) => - DeleteAsync(value.Id, value.PartitionKey, cancellationToken); + DeleteAsync(value.Id, BuildPartitionKey(value.PartitionKeys), cancellationToken); /// public ValueTask DeleteAsync( string id, string? partitionKeyValue = null, CancellationToken cancellationToken = default) => - DeleteAsync(id, new PartitionKey(partitionKeyValue ?? id), cancellationToken); + DeleteAsync(id, BuildPartitionKey(partitionKeyValue ?? id), cancellationToken); + + public ValueTask DeleteAsync( + string id, + IEnumerable partitionKeyValues, + CancellationToken cancellationToken = default) => + DeleteAsync(id, BuildPartitionKey(partitionKeyValues, id), cancellationToken); /// public async ValueTask DeleteAsync( diff --git a/src/Microsoft.Azure.CosmosRepository/Repositories/DefaultRepository.Exists.cs b/src/Microsoft.Azure.CosmosRepository/Repositories/DefaultRepository.Exists.cs index bafcbdbc3..4ad9b1d83 100644 --- a/src/Microsoft.Azure.CosmosRepository/Repositories/DefaultRepository.Exists.cs +++ b/src/Microsoft.Azure.CosmosRepository/Repositories/DefaultRepository.Exists.cs @@ -12,7 +12,7 @@ public ValueTask ExistsAsync( string id, string? partitionKeyValue = null, CancellationToken cancellationToken = default) => - ExistsAsync(id, new PartitionKey(partitionKeyValue ?? id), cancellationToken); + ExistsAsync(id, BuildPartitionKey(partitionKeyValue ?? id), cancellationToken); /// public async ValueTask ExistsAsync( @@ -45,12 +45,29 @@ public async ValueTask ExistsAsync( public async ValueTask ExistsAsync( Expression> predicate, CancellationToken cancellationToken = default) + { + return await ExistsAsync(predicate, default, cancellationToken); + } + + //TODO: Write docs + public async ValueTask ExistsAsync( + Expression> predicate, + PartitionKey partitionKey, + CancellationToken cancellationToken = default) { Container container = await containerProvider.GetContainerAsync().ConfigureAwait(false); + var requestOptions = new QueryRequestOptions(); + + if (partitionKey != default) + { + requestOptions.PartitionKey = partitionKey; + } + IQueryable query = container.GetItemLinqQueryable( + requestOptions: requestOptions, linqSerializerOptions: optionsMonitor.CurrentValue.SerializationOptions) .Where(repositoryExpressionProvider.Build(predicate)); @@ -59,4 +76,12 @@ public async ValueTask ExistsAsync( var count = await cosmosQueryableProcessor.CountAsync(query, cancellationToken); return count > 0; } + + + //TODO: Write doc + public async ValueTask ExistsAsync(string id, IEnumerable partitionKeyValues, CancellationToken cancellationToken = default) + { + return await ExistsAsync(id, BuildPartitionKey(partitionKeyValues, id), cancellationToken); + } + } diff --git a/src/Microsoft.Azure.CosmosRepository/Repositories/DefaultRepository.Paging.cs b/src/Microsoft.Azure.CosmosRepository/Repositories/DefaultRepository.Paging.cs index 4bd2a486f..4f96d2ece 100644 --- a/src/Microsoft.Azure.CosmosRepository/Repositories/DefaultRepository.Paging.cs +++ b/src/Microsoft.Azure.CosmosRepository/Repositories/DefaultRepository.Paging.cs @@ -14,6 +14,29 @@ public async ValueTask> PageAsync( string? continuationToken = null, bool returnTotal = false, CancellationToken cancellationToken = default) + { + return await InternalPageAsync(predicate, default, pageSize, continuationToken, returnTotal, cancellationToken); + } + + //TODO: Write doc + public async ValueTask> PageAsync( + PartitionKey partitionKey, + Expression>? predicate = null, + int pageSize = 25, + string? continuationToken = null, + bool returnTotal = false, + CancellationToken cancellationToken = default) + { + return await InternalPageAsync(predicate, partitionKey, pageSize, continuationToken, returnTotal, cancellationToken); + } + + private async ValueTask> InternalPageAsync( + Expression>? predicate = null, + PartitionKey partitionKey = default, + int pageSize = 25, + string? continuationToken = null, + bool returnTotal = false, + CancellationToken cancellationToken = default) { Container container = await containerProvider.GetContainerAsync() .ConfigureAwait(false); @@ -23,6 +46,11 @@ public async ValueTask> PageAsync( MaxItemCount = pageSize }; + if (partitionKey != default) + { + options.PartitionKey = partitionKey; + } + IQueryable query = container .GetItemLinqQueryable( requestOptions: options, @@ -54,6 +82,17 @@ public async ValueTask> PageAsync( resultingContinuationToken); } + public async ValueTask> PageAsync( + PartitionKey partitionKey, + Expression>? predicate = null, + int pageNumber = 1, + int pageSize = 25, + bool returnTotal = false, + CancellationToken cancellationToken = default) + { + return await InternalPageAsync(predicate, partitionKey, pageNumber, pageSize, returnTotal, cancellationToken); + } + /// public async ValueTask> PageAsync( Expression>? predicate = null, @@ -61,12 +100,32 @@ public async ValueTask> PageAsync( int pageSize = 25, bool returnTotal = false, CancellationToken cancellationToken = default) + { + return await InternalPageAsync(predicate, default, pageNumber, pageSize, returnTotal, cancellationToken); + } + + + private async ValueTask> InternalPageAsync( + Expression>? predicate = null, + PartitionKey partitionKey = default, + int pageNumber = 1, + int pageSize = 25, + bool returnTotal = false, + CancellationToken cancellationToken = default) { Container container = await containerProvider.GetContainerAsync() .ConfigureAwait(false); + var options = new QueryRequestOptions(); + + if (partitionKey != default) + { + options.PartitionKey = partitionKey; + } + IQueryable query = container .GetItemLinqQueryable( + requestOptions: options, linqSerializerOptions: optionsMonitor.CurrentValue.SerializationOptions) .Where(repositoryExpressionProvider .Build(predicate ?? repositoryExpressionProvider.Default())); diff --git a/src/Microsoft.Azure.CosmosRepository/Repositories/DefaultRepository.Read.cs b/src/Microsoft.Azure.CosmosRepository/Repositories/DefaultRepository.Read.cs index 00438f95c..d327b66da 100644 --- a/src/Microsoft.Azure.CosmosRepository/Repositories/DefaultRepository.Read.cs +++ b/src/Microsoft.Azure.CosmosRepository/Repositories/DefaultRepository.Read.cs @@ -23,6 +23,40 @@ internal sealed partial class DefaultRepository } } + //TODO: Write doc + public async ValueTask TryGetAsync( + string id, + PartitionKey partitionKey, + CancellationToken cancellationToken = default) + { + try + { + return await GetAsync(id, partitionKey, cancellationToken); + } + catch (CosmosException e) when (e.StatusCode is HttpStatusCode.NotFound) + { + logger.LogItemNotFoundHandled(id, partitionKey.ToString() ?? id, e); + return default; + } + } + + //TODO: Write doc + public async ValueTask TryGetAsync( + string id, + IEnumerable partitionKeyValues, + CancellationToken cancellationToken = default) + { + try + { + return await GetAsync(id, partitionKeyValues, cancellationToken); + } + catch (CosmosException e) when (e.StatusCode is HttpStatusCode.NotFound) + { + logger.LogItemNotFoundHandled(id, partitionKeyValues ?? [id], e); + return default; + } + } + /// public ValueTask GetAsync( string id, @@ -30,6 +64,13 @@ public ValueTask GetAsync( CancellationToken cancellationToken = default) => GetAsync(id, new PartitionKey(partitionKeyValue ?? id), cancellationToken); + //TODO: Write doc + public ValueTask GetAsync( + string id, + IEnumerable partitionKeyValues, + CancellationToken cancellationToken = default) => + GetAsync(id, BuildPartitionKey(partitionKeyValues), cancellationToken); + /// public async ValueTask GetAsync( string id, @@ -58,16 +99,63 @@ await container.ReadItemAsync(id, partitionKey, cancellationToken: cancel return repositoryExpressionProvider.CheckItem(item); } + //TODO: Write doc + public async ValueTask> GetAsync( + PartitionKey partitionKey, + CancellationToken cancellationToken = default) + { + Container container = + await containerProvider.GetContainerAsync().ConfigureAwait(false); + + IQueryable query = container.GetItemLinqQueryable( + requestOptions: new QueryRequestOptions() { PartitionKey = partitionKey }, + linqSerializerOptions: optionsMonitor.CurrentValue.SerializationOptions); + + logger.LogQueryConstructed(query); + + (IEnumerable items, var charge) = + await cosmosQueryableProcessor.IterateAsync(query, cancellationToken); + + logger.LogQueryExecuted(query, charge); + + return items; + } + /// public async ValueTask> GetAsync( Expression> predicate, CancellationToken cancellationToken = default) + { + return await GetAsync(predicate, default, cancellationToken); + } + + //TODO: Write doc + public async ValueTask> GetAsync( + PartitionKey partitionKey, + Expression> predicate, + CancellationToken cancellationToken = default) + { + return await GetAsync(predicate, partitionKey, cancellationToken); + } + + private async ValueTask> GetAsync( + Expression> predicate, + PartitionKey partitionKey = default, + CancellationToken cancellationToken = default) { Container container = await containerProvider.GetContainerAsync().ConfigureAwait(false); + var requestOptions = new QueryRequestOptions(); + + if (partitionKey != default) + { + requestOptions.PartitionKey = partitionKey; + } + IQueryable query = container.GetItemLinqQueryable( + requestOptions: requestOptions, linqSerializerOptions: optionsMonitor.CurrentValue.SerializationOptions) .Where(repositoryExpressionProvider.Build(predicate)); diff --git a/src/Microsoft.Azure.CosmosRepository/Repositories/DefaultRepository.Specs.cs b/src/Microsoft.Azure.CosmosRepository/Repositories/DefaultRepository.Specs.cs index 47e5f6857..1c9686e8a 100644 --- a/src/Microsoft.Azure.CosmosRepository/Repositories/DefaultRepository.Specs.cs +++ b/src/Microsoft.Azure.CosmosRepository/Repositories/DefaultRepository.Specs.cs @@ -19,9 +19,11 @@ public async ValueTask QueryAsync( QueryRequestOptions options = new(); if (specification.UseContinuationToken) - { options.MaxItemCount = specification.PageSize; - } + + if (specification.PartitionKey is not null) + options.PartitionKey = specification.PartitionKey; + IQueryable query = container .GetItemLinqQueryable( @@ -32,6 +34,7 @@ public async ValueTask QueryAsync( query = specificationEvaluator.GetQuery(query, specification); + logger.LogQueryConstructed(query); (List items, var charge, var continuationToken) = diff --git a/src/Microsoft.Azure.CosmosRepository/Repositories/DefaultRepository.Update.cs b/src/Microsoft.Azure.CosmosRepository/Repositories/DefaultRepository.Update.cs index 4de7db98f..c94c7769b 100644 --- a/src/Microsoft.Azure.CosmosRepository/Repositories/DefaultRepository.Update.cs +++ b/src/Microsoft.Azure.CosmosRepository/Repositories/DefaultRepository.Update.cs @@ -2,6 +2,7 @@ // Licensed under the MIT License. // ReSharper disable once CheckNamespace + namespace Microsoft.Azure.CosmosRepository; internal sealed partial class DefaultRepository @@ -24,8 +25,10 @@ await containerProvider.GetContainerAsync() : valueWithEtag.Etag; } + PartitionKey partitionKey = BuildPartitionKey(value); + ItemResponse response = - await container.UpsertItemAsync(value, new PartitionKey(value.PartitionKey), options, + await container.UpsertItemAsync(value, partitionKey, options, cancellationToken) .ConfigureAwait(false); @@ -49,9 +52,48 @@ public async ValueTask> UpdateAsync( return updateTasks.Select(x => x.Result); } + //TODO: Write docs + public async ValueTask UpdateAsync(string id, + Action> builder, + string? etag = default, + CancellationToken cancellationToken = default) + { + await InternalUpdateAsync(id, builder, null, etag, cancellationToken); + } + + /// + public async ValueTask UpdateAsync(string id, + Action> builder, + string? partitionKeyValue, + string? etag = default, + CancellationToken cancellationToken = default) + { + await InternalUpdateAsync(id, builder, BuildPartitionKey(partitionKeyValue, id), etag, cancellationToken); + } + + //TODO: Write docs public async ValueTask UpdateAsync(string id, + IEnumerable partitionKeyValues, Action> builder, - string? partitionKeyValue = null, + string? etag = default, + CancellationToken cancellationToken = default) + { + await InternalUpdateAsync(id, builder, BuildPartitionKey(partitionKeyValues, id), etag, cancellationToken); + } + + //TODO: Write docs + public async ValueTask UpdateAsync(string id, + PartitionKey partitionKey, + Action> builder, + string? etag = null, + CancellationToken cancellationToken = default) + { + await InternalUpdateAsync(id, builder, partitionKey, etag, cancellationToken); + } + + private async ValueTask InternalUpdateAsync(string id, + Action> builder, + PartitionKey? partitionKey = null, string? etag = default, CancellationToken cancellationToken = default) { @@ -63,15 +105,13 @@ public async ValueTask UpdateAsync(string id, Container container = await containerProvider.GetContainerAsync(); - partitionKeyValue ??= id; - PatchItemRequestOptions patchItemRequestOptions = new(); if (etag != default && !string.IsNullOrWhiteSpace(etag)) { patchItemRequestOptions.IfMatchEtag = etag; } - await container.PatchItemAsync(id, new PartitionKey(partitionKeyValue), + await container.PatchItemAsync(id, partitionKey ?? new PartitionKey(id), patchOperationBuilder.PatchOperations, patchItemRequestOptions, cancellationToken); } } \ No newline at end of file diff --git a/src/Microsoft.Azure.CosmosRepository/Repositories/DefaultRepository.cs b/src/Microsoft.Azure.CosmosRepository/Repositories/DefaultRepository.cs index ca09bc816..b67d619a1 100644 --- a/src/Microsoft.Azure.CosmosRepository/Repositories/DefaultRepository.cs +++ b/src/Microsoft.Azure.CosmosRepository/Repositories/DefaultRepository.cs @@ -62,4 +62,100 @@ await iterator.ReadNextAsync(cancellationToken) return (results, charge, continuationToken); } + + /// + /// Builds a partition key from the first item in a list. This method assumes all items in the list belong to the same partition. + /// Throws an exception if the list is empty. + /// + /// A list of items from which the partition key is to be derived. + /// A PartitionKey object constructed from the first item in the list. + /// Thrown when the items list is empty. + internal static PartitionKey BuildPartitionKey(List items) + { + if (items.Count is 0) + { + throw new ArgumentException( + "Unable to perform batch operation with no items", + nameof(items)); + } + return BuildPartitionKey(items[0]); + } + + /// + /// Constructs a partition key from a collection of string values. If the collection is empty or null, + /// the method uses the defaultValue, if provided, to construct the PartitionKey. + /// + /// An IEnumerable collection of string values for constructing the partition key. + /// An optional default value used to construct the PartitionKey if values are null or empty. + /// Thrown when the number of provided partition key values exceeds 3. + /// A PartitionKey object constructed from the values or the defaultValue if values are empty or null. + internal static PartitionKey BuildPartitionKey(IEnumerable values, string? defaultValue = null) + { + var builder = new PartitionKeyBuilder(); + var keys = values?.ToList(); + if (keys is null or { Count: 0 }) + { + return !string.IsNullOrWhiteSpace(defaultValue) + ? new PartitionKey(defaultValue) + : default; + } + + if (keys?.Count > 3) + { + throw new ArgumentException( + "Unable to build partition key. The max allowed number of partition key values is 3.", + nameof(values)); + } + + + foreach (var value in values!) + { + builder.Add(value); + } + + return builder.Build(); + } + + /// + /// Creates a partition key from a single string value. + /// + /// The string value to use for constructing the partition key. + /// A PartitionKey object constructed from the provided string value. + internal static PartitionKey BuildPartitionKey(string? value, string? defaultValue = null) + { + if (string.IsNullOrWhiteSpace(value) && !string.IsNullOrWhiteSpace(defaultValue)) + { + return new PartitionKey(defaultValue); + } + return new PartitionKey(value); + } + + /// + /// Retrieves a partition key from an item by extracting its partition keys and using them to construct a new PartitionKey. + /// Throws an exception if the item is null. + /// + /// The item from which to extract the partition keys. + /// A PartitionKey object constructed from the item's partition keys. + /// Thrown when the provided item is null. + internal static PartitionKey BuildPartitionKey(TItem item) + { + if (item is null) + { + throw new ArgumentException( + "Unable to perform operation with null item", + nameof(item)); + } + + return BuildPartitionKey(item.PartitionKeys); + } + + public ValueTask UpdateAsync(string id, Action> builder, IEnumerable partitionKeyValues, string? etag = null, CancellationToken cancellationToken = default) + { + throw new NotImplementedException(); + } + + public ValueTask UpdateAsync(string id, Action> builder, PartitionKey partitionKey, string? etag = null, CancellationToken cancellationToken = default) + { + throw new NotImplementedException(); + } } \ No newline at end of file diff --git a/src/Microsoft.Azure.CosmosRepository/Repositories/IReadOnlyRepository.cs b/src/Microsoft.Azure.CosmosRepository/Repositories/IReadOnlyRepository.cs index 222596d26..88bc44e08 100644 --- a/src/Microsoft.Azure.CosmosRepository/Repositories/IReadOnlyRepository.cs +++ b/src/Microsoft.Azure.CosmosRepository/Repositories/IReadOnlyRepository.cs @@ -39,6 +39,18 @@ public interface IReadOnlyRepository where TItem : IItem string? partitionKeyValue = null, CancellationToken cancellationToken = default); + //TODO: Write doc + ValueTask TryGetAsync( + string id, + IEnumerable partitionKeyValues, + CancellationToken cancellationToken = default); + + //TODO: Write doc + ValueTask TryGetAsync( + string id, + PartitionKey partitionKey, + CancellationToken cancellationToken = default); + /// /// Gets the implementation class instance as a that corresponds to the given . /// @@ -54,6 +66,12 @@ ValueTask GetAsync( string? partitionKeyValue = null, CancellationToken cancellationToken = default); + //TODO: Write doc + ValueTask GetAsync( + string id, + IEnumerable partitionKeyValues, + CancellationToken cancellationToken = default); + /// /// Gets the implementation class instance as a that corresponds to the given . /// @@ -69,6 +87,11 @@ ValueTask GetAsync( PartitionKey partitionKey, CancellationToken cancellationToken = default); + //TODO: Write doc + ValueTask> GetAsync( + PartitionKey partitionKey, + CancellationToken cancellationToken = default); + /// /// Gets an collection of /// implementation classes that match the given . @@ -83,6 +106,12 @@ ValueTask> GetAsync( Expression> predicate, CancellationToken cancellationToken = default); + //TODO: Write doc + ValueTask> GetAsync( + PartitionKey partitionKey, + Expression> predicate, + CancellationToken cancellationToken = default); + /// /// Gets an collection of /// by the given Cosmos SQL query @@ -118,6 +147,12 @@ ValueTask ExistsAsync( string? partitionKeyValue = null, CancellationToken cancellationToken = default); + //TODO: Write doc + ValueTask ExistsAsync( + string id, + IEnumerable partitionKeyValues, + CancellationToken cancellationToken = default); + /// /// Queries cosmos DB to see if an item exists. /// @@ -142,6 +177,12 @@ ValueTask ExistsAsync( Expression> predicate, CancellationToken cancellationToken = default); + //TODO: Write doc + ValueTask ExistsAsync( + Expression> predicate, + PartitionKey partitionKey, + CancellationToken cancellationToken = default); + /// /// Queries cosmos DB to obtain the count of items. /// @@ -166,22 +207,21 @@ ValueTask CountAsync( Expression> predicate, CancellationToken cancellationToken = default); - /// - /// Offers a load more paging implementation for infinite scroll scenarios. - /// Allows for efficient paging making use of cosmos DBs continuation tokens, making this implementation cost effective. - /// - /// A filter criteria for the paging operation, if null it will get all s - /// The size of the page to return from cosmos db. - /// The token returned from a previous query, if null starts at the beginning of the data - /// Specifies whether or not to return the total number of items that matched the query. This defaults to false as it can be a very expensive operation. - /// The cancellation token to use when making asynchronous operations. - /// An of s - /// This method makes use of cosmos dbs continuation tokens for efficient, cost effective paging utilising low RUs - ValueTask> PageAsync( - Expression>? predicate = null, - int pageSize = 25, - string? continuationToken = null, - bool returnTotal = false, + //TODO: Write doc + ValueTask CountAsync( + Expression> predicate, + IEnumerable partitionKeyValues, + CancellationToken cancellationToken = default); + + //TODO: Write doc + ValueTask CountAsync( + Expression> predicate, + PartitionKey partitionKey, + CancellationToken cancellationToken = default); + + //TODO: Write doc + ValueTask CountAsync( + PartitionKey partitionKey, CancellationToken cancellationToken = default); /// @@ -220,6 +260,43 @@ ValueTask> PageAsync( bool returnTotal = false, CancellationToken cancellationToken = default); + //TODO: Write doc + ValueTask> PageAsync( + PartitionKey partitionKey, + Expression>? predicate = null, + int pageNumber = 1, + int pageSize = 25, + bool returnTotal = false, + CancellationToken cancellationToken = default); + + /// + /// Offers a load more paging implementation for infinite scroll scenarios. + /// Allows for efficient paging making use of cosmos DBs continuation tokens, making this implementation cost effective. + /// + /// A filter criteria for the paging operation, if null it will get all s + /// The size of the page to return from cosmos db. + /// The token returned from a previous query, if null starts at the beginning of the data + /// Specifies whether or not to return the total number of items that matched the query. This defaults to false as it can be a very expensive operation. + /// The cancellation token to use when making asynchronous operations. + /// An of s + /// This method makes use of cosmos dbs continuation tokens for efficient, cost effective paging utilising low RUs + ValueTask> PageAsync( + Expression>? predicate = null, + int pageSize = 25, + string? continuationToken = null, + bool returnTotal = false, + CancellationToken cancellationToken = default); + + //TODO: Write doc + ValueTask> PageAsync( + PartitionKey partitionKey, + Expression>? predicate = null, + int pageSize = 25, + string? continuationToken = null, + bool returnTotal = false, + CancellationToken cancellationToken = default); + + #if NET7_0_OR_GREATER /// /// Wraps the existing paging support to return an @@ -232,6 +309,7 @@ ValueTask> PageAsync( /// This method makes use of Cosmos DB's continuation tokens for efficient, cost effective paging utilizing low RUs async IAsyncEnumerable PageAsync( Expression>? predicate = null, + PartitionKey partitionKey = default, int limit = 1_000, [EnumeratorCancellation] CancellationToken cancellationToken = default) { @@ -243,6 +321,7 @@ async IAsyncEnumerable PageAsync( && cancellationToken.IsCancellationRequested is false) { IPageQueryResult page = await PageAsync( + partitionKey, predicate, pageNumber: ++ currentPage, 25, diff --git a/src/Microsoft.Azure.CosmosRepository/Repositories/IWriteOnlyRepository.cs b/src/Microsoft.Azure.CosmosRepository/Repositories/IWriteOnlyRepository.cs index 3c0aa18ad..d048bf036 100644 --- a/src/Microsoft.Azure.CosmosRepository/Repositories/IWriteOnlyRepository.cs +++ b/src/Microsoft.Azure.CosmosRepository/Repositories/IWriteOnlyRepository.cs @@ -81,7 +81,53 @@ ValueTask> UpdateAsync( ValueTask UpdateAsync( string id, Action> builder, - string? partitionKeyValue = null, + string partitionKeyValue, + string? etag = default, + CancellationToken cancellationToken = default); + + /// + /// Updates the given cosmos item using the provided and supported patch operations. + /// + /// The string identifier. + /// The that will define the update operations to perform. + /// The cancellation token to use when making asynchronous operations. + /// Indicate to set IfMatchEtag in the ItemRequestOptions in the underlying Cosmos call. This requires TItem to implement the IItemWithEtag interface. + /// A representing the asynchronous operation. + ValueTask UpdateAsync( + string id, + Action> builder, + string? etag = default, + CancellationToken cancellationToken = default); + + /// + /// Updates the given cosmos item using the provided and supported patch operations. + /// + /// The string identifier. + /// The partition key values if different than the . + /// The that will define the update operations to perform. + /// The cancellation token to use when making asynchronous operations. + /// Indicate to set IfMatchEtag in the ItemRequestOptions in the underlying Cosmos call. This requires TItem to implement the IItemWithEtag interface. + /// A representing the asynchronous operation. + ValueTask UpdateAsync( + string id, + Action> builder, + IEnumerable partitionKeyValues, + string? etag = default, + CancellationToken cancellationToken = default); + + /// + /// Updates the given cosmos item using the provided and supported patch operations. + /// + /// The string identifier. + /// The partition key if different than the . + /// The that will define the update operations to perform. + /// The cancellation token to use when making asynchronous operations. + /// Indicate to set IfMatchEtag in the ItemRequestOptions in the underlying Cosmos call. This requires TItem to implement the IItemWithEtag interface. + /// A representing the asynchronous operation. + ValueTask UpdateAsync( + string id, + Action> builder, + PartitionKey partitionKey, string? etag = default, CancellationToken cancellationToken = default); @@ -107,6 +153,12 @@ ValueTask DeleteAsync( string? partitionKeyValue = null, CancellationToken cancellationToken = default); + //TODO: Write docs + ValueTask DeleteAsync( + string id, + IEnumerable partitionKeyValues, + CancellationToken cancellationToken = default); + /// /// Deletes the cosmos object that corresponds to the given . /// diff --git a/src/Microsoft.Azure.CosmosRepository/Repositories/InMemoryRepository.Update.cs b/src/Microsoft.Azure.CosmosRepository/Repositories/InMemoryRepository.Update.cs index 4fd45f65d..210ce44e2 100644 --- a/src/Microsoft.Azure.CosmosRepository/Repositories/InMemoryRepository.Update.cs +++ b/src/Microsoft.Azure.CosmosRepository/Repositories/InMemoryRepository.Update.cs @@ -70,7 +70,7 @@ public async ValueTask> UpdateAsync(IEnumerable values /// public async ValueTask UpdateAsync(string id, Action> builder, - string? partitionKeyValue = null, + string partitionKeyValue, string? etag = default, CancellationToken cancellationToken = default) { @@ -80,8 +80,6 @@ public async ValueTask UpdateAsync(string id, await Task.CompletedTask; #endif - partitionKeyValue ??= id; - TItem? item = InMemoryStorage .GetValues() .Select(DeserializeItem) diff --git a/src/Microsoft.Azure.CosmosRepository/Repositories/InMemoryRepository.cs b/src/Microsoft.Azure.CosmosRepository/Repositories/InMemoryRepository.cs index 8d9d2366f..8e3cb21fe 100644 --- a/src/Microsoft.Azure.CosmosRepository/Repositories/InMemoryRepository.cs +++ b/src/Microsoft.Azure.CosmosRepository/Repositories/InMemoryRepository.cs @@ -3,6 +3,7 @@ // ReSharper disable once CheckNamespace + namespace Microsoft.Azure.CosmosRepository; /// @@ -22,4 +23,84 @@ public InMemoryRepository(ISpecificationEvaluator specificationEvaluator) => private void NotFound() => throw new CosmosException(string.Empty, HttpStatusCode.NotFound, 0, string.Empty, 0); private void Conflict() => throw new CosmosException(string.Empty, HttpStatusCode.Conflict, 0, string.Empty, 0); + + public ValueTask TryGetAsync(string id, IEnumerable partitionKeyValues, CancellationToken cancellationToken = default) + { + throw new NotImplementedException(); + } + + public ValueTask TryGetAsync(string id, PartitionKey partitionKey, CancellationToken cancellationToken = default) + { + throw new NotImplementedException(); + } + + public ValueTask GetAsync(string id, IEnumerable partitionKeyValues, CancellationToken cancellationToken = default) + { + throw new NotImplementedException(); + } + + public ValueTask> GetAsync(PartitionKey partitionKey, CancellationToken cancellationToken = default) + { + throw new NotImplementedException(); + } + + public ValueTask> GetAsync(PartitionKey partitionKey, Expression> predicate, CancellationToken cancellationToken = default) + { + throw new NotImplementedException(); + } + + public ValueTask ExistsAsync(string id, IEnumerable partitionKeyValues, CancellationToken cancellationToken = default) + { + throw new NotImplementedException(); + } + + public ValueTask ExistsAsync(Expression> predicate, PartitionKey partitionKey, CancellationToken cancellationToken = default) + { + throw new NotImplementedException(); + } + + public ValueTask CountAsync(Expression> predicate, IEnumerable partitionKeyValues, CancellationToken cancellationToken = default) + { + throw new NotImplementedException(); + } + + public ValueTask CountAsync(Expression> predicate, PartitionKey partitionKey, CancellationToken cancellationToken = default) + { + throw new NotImplementedException(); + } + + public ValueTask CountAsync(PartitionKey partitionKey, CancellationToken cancellationToken = default) + { + throw new NotImplementedException(); + } + + public ValueTask> PageAsync(PartitionKey partitionKey, Expression>? predicate = null, int pageNumber = 1, int pageSize = 25, bool returnTotal = false, CancellationToken cancellationToken = default) + { + throw new NotImplementedException(); + } + + public ValueTask> PageAsync(PartitionKey partitionKey, Expression>? predicate = null, int pageSize = 25, string? continuationToken = null, bool returnTotal = false, CancellationToken cancellationToken = default) + { + throw new NotImplementedException(); + } + + public ValueTask UpdateAsync(string id, Action> builder, string? etag = null, CancellationToken cancellationToken = default) + { + throw new NotImplementedException(); + } + + public ValueTask UpdateAsync(string id, Action> builder, IEnumerable partitionKeyValues, string? etag = null, CancellationToken cancellationToken = default) + { + throw new NotImplementedException(); + } + + public ValueTask UpdateAsync(string id, Action> builder, PartitionKey partitionKey, string? etag = null, CancellationToken cancellationToken = default) + { + throw new NotImplementedException(); + } + + public ValueTask DeleteAsync(string id, IEnumerable partitionKeyValues, CancellationToken cancellationToken = default) + { + throw new NotImplementedException(); + } } \ No newline at end of file diff --git a/src/Microsoft.Azure.CosmosRepository/Services/DefaultCosmosContainerService.cs b/src/Microsoft.Azure.CosmosRepository/Services/DefaultCosmosContainerService.cs index 6adad2b61..9268a47ae 100644 --- a/src/Microsoft.Azure.CosmosRepository/Services/DefaultCosmosContainerService.cs +++ b/src/Microsoft.Azure.CosmosRepository/Services/DefaultCosmosContainerService.cs @@ -45,11 +45,15 @@ private async Task GetContainerAsync(Type itemType, bool forceContain Id = _options.ContainerPerItemType ? itemConfiguration.ContainerName : _options.ContainerId, - PartitionKeyPath = itemConfiguration.PartitionKeyPath, UniqueKeyPolicy = itemConfiguration.UniqueKeyPolicy ?? new(), DefaultTimeToLive = itemConfiguration.DefaultTimeToLive }; + if (itemConfiguration.PartitionKeyPaths.Count() > 1) + containerProperties.PartitionKeyPaths = itemConfiguration.PartitionKeyPaths.ToList(); + else + containerProperties.PartitionKeyPath = itemConfiguration.PartitionKeyPaths.Last(); + Container container = _options.IsAutoResourceCreationIfNotExistsEnabled ? await database.CreateContainerIfNotExistsAsync( diff --git a/src/Microsoft.Azure.CosmosRepository/Specification/BaseSpecification.cs b/src/Microsoft.Azure.CosmosRepository/Specification/BaseSpecification.cs index 931b90f2c..6cd9e3ac1 100644 --- a/src/Microsoft.Azure.CosmosRepository/Specification/BaseSpecification.cs +++ b/src/Microsoft.Azure.CosmosRepository/Specification/BaseSpecification.cs @@ -43,6 +43,9 @@ internal void Add(OrderExpressionInfo expression) => /// public int? PageNumber { get; internal set; } + //TODO: Write doc + public PartitionKey? PartitionKey { get; internal set; } + /// public int PageSize { get; internal set; } = 25; diff --git a/src/Microsoft.Azure.CosmosRepository/Specification/Builder/ISpecificationBuilder.cs b/src/Microsoft.Azure.CosmosRepository/Specification/Builder/ISpecificationBuilder.cs index 4fea5cc9a..dcf0499e6 100644 --- a/src/Microsoft.Azure.CosmosRepository/Specification/Builder/ISpecificationBuilder.cs +++ b/src/Microsoft.Azure.CosmosRepository/Specification/Builder/ISpecificationBuilder.cs @@ -58,4 +58,11 @@ public interface ISpecificationBuilder /// The token used by Cosmos DB to provide efficient, cost effective paging. /// An instance of a ISpecificationBuilder ContinuationToken(string continuationToken); + + /// + /// Sets the partition key for the query. + /// + /// The partition key + /// An instance of a + ISpecificationBuilder PartitionKey(PartitionKey partitionKey); } diff --git a/src/Microsoft.Azure.CosmosRepository/Specification/Builder/SpecificationBuilder.cs b/src/Microsoft.Azure.CosmosRepository/Specification/Builder/SpecificationBuilder.cs index 1579bedca..0892a89d2 100644 --- a/src/Microsoft.Azure.CosmosRepository/Specification/Builder/SpecificationBuilder.cs +++ b/src/Microsoft.Azure.CosmosRepository/Specification/Builder/SpecificationBuilder.cs @@ -45,7 +45,7 @@ public ISpecificationBuilder PageSize(int pageSize) return this; } - + /// public ISpecificationBuilder PageNumber(int pageNumber) { Specification.PageNumber = pageNumber; @@ -64,4 +64,11 @@ public ISpecificationBuilder ContinuationToken(string continuati Specification.ContinuationToken = continuationToken; return this; } + + /// + public ISpecificationBuilder PartitionKey(PartitionKey partitionKey) + { + Specification.PartitionKey = partitionKey; + return this; + } } \ No newline at end of file diff --git a/src/Microsoft.Azure.CosmosRepository/Specification/ISpecification.cs b/src/Microsoft.Azure.CosmosRepository/Specification/ISpecification.cs index 20d1ed7a2..8083125a2 100644 --- a/src/Microsoft.Azure.CosmosRepository/Specification/ISpecification.cs +++ b/src/Microsoft.Azure.CosmosRepository/Specification/ISpecification.cs @@ -38,6 +38,8 @@ TResult PostProcessingAction( /// int? PageNumber { get; } + //TODO: Write doc + public PartitionKey? PartitionKey { get; } /// /// Paginate results, selects how many results should be returned diff --git a/tests/Microsoft.Azure.CosmosEventSourcingAcceptanceTests/Microsoft.Azure.CosmosEventSourcingAcceptanceTests.csproj b/tests/Microsoft.Azure.CosmosEventSourcingAcceptanceTests/Microsoft.Azure.CosmosEventSourcingAcceptanceTests.csproj index 15aada29f..f0c6b64e2 100644 --- a/tests/Microsoft.Azure.CosmosEventSourcingAcceptanceTests/Microsoft.Azure.CosmosEventSourcingAcceptanceTests.csproj +++ b/tests/Microsoft.Azure.CosmosEventSourcingAcceptanceTests/Microsoft.Azure.CosmosEventSourcingAcceptanceTests.csproj @@ -28,4 +28,14 @@ + + + Always + + + + + $(NoWarn);NU1507 + + diff --git a/tests/Microsoft.Azure.CosmosEventSourcingTests/Microsoft.Azure.CosmosEventSourcingTests.csproj b/tests/Microsoft.Azure.CosmosEventSourcingTests/Microsoft.Azure.CosmosEventSourcingTests.csproj index 828ca6953..eaa2071e3 100644 --- a/tests/Microsoft.Azure.CosmosEventSourcingTests/Microsoft.Azure.CosmosEventSourcingTests.csproj +++ b/tests/Microsoft.Azure.CosmosEventSourcingTests/Microsoft.Azure.CosmosEventSourcingTests.csproj @@ -5,6 +5,7 @@ enable false True + NU5125;NU1507 diff --git a/tests/Microsoft.Azure.CosmosEventSourcingTests/Stores/EventStoreTests.ReadAggregate.cs b/tests/Microsoft.Azure.CosmosEventSourcingTests/Stores/EventStoreTests.ReadAggregate.cs index 25499b15d..ea7321905 100644 --- a/tests/Microsoft.Azure.CosmosEventSourcingTests/Stores/EventStoreTests.ReadAggregate.cs +++ b/tests/Microsoft.Azure.CosmosEventSourcingTests/Stores/EventStoreTests.ReadAggregate.cs @@ -7,6 +7,7 @@ using System.Runtime.CompilerServices; using System.Threading.Tasks; using FluentAssertions; +using Microsoft.Azure.Cosmos; using Microsoft.Azure.CosmosEventSourcing.Aggregates; using Microsoft.Azure.CosmosEventSourcing.Events; using Microsoft.Azure.CosmosEventSourcing.Exceptions; @@ -95,7 +96,7 @@ public async Task ReadAggregateAsync_AggregateWithReplayMethod_ReplaysEvents() repository .Setup(o => - o.GetAsync(x => x.PartitionKey == "A", default)) + o.GetAsync(new PartitionKey("A"), default)) .ReturnsAsync(events); //Act @@ -124,7 +125,7 @@ public async Task ReadAggregateAsync_AggregateMapper_MapsAggregateCorrectly() repository .Setup(o => - o.GetAsync(x => x.PartitionKey == "A", default)) + o.GetAsync(new PartitionKey("A"), default)) .ReturnsAsync(events); //Act diff --git a/tests/Microsoft.Azure.CosmosEventSourcingTests/Stores/EventStoreTests.cs b/tests/Microsoft.Azure.CosmosEventSourcingTests/Stores/EventStoreTests.cs index 394cc3c54..47d5aa76a 100644 --- a/tests/Microsoft.Azure.CosmosEventSourcingTests/Stores/EventStoreTests.cs +++ b/tests/Microsoft.Azure.CosmosEventSourcingTests/Stores/EventStoreTests.cs @@ -7,6 +7,7 @@ using System.Runtime.CompilerServices.Context; using System.Threading.Tasks; using FluentAssertions; +using Microsoft.Azure.Cosmos; using Microsoft.Azure.CosmosEventSourcing.Events; using Microsoft.Azure.CosmosEventSourcing.Exceptions; using Microsoft.Azure.CosmosEventSourcing.Stores; @@ -141,7 +142,7 @@ public async Task GetAsync_EventsInDb_GetsAllEvents() _readonlyRepository .Setup(o => - o.GetAsync(x => x.PartitionKey == Pk, default)) + o.GetAsync(new PartitionKey(Pk), default)) .ReturnsAsync(_eventItemsWithAtomicEvents); //Act @@ -179,7 +180,8 @@ public async Task StreamAsync_EventsInDb_StreamsAllEvents() _readonlyRepository .SetupSequence(o => o.PageAsync( - x => x.PartitionKey == Pk, + new PartitionKey(Pk), + default, 5, It.IsAny(), false, diff --git a/tests/Microsoft.Azure.CosmosRepositoryTests/DefaultRepositoryFactoryTests.cs b/tests/Microsoft.Azure.CosmosRepositoryTests/DefaultRepositoryFactoryTests.cs index 9999e61f2..c74cd5ba8 100644 --- a/tests/Microsoft.Azure.CosmosRepositoryTests/DefaultRepositoryFactoryTests.cs +++ b/tests/Microsoft.Azure.CosmosRepositoryTests/DefaultRepositoryFactoryTests.cs @@ -1,6 +1,7 @@ // Copyright (c) David Pine. All rights reserved. // Licensed under the MIT License. + namespace Microsoft.Azure.CosmosRepositoryTests; public class DefaultRepositoryFactoryTests @@ -78,6 +79,8 @@ public abstract class CustomEntityBase : IItem string IItem.PartitionKey => GetPartitionKeyValue(); + IEnumerable IItem.PartitionKeys => [GetPartitionKeyValue()]; + public CustomEntityBase() => Type = GetType().Name; protected virtual string GetPartitionKeyValue() => Id; diff --git a/tests/Microsoft.Azure.CosmosRepositoryTests/DefaultRepositoryTests.cs b/tests/Microsoft.Azure.CosmosRepositoryTests/DefaultRepositoryTests.cs index 6b5a5bae7..ca7cede43 100644 --- a/tests/Microsoft.Azure.CosmosRepositoryTests/DefaultRepositoryTests.cs +++ b/tests/Microsoft.Azure.CosmosRepositoryTests/DefaultRepositoryTests.cs @@ -55,7 +55,7 @@ public async Task GetAsyncGivenExpressionQueriesContainerCorrectly() _container .Setup(o => o.GetItemLinqQueryable( - false, null, null, It.IsAny())) + false, null, It.IsAny(), It.IsAny())) .Returns(queryable); _queryableProcessor.Setup(o => o.IterateAsync(queryable, It.IsAny())) @@ -80,6 +80,8 @@ public async Task ExistsAsyncGivenExpressionQueriesContainerCorrectly() new(){Id = "ab"} }; + QueryRequestOptions options = new (); + Expression> predicate = item => item.Id == "a" || item.Id == "ab"; Expression> expectedPredicate = _expressionProvider.Build(predicate)!; @@ -89,7 +91,7 @@ public async Task ExistsAsyncGivenExpressionQueriesContainerCorrectly() _container .Setup(o => o.GetItemLinqQueryable( - false, null, null, It.IsAny())) + false, null, It.IsAny(), It.IsAny())) .Returns(queryable); _queryableProcessor.Setup(o => o.CountAsync(queryable, It.IsAny())) @@ -414,7 +416,7 @@ public async Task UpdateAsync_Patch_WhenEtagIsEmpty_UseDefaultEtagValueInPatchOp _containerProviderForTestItemWithETag.Setup(cp => cp.GetContainerAsync()).ReturnsAsync(_container.Object); // Act - await RepositoryForItemWithETag.UpdateAsync(id, _ => { }, null, etag, default); + await RepositoryForItemWithETag.UpdateAsync(id, _ => { }, etag, default); // Assert _container.Verify( @@ -433,7 +435,7 @@ public async Task UpdateAsync_Patch_WhenEtagIsWhiteSpace_UseDefaultEtagValueInPa _containerProviderForTestItemWithETag.Setup(cp => cp.GetContainerAsync()).ReturnsAsync(_container.Object); // Act - await RepositoryForItemWithETag.UpdateAsync(id, _ => { }, null, etag, default); + await RepositoryForItemWithETag.UpdateAsync(id, _ => { }, etag, default); // Assert _container.Verify( @@ -452,7 +454,7 @@ public async Task UpdateAsync_Patch_WhenEtagIsSet_UseSetEtagValueInPatchOptions( _containerProviderForTestItemWithETag.Setup(cp => cp.GetContainerAsync()).ReturnsAsync(_container.Object); // Act - await RepositoryForItemWithETag.UpdateAsync(id, _ => { }, null, etag, default); + await RepositoryForItemWithETag.UpdateAsync(id, _ => { }, etag, default); // Assert _container.Verify( diff --git a/tests/Microsoft.Azure.CosmosRepositoryTests/InMemoryRepositoryTests.cs b/tests/Microsoft.Azure.CosmosRepositoryTests/InMemoryRepositoryTests.cs index f64a22bc3..8249b48cc 100644 --- a/tests/Microsoft.Azure.CosmosRepositoryTests/InMemoryRepositoryTests.cs +++ b/tests/Microsoft.Azure.CosmosRepositoryTests/InMemoryRepositoryTests.cs @@ -827,7 +827,7 @@ public async Task UpdateAsync_PropertiesToPatch_UpdatesValues() InMemoryStorage.GetDictionary().TryAddAsJson(dog.Id, dog); //Act - await _dogRepository.UpdateAsync(dog.Id, builder => builder.Replace(d => d.Name, "kenny"), dog.Breed); + await _dogRepository.UpdateAsync(dog.Id, builder => builder.Replace(d => d.Name, "kenny"), dog.Breed, default, default); //Assert Dog addedDog = await _dogRepository.GetAsync(dog.Id, dog.Breed); @@ -851,7 +851,7 @@ public async Task UpdateAsync_PropertiesToPatch_WhenEtagMatches_UpdatesValues() InMemoryStorage.GetDictionary().TryAddAsJson(person.Id, person); //Act - await _personRepository.UpdateAsync(person.Id, builder => builder.Replace(p => p.Name, "kenny"), default, + await _personRepository.UpdateAsync(person.Id, builder => builder.Replace(p => p.Name, "kenny"), originalEtag, default); //Assert @@ -874,8 +874,7 @@ public async Task UpdateAsync_PropertiesToPatch_WhenEtagIsNull_UpdatesValues() InMemoryStorage.GetDictionary().TryAddAsJson(person.Id, person); //Act - await _personRepository.UpdateAsync(person.Id, builder => builder.Replace(p => p.Name, "kenny"), default, null, - default); + await _personRepository.UpdateAsync(person.Id, builder => builder.Replace(p => p.Name, "kenny")); //Assert Person updatedPerson = _personRepository.DeserializeItem(InMemoryStorage.GetDictionary().First().Value); @@ -898,7 +897,7 @@ public async Task UpdateAsync_PropertiesToPatch_WhenEtagIsEmpty_UpdatesValues() InMemoryStorage.GetDictionary().TryAddAsJson(person.Id, person); //Act - await _personRepository.UpdateAsync(person.Id, builder => builder.Replace(p => p.Name, "kenny"), default, + await _personRepository.UpdateAsync(person.Id, builder => builder.Replace(p => p.Name, "kenny"), string.Empty, default); //Assert @@ -921,7 +920,7 @@ public async Task UpdateAsync_PropertiesToPatch_WhenEtagIsWhiteSpace_UpdatesValu InMemoryStorage.GetDictionary().TryAddAsJson(person.Id, person); //Act - await _personRepository.UpdateAsync(person.Id, builder => builder.Replace(p => p.Name, "kenny"), default, + await _personRepository.UpdateAsync(person.Id, builder => builder.Replace(p => p.Name, "kenny"), " ", default); //Assert @@ -945,7 +944,7 @@ public async Task UpdateAsync_PropertiesToPatch_WhenEtagDoesNotMatch_ThrowsCosmo //Act & Assert CosmosException cosmosException = await Assert.ThrowsAsync(() => - _personRepository.UpdateAsync(person.Id, builder => builder.Replace(p => p.Name, "kenny"), default, + _personRepository.UpdateAsync(person.Id, builder => builder.Replace(p => p.Name, "kenny"), Guid.NewGuid().ToString(), default).AsTask()); Assert.Equal(HttpStatusCode.PreconditionFailed, cosmosException.StatusCode); diff --git a/tests/Microsoft.Azure.CosmosRepositoryTests/Microsoft.Azure.CosmosRepositoryTests.csproj b/tests/Microsoft.Azure.CosmosRepositoryTests/Microsoft.Azure.CosmosRepositoryTests.csproj index 71c0e4bd1..a4099a681 100644 --- a/tests/Microsoft.Azure.CosmosRepositoryTests/Microsoft.Azure.CosmosRepositoryTests.csproj +++ b/tests/Microsoft.Azure.CosmosRepositoryTests/Microsoft.Azure.CosmosRepositoryTests.csproj @@ -28,4 +28,7 @@ + + $(NoWarn);NU1507 + diff --git a/tests/Microsoft.Azure.CosmosRepositoryTests/Options/RepositoryOptionsTests.cs b/tests/Microsoft.Azure.CosmosRepositoryTests/Options/RepositoryOptionsTests.cs index 541e01355..e84d53fc1 100644 --- a/tests/Microsoft.Azure.CosmosRepositoryTests/Options/RepositoryOptionsTests.cs +++ b/tests/Microsoft.Azure.CosmosRepositoryTests/Options/RepositoryOptionsTests.cs @@ -17,7 +17,7 @@ public void RepositoryOptionsBuilderConfiguresItemCorrectly() Assert.Single(options.ContainerOptions); Assert.Equal("products", options.ContainerOptions[0].Name); - Assert.Equal("/category", options.ContainerOptions[0].PartitionKey); + Assert.Equal("/category", options.ContainerOptions[0].PartitionKeys?[0]); } [Fact] diff --git a/tests/Microsoft.Azure.CosmosRepositoryTests/Providers/DefaultCosmosClientProviderTests.cs b/tests/Microsoft.Azure.CosmosRepositoryTests/Providers/DefaultCosmosClientProviderTests.cs index 353677d64..804ec71a6 100644 --- a/tests/Microsoft.Azure.CosmosRepositoryTests/Providers/DefaultCosmosClientProviderTests.cs +++ b/tests/Microsoft.Azure.CosmosRepositoryTests/Providers/DefaultCosmosClientProviderTests.cs @@ -1,4 +1,4 @@ -// Copyright (c) David Pine. All rights reserved. +// Copyright (c) David Pine. All rights reserved. // Licensed under the MIT License. namespace Microsoft.Azure.CosmosRepositoryTests.Providers; @@ -68,4 +68,4 @@ public async Task DefaultCosmosClientProviderCorrectlyDisposesOverloadWithTokenC Assert.IsType(ex.GetBaseException()); } } -} +} \ No newline at end of file diff --git a/tests/Microsoft.Azure.CosmosRepositoryTests/Providers/DefaultCosmosItemConfigurationProviderTests.cs b/tests/Microsoft.Azure.CosmosRepositoryTests/Providers/DefaultCosmosItemConfigurationProviderTests.cs index 83462e489..706514cd9 100644 --- a/tests/Microsoft.Azure.CosmosRepositoryTests/Providers/DefaultCosmosItemConfigurationProviderTests.cs +++ b/tests/Microsoft.Azure.CosmosRepositoryTests/Providers/DefaultCosmosItemConfigurationProviderTests.cs @@ -29,7 +29,7 @@ public void GetOptionsAlwaysGetOptionsForItem() var throughputProperties = ThroughputProperties.CreateAutoscaleThroughput(400); _containerNameProvider.Setup(o => o.GetContainerName(typeof(Item1))).Returns(t => t.FullName!); - _partitionKeyPathProvider.Setup(o => o.GetPartitionKeyPath(typeof(Item1))).Returns("/id"); + _partitionKeyPathProvider.Setup(o => o.GetPartitionKeyPaths(typeof(Item1))).Returns(["/id"]); _uniqueKeyPolicyProvider.Setup(o => o.GetUniqueKeyPolicy(typeof(Item1))).Returns(uniqueKeyPolicy); _defaultTimeToLiveProvider.Setup(o => o.GetDefaultTimeToLive(typeof(Item1))).Returns(10); _syncContainerPropertiesProvider.Setup(o => o.GetWhetherToSyncContainerProperties(typeof(Item1))).Returns(true); @@ -38,7 +38,7 @@ public void GetOptionsAlwaysGetOptionsForItem() ItemConfiguration configuration = provider.GetItemConfiguration(); Assert.Equal(typeof(Item1).FullName, configuration.ContainerName); - Assert.Equal("/id", configuration.PartitionKeyPath); + Assert.Equal("/id", configuration.PartitionKeyPaths.Last()); Assert.Equal(uniqueKeyPolicy, configuration.UniqueKeyPolicy); Assert.Equal(10, configuration.DefaultTimeToLive); Assert.True(configuration.SyncContainerProperties); diff --git a/tests/Microsoft.Azure.CosmosRepositoryTests/Providers/DefaultCosmosPartitionKeyPathProviderTests.cs b/tests/Microsoft.Azure.CosmosRepositoryTests/Providers/DefaultCosmosPartitionKeyPathProviderTests.cs index 79b21d974..306e2f1d5 100644 --- a/tests/Microsoft.Azure.CosmosRepositoryTests/Providers/DefaultCosmosPartitionKeyPathProviderTests.cs +++ b/tests/Microsoft.Azure.CosmosRepositoryTests/Providers/DefaultCosmosPartitionKeyPathProviderTests.cs @@ -24,8 +24,11 @@ public void CosmosCosmosPartitionKeyPathProviderCorrectlyGetsPathWhenOptionsAreD ICosmosPartitionKeyPathProvider provider = new DefaultCosmosPartitionKeyPathProvider(_options.Object); - var path = provider.GetPartitionKeyPath(); - Assert.Equal("/firstName", path); + var paths = provider.GetPartitionKeyPaths(); + + Assert.NotEmpty(paths); + Assert.Single(paths); + Assert.Equal("/firstName", paths.First()); } [Fact] @@ -35,8 +38,10 @@ public void CosmosCosmosPartitionKeyPathProviderCorrectlyGetsPathWhenOptionsAreD ICosmosPartitionKeyPathProvider provider = new DefaultCosmosPartitionKeyPathProvider(_options.Object); - var path = provider.GetPartitionKeyPath(); - Assert.Equal("/email", path); + var paths = provider.GetPartitionKeyPaths(); + Assert.NotEmpty(paths); + Assert.Single(paths); + Assert.Equal("/email", paths.First()); } [Fact] @@ -44,8 +49,11 @@ public void CosmosPartitionKeyPathProviderCorrectlyGetsPathWhenAttributeIsDefine { ICosmosPartitionKeyPathProvider provider = new DefaultCosmosPartitionKeyPathProvider(_options.Object); - var path = provider.GetPartitionKeyPath(); - Assert.Equal("/pickles", path); + var paths = provider.GetPartitionKeyPaths(); + + Assert.NotEmpty(paths); + Assert.Single(paths); + Assert.Equal("/pickles", paths.First()); Assert.Equal("[\"Hey, where's the chips?!\"]", new Cosmos.PartitionKey(((IItem)new PickleChipsItem()).PartitionKey).ToString()); } }