From 5a8c280b2129dee5ab3fb1fc011b038bd23b1959 Mon Sep 17 00:00:00 2001 From: "Eric Sibly [chullybun]" Date: Wed, 7 Aug 2024 11:31:19 -0700 Subject: [PATCH] v3.24.1 (#116) - *Fixed*: `CosmosDb.SelectMultiSetWithResultAsync` updated to skip items that are not considered valid; ensures same outcome as if using a `CosmosDbModelQueryBase` with respect to filtering. --- CHANGELOG.md | 3 ++ Common.targets | 2 +- src/CoreEx.Cosmos/CosmosDb.cs | 5 +- src/CoreEx.Cosmos/CosmosDbContainerBaseT.cs | 18 +++++-- src/CoreEx.Cosmos/CosmosDbContainerT.cs | 3 ++ src/CoreEx.Cosmos/CosmosDbValueContainer.cs | 5 +- src/CoreEx.Cosmos/ICosmosDbContainer.cs | 13 ++++- .../Model/CosmosDbModelContainer.cs | 40 +++++++++++----- .../Model/CosmosDbModelContainerBase.cs | 15 +++++- .../Model/CosmosDbValueModelContainer.cs | 47 ++++++++++++------- .../Model/ICosmosDbModelContainer.cs | 14 ++++-- .../Model/ICosmosDbModelContainerT.cs | 13 +++++ 12 files changed, 138 insertions(+), 40 deletions(-) create mode 100644 src/CoreEx.Cosmos/Model/ICosmosDbModelContainerT.cs diff --git a/CHANGELOG.md b/CHANGELOG.md index 5a77773d..3679fb99 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,9 @@ Represents the **NuGet** versions. +## v3.24.1 +- *Fixed*: `CosmosDb.SelectMultiSetWithResultAsync` updated to skip items that are not considered valid; ensures same outcome as if using a `CosmosDbModelQueryBase` with respect to filtering. + ## v3.24.0 - *Enhancement:* `CosmosDb.SelectMultiSetWithResultAsync` and `SelectMultiSetAsync` added to enable the selection of multiple sets of data in a single operation; see also `MultiSetSingleArgs` and `MultiSetCollArgs`. - *Enhancement:* `CosmosDbValue.Type` is now updatable and defaults from `CosmosDbValueModelContainer.TypeName` (updateable using `UseTypeName`). diff --git a/Common.targets b/Common.targets index 0a05614c..b2ed874b 100644 --- a/Common.targets +++ b/Common.targets @@ -1,6 +1,6 @@  - 3.24.0 + 3.24.1 preview Avanade Avanade diff --git a/src/CoreEx.Cosmos/CosmosDb.cs b/src/CoreEx.Cosmos/CosmosDb.cs index f219e44b..8961e497 100644 --- a/src/CoreEx.Cosmos/CosmosDb.cs +++ b/src/CoreEx.Cosmos/CosmosDb.cs @@ -181,7 +181,7 @@ public async Task SelectMultiSetWithResultAsync(PartitionKey partitionKe if (multiSetList.Any(x => x.Container.CosmosDb != this)) throw new ArgumentException($"All {nameof(IMultiSetArgs)} containers must be from this same database.", nameof(multiSetArgs)); - if (multiSetList.Any(x => !x.Container.IsCosmosDbValueEncapsulated)) + if (multiSetList.Any(x => !x.Container.IsCosmosDbValueModel)) throw new ArgumentException($"All {nameof(IMultiSetArgs)} containers must be of type CosmosDbValueContainer.", nameof(multiSetArgs)); var container = multiSetList[0].Container; @@ -232,6 +232,9 @@ public async Task SelectMultiSetWithResultAsync(PartitionKey partitionKe ? jd.Deserialize(msa.Container.ModelValueType, (JsonSerializerOptions)js.Options) : js.Deserialize(jd.ToString(), msa.Container.ModelValueType); + if (!msa.Container.IsModelValid(model, msa.Container.DbArgs, true)) + continue; + var result = msa.AddItem(msa.Container.MapToValue(model)); if (result.IsFailure) return result; diff --git a/src/CoreEx.Cosmos/CosmosDbContainerBaseT.cs b/src/CoreEx.Cosmos/CosmosDbContainerBaseT.cs index 511e3aee..c875bd88 100644 --- a/src/CoreEx.Cosmos/CosmosDbContainerBaseT.cs +++ b/src/CoreEx.Cosmos/CosmosDbContainerBaseT.cs @@ -31,12 +31,24 @@ public abstract class CosmosDbContainerBase(ICosmosDb cosmosDb Type ICosmosDbContainer.ModelValueType => typeof(CosmosDbValue); /// - bool ICosmosDbContainer.IsCosmosDbValueEncapsulated => IsCosmosDbValueEncapsulated; + bool ICosmosDbContainer.IsCosmosDbValueModel => IsCosmosDbValueModel; /// /// Indicates whether the is encapsulated within a . /// - protected bool IsCosmosDbValueEncapsulated { get; set; } = false; + protected bool IsCosmosDbValueModel { get; set; } = false; + + /// + bool ICosmosDbContainer.IsModelValid(object? model, CoreEx.Cosmos.CosmosDbArgs args, bool checkAuthorized) => IsModelValid(model, args, checkAuthorized); + + /// + /// Checks whether the is in a valid state for the operation. + /// + /// The model value (also depends on ). + /// The specific for the operation. + /// Indicates whether an additional authorization check should be performed against the . + /// true indicates that the model is in a valid state; otherwise, false. + protected abstract bool IsModelValid(object? model, CosmosDbArgs args, bool checkAuthorized); /// object? ICosmosDbContainer.MapToValue(object? model) => MapToValue(model); @@ -44,7 +56,7 @@ public abstract class CosmosDbContainerBase(ICosmosDb cosmosDb /// /// Maps the model into the entity value. /// - /// The model value (also depends on ). + /// The model value (also depends on ). /// The entity value. protected abstract T? MapToValue(object? model); diff --git a/src/CoreEx.Cosmos/CosmosDbContainerT.cs b/src/CoreEx.Cosmos/CosmosDbContainerT.cs index 03b62357..70522896 100644 --- a/src/CoreEx.Cosmos/CosmosDbContainerT.cs +++ b/src/CoreEx.Cosmos/CosmosDbContainerT.cs @@ -49,6 +49,9 @@ public CosmosDbContainer UsePartitionKey(Func p return this; } + /// + protected override bool IsModelValid(object? model, CosmosDbArgs args, bool checkAuthorized) => ModelContainer.IsModelValid((TModel?)model, args, checkAuthorized); + /// protected override T? MapToValue(object? model) => MapToValue((TModel?)model!); diff --git a/src/CoreEx.Cosmos/CosmosDbValueContainer.cs b/src/CoreEx.Cosmos/CosmosDbValueContainer.cs index c999c30b..0922a514 100644 --- a/src/CoreEx.Cosmos/CosmosDbValueContainer.cs +++ b/src/CoreEx.Cosmos/CosmosDbValueContainer.cs @@ -33,7 +33,7 @@ namespace CoreEx.Cosmos public CosmosDbValueContainer(ICosmosDb cosmosDb, string containerId, CosmosDbArgs? dbArgs = null) : base(cosmosDb, containerId, dbArgs) { _modelContainer = new(() => new CosmosDbValueModelContainer(CosmosDb, Container.Id, DbArgs)); - IsCosmosDbValueEncapsulated = true; + IsCosmosDbValueModel = true; } /// @@ -66,6 +66,9 @@ public CosmosDbValueContainer UsePartitionKey(Func + protected override bool IsModelValid(object? model, CosmosDbArgs args, bool checkAuthorized) => ModelContainer.IsModelValid((CosmosDbValue?)model, args, checkAuthorized); + /// protected override T? MapToValue(object? model) => MapToValue((CosmosDbValue)model!); diff --git a/src/CoreEx.Cosmos/ICosmosDbContainer.cs b/src/CoreEx.Cosmos/ICosmosDbContainer.cs index d423ea11..c9c6c9e0 100644 --- a/src/CoreEx.Cosmos/ICosmosDbContainer.cs +++ b/src/CoreEx.Cosmos/ICosmosDbContainer.cs @@ -27,12 +27,21 @@ public interface ICosmosDbContainer : ICosmosDbContainerCore /// /// Indicates whether the is encapsulated within a . /// - bool IsCosmosDbValueEncapsulated { get; } + bool IsCosmosDbValueModel { get; } + + /// + /// Checks whether the is in a valid state for the operation. + /// + /// The model value (also depends on ). + /// The specific for the operation. + /// Indicates whether an additional authorization check should be performed against the . + /// true indicates that the model is in a valid state; otherwise, false. + bool IsModelValid(object? model, CosmosDbArgs args, bool checkAuthorized); /// /// Maps the model into the entity value. /// - /// The model value (also depends on ). + /// The model value (also depends on ). /// The entity value. object? MapToValue(object? model); } diff --git a/src/CoreEx.Cosmos/Model/CosmosDbModelContainer.cs b/src/CoreEx.Cosmos/Model/CosmosDbModelContainer.cs index 79bf69d7..11e15c07 100644 --- a/src/CoreEx.Cosmos/Model/CosmosDbModelContainer.cs +++ b/src/CoreEx.Cosmos/Model/CosmosDbModelContainer.cs @@ -65,10 +65,28 @@ public PartitionKey GetPartitionKey(TModel model, CosmosDbArgs dbArgs) /// The entity value. internal static TModel? GetResponseValue(Response resp) => resp?.Resource == null ? default : resp.Resource; + /// + protected override bool IsModelValid(object? model, CosmosDbArgs args, bool checkAuthorized) => IsModelValid((TModel?)model, args, checkAuthorized); + + /// + /// Checks whether the is in a valid state for the operation. + /// + /// The model value. + /// The specific for the operation. + /// Indicates whether an additional authorization check should be performed against the . + /// true indicates that the model is in a valid state; otherwise, false. + public bool IsModelValid(TModel? model, CosmosDbArgs args, bool checkAuthorized) + => !(model == null + || (args.FilterByTenantId && model is ITenantId tenantId && tenantId.TenantId != args.GetTenantId()) + || (model is ILogicallyDeleted ld && ld.IsDeleted.HasValue && ld.IsDeleted.Value) + || (checkAuthorized && IsAuthorized(model).IsFailure)); + /// - /// Check the value to determine whether the user is authorized using the . + /// Checks the value to determine whether the user is authorized with the . /// - internal Result CheckAuthorized(TModel model) + /// The model value. + /// Either or . + public Result IsAuthorized(TModel model) { if (model != default) { @@ -159,10 +177,10 @@ internal Result CheckAuthorized(TModel model) { var pk = args.PartitionKey ?? DbArgs.PartitionKey ?? PartitionKey.None; var resp = await Container.ReadItemAsync(id, pk, args.GetItemRequestOptions(), ct).ConfigureAwait(false); - if (resp.Resource == null || args.FilterByTenantId && resp.Resource is ITenantId tenantId && tenantId.TenantId != DbArgs.GetTenantId() || resp.Resource is ILogicallyDeleted ld && ld.IsDeleted.HasValue && ld.IsDeleted.Value) + if (!IsModelValid(resp.Resource, args, false)) return args.NullOnNotFound ? Result.None : Result.NotFoundError(); - return Result.Go(CheckAuthorized(resp)).ThenAs(() => GetResponseValue(resp)); + return Result.Go(IsAuthorized(resp)).ThenAs(() => GetResponseValue(resp)); } catch (CosmosException dcex) when (args.NullOnNotFound && dcex.StatusCode == System.Net.HttpStatusCode.NotFound) { return args.NullOnNotFound ? Result.None : Result.NotFoundError(); } }, cancellationToken, nameof(GetWithResultAsync)); @@ -204,7 +222,7 @@ public Task> CreateWithResultAsync(CosmosDbArgs dbArgs, TModel mo Cleaner.ResetTenantId(m); var pk = GetPartitionKey(model, dbArgs); return await Result - .Go(CheckAuthorized(model)) + .Go(IsAuthorized(model)) .ThenAsAsync(() => Container.CreateItemAsync(model, pk, args.GetItemRequestOptions(), ct)) .ThenAs(resp => GetResponseValue(resp!)!); }, cancellationToken, nameof(CreateWithResultAsync)); @@ -262,11 +280,11 @@ internal Task> UpdateWithResultInternalAsync(CosmosDbArgs dbArgs, var id = GetCosmosId(m); var pk = GetPartitionKey(model, dbArgs); var resp = await Container.ReadItemAsync(id, pk, ro, ct).ConfigureAwait(false); - if (resp.Resource == null || (args.FilterByTenantId && resp.Resource is ITenantId tenantId && tenantId.TenantId != DbArgs.GetTenantId()) || (resp.Resource is ILogicallyDeleted ld && ld.IsDeleted.HasValue && ld.IsDeleted.Value)) + if (!IsModelValid(resp.Resource, args, false)) return Result.NotFoundError(); return await Result - .Go(CheckAuthorized(resp)) + .Go(IsAuthorized(resp)) .When(() => m is IETag etag2 && etag2.ETag != null && ETagGenerator.FormatETag(etag2.ETag) != resp.ETag, () => Result.ConcurrencyError()) .Then(() => { @@ -275,7 +293,7 @@ internal Task> UpdateWithResultInternalAsync(CosmosDbArgs dbArgs, Cleaner.ResetTenantId(resp.Resource); // Re-check auth to make sure not updating to something not allowed. - return CheckAuthorized(resp); + return IsAuthorized(resp); }) .ThenAsAsync(async () => { @@ -336,7 +354,7 @@ public Task DeleteWithResultAsync(CosmosDbArgs dbArgs, CompositeKey key, var ro = args.GetItemRequestOptions(); var pk = args.PartitionKey ?? DbArgs.PartitionKey ?? PartitionKey.None; var resp = await Container.ReadItemAsync(id, pk, ro, ct).ConfigureAwait(false); - if (resp.Resource == null || (args.FilterByTenantId && resp.Resource is ITenantId tenantId && tenantId.TenantId != DbArgs.GetTenantId())) + if (!IsModelValid(resp.Resource, args, false)) return Result.Success; // Delete; either logically or physically. @@ -347,7 +365,7 @@ public Task DeleteWithResultAsync(CosmosDbArgs dbArgs, CompositeKey key, ild.IsDeleted = true; return await Result - .Go(CheckAuthorized(resp.Resource)) + .Go(IsAuthorized(resp.Resource)) .ThenAsync(async () => { ro.SessionToken = resp.Headers?.Session; @@ -357,7 +375,7 @@ public Task DeleteWithResultAsync(CosmosDbArgs dbArgs, CompositeKey key, } return await Result - .Go(CheckAuthorized(resp.Resource)) + .Go(IsAuthorized(resp.Resource)) .ThenAsync(async () => { ro.SessionToken = resp.Headers?.Session; diff --git a/src/CoreEx.Cosmos/Model/CosmosDbModelContainerBase.cs b/src/CoreEx.Cosmos/Model/CosmosDbModelContainerBase.cs index 2d5f69b5..90228e15 100644 --- a/src/CoreEx.Cosmos/Model/CosmosDbModelContainerBase.cs +++ b/src/CoreEx.Cosmos/Model/CosmosDbModelContainerBase.cs @@ -15,5 +15,18 @@ namespace CoreEx.Cosmos.Model /// The identifier. /// The optional . public abstract class CosmosDbModelContainerBase(ICosmosDb cosmosDb, string containerId, CosmosDbArgs? dbArgs = null) : CosmosDbContainer(cosmosDb, containerId, dbArgs), ICosmosDbModelContainer - where TModel : class, IEntityKey, new () where TSelf : CosmosDbModelContainerBase { } + where TModel : class, IEntityKey, new () where TSelf : CosmosDbModelContainerBase + { + /// + bool ICosmosDbModelContainer.IsModelValid(object? model, CoreEx.Cosmos.CosmosDbArgs args, bool checkAuthorized) => IsModelValid(model, args, checkAuthorized); + + /// + /// Checks whether the is in a valid state for the operation. + /// + /// The model to be checked. + /// The specific for the operation. + /// Indicates whether an additional authorization check should be performed against the . + /// true indicates that the model is in a valid state; otherwise, false. + protected abstract bool IsModelValid(object? model, CosmosDbArgs args, bool checkAuthorized); + } } \ No newline at end of file diff --git a/src/CoreEx.Cosmos/Model/CosmosDbValueModelContainer.cs b/src/CoreEx.Cosmos/Model/CosmosDbValueModelContainer.cs index 40a352b4..e6dc289e 100644 --- a/src/CoreEx.Cosmos/Model/CosmosDbValueModelContainer.cs +++ b/src/CoreEx.Cosmos/Model/CosmosDbValueModelContainer.cs @@ -75,10 +75,29 @@ public PartitionKey GetPartitionKey(CosmosDbValue model, CosmosDbArgs db /// The entity value. internal static CosmosDbValue? GetResponseValue(Response> resp) => resp?.Resource; + /// + protected override bool IsModelValid(object? model, CosmosDbArgs args, bool checkAuthorized) => IsModelValid((CosmosDbValue?)model, args, checkAuthorized); + + /// + /// Checks whether the is in a valid state for the operation. + /// + /// The model value. + /// The specific for the operation. + /// Indicates whether an additional authorization check should be performed against the . + /// true indicates that the model is in a valid state; otherwise, false. + public bool IsModelValid(CosmosDbValue? model, CosmosDbArgs args, bool checkAuthorized) + => !(model == null + || model.Type != TypeName + || (args.FilterByTenantId && model.Value is ITenantId tenantId && tenantId.TenantId != args.GetTenantId()) + || (model.Value is ILogicallyDeleted ld && ld.IsDeleted.HasValue && ld.IsDeleted.Value) + || (checkAuthorized && IsAuthorized(model).IsFailure)); + /// - /// Check the value to determine whether users are authorised using the CosmosDb.AuthorizationFilter. + /// Checks the value to determine whether the user is authorized with the . /// - private Result CheckAuthorized(CosmosDbValue model) + /// The model value. + /// Either or . + public Result IsAuthorized(CosmosDbValue model) { if (model != null && model.Value != default) { @@ -169,10 +188,10 @@ private Result CheckAuthorized(CosmosDbValue model) { var pk = args.PartitionKey ?? DbArgs.PartitionKey ?? PartitionKey.None; var resp = await Container.ReadItemAsync>(id, pk, args.GetItemRequestOptions(), ct).ConfigureAwait(false); - if (resp.Resource == null || resp.Resource.Type != TypeName || args.FilterByTenantId && resp.Resource.Value is ITenantId tenantId && tenantId.TenantId != DbArgs.GetTenantId() || resp.Resource.Value is ILogicallyDeleted ld && ld.IsDeleted.HasValue && ld.IsDeleted.Value) + if (!IsModelValid(resp.Resource, args, false)) return args.NullOnNotFound ? Result?>.None : Result?>.NotFoundError(); - return Result.Go(CheckAuthorized(resp)).ThenAs(() => GetResponseValue(resp)); + return Result.Go(IsAuthorized(resp)).ThenAs(() => GetResponseValue(resp)); } catch (CosmosException dcex) when (args.NullOnNotFound && dcex.StatusCode == System.Net.HttpStatusCode.NotFound) { return args.NullOnNotFound ? Result?>.None : Result?>.NotFoundError(); } }, cancellationToken, nameof(GetWithResultAsync)); @@ -214,7 +233,7 @@ public Task>> CreateWithResultAsync(CosmosDbArgs db Cleaner.ResetTenantId(m); var pk = GetPartitionKey(m, dbArgs); return await Result - .Go(CheckAuthorized(m)) + .Go(IsAuthorized(m)) .ThenAsAsync(async () => { ((ICosmosDbValue)m).PrepareBefore(dbArgs, TypeName); @@ -277,14 +296,11 @@ internal Task>> UpdateWithResultInternalAsync(Cosmo var id = m.Id; var pk = GetPartitionKey(m, dbArgs); var resp = await Container.ReadItemAsync>(id, pk, ro, ct).ConfigureAwait(false); - if (resp?.Resource == null || resp.Resource.Type != TypeName) - return Result>.NotFoundError(); - - if ((args.FilterByTenantId && resp.Resource.Value is ITenantId tenantId && tenantId.TenantId != DbArgs.GetTenantId()) || (resp.Resource.Value is ILogicallyDeleted ld && ld.IsDeleted.HasValue && ld.IsDeleted.Value)) + if (!IsModelValid(resp.Resource, args, false)) return Result>.NotFoundError(); return await Result - .Go(CheckAuthorized(resp.Resource)) + .Go(IsAuthorized(resp.Resource)) .When(() => m is IETag etag2 && etag2.ETag != null && ETagGenerator.FormatETag(etag2.ETag) != resp.ETag, () => Result.ConcurrencyError()) .Then(() => { @@ -294,7 +310,7 @@ internal Task>> UpdateWithResultInternalAsync(Cosmo ((ICosmosDbValue)resp.Resource).PrepareBefore(dbArgs, TypeName); // Re-check auth to make sure not updating to something not allowed. - return CheckAuthorized(resp); + return IsAuthorized(resp); }) .ThenAsAsync(async () => { @@ -355,10 +371,7 @@ public Task DeleteWithResultAsync(CosmosDbArgs dbArgs, CompositeKey key, var ro = args.GetItemRequestOptions(); var pk = args.PartitionKey ?? DbArgs.PartitionKey ?? PartitionKey.None; var resp = await Container.ReadItemAsync>(id, pk, ro, ct).ConfigureAwait(false); - if (resp?.Resource == null || resp.Resource.Type != TypeName) - return Result.Success; - - if ((args.FilterByTenantId && resp.Resource.Value is ITenantId tenantId && tenantId.TenantId != DbArgs.GetTenantId())) + if (!IsModelValid(resp.Resource, args, false)) return Result.Success; // Delete; either logically or physically. @@ -369,7 +382,7 @@ public Task DeleteWithResultAsync(CosmosDbArgs dbArgs, CompositeKey key, ild.IsDeleted = true; return await Result - .Go(CheckAuthorized(resp.Resource)) + .Go(IsAuthorized(resp.Resource)) .ThenAsync(async () => { ro.SessionToken = resp.Headers?.Session; @@ -379,7 +392,7 @@ public Task DeleteWithResultAsync(CosmosDbArgs dbArgs, CompositeKey key, } return await Result - .Go(CheckAuthorized(resp.Resource)) + .Go(IsAuthorized(resp.Resource)) .ThenAsync(async () => { ro.SessionToken = resp.Headers?.Session; diff --git a/src/CoreEx.Cosmos/Model/ICosmosDbModelContainer.cs b/src/CoreEx.Cosmos/Model/ICosmosDbModelContainer.cs index 0b7494ac..812d1a29 100644 --- a/src/CoreEx.Cosmos/Model/ICosmosDbModelContainer.cs +++ b/src/CoreEx.Cosmos/Model/ICosmosDbModelContainer.cs @@ -1,13 +1,21 @@ // Copyright (c) Avanade. Licensed under the MIT License. See https://github.com/Avanade/CoreEx using Microsoft.Azure.Cosmos; -using System; namespace CoreEx.Cosmos.Model { /// /// Enables the model-only . /// - /// The cosmos model . - public interface ICosmosDbModelContainer : ICosmosDbContainerCore where TModel : class, new() { } + public interface ICosmosDbModelContainer : ICosmosDbContainerCore + { + /// + /// Checks whether the is in a valid state for the operation. + /// + /// The model to be checked. + /// The specific for the operation. + /// Indicates whether an additional authorization check should be performed against the . + /// true indicates that the model is in a valid state; otherwise, false. + bool IsModelValid(object? model, CosmosDbArgs args, bool checkAuthorized); + } } \ No newline at end of file diff --git a/src/CoreEx.Cosmos/Model/ICosmosDbModelContainerT.cs b/src/CoreEx.Cosmos/Model/ICosmosDbModelContainerT.cs new file mode 100644 index 00000000..4bbb4b96 --- /dev/null +++ b/src/CoreEx.Cosmos/Model/ICosmosDbModelContainerT.cs @@ -0,0 +1,13 @@ +// Copyright (c) Avanade. Licensed under the MIT License. See https://github.com/Avanade/CoreEx + +using Microsoft.Azure.Cosmos; +using System; + +namespace CoreEx.Cosmos.Model +{ + /// + /// Enables the model-only . + /// + /// The cosmos model . + public interface ICosmosDbModelContainer : ICosmosDbModelContainer where TModel : class, new() { } +} \ No newline at end of file