Skip to content

Commit

Permalink
v3.21.0 (#105)
Browse files Browse the repository at this point in the history
- *Enhancement*: `CoreEx.Cosmos` improvements:
  - Added `CosmosDbArgs` to `CosmosDbContainerBase` to allow per container configuration where required.
  - Partition key specification centralized into `CosmosDbArgs`.
  - `ITenantId` and `ILogicallyDeleted` support integrated into `CosmosDbContainerBase`, etc. to offer consistent behavior with `EfDb`.
  • Loading branch information
chullybun authored Jun 25, 2024
1 parent 4b8486d commit c6da97e
Show file tree
Hide file tree
Showing 14 changed files with 157 additions and 74 deletions.
6 changes: 6 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,12 @@

Represents the **NuGet** versions.

## v3.21.0
- *Enhancement*: `CoreEx.Cosmos` improvements:
- Added `CosmosDbArgs` to `CosmosDbContainerBase` to allow per container configuration where required.
- Partition key specification centralized into `CosmosDbArgs`.
- `ITenantId` and `ILogicallyDeleted` support integrated into `CosmosDbContainerBase`, etc. to offer consistent behavior with `EfDb`.

## v3.20.0
- *Fixed*: Include all constructor parameters when using `AddReferenceDataOrchestrator`.
- *Enhancement*: Integrated dynamic `ITenantId` filtering into `EfDb` (controlled with `EfDbArgs`).
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.20.0</Version>
<Version>3.21.0</Version>
<LangVersion>preview</LangVersion>
<Authors>Avanade</Authors>
<Company>Avanade</Company>
Expand Down
24 changes: 4 additions & 20 deletions src/CoreEx.Cosmos/CosmosDb.cs
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,6 @@ public class CosmosDb(Database database, IMapper mapper, CosmosDbInvoker? invoke
private Action<RequestOptions>? _updateRequestOptionsAction;
private Action<QueryRequestOptions>? _updateQueryRequestOptionsAction;
private readonly ConcurrentDictionary<Key, Func<IQueryable, IQueryable>> _filters = new();
private PartitionKey? _partitionKey;

/// <summary>
/// Provides key as combination of model type and container identifier.
Expand All @@ -43,32 +42,17 @@ private readonly struct Key(Type modelType, string containerId)
/// <inheritdoc/>
public CosmosDbInvoker Invoker { get; } = invoker ?? (_invoker ??= new CosmosDbInvoker());

/// <inheritdoc/>
public virtual PartitionKey? PartitionKey => _partitionKey;

/// <inheritdoc/>
public CosmosDbArgs DbArgs { get; set; } = new CosmosDbArgs();

/// <summary>
/// Uses (sets) the <see cref="PartitionKey"/>.
/// </summary>
/// <param name="partitionKey">The <see cref="Microsoft.Azure.Cosmos.PartitionKey"/>.</param>
/// <returns>The <see cref="CosmosDb"/> instance to support fluent-style method-chaining.</returns>
/// <remarks>As the <see cref="PartitionKey"/> property can be overridden by an inheritor this may have no affect.</remarks>
public CosmosDb UsePartitionKey(PartitionKey? partitionKey)
{
_partitionKey = partitionKey;
return this;
}

/// <inheritdoc/>
public Container GetCosmosContainer(string containerId) => Database.GetContainer(containerId);

/// <inheritdoc/>
public CosmosDbContainer<T, TModel> Container<T, TModel>(string containerId) where T : class, IEntityKey, new() where TModel : class, IIdentifier<string>, new() => new(this, containerId);
public CosmosDbContainer<T, TModel> Container<T, TModel>(string containerId, CosmosDbArgs? dbArgs = null) where T : class, IEntityKey, new() where TModel : class, IIdentifier<string>, new() => new(this, containerId, dbArgs);

/// <inheritdoc/>
public CosmosDbValueContainer<T, TModel> ValueContainer<T, TModel>(string containerId) where T : class, IEntityKey, new() where TModel : class, IIdentifier, new() => new(this, containerId);
public CosmosDbValueContainer<T, TModel> ValueContainer<T, TModel>(string containerId, CosmosDbArgs? dbArgs = null) where T : class, IEntityKey, new() where TModel : class, IIdentifier, new() => new(this, containerId, dbArgs);

/// <inheritdoc/>
public CosmosDbModelQuery<TModel> ModelQuery<TModel>(string containerId, CosmosDbArgs dbArgs, Func<IQueryable<TModel>, IQueryable<TModel>>? query) where TModel : class, IIdentifier<string>, new()
Expand Down Expand Up @@ -143,7 +127,7 @@ public CosmosDb QueryRequestOptions(Action<QueryRequestOptions> updateQueryReque
QueryRequestOptions ICosmosDb.GetQueryRequestOptions<T, TModel>(CosmosDbArgs dbArgs) where T : class where TModel : class
{
var ro = dbArgs.QueryRequestOptions ?? new QueryRequestOptions();
ro.PartitionKey ??= dbArgs.PartitionKey ?? PartitionKey;
ro.PartitionKey ??= dbArgs.PartitionKey ?? DbArgs.PartitionKey;

UpdateQueryRequestOptions(ro);
return ro;
Expand All @@ -153,7 +137,7 @@ QueryRequestOptions ICosmosDb.GetQueryRequestOptions<T, TModel>(CosmosDbArgs dbA
QueryRequestOptions ICosmosDb.GetQueryRequestOptions<TModel>(CosmosDbArgs dbArgs) where TModel : class
{
var ro = dbArgs.QueryRequestOptions ?? new QueryRequestOptions();
ro.PartitionKey ??= dbArgs.PartitionKey ?? PartitionKey;
ro.PartitionKey ??= dbArgs.PartitionKey ?? DbArgs.PartitionKey;

UpdateQueryRequestOptions(ro);
return ro;
Expand Down
22 changes: 21 additions & 1 deletion src/CoreEx.Cosmos/CosmosDbArgs.cs
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
// Copyright (c) Avanade. Licensed under the MIT License. See https://github.com/Avanade/CoreEx

using Microsoft.Azure.Cosmos;
using System;
using System.Net;

namespace CoreEx.Cosmos
Expand All @@ -26,8 +27,17 @@ public CosmosDbArgs(CosmosDbArgs template, PartitionKey? partitionKey = null)
ItemRequestOptions = template.ItemRequestOptions;
QueryRequestOptions = template.QueryRequestOptions;
NullOnNotFound = template.NullOnNotFound;
CleanUpResult = template.CleanUpResult;
FilterByTenantId = template.FilterByTenantId;
GetTenantId = template.GetTenantId;
}

/// <summary>
/// Initializes a new instance of the <see cref="CosmosDbArgs"/> struct.
/// </summary>
/// <param name="partitionKey">The <see cref="Microsoft.Azure.Cosmos.PartitionKey"/>.</param>
public CosmosDbArgs(PartitionKey partitionKey) => PartitionKey = partitionKey;

/// <summary>
/// Initializes a new instance of the <see cref="CosmosDbArgs"/> struct for <b>Get</b>, <b>Create</b>, <b>Update</b>, and <b>Delete</b> operations with the specified <see cref="ItemRequestOptions"/>.
/// </summary>
Expand All @@ -51,7 +61,7 @@ public CosmosDbArgs(QueryRequestOptions requestOptions, PartitionKey? partitionK
}

/// <summary>
/// Gets or sets the <see cref="Microsoft.Azure.Cosmos.PartitionKey"/>.
/// Gets the <see cref="Microsoft.Azure.Cosmos.PartitionKey"/>.
/// </summary>
public PartitionKey? PartitionKey { get; } = null;

Expand All @@ -74,5 +84,15 @@ public CosmosDbArgs(QueryRequestOptions requestOptions, PartitionKey? partitionK
/// Indicates whether the result should be <see cref="Entities.Cleaner.Clean{T}(T)">cleaned up</see>.
/// </summary>
public bool CleanUpResult { get; set; } = false;

/// <summary>
/// Indicates that when the underlying model implements <see cref="Entities.ITenantId.TenantId"/> it is to be filtered by the corresponding <see cref="GetTenantId"/> value. Defaults to <c>true</c>.
/// </summary>
public bool FilterByTenantId { get; set; }

/// <summary>
/// Gets or sets the <i>get</i> tenant identifier function; defaults to <see cref="ExecutionContext.Current"/> <see cref="ExecutionContext.TenantId"/>.
/// </summary>
public Func<string?> GetTenantId { get; set; } = () => ExecutionContext.HasCurrent ? ExecutionContext.Current.TenantId : null;
}
}
46 changes: 32 additions & 14 deletions src/CoreEx.Cosmos/CosmosDbContainer.cs
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,8 @@ namespace CoreEx.Cosmos
/// <typeparam name="TModel">The cosmos model <see cref="Type"/>.</typeparam>
/// <param name="cosmosDb">The <see cref="ICosmosDb"/>.</param>
/// <param name="containerId">The <see cref="Microsoft.Azure.Cosmos.Container"/> identifier.</param>
public class CosmosDbContainer<T, TModel>(ICosmosDb cosmosDb, string containerId) : CosmosDbContainerBase<T, TModel, CosmosDbContainer<T, TModel>>(cosmosDb, containerId) where T : class, IEntityKey, new() where TModel : class, IIdentifier<string>, new()
/// <param name="dbArgs">The optional <see cref="CosmosDbArgs"/>.</param>
public class CosmosDbContainer<T, TModel>(ICosmosDb cosmosDb, string containerId, CosmosDbArgs? dbArgs = null) : CosmosDbContainerBase<T, TModel, CosmosDbContainer<T, TModel>>(cosmosDb, containerId, dbArgs) where T : class, IEntityKey, new() where TModel : class, IIdentifier<string>, new()
{
/// <summary>
/// Gets the <b>value</b> from the response updating any special properties as required.
Expand All @@ -45,11 +46,11 @@ internal T GetValue(TModel model)
if (val is IETag et && et.ETag != null)
et.ETag = ETagGenerator.ParseETag(et.ETag);

return CosmosDb.DbArgs.CleanUpResult ? Cleaner.Clean(val) : val;
return DbArgs.CleanUpResult ? Cleaner.Clean(val) : val;
}

/// <summary>
/// Check the value to determine whether users are authorised using the CosmosDbArgs.AuthorizationFilter.
/// Check the value to determine whether users are authorised using the CosmosDb.AuthorizationFilter.
/// </summary>
private Result CheckAuthorized(TModel model)
{
Expand All @@ -68,15 +69,15 @@ private Result CheckAuthorized(TModel model)
/// </summary>
/// <param name="query">The function to perform additional query execution.</param>
/// <returns>The <see cref="CosmosDbQuery{T, TModel}"/>.</returns>
public CosmosDbQuery<T, TModel> Query(Func<IQueryable<TModel>, IQueryable<TModel>>? query) => Query(new CosmosDbArgs(CosmosDb.DbArgs), query);
public CosmosDbQuery<T, TModel> Query(Func<IQueryable<TModel>, IQueryable<TModel>>? query) => Query(new CosmosDbArgs(DbArgs), query);

/// <summary>
/// Gets (creates) a <see cref="CosmosDbQuery{T, TModel}"/> to enable LINQ-style queries.
/// </summary>
/// <param name="partitionKey">The <see cref="PartitionKey"/>.</param>
/// <param name="query">The function to perform additional query execution.</param>
/// <returns>The <see cref="CosmosDbQuery{T, TModel}"/>.</returns>
public CosmosDbQuery<T, TModel> Query(PartitionKey? partitionKey = null, Func<IQueryable<TModel>, IQueryable<TModel>>? query = null) => Query(new CosmosDbArgs(CosmosDb.DbArgs, partitionKey), query);
public CosmosDbQuery<T, TModel> Query(PartitionKey? partitionKey = null, Func<IQueryable<TModel>, IQueryable<TModel>>? query = null) => Query(new CosmosDbArgs(DbArgs, partitionKey), query);

/// <summary>
/// Gets (creates) a <see cref="CosmosDbQuery{T, TModel}"/> to enable LINQ-style queries.
Expand All @@ -91,15 +92,15 @@ private Result CheckAuthorized(TModel model)
/// </summary>
/// <param name="query">The function to perform additional query execution.</param>
/// <returns>The <see cref="CosmosDbModelQuery{TModel}"/>.</returns>
public CosmosDbModelQuery<TModel> ModelQuery(Func<IQueryable<TModel>, IQueryable<TModel>>? query) => ModelQuery(new CosmosDbArgs(CosmosDb.DbArgs), query);
public CosmosDbModelQuery<TModel> ModelQuery(Func<IQueryable<TModel>, IQueryable<TModel>>? query) => ModelQuery(new CosmosDbArgs(DbArgs), query);

/// <summary>
/// Gets (creates) a <see cref="CosmosDbModelQuery{TModel}"/> to enable LINQ-style queries.
/// </summary>
/// <param name="partitionKey">The <see cref="PartitionKey"/>.</param>
/// <param name="query">The function to perform additional query execution.</param>
/// <returns>The <see cref="CosmosDbModelQuery{TModel}"/>.</returns>
public CosmosDbModelQuery<TModel> ModelQuery(PartitionKey? partitionKey = null, Func<IQueryable<TModel>, IQueryable<TModel>>? query = null) => ModelQuery(new CosmosDbArgs(CosmosDb.DbArgs, partitionKey), query);
public CosmosDbModelQuery<TModel> ModelQuery(PartitionKey? partitionKey = null, Func<IQueryable<TModel>, IQueryable<TModel>>? query = null) => ModelQuery(new CosmosDbArgs(DbArgs, partitionKey), query);

/// <summary>
/// Gets (creates) a <see cref="CosmosDbModelQuery{TModel}"/> to enable LINQ-style queries.
Expand All @@ -114,10 +115,13 @@ private Result CheckAuthorized(TModel model)
{
try
{
var val = await Container.ReadItemAsync<TModel>(key, args.PartitionKey ?? CosmosDb.PartitionKey ?? PartitionKey.None, CosmosDb.GetItemRequestOptions<T, TModel>(args), ct).ConfigureAwait(false);
return Result.Go(CheckAuthorized(val)).ThenAs(() => GetResponseValue(val));
var resp = await Container.ReadItemAsync<TModel>(key, args.PartitionKey ?? DbArgs.PartitionKey ?? PartitionKey.None, CosmosDb.GetItemRequestOptions<T, TModel>(args), 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))
return args.NullOnNotFound ? Result<T?>.None : Result<T?>.NotFoundError();

return Result.Go(CheckAuthorized(resp)).ThenAs(() => GetResponseValue(resp));
}
catch (CosmosException dcex) when (args.NullOnNotFound && dcex.StatusCode == System.Net.HttpStatusCode.NotFound) { return Result<T?>.None; }
catch (CosmosException dcex) when (args.NullOnNotFound && dcex.StatusCode == System.Net.HttpStatusCode.NotFound) { return args.NullOnNotFound ? Result<T?>.None : Result<T?>.NotFoundError(); }
}, cancellationToken, nameof(GetWithResultAsync));

/// <inheritdoc/>
Expand Down Expand Up @@ -148,7 +152,7 @@ public override Task<Result<T>> UpdateWithResultAsync(T value, CosmosDbArgs dbAr

// Must read existing to update.
var resp = await Container.ReadItemAsync<TModel>(key, pk, ro, ct).ConfigureAwait(false);
if (resp.Resource == null)
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))
return Result<T>.NotFoundError();

return await Result
Expand Down Expand Up @@ -178,10 +182,24 @@ public override Task<Result> DeleteWithResultAsync(object? id, CosmosDbArgs dbAr
{
// Must read the existing to validate.
var ro = CosmosDb.GetItemRequestOptions<T, TModel>(args);
var pk = dbArgs.PartitionKey ?? CosmosDb.PartitionKey ?? PartitionKey.None;
var pk = dbArgs.PartitionKey ?? DbArgs.PartitionKey ?? PartitionKey.None;
var resp = await Container.ReadItemAsync<TModel>(key, pk, ro, ct).ConfigureAwait(false);
if (resp?.Resource == null)
return Result.NotFoundError();
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))
return Result.Success;

// Delete; either logically or physically.
if (resp.Resource is ILogicallyDeleted ild)
{
ild.IsDeleted = true;
return await Result
.Go(CheckAuthorized(resp.Resource))
.ThenAsync(async () =>
{
ro.SessionToken = resp.Headers?.Session;
await Container.ReplaceItemAsync(resp.Resource, key, pk, ro, ct).ConfigureAwait(false);
return Result.Success;
});
}

return await Result
.Go(CheckAuthorized(resp.Resource))
Expand Down
Loading

0 comments on commit c6da97e

Please sign in to comment.