Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

V3.24.0 #115

Merged
merged 3 commits into from
Aug 6, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,10 @@

Represents the **NuGet** versions.

## 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`).

## 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.
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.5</Version>
<Version>3.24.0</Version>
<LangVersion>preview</LangVersion>
<Authors>Avanade</Authors>
<Company>Avanade</Company>
Expand Down
28 changes: 22 additions & 6 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 ?? container.ThrowIfNull().DbArgs, cancellationToken);
=> ImportBatchAsync(container.ThrowIfNull(nameof(container)).CosmosDb!, container.Container.Id!, items, modelUpdater, dbArgs ?? container.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 ?? container.ThrowIfNull().DbArgs, cancellationToken);
=> ImportBatchAsync(container.ThrowIfNull(nameof(container)).CosmosDb!, container.Container.Id!, jsonDataReader, name, modelUpdater, dbArgs ?? container.DbArgs, cancellationToken);

/// <summary>
/// Imports (creates) a batch of <see cref="CosmosDbValue{TModel}"/> <paramref name="items"/>.
Expand All @@ -131,7 +131,7 @@ public static async Task ImportBatchAsync<TModel>(this ICosmosDb cosmosDb, strin
foreach (var item in items)
{
var cdv = new CosmosDbValue<TModel>(item);
((ICosmosDbValue)cdv).PrepareBefore(dbArgs.Value);
((ICosmosDbValue)cdv).PrepareBefore(dbArgs.Value, null);

if (SequentialExecution)
await container.CreateItemAsync(modelUpdater?.Invoke(cdv) ?? cdv, dbArgs.Value.PartitionKey, dbArgs.Value.ItemRequestOptions, cancellationToken).ConfigureAwait(false);
Expand All @@ -154,7 +154,15 @@ 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 ?? container.ThrowIfNull().DbArgs, cancellationToken);
{
CosmosDbValue<TModel> func(CosmosDbValue<TModel> cvm)
{
cvm.Type = container.ModelContainer.TypeName;
return modelUpdater?.Invoke(cvm) ?? cvm;
}

return ImportValueBatchAsync(container.ThrowIfNull(nameof(container)).CosmosDb!, container.Container.Id!, items, func, dbArgs ?? container.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 +201,15 @@ 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 ?? container.ThrowIfNull().DbArgs, cancellationToken);
{
CosmosDbValue<TModel> func(CosmosDbValue<TModel> cvm)
{
cvm.Type = container.ModelContainer.TypeName;
return modelUpdater?.Invoke(cvm) ?? cvm;
}

return ImportValueBatchAsync(container.ThrowIfNull(nameof(container)).CosmosDb!, container.Container.Id!, jsonDataReader, name ?? container.ModelContainer.TypeName, (Func<CosmosDbValue<TModel>, CosmosDbValue<TModel>>)func, dbArgs ?? container.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 @@ -224,7 +240,7 @@ public static async Task<bool> ImportValueBatchAsync(this ICosmosDb cosmosDb, st
foreach (var item in items.Where(x => x is not null))
{
var cdv = Activator.CreateInstance(t, item)!;
((ICosmosDbValue)cdv).PrepareBefore(dbArgs.Value);
((ICosmosDbValue)cdv).PrepareBefore(dbArgs.Value, null);

if (SequentialExecution)
await container.CreateItemAsync(modelUpdater?.Invoke(cdv) ?? cdv, dbArgs.Value.PartitionKey, dbArgs.Value.ItemRequestOptions, cancellationToken).ConfigureAwait(false);
Expand Down
2 changes: 1 addition & 1 deletion src/CoreEx.Cosmos/CoreEx.Cosmos.csproj
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
<Project Sdk="Microsoft.NET.Sdk">
<Project Sdk="Microsoft.NET.Sdk">

<PropertyGroup>
<TargetFrameworks>net6.0;net7.0;net8.0;netstandard2.1</TargetFrameworks>
Expand Down
165 changes: 165 additions & 0 deletions src/CoreEx.Cosmos/CosmosDb.cs
Original file line number Diff line number Diff line change
@@ -1,13 +1,22 @@
// Copyright (c) Avanade. Licensed under the MIT License. See https://github.com/Avanade/CoreEx

using CoreEx.Cosmos.Model;
using CoreEx.Cosmos.Extended;
using CoreEx.Entities;
using CoreEx.Json;
using CoreEx.Mapping;
using CoreEx.Results;
using Microsoft.Azure.Cosmos;
using System;
using System.Collections.Concurrent;
using System.Linq;
using System.Threading.Tasks;
using System.Collections;
using System.Threading;
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using System.Text;
using System.Text.Json;

namespace CoreEx.Cosmos
{
Expand Down Expand Up @@ -94,5 +103,161 @@ public CosmosDb UseAuthorizeFilter<TModel>(string containerId, Func<IQueryable,
System.Net.HttpStatusCode.PreconditionFailed => Result.Fail(new ConcurrencyException(null, cex)),
_ => Result.Fail(cex)
};

/// <summary>
/// Executes a multi-dataset query command with one or more <see cref="IMultiSetArgs"/> with a <see cref="Result{T}"/>.
/// </summary>
/// <param name="partitionKey">The <see cref="PartitionKey"/>.</param>
/// <param name="multiSetArgs">One or more <see cref="IMultiSetArgs"/>.</param>
/// <remarks>See <see cref="SelectMultiSetWithResultAsync(PartitionKey, string?, IEnumerable{IMultiSetArgs}, CancellationToken)"/> for further details.</remarks>
public Task SelectMultiSetAsync(PartitionKey partitionKey, params IMultiSetArgs[] multiSetArgs) => SelectMultiSetAsync(partitionKey, multiSetArgs, default);

/// <summary>
/// Executes a multi-dataset query command with one or more <see cref="IMultiSetArgs"/> with a <see cref="Result{T}"/>.
/// </summary>
/// <param name="partitionKey">The <see cref="PartitionKey"/>.</param>
/// <param name="multiSetArgs">One or more <see cref="IMultiSetArgs"/>.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/>.</param>
/// <remarks>See <see cref="SelectMultiSetWithResultAsync(PartitionKey, string?, IEnumerable{IMultiSetArgs}, CancellationToken)"/> for further details.</remarks>
public Task SelectMultiSetAsync(PartitionKey partitionKey, IEnumerable<IMultiSetArgs> multiSetArgs, CancellationToken cancellationToken = default) => SelectMultiSetAsync(partitionKey, null, multiSetArgs, cancellationToken);

/// <summary>
/// Executes a multi-dataset query command with one or more <see cref="IMultiSetArgs"/> with a <see cref="Result{T}"/>.
/// </summary>
/// <param name="partitionKey">The <see cref="PartitionKey"/>.</param>
/// <param name="sql">The override SQL statement; will default where not specified.</param>
/// <param name="multiSetArgs">One or more <see cref="IMultiSetArgs"/>.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/>.</param>
/// <remarks>See <see cref="SelectMultiSetWithResultAsync(PartitionKey, string?, IEnumerable{IMultiSetArgs}, CancellationToken)"/> for further details.</remarks>
public async Task SelectMultiSetAsync(PartitionKey partitionKey, string? sql, IEnumerable<IMultiSetArgs> multiSetArgs, CancellationToken cancellationToken = default)
=> (await SelectMultiSetWithResultAsync(partitionKey, sql, multiSetArgs, cancellationToken).ConfigureAwait(false)).ThrowOnError();

/// <summary>
/// Executes a multi-dataset query command with one or more <see cref="IMultiSetArgs"/> with a <see cref="Result{T}"/>.
/// </summary>
/// <param name="partitionKey">The <see cref="PartitionKey"/>.</param>
/// <param name="multiSetArgs">One or more <see cref="IMultiSetArgs"/>.</param>
/// <remarks>See <see cref="SelectMultiSetWithResultAsync(PartitionKey, string?, IEnumerable{IMultiSetArgs}, CancellationToken)"/> for further details.</remarks>
public Task<Result> SelectMultiSetWithResultAsync(PartitionKey partitionKey, params IMultiSetArgs[] multiSetArgs) => SelectMultiSetWithResultAsync(partitionKey, multiSetArgs, default);

/// <summary>
/// Executes a multi-dataset query command with one or more <see cref="IMultiSetArgs"/> with a <see cref="Result{T}"/>.
/// </summary>
/// <param name="partitionKey">The <see cref="PartitionKey"/>.</param>
/// <param name="multiSetArgs">One or more <see cref="IMultiSetArgs"/>.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/>.</param>
/// <remarks>See <see cref="SelectMultiSetWithResultAsync(PartitionKey, string?, IEnumerable{IMultiSetArgs}, CancellationToken)"/> for further details.</remarks>
public Task<Result> SelectMultiSetWithResultAsync(PartitionKey partitionKey, IEnumerable<IMultiSetArgs> multiSetArgs, CancellationToken cancellationToken = default) => SelectMultiSetWithResultAsync(partitionKey, null, multiSetArgs, cancellationToken);

/// <summary>
/// Executes a multi-dataset query command with one or more <see cref="IMultiSetArgs"/> with a <see cref="Result{T}"/>.
/// </summary>
/// <param name="partitionKey">The <see cref="PartitionKey"/>.</param>
/// <param name="sql">The override SQL statement; will default where not specified.</param>
/// <param name="multiSetArgs">One or more <see cref="IMultiSetArgs"/>.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/>.</param>
/// <remarks>The <paramref name="multiSetArgs"/> must all be from the same <see cref="CosmosDb"/>, be of type <see cref="CosmosDbValueContainer{T, TModel}"/>, and reference the same <see cref="Container.Id"/>. Each
/// <paramref name="multiSetArgs"/> is verified and executed in the order specified.
/// <para>The underlying SQL will be automatically created from the specified <paramref name="multiSetArgs"/> where not explicitly supplied. Essentially, it is a simple query where all <i>types</i> inferred from the <paramref name="multiSetArgs"/>
/// are included, for example: <c>SELECT * FROM c WHERE c.type in ("TypeNameA", "TypeNameB")</c></para>
/// <para>Example usage is:
/// <code>
/// private async Task&lt;Result&lt;MemberDetail?&gt;&gt; GetDetailOnImplementationAsync(int id)
/// {
/// MemberDetail? md = null;
/// return await Result.GoAsync(() =&gt; _cosmos.SelectMultiSetWithResultAsync(new AzCosmos.PartitionKey(id.ToString()),
/// _cosmos.Members.CreateMultiSetSingleArgs(m =&gt; md = m.CreateCopyFromAs&lt;MemberDetail&gt;(), isMandatory: false, stopOnNull: true),
/// _cosmos.MemberAddresses.CreateMultiSetCollArgs(mac =&gt; md.Adjust(x =&gt; x.Addresses = new (mac)))))
/// .ThenAs(() =&gt; md).ConfigureAwait(false);
/// }
/// </code></para></remarks>
public async Task<Result> SelectMultiSetWithResultAsync(PartitionKey partitionKey, string? sql, IEnumerable<IMultiSetArgs> multiSetArgs, CancellationToken cancellationToken = default)
{
// Verify that the multi set arguments are valid for this type of get query.
var multiSetList = multiSetArgs?.ToArray() ?? null;
if (multiSetList == null || multiSetList.Length == 0)
throw new ArgumentException($"At least one {nameof(IMultiSetArgs)} must be supplied.", nameof(multiSetArgs));

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))
throw new ArgumentException($"All {nameof(IMultiSetArgs)} containers must be of type CosmosDbValueContainer.", nameof(multiSetArgs));

var container = multiSetList[0].Container;
var types = new Dictionary<string, IMultiSetArgs>([ new KeyValuePair<string, IMultiSetArgs>(container.ModelType.Name, multiSetList[0]) ]);
var sb = string.IsNullOrEmpty(sql) ? new StringBuilder($"SELECT * FROM c WHERE c.type in (\"{container.ModelType.Name}\"") : null;

for (int i = 1; i < multiSetList.Length; i++)
{
if (multiSetList[i].Container.Container.Id != container.Container.Id)
throw new ArgumentException($"All {nameof(IMultiSetArgs)} containers must reference the same container id.", nameof(multiSetArgs));

if (!types.TryAdd(multiSetList[i].Container.ModelType.Name, multiSetList[i]))
throw new ArgumentException($"All {nameof(IMultiSetArgs)} containers must be of different model type.", nameof(multiSetArgs));

sb?.Append($", \"{multiSetList[i].Container.ModelType.Name}\"");
}

sb?.Append(')');

// Execute the Cosmos DB query.
var result = await Invoker.InvokeAsync(this, container, sb?.ToString() ?? sql, types, async (_, container, sql, types, ct) =>
{
// Set up for work.
var da = new CosmosDbArgs(container.DbArgs, partitionKey);
var qsi = container.Container.GetItemQueryStreamIterator(sql, requestOptions: da.GetQueryRequestOptions());
IJsonSerializer js = ExecutionContext.GetService<IJsonSerializer>() ?? CoreEx.Json.JsonSerializer.Default;
var isStj = js is Text.Json.JsonSerializer;

while (qsi.HasMoreResults)
{
var rm = await qsi.ReadNextAsync(ct).ConfigureAwait(false);
if (!rm.IsSuccessStatusCode)
return Result.Fail(new InvalidOperationException(rm.ErrorMessage));

var json = JsonDocument.Parse(rm.Content);
if (!json.RootElement.TryGetProperty("Documents", out var jds) || jds.ValueKind != JsonValueKind.Array)
return Result.Fail(new InvalidOperationException("Cosmos response JSON 'Documents' property either not found in result or is not an array."));

foreach (var jd in jds.EnumerateArray())
{
if (!jd.TryGetProperty("type", out var jt) || jt.ValueKind != JsonValueKind.String)
return Result.Fail(new InvalidOperationException("Cosmos response documents item 'type' property either not found in result or is not a string."));

if (!types.TryGetValue(jt.GetString()!, out var msa))
continue; // Ignore any unexpected type.

var model = isStj
? jd.Deserialize(msa.Container.ModelValueType, (JsonSerializerOptions)js.Options)
: js.Deserialize(jd.ToString(), msa.Container.ModelValueType);

var result = msa.AddItem(msa.Container.MapToValue(model));
if (result.IsFailure)
return result;
}
}

return Result.Success;
}, cancellationToken).ConfigureAwait(false);

if (result.IsFailure)
return result;

// Validate the multi-set args and action each accordingly.
foreach (var msa in multiSetList)
{
var r = msa.Verify();
if (r.IsFailure)
return r.AsResult();

if (!r.Value && msa.StopOnNull)
break;

msa.Invoke();
}

return Result.Success;
}
}
}
Loading
Loading