Skip to content

Commit

Permalink
v3.24.1 (#116)
Browse files Browse the repository at this point in the history
- *Fixed*: `CosmosDb.SelectMultiSetWithResultAsync` updated to skip items that are not considered valid; ensures same outcome as if using a `CosmosDbModelQueryBase` with respect to filtering.
  • Loading branch information
chullybun authored Aug 7, 2024
1 parent 3cc68cc commit 5a8c280
Show file tree
Hide file tree
Showing 12 changed files with 138 additions and 40 deletions.
3 changes: 3 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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<TModel>.TypeName` (updateable using `UseTypeName`).
Expand Down
2 changes: 1 addition & 1 deletion Common.targets
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
<Project>
<PropertyGroup>
<Version>3.24.0</Version>
<Version>3.24.1</Version>
<LangVersion>preview</LangVersion>
<Authors>Avanade</Authors>
<Company>Avanade</Company>
Expand Down
5 changes: 4 additions & 1 deletion src/CoreEx.Cosmos/CosmosDb.cs
Original file line number Diff line number Diff line change
Expand Up @@ -181,7 +181,7 @@ public async Task<Result> 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;
Expand Down Expand Up @@ -232,6 +232,9 @@ public async Task<Result> 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;
Expand Down
18 changes: 15 additions & 3 deletions src/CoreEx.Cosmos/CosmosDbContainerBaseT.cs
Original file line number Diff line number Diff line change
Expand Up @@ -31,20 +31,32 @@ public abstract class CosmosDbContainerBase<T, TModel, TSelf>(ICosmosDb cosmosDb
Type ICosmosDbContainer.ModelValueType => typeof(CosmosDbValue<TModel>);

/// <inheritdoc/>
bool ICosmosDbContainer.IsCosmosDbValueEncapsulated => IsCosmosDbValueEncapsulated;
bool ICosmosDbContainer.IsCosmosDbValueModel => IsCosmosDbValueModel;

/// <summary>
/// Indicates whether the <typeparamref name="TModel"/> is encapsulated within a <see cref="CosmosDbValue{TModel}"/>.
/// </summary>
protected bool IsCosmosDbValueEncapsulated { get; set; } = false;
protected bool IsCosmosDbValueModel { get; set; } = false;

/// <inheritdoc/>
bool ICosmosDbContainer.IsModelValid(object? model, CoreEx.Cosmos.CosmosDbArgs args, bool checkAuthorized) => IsModelValid(model, args, checkAuthorized);

/// <summary>
/// Checks whether the <paramref name="model"/> is in a valid state for the operation.
/// </summary>
/// <param name="model">The model value (also depends on <see cref="IsCosmosDbValueModel"/>).</param>
/// <param name="args">The specific <see cref="CosmosDbArgs"/> for the operation.</param>
/// <param name="checkAuthorized">Indicates whether an additional authorization check should be performed against the <paramref name="model"/>.</param>
/// <returns><c>true</c> indicates that the model is in a valid state; otherwise, <c>false</c>.</returns>
protected abstract bool IsModelValid(object? model, CosmosDbArgs args, bool checkAuthorized);

/// <inheritdoc/>
object? ICosmosDbContainer.MapToValue(object? model) => MapToValue(model);

/// <summary>
/// Maps the model into the entity value.
/// </summary>
/// <param name="model">The model value (also depends on <see cref="IsCosmosDbValueEncapsulated"/>).</param>
/// <param name="model">The model value (also depends on <see cref="IsCosmosDbValueModel"/>).</param>
/// <returns>The entity value.</returns>
protected abstract T? MapToValue(object? model);

Expand Down
3 changes: 3 additions & 0 deletions src/CoreEx.Cosmos/CosmosDbContainerT.cs
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,9 @@ public CosmosDbContainer<T, TModel> UsePartitionKey(Func<TModel, PartitionKey> p
return this;
}

/// <inheritdoc/>
protected override bool IsModelValid(object? model, CosmosDbArgs args, bool checkAuthorized) => ModelContainer.IsModelValid((TModel?)model, args, checkAuthorized);

/// <inheritdoc/>
protected override T? MapToValue(object? model) => MapToValue((TModel?)model!);

Expand Down
5 changes: 4 additions & 1 deletion src/CoreEx.Cosmos/CosmosDbValueContainer.cs
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ namespace CoreEx.Cosmos
public CosmosDbValueContainer(ICosmosDb cosmosDb, string containerId, CosmosDbArgs? dbArgs = null) : base(cosmosDb, containerId, dbArgs)
{
_modelContainer = new(() => new CosmosDbValueModelContainer<TModel>(CosmosDb, Container.Id, DbArgs));
IsCosmosDbValueEncapsulated = true;
IsCosmosDbValueModel = true;
}

/// <summary>
Expand Down Expand Up @@ -66,6 +66,9 @@ public CosmosDbValueContainer<T, TModel> UsePartitionKey(Func<CosmosDbValue<TMod
return MapToValue(resp.Resource);
}

/// <inheritdoc/>
protected override bool IsModelValid(object? model, CosmosDbArgs args, bool checkAuthorized) => ModelContainer.IsModelValid((CosmosDbValue<TModel>?)model, args, checkAuthorized);

/// <inheritdoc/>
protected override T? MapToValue(object? model) => MapToValue((CosmosDbValue<TModel>)model!);

Expand Down
13 changes: 11 additions & 2 deletions src/CoreEx.Cosmos/ICosmosDbContainer.cs
Original file line number Diff line number Diff line change
Expand Up @@ -27,12 +27,21 @@ public interface ICosmosDbContainer : ICosmosDbContainerCore
/// <summary>
/// Indicates whether the <see cref="ModelType"/> is encapsulated within a <see cref="CosmosDbValue{TModel}"/>.
/// </summary>
bool IsCosmosDbValueEncapsulated { get; }
bool IsCosmosDbValueModel { get; }

/// <summary>
/// Checks whether the <paramref name="model"/> is in a valid state for the operation.
/// </summary>
/// <param name="model">The model value (also depends on <see cref="IsCosmosDbValueModel"/>).</param>
/// <param name="args">The specific <see cref="CosmosDbArgs"/> for the operation.</param>
/// <param name="checkAuthorized">Indicates whether an additional authorization check should be performed against the <paramref name="model"/>.</param>
/// <returns><c>true</c> indicates that the model is in a valid state; otherwise, <c>false</c>.</returns>
bool IsModelValid(object? model, CosmosDbArgs args, bool checkAuthorized);

/// <summary>
/// Maps the model into the entity value.
/// </summary>
/// <param name="model">The model value (also depends on <see cref="IsCosmosDbValueEncapsulated"/>).</param>
/// <param name="model">The model value (also depends on <see cref="IsCosmosDbValueModel"/>).</param>
/// <returns>The entity value.</returns>
object? MapToValue(object? model);
}
Expand Down
40 changes: 29 additions & 11 deletions src/CoreEx.Cosmos/Model/CosmosDbModelContainer.cs
Original file line number Diff line number Diff line change
Expand Up @@ -65,10 +65,28 @@ public PartitionKey GetPartitionKey(TModel model, CosmosDbArgs dbArgs)
/// <returns>The entity value.</returns>
internal static TModel? GetResponseValue(Response<TModel> resp) => resp?.Resource == null ? default : resp.Resource;

/// <inheritdoc/>
protected override bool IsModelValid(object? model, CosmosDbArgs args, bool checkAuthorized) => IsModelValid((TModel?)model, args, checkAuthorized);

/// <summary>
/// Checks whether the <paramref name="model"/> is in a valid state for the operation.
/// </summary>
/// <param name="model">The model value.</param>
/// <param name="args">The specific <see cref="CosmosDbArgs"/> for the operation.</param>
/// <param name="checkAuthorized">Indicates whether an additional authorization check should be performed against the <paramref name="model"/>.</param>
/// <returns><c>true</c> indicates that the model is in a valid state; otherwise, <c>false</c>.</returns>
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));

/// <summary>
/// Check the value to determine whether the user is authorized using the <see cref="CosmosDb.GetAuthorizeFilter{TModel}(string)"/>.
/// Checks the value to determine whether the user is authorized with the <see cref="CosmosDb.GetAuthorizeFilter{TModel}(string)"/>.
/// </summary>
internal Result CheckAuthorized(TModel model)
/// <param name="model">The model value.</param>
/// <remarks>Either <see cref="Result.Success"/> or <see cref="Result.AuthorizationError"/>.</remarks>
public Result IsAuthorized(TModel model)
{
if (model != default)
{
Expand Down Expand Up @@ -159,10 +177,10 @@ internal Result CheckAuthorized(TModel model)
{
var pk = args.PartitionKey ?? DbArgs.PartitionKey ?? PartitionKey.None;
var resp = await Container.ReadItemAsync<TModel>(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<TModel?>.None : Result<TModel?>.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<TModel?>.None : Result<TModel?>.NotFoundError(); }
}, cancellationToken, nameof(GetWithResultAsync));
Expand Down Expand Up @@ -204,7 +222,7 @@ public Task<Result<TModel>> 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));
Expand Down Expand Up @@ -262,11 +280,11 @@ internal Task<Result<TModel>> UpdateWithResultInternalAsync(CosmosDbArgs dbArgs,
var id = GetCosmosId(m);
var pk = GetPartitionKey(model, dbArgs);
var resp = await Container.ReadItemAsync<TModel>(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<TModel>.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(() =>
{
Expand All @@ -275,7 +293,7 @@ internal Task<Result<TModel>> 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 () =>
{
Expand Down Expand Up @@ -336,7 +354,7 @@ public Task<Result> DeleteWithResultAsync(CosmosDbArgs dbArgs, CompositeKey key,
var ro = args.GetItemRequestOptions();
var pk = args.PartitionKey ?? DbArgs.PartitionKey ?? PartitionKey.None;
var resp = await Container.ReadItemAsync<TModel>(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.
Expand All @@ -347,7 +365,7 @@ public Task<Result> 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;
Expand All @@ -357,7 +375,7 @@ public Task<Result> DeleteWithResultAsync(CosmosDbArgs dbArgs, CompositeKey key,
}

return await Result
.Go(CheckAuthorized(resp.Resource))
.Go(IsAuthorized(resp.Resource))
.ThenAsync(async () =>
{
ro.SessionToken = resp.Headers?.Session;
Expand Down
15 changes: 14 additions & 1 deletion src/CoreEx.Cosmos/Model/CosmosDbModelContainerBase.cs
Original file line number Diff line number Diff line change
Expand Up @@ -15,5 +15,18 @@ namespace CoreEx.Cosmos.Model
/// <param name="containerId">The <see cref="Microsoft.Azure.Cosmos.Container"/> identifier.</param>
/// <param name="dbArgs">The optional <see cref="CosmosDbArgs"/>.</param>
public abstract class CosmosDbModelContainerBase<TModel, TSelf>(ICosmosDb cosmosDb, string containerId, CosmosDbArgs? dbArgs = null) : CosmosDbContainer(cosmosDb, containerId, dbArgs), ICosmosDbModelContainer<TModel>
where TModel : class, IEntityKey, new () where TSelf : CosmosDbModelContainerBase<TModel, TSelf> { }
where TModel : class, IEntityKey, new () where TSelf : CosmosDbModelContainerBase<TModel, TSelf>
{
/// <inheritdoc/>
bool ICosmosDbModelContainer.IsModelValid(object? model, CoreEx.Cosmos.CosmosDbArgs args, bool checkAuthorized) => IsModelValid(model, args, checkAuthorized);

/// <summary>
/// Checks whether the <paramref name="model"/> is in a valid state for the operation.
/// </summary>
/// <param name="model">The model to be checked.</param>
/// <param name="args">The specific <see cref="CosmosDbArgs"/> for the operation.</param>
/// <param name="checkAuthorized">Indicates whether an additional authorization check should be performed against the <paramref name="model"/>.</param>
/// <returns><c>true</c> indicates that the model is in a valid state; otherwise, <c>false</c>.</returns>
protected abstract bool IsModelValid(object? model, CosmosDbArgs args, bool checkAuthorized);
}
}
Loading

0 comments on commit 5a8c280

Please sign in to comment.