Skip to content

Commit

Permalink
v3.23.5 (#114)
Browse files Browse the repository at this point in the history
- *Fixed:* `CosmosDbValue<TModel>.PrepareBefore` corrected to set the `PartitionKey` where the underlying `Value` implements `IPartitionKey`.
- *Fixed:* `CosmosDbBatch` corrected to default to the `CosmosDbContainerBase<TSelf>.DbArgs` where not specified.
- *Fixed:* `CosmosDbArgs.AutoMapETag` added, indicates whether when mapping the model to the corresponding entity that the `IETag.ETag` is to be automatically mapped (default is `true`, existing behavior).
  • Loading branch information
chullybun authored Aug 5, 2024
1 parent f8dfa4c commit 43b7e65
Show file tree
Hide file tree
Showing 13 changed files with 198 additions and 211 deletions.
5 changes: 5 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,11 @@

Represents the **NuGet** versions.

## v3.23.5
- *Fixed:* `CosmosDbValue<TModel>.PrepareBefore` corrected to set the `PartitionKey` where the underlying `Value` implements `IPartitionKey`.
- *Fixed:* `CosmosDbBatch` corrected to default to the `CosmosDbContainerBase<TSelf>.DbArgs` where not specified.
- *Fixed:* `CosmosDbArgs.AutoMapETag` added, indicates whether when mapping the model to the corresponding entity that the `IETag.ETag` is to be automatically mapped (default is `true`, existing behavior).

## v3.23.4
- *Fixed:* Added `Result<T>.AdjustsAsync` to support asynchronous adjustments.

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.23.4</Version>
<Version>3.23.5</Version>
<LangVersion>preview</LangVersion>
<Authors>Avanade</Authors>
<Company>Avanade</Company>
Expand Down
8 changes: 4 additions & 4 deletions src/CoreEx.Cosmos/Batch/CosmosDbBatch.cs
Original file line number Diff line number Diff line change
Expand Up @@ -67,7 +67,7 @@ public static async Task ImportBatchAsync<TModel>(this ICosmosDb cosmosDb, strin
/// <param name="cancellationToken">The <see cref="CancellationToken"/>.</param>
/// <remarks>Each item is added individually and is not transactional.</remarks>
public static Task ImportBatchAsync<T, TModel>(this CosmosDbContainer<T, TModel> container, IEnumerable<TModel> items, Func<TModel, TModel>? modelUpdater = null, CosmosDbArgs? dbArgs = null, CancellationToken cancellationToken = default) where T : class, IEntityKey, new() where TModel : class, IEntityKey, new()
=> ImportBatchAsync(container?.CosmosDb!, container?.Container.Id!, items, modelUpdater, dbArgs, cancellationToken);
=> ImportBatchAsync(container?.CosmosDb!, container?.Container.Id!, items, modelUpdater, dbArgs ?? container.ThrowIfNull().DbArgs, cancellationToken);

/// <summary>
/// Imports (creates) a batch of named items from the <paramref name="jsonDataReader"/> into the specified <paramref name="containerId"/>.
Expand Down Expand Up @@ -106,7 +106,7 @@ public static async Task ImportBatchAsync<TModel>(this ICosmosDb cosmosDb, strin
/// <returns><c>true</c> indicates that one or more items were deserialized and imported; otherwise, <c>false</c> for none found.</returns>
/// <remarks>Each item is added individually and is not transactional.</remarks>
public static Task<bool> ImportBatchAsync<T, TModel>(this CosmosDbContainer<T, TModel> container, JsonDataReader jsonDataReader, string? name = null, Func<TModel, TModel>? modelUpdater = null, CosmosDbArgs? dbArgs = null, CancellationToken cancellationToken = default) where T : class, IEntityKey, new() where TModel : class, IEntityKey, new()
=> ImportBatchAsync(container?.CosmosDb!, container?.Container.Id!, jsonDataReader, name, modelUpdater, dbArgs, cancellationToken);
=> ImportBatchAsync(container?.CosmosDb!, container?.Container.Id!, jsonDataReader, name, modelUpdater, dbArgs ?? container.ThrowIfNull().DbArgs, cancellationToken);

/// <summary>
/// Imports (creates) a batch of <see cref="CosmosDbValue{TModel}"/> <paramref name="items"/>.
Expand Down Expand Up @@ -154,7 +154,7 @@ public static async Task ImportBatchAsync<TModel>(this ICosmosDb cosmosDb, strin
/// <param name="cancellationToken">The <see cref="CancellationToken"/>.</param>
/// <remarks>Each item is added individually and is not transactional.</remarks>
public static Task ImportValueBatchAsync<T, TModel>(this CosmosDbValueContainer<T, TModel> container, IEnumerable<TModel> items, Func<CosmosDbValue<TModel>, CosmosDbValue<TModel>>? modelUpdater = null, CosmosDbArgs? dbArgs = null, CancellationToken cancellationToken = default) where T : class, IEntityKey, new() where TModel : class, IEntityKey, new()
=> ImportValueBatchAsync(container?.CosmosDb!, container?.Container.Id!, items, modelUpdater, dbArgs, cancellationToken);
=> ImportValueBatchAsync(container?.CosmosDb!, container?.Container.Id!, items, modelUpdater, dbArgs ?? container.ThrowIfNull().DbArgs, cancellationToken);

/// <summary>
/// Imports (creates) a batch of named <see cref="CosmosDbValue{TModel}"/> items from the <paramref name="jsonDataReader"/> into the specified <paramref name="containerId"/>.
Expand Down Expand Up @@ -193,7 +193,7 @@ public static async Task ImportBatchAsync<TModel>(this ICosmosDb cosmosDb, strin
/// <returns><c>true</c> indicates that one or more items were deserialized and imported; otherwise, <c>false</c> for none found.</returns>
/// <remarks>Each item is added individually and is not transactional.</remarks>
public static Task<bool> ImportValueBatchAsync<T, TModel>(this CosmosDbValueContainer<T, TModel> container, JsonDataReader jsonDataReader, string? name = null, Func<CosmosDbValue<TModel>, CosmosDbValue<TModel>>? modelUpdater = null, CosmosDbArgs? dbArgs = null, CancellationToken cancellationToken = default) where T : class, IEntityKey, new() where TModel : class, IEntityKey, new()
=> ImportValueBatchAsync(container?.CosmosDb!, container?.Container.Id!, jsonDataReader, name, modelUpdater, dbArgs, cancellationToken);
=> ImportValueBatchAsync(container?.CosmosDb!, container?.Container.Id!, jsonDataReader, name, modelUpdater, dbArgs ?? container.ThrowIfNull().DbArgs, cancellationToken);

/// <summary>
/// Imports (creates) a batch of named <see cref="CosmosDbValue{TModel}"/> items from the <paramref name="jsonDataReader"/> into the specified <paramref name="containerId"/>.
Expand Down
40 changes: 11 additions & 29 deletions src/CoreEx.Cosmos/CosmosDbArgs.cs
Original file line number Diff line number Diff line change
Expand Up @@ -21,18 +21,18 @@ public CosmosDbArgs() { }
/// Initializes a new instance of the <see cref="CosmosDbArgs"/> struct.
/// </summary>
/// <param name="template">The template <see cref="CosmosDbArgs"/> to copy from.</param>
/// <param name="partitionKey">The <see cref="Microsoft.Azure.Cosmos.PartitionKey"/>.</param>
/// <param name="partitionKey">The override <see cref="Microsoft.Azure.Cosmos.PartitionKey"/>.</param>
public CosmosDbArgs(CosmosDbArgs template, PartitionKey? partitionKey = null)
{
PartitionKey = partitionKey ?? template.PartitionKey;
ItemRequestOptions = template.ItemRequestOptions;
QueryRequestOptions = template.QueryRequestOptions;
NullOnNotFound = template.NullOnNotFound;
AutoMapETag = template.AutoMapETag;
CleanUpResult = template.CleanUpResult;
FilterByTenantId = template.FilterByTenantId;
GetTenantId = template.GetTenantId;
FormatIdentifier = template.FormatIdentifier;
ParseIdentifier = template.ParseIdentifier;
}

/// <summary>
Expand Down Expand Up @@ -98,12 +98,17 @@ private readonly QueryRequestOptions UpdateQueryRequestionOptionsPartitionKey(Qu
}

/// <summary>
/// Indicates that a <c>null</c> is to be returned where the <b>response</b> has a <see cref="HttpStatusCode"/> of <see cref="HttpStatusCode.NotFound"/> on <b>Get</b>.
/// Indicates whether a <c>null</c> is to be returned where the <b>response</b> has a <see cref="HttpStatusCode"/> of <see cref="HttpStatusCode.NotFound"/> on <b>Get</b>. Defaults to <c>true</c>.
/// </summary>
public bool NullOnNotFound { get; set; } = true;

/// <summary>
/// Indicates whether the result should be <see cref="Entities.Cleaner.Clean{T}(T)">cleaned up</see>.
/// Indicates whether when mapping the model to the corresponding entity that the <see cref="IETag.ETag"/> is to be automatically mapped. Defaults to <c>true</c>.
/// </summary>
public bool AutoMapETag { get; set; } = true;

/// <summary>
/// Indicates whether the result should be <see cref="Entities.Cleaner.Clean{T}(T)">cleaned up</see>. Defaults to <c>false</c>.
/// </summary>
public bool CleanUpResult { get; set; } = false;

Expand All @@ -121,35 +126,12 @@ private readonly QueryRequestOptions UpdateQueryRequestionOptionsPartitionKey(Qu
/// Formats a <see cref="CompositeKey"/> to a <see cref="string"/> representation (used by <see cref="CosmosDbContainerBase{T, TModel, TSelf}.GetCosmosId(T)"/> and <see cref="ICosmosDbValue.PrepareBefore"/>).
/// </summary>
/// <returns>The identifier as a <see cref="string"/>.</returns>
/// <remarks>Defaults to <see cref="DefaultFormatIdentifier"/>.</remarks>
public Func<CompositeKey, string?> FormatIdentifier { get; set; } = DefaultFormatIdentifier;

/// <summary>
/// Parses a <see cref="string"/> identifier and updates the underlying value where it implements <see cref="IIdentifier.Id"/> (used by the <see cref="ICosmosDbValue.PrepareAfter"/>).
/// </summary>
/// <returns>The parsed identifier.</returns>
public Action<object, string?> ParseIdentifier { get; set; } = DefaultParseIdentifier;

/// <summary>
/// Provides the default <see cref="FormatIdentifier"/> implementation.
/// Provides the default <see cref="FormatIdentifier"/> implementation; being the <see cref="CompositeKey"/> <see cref="object.ToString"/>.
/// </summary>
public static Func<CompositeKey, string?> DefaultFormatIdentifier { get; } = key => key.ToString();

/// <summary>
/// Provides the default <see cref="ParseIdentifier"/> implementation.
/// </summary>
public static Action<object, string?> DefaultParseIdentifier { get; } = (value, id) =>
{
if (value.ThrowIfNull(nameof(value)) is IIdentifier iid)
{
iid.Id = iid.IdType switch
{
Type t when t == typeof(string) => id,
Type t when t == typeof(int) => id == null ? 0 : int.Parse(id, System.Globalization.CultureInfo.InvariantCulture),
Type t when t == typeof(long) => id == null ? 0 : long.Parse(id, System.Globalization.CultureInfo.InvariantCulture),
Type t when t == typeof(Guid) => id == null ? Guid.Empty : Guid.Parse(id),
_ => throw new NotSupportedException("An IIdentifier.IdType must be one of the following types: string, int, long, or Guid.")
};
}
};
}
}
116 changes: 20 additions & 96 deletions src/CoreEx.Cosmos/CosmosDbContainer.cs
Original file line number Diff line number Diff line change
@@ -1,119 +1,43 @@
// Copyright (c) Avanade. Licensed under the MIT License. See https://github.com/Avanade/CoreEx

using CoreEx.Abstractions;
using CoreEx.Cosmos.Model;
using CoreEx.Entities;
using CoreEx.Mapping;
using CoreEx.Results;
using Microsoft.Azure.Cosmos;
using System;
using System.Diagnostics.CodeAnalysis;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;

namespace CoreEx.Cosmos
{
/// <summary>
/// Provides <see cref="Container"/> operations for a <see cref="CosmosDb"/> container.
/// Provides the core <see cref="Container"/> capabilities.
/// </summary>
/// <typeparam name="T">The entity <see cref="Type"/>.</typeparam>
/// <typeparam name="TModel">The cosmos model <see cref="Type"/>.</typeparam>
public class CosmosDbContainer<T, TModel> : CosmosDbContainerBase<T, TModel, CosmosDbContainer<T, TModel>> where T : class, IEntityKey, new() where TModel : class, IEntityKey, new()
/// <param name="cosmosDb">The <see cref="ICosmosDb"/>.</param>
/// <param name="containerId">The <see cref="Microsoft.Azure.Cosmos.Container"/> identifier.</param>
/// <param name="dbArgs">The optional <see cref="CosmosDbArgs"/>.</param>
public class CosmosDbContainer(ICosmosDb cosmosDb, string containerId, CosmosDbArgs? dbArgs = null) : ICosmosDbContainer
{
private readonly Lazy<CosmosDbModelContainer<TModel>> _modelContainer;
private CosmosDbArgs? _dbArgs = dbArgs;

/// <summary>
/// Initializes a new instance of the <see cref="CosmosDbContainer{T, TModel}"/> class.
/// </summary>
/// <param name="cosmosDb">The <see cref="ICosmosDb"/>.</param>
/// <param name="containerId">The <see cref="Microsoft.Azure.Cosmos.Container"/> identifier.</param>
/// <param name="dbArgs">The optional <see cref="CosmosDbArgs"/>.</param>
public CosmosDbContainer(ICosmosDb cosmosDb, string containerId, CosmosDbArgs? dbArgs = null) : base(cosmosDb, containerId, dbArgs)
=> _modelContainer = new(() => new CosmosDbModelContainer<TModel>(CosmosDb, Container.Id, DbArgs));

/// <summary>
/// Gets the underlying <see cref="CosmosDbModelContainer{TModel}"/>.
/// </summary>
public CosmosDbModelContainer<TModel> ModelContainer => _modelContainer.Value;
/// <inheritdoc/>
public ICosmosDb CosmosDb { get; } = cosmosDb.ThrowIfNull(nameof(cosmosDb));

/// <summary>
/// Sets the function to determine the <see cref="PartitionKey"/>; used for <see cref="CosmosDbModelContainer{TModel}.GetPartitionKey(TModel, CosmosDbArgs)"/> (only <b>Create</b> and <b>Update</b> operations).
/// </summary>
/// <param name="partitionKey">The function to determine the <see cref="PartitionKey"/>.</param>
/// <returns>The <see cref="CosmosDbContainer{T, TModel}"/> instance to support fluent-style method-chaining.</returns>
/// <remarks>This is used where there is a value and the corresponding <see cref="PartitionKey"/> needs to be dynamically determined.</remarks>
public CosmosDbContainer<T, TModel> UsePartitionKey(Func<TModel, PartitionKey> partitionKey)
{
ModelContainer.UsePartitionKey(partitionKey);
return this;
}
/// <inheritdoc/>
public Container Container { get; } = cosmosDb.GetCosmosContainer(containerId);

/// <summary>
/// Gets the <b>value</b> formatting/updating any special properties as required.
/// Gets or sets the Container-specific <see cref="CosmosDbArgs"/>.
/// </summary>
/// <param>The model value.</param>
/// <returns>The entity value.</returns>
[return: NotNullIfNotNull(nameof(model))]
public T? GetValue(TModel? model)
/// <remarks>Defaults to <see cref="ICosmosDb.DbArgs"/> on first access.</remarks>
public CosmosDbArgs DbArgs
{
var val = CosmosDb.Mapper.Map<TModel, T>(model, OperationTypes.Get)!;
if (val is IETag et && et.ETag != null)
et.ETag = ETagGenerator.ParseETag(et.ETag);

return DbArgs.CleanUpResult ? Cleaner.Clean(val) : val;
get => _dbArgs ??= new CosmosDbArgs(CosmosDb.DbArgs);
set => _dbArgs = value;
}

/// <summary>
/// Gets (creates) a <see cref="CosmosDbQuery{T, TModel}"/> to enable LINQ-style queries.
/// </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(DbArgs), query);

/// <summary>
/// Gets (creates) a <see cref="CosmosDbQuery{T, TModel}"/> to enable LINQ-style queries.
/// Gets the <b>CosmosDb</b> identifier from the <see cref="CompositeKey"/>.
/// </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(DbArgs, partitionKey), query);

/// <summary>
/// Gets (creates) a <see cref="CosmosDbQuery{T, TModel}"/> to enable LINQ-style queries.
/// </summary>
/// <param name="dbArgs">The <see cref="CosmosDbArgs"/>.</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(CosmosDbArgs dbArgs, Func<IQueryable<TModel>, IQueryable<TModel>>? query = null) => new(this, dbArgs, query);

/// <inheritdoc/>
public async override Task<Result<T?>> GetWithResultAsync(CosmosDbArgs dbArgs, CompositeKey key, CancellationToken cancellationToken = default)
{
var result = await ModelContainer.GetWithResultAsync(dbArgs, key, cancellationToken).ConfigureAwait(false);
return result.ThenAs(GetValue);
}

/// <inheritdoc/>
public override async Task<Result<T>> CreateWithResultAsync(CosmosDbArgs dbArgs, T value, CancellationToken cancellationToken = default)
{
ChangeLog.PrepareCreated(value.ThrowIfNull(nameof(value)));
TModel model = CosmosDb.Mapper.Map<T, TModel>(value, OperationTypes.Create)!;

var result = await ModelContainer.CreateWithResultAsync(dbArgs, model, cancellationToken).ConfigureAwait(false);
return result.ThenAs(model => GetValue(model)!);
}

/// <inheritdoc/>
public override async Task<Result<T>> UpdateWithResultAsync(CosmosDbArgs dbArgs, T value, CancellationToken cancellationToken = default)
{
ChangeLog.PrepareUpdated(value);
var model = CosmosDb.Mapper.Map<T, TModel>(value.ThrowIfNull(nameof(value)), OperationTypes.Update)!;
var result = await ModelContainer.UpdateWithResultInternalAsync(dbArgs, model, m => CosmosDb.Mapper.Map(value, m, OperationTypes.Update), cancellationToken).ConfigureAwait(false);
return result.ThenAs(model => GetValue(model)!);
}

/// <inheritdoc/>
public override Task<Result> DeleteWithResultAsync(CosmosDbArgs dbArgs, CompositeKey key, CancellationToken cancellationToken = default) => ModelContainer.DeleteWithResultAsync(dbArgs, key, cancellationToken);
/// <param name="key">The <see cref="CompositeKey"/>.</param>
/// <returns>The <b>CosmosDb</b> identifier.</returns>
/// <remarks>Uses the <see cref="CosmosDbArgs.FormatIdentifier"/> to format the <paramref name="key"/> as a string (as required).</remarks>
public virtual string GetCosmosId(CompositeKey key) => DbArgs.FormatIdentifier(key) ?? throw new InvalidOperationException("The CompositeKey formatting must not result in a null.");
}
}
Loading

0 comments on commit 43b7e65

Please sign in to comment.