Skip to content

Commit

Permalink
Merge branch 'master' into philliphoff-generated-actors
Browse files Browse the repository at this point in the history
  • Loading branch information
halspang authored Feb 16, 2024
2 parents b907c9b + e244e88 commit cb28b7e
Show file tree
Hide file tree
Showing 8 changed files with 169 additions and 32 deletions.
21 changes: 7 additions & 14 deletions src/Dapr.AspNetCore/DaprMvcBuilderExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ namespace Microsoft.Extensions.DependencyInjection
using Dapr.Client;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.ApplicationModels;
using Microsoft.Extensions.DependencyInjection.Extensions;

/// <summary>
/// Provides extension methods for <see cref="IMvcBuilder" />.
Expand All @@ -40,27 +41,19 @@ public static IMvcBuilder AddDapr(this IMvcBuilder builder, Action<DaprClientBui
throw new ArgumentNullException(nameof(builder));
}

// This pattern prevents registering services multiple times in the case AddDapr is called
// by non-user-code.
if (builder.Services.Any(s => s.ImplementationType == typeof(DaprMvcMarkerService)))
{
return builder;
}

builder.Services.AddDaprClient(configureClient);

builder.Services.AddSingleton<DaprMvcMarkerService>();
builder.Services.AddSingleton<IApplicationModelProvider, StateEntryApplicationModelProvider>();
builder.Services.TryAddSingleton<IApplicationModelProvider, StateEntryApplicationModelProvider>();

builder.Services.Configure<MvcOptions>(options =>
{
options.ModelBinderProviders.Insert(0, new StateEntryModelBinderProvider());
if (!options.ModelBinderProviders.Any(p => p is StateEntryModelBinderProvider))
{
options.ModelBinderProviders.Insert(0, new StateEntryModelBinderProvider());
}
});

return builder;
}

private class DaprMvcMarkerService
{
}
}
}
13 changes: 0 additions & 13 deletions src/Dapr.AspNetCore/DaprServiceCollectionExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -36,15 +36,6 @@ public static void AddDaprClient(this IServiceCollection services, Action<DaprCl
throw new ArgumentNullException(nameof(services));
}

// This pattern prevents registering services multiple times in the case AddDaprClient is called
// by non-user-code.
if (services.Any(s => s.ImplementationType == typeof(DaprClientMarkerService)))
{
return;
}

services.AddSingleton<DaprClientMarkerService>();

services.TryAddSingleton(_ =>
{
var builder = new DaprClientBuilder();
Expand All @@ -56,9 +47,5 @@ public static void AddDaprClient(this IServiceCollection services, Action<DaprCl
return builder.Build();
});
}

private class DaprClientMarkerService
{
}
}
}
38 changes: 38 additions & 0 deletions src/Dapr.Client/BulkStateItem.cs
Original file line number Diff line number Diff line change
Expand Up @@ -49,4 +49,42 @@ public BulkStateItem(string key, string value, string etag)
/// </summary>
public string ETag { get; }
}

/// <summary>
/// Represents a state object returned from a bulk get state operation where the value has
/// been deserialized to the specified type.
/// </summary>
public readonly struct BulkStateItem<TValue>
{
/// <summary>
/// Initializes a new instance of the <see cref="BulkStateItem"/> class.
/// </summary>
/// <param name="key">The state key.</param>
/// <param name="value">The typed value.</param>
/// <param name="etag">The ETag.</param>
/// <remarks>
/// Application code should not need to create instances of <see cref="BulkStateItem" />.
/// </remarks>
public BulkStateItem(string key, TValue value, string etag)
{
this.Key = key;
this.Value = value;
this.ETag = etag;
}

/// <summary>
/// Gets the state key.
/// </summary>
public string Key { get; }

/// <summary>
/// Gets the deserialized value of the indicated type.
/// </summary>
public TValue Value { get; }

/// <summary>
/// Get the ETag.
/// </summary>
public string ETag { get; }
}
}
14 changes: 14 additions & 0 deletions src/Dapr.Client/DaprClient.cs
Original file line number Diff line number Diff line change
Expand Up @@ -717,6 +717,20 @@ public abstract Task<TResponse> InvokeMethodGrpcAsync<TRequest, TResponse>(
/// <returns>A <see cref="Task{IReadOnlyList}" /> that will return the list of values when the operation has completed.</returns>
public abstract Task<IReadOnlyList<BulkStateItem>> GetBulkStateAsync(string storeName, IReadOnlyList<string> keys, int? parallelism, IReadOnlyDictionary<string, string> metadata = default, CancellationToken cancellationToken = default);

/// <summary>
/// Gets a list of deserialized values associated with the <paramref name="keys" /> from the Dapr state store. This overload should be used
/// if you expect the values of all the retrieved items to match the shape of the indicated <typeparam name="TValue"/>. If you expect that
/// the values may differ in type from one another, do not specify the type parameter and instead use the original <see cref="GetBulkStateAsync"/> method
/// so the serialized string values will be returned instead.
/// </summary>
/// <param name="storeName">The name of state store to read from.</param>
/// <param name="keys">The list of keys to get values for.</param>
/// <param name="parallelism">The number of concurrent get operations the Dapr runtime will issue to the state store. a value equal to or smaller than 0 means max parallelism.</param>
/// <param name="metadata">A collection of metadata key-value pairs that will be provided to the state store. The valid metadata keys and values are determined by the type of state store used.</param>
/// <param name="cancellationToken">A <see cref="CancellationToken" /> that can be used to cancel the operation.</param>
/// <returns>A <see cref="Task{IReadOnlyList}" /> that will return the list of deserialized values when the operation has completed.</returns>
public abstract Task<IReadOnlyList<BulkStateItem<TValue>>> GetBulkStateAsync<TValue>(string storeName, IReadOnlyList<string> keys, int? parallelism, IReadOnlyDictionary<string, string> metadata = default, CancellationToken cancellationToken = default);

/// <summary>
/// Saves a list of <paramref name="items" /> to the Dapr state store.
/// </summary>
Expand Down
55 changes: 50 additions & 5 deletions src/Dapr.Client/DaprClientGrpc.cs
Original file line number Diff line number Diff line change
Expand Up @@ -601,15 +601,58 @@ public override async Task<TResponse> InvokeMethodGrpcAsync<TRequest, TResponse>

#region State Apis

/// <inheritdoc />
public override async Task<IReadOnlyList<BulkStateItem>> GetBulkStateAsync(string storeName, IReadOnlyList<string> keys, int? parallelism, IReadOnlyDictionary<string, string> metadata = default, CancellationToken cancellationToken = default)
{
var rawBulkState = await GetBulkStateRawAsync(storeName, keys, parallelism, metadata, cancellationToken);

var bulkResponse = new List<BulkStateItem>();
foreach (var item in rawBulkState)
{
bulkResponse.Add(new BulkStateItem(item.Key, item.Value.ToStringUtf8(), item.Etag));
}

return bulkResponse;
}

/// <inheritdoc/>
public override async Task<IReadOnlyList<BulkStateItem<TValue>>> GetBulkStateAsync<TValue>(
string storeName,
IReadOnlyList<string> keys,
int? parallelism,
IReadOnlyDictionary<string, string> metadata = default,
CancellationToken cancellationToken = default)
{
var rawBulkState = await GetBulkStateRawAsync(storeName, keys, parallelism, metadata, cancellationToken);

var bulkResponse = new List<BulkStateItem<TValue>>();
foreach (var item in rawBulkState)
{
var deserializedValue = TypeConverters.FromJsonByteString<TValue>(item.Value, this.JsonSerializerOptions);
bulkResponse.Add(new BulkStateItem<TValue>(item.Key, deserializedValue, item.Etag));
}

return bulkResponse;
}

/// <summary>
/// Retrieves the bulk state data, but rather than deserializing the values, leaves the specific handling
/// to the public callers of this method to avoid duplicate deserialization.
/// </summary>
private async Task<IReadOnlyList<(string Key, string Etag, ByteString Value)>> GetBulkStateRawAsync(
string storeName,
IReadOnlyList<string> keys,
int? parallelism,
IReadOnlyDictionary<string, string> metadata = default,
CancellationToken cancellationToken = default)
{
ArgumentVerifier.ThrowIfNullOrEmpty(storeName, nameof(storeName));
if (keys.Count == 0)
throw new ArgumentException("keys do not contain any elements");

var envelope = new Autogenerated.GetBulkStateRequest()
{
StoreName = storeName,
StoreName = storeName,
Parallelism = parallelism ?? default
};

Expand All @@ -632,18 +675,20 @@ public override async Task<IReadOnlyList<BulkStateItem>> GetBulkStateAsync(strin
}
catch (RpcException ex)
{
throw new DaprException("State operation failed: the Dapr endpoint indicated a failure. See InnerException for details.", ex);
throw new DaprException(
"State operation failed: the Dapr endpoint indicated a failure. See InnerException for details.",
ex);
}

var bulkResponse = new List<BulkStateItem>();
var bulkResponse = new List<(string Key, string Etag, ByteString Value)>();
foreach (var item in response.Items)
{
bulkResponse.Add(new BulkStateItem(item.Key, item.Data.ToStringUtf8(), item.Etag));
bulkResponse.Add((item.Key, item.Etag, item.Data));
}

return bulkResponse;
}

/// <inheritdoc/>
public override async Task<TValue> GetStateAsync<TValue>(
string storeName,
Expand Down
18 changes: 18 additions & 0 deletions test/Dapr.AspNetCore.Test/DaprMvcBuilderExtensionsTest.cs
Original file line number Diff line number Diff line change
Expand Up @@ -84,5 +84,23 @@ public void AddDapr_RegistersDaprOnlyOnce()

Assert.False(daprClient.JsonSerializerOptions.PropertyNameCaseInsensitive);
}

#if NET8_0_OR_GREATER
[Fact]
public void AddDapr_WithKeyedServices()
{
var services = new ServiceCollection();

services.AddKeyedSingleton("key1", new Object());

services.AddControllers().AddDapr();

var serviceProvider = services.BuildServiceProvider();

var daprClient = serviceProvider.GetService<DaprClient>();

Assert.NotNull(daprClient);
}
#endif
}
}
18 changes: 18 additions & 0 deletions test/Dapr.AspNetCore.Test/DaprServiceCollectionExtensionsTest.cs
Original file line number Diff line number Diff line change
Expand Up @@ -47,5 +47,23 @@ public void AddDaprClient_RegistersDaprClientOnlyOnce()

Assert.True(daprClient.JsonSerializerOptions.PropertyNameCaseInsensitive);
}

#if NET8_0_OR_GREATER
[Fact]
public void AddDaprClient_WithKeyedServices()
{
var services = new ServiceCollection();

services.AddKeyedSingleton("key1", new Object());

services.AddDaprClient();

var serviceProvider = services.BuildServiceProvider();

var daprClient = serviceProvider.GetService<DaprClient>();

Assert.NotNull(daprClient);
}
#endif
}
}
24 changes: 24 additions & 0 deletions test/Dapr.Client.Test/StateApiTest.cs
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,30 @@ public async Task GetBulkStateAsync_CanReadState()
state.Should().HaveCount(1);
}

[Fact]
public async Task GetBulkStateAsync_CanReadDeserializedState()
{
await using var client = TestClient.CreateForDaprClient();

var key = "test";
var request = await client.CaptureGrpcRequestAsync(async daprClient =>
{
return await daprClient.GetBulkStateAsync<Widget>("testStore", new List<string>() {key}, null);
});

// Create Response & Respond
const string size = "small";
const string color = "yellow";
var data = new Widget() {Size = size, Color = color};
var envelope = MakeGetBulkStateResponse<Widget>(key, data);
var state = await request.CompleteWithMessageAsync(envelope);

// Get response and validate
state.Should().HaveCount(1);
state[0].Value.Size.Should().Match(size);
state[0].Value.Color.Should().Match(color);
}

[Fact]
public async Task GetBulkStateAsync_WrapsRpcException()
{
Expand Down

0 comments on commit cb28b7e

Please sign in to comment.