mirror of https://github.com/dapr/dotnet-sdk.git
Compare commits
7 Commits
Author | SHA1 | Date |
---|---|---|
|
6b49bed7f1 | |
|
faeeb8eaca | |
|
0873c5ef6f | |
|
32d06a7136 | |
|
6f07643280 | |
|
55895fa19d | |
|
c14fcea0d4 |
|
@ -48,8 +48,8 @@ jobs:
|
|||
GOOS: linux
|
||||
GOARCH: amd64
|
||||
GOPROXY: https://proxy.golang.org
|
||||
DAPR_CLI_VER: 1.14.0
|
||||
DAPR_RUNTIME_VER: 1.14.0
|
||||
DAPR_CLI_VER: 1.15.0
|
||||
DAPR_RUNTIME_VER: 1.15.3
|
||||
DAPR_INSTALL_URL: https://raw.githubusercontent.com/dapr/cli/release-1.14/install/install.sh
|
||||
DAPR_CLI_REF: ''
|
||||
steps:
|
||||
|
|
|
@ -9,12 +9,12 @@
|
|||
<PackageVersion Include="coverlet.msbuild" Version="6.0.2" />
|
||||
<PackageVersion Include="GitHubActionsTestLogger" Version="1.1.2" />
|
||||
<PackageVersion Include="Google.Api.CommonProtos" Version="2.2.0" />
|
||||
<PackageVersion Include="Google.Protobuf" Version="3.28.2" />
|
||||
<PackageVersion Include="Grpc.AspNetCore" Version="2.66.0" />
|
||||
<PackageVersion Include="Google.Protobuf" Version="3.30.2" />
|
||||
<PackageVersion Include="Grpc.AspNetCore" Version="2.71.0" />
|
||||
<PackageVersion Include="Grpc.Core.Testing" Version="2.46.6" />
|
||||
<PackageVersion Include="Grpc.Net.Client" Version="2.66.0" />
|
||||
<PackageVersion Include="Grpc.Net.ClientFactory" Version="2.66.0" />
|
||||
<PackageVersion Include="Grpc.Tools" Version="2.67.0" />
|
||||
<PackageVersion Include="Grpc.Net.Client" Version="2.71.0" />
|
||||
<PackageVersion Include="Grpc.Net.ClientFactory" Version="2.71.0" />
|
||||
<PackageVersion Include="Grpc.Tools" Version="2.71.0" />
|
||||
<PackageVersion Include="Microsoft.AspNetCore.Mvc.Testing" Version="6.0.35" />
|
||||
<PackageVersion Include="Microsoft.AspNetCore.TestHost" Version="6.0.35" />
|
||||
<PackageVersion Include="Microsoft.CodeAnalysis.Analyzers" Version="3.3.4" />
|
||||
|
@ -23,8 +23,8 @@
|
|||
<PackageVersion Include="Microsoft.CodeAnalysis.CSharp.SourceGenerators.Testing" Version="1.1.2" />
|
||||
<PackageVersion Include="Microsoft.CodeAnalysis.CSharp.SourceGenerators.Testing.XUnit" Version="1.1.2" />
|
||||
<PackageVersion Include="Microsoft.CodeAnalysis.CSharp.Workspaces" Version="4.8.0" />
|
||||
<PackageVersion Include="Microsoft.DurableTask.Client.Grpc" Version="1.5.0" />
|
||||
<PackageVersion Include="Microsoft.DurableTask.Worker.Grpc" Version="1.5.0" />
|
||||
<PackageVersion Include="Microsoft.DurableTask.Client.Grpc" Version="1.10.0" />
|
||||
<PackageVersion Include="Microsoft.DurableTask.Worker.Grpc" Version="1.10.0" />
|
||||
<PackageVersion Include="Microsoft.Extensions.Configuration" Version="6.0.1" />
|
||||
<PackageVersion Include="Microsoft.Extensions.Configuration.Abstractions" Version="6.0.0" />
|
||||
<PackageVersion Include="Microsoft.Extensions.DependencyInjection" Version="8.0.1" />
|
||||
|
|
|
@ -20,7 +20,7 @@ To load secrets into configuration call the _AddDaprSecretStore_ extension metho
|
|||
Use Dapr to run the application:
|
||||
|
||||
```shell
|
||||
dapr run --app-id SecretStoreConfigurationProviderSample --components-path ./components/ -- dotnet run
|
||||
dapr run --app-id SecretStoreConfigurationProviderSample --resources-path ./components/ -- dotnet run
|
||||
```
|
||||
|
||||
### 2. Test the application
|
||||
|
|
|
@ -147,7 +147,7 @@ cd examples/Client/ConfigurationApi
|
|||
To run the `ConfigurationExample`, execute the following command:
|
||||
|
||||
```bash
|
||||
dapr run --app-id configexample --components-path ./Components -- dotnet run
|
||||
dapr run --app-id configexample --resources-path ./Components -- dotnet run
|
||||
```
|
||||
|
||||
### Get Configuration
|
||||
|
|
|
@ -37,9 +37,8 @@ namespace Cryptography.Examples
|
|||
await using var encryptFs = new FileStream(fileName, FileMode.Open);
|
||||
|
||||
var bufferedEncryptedBytes = new ArrayBufferWriter<byte>();
|
||||
await foreach (var bytes in (await client.EncryptAsync(componentName, encryptFs, keyName,
|
||||
new EncryptionOptions(KeyWrapAlgorithm.Rsa), cancellationToken))
|
||||
.WithCancellation(cancellationToken))
|
||||
await foreach (var bytes in (client.EncryptAsync(componentName, encryptFs, keyName,
|
||||
new EncryptionOptions(KeyWrapAlgorithm.Rsa), cancellationToken)))
|
||||
{
|
||||
bufferedEncryptedBytes.Write(bytes.Span);
|
||||
}
|
||||
|
@ -53,8 +52,8 @@ namespace Cryptography.Examples
|
|||
|
||||
//We'll stream the decrypted bytes from a MemoryStream into the above temporary file
|
||||
await using var encryptedMs = new MemoryStream(bufferedEncryptedBytes.WrittenMemory.ToArray());
|
||||
await foreach (var result in (await client.DecryptAsync(componentName, encryptedMs, keyName,
|
||||
cancellationToken)).WithCancellation(cancellationToken))
|
||||
await foreach (var result in (client.DecryptAsync(componentName, encryptedMs, keyName,
|
||||
cancellationToken)))
|
||||
{
|
||||
decryptFs.Write(result.Span);
|
||||
}
|
||||
|
|
|
@ -24,7 +24,7 @@ cd examples/Client/DistributedLock
|
|||
In order to run the application that generates data for the workers to process, simply run the following command:
|
||||
|
||||
```bash
|
||||
dapr run --components-path ./Components --app-id generator -- dotnet run
|
||||
dapr run --resources-path ./Components --app-id generator -- dotnet run
|
||||
```
|
||||
|
||||
This application will create a new file to process once every 10 seconds. The files are stored in `DistributedLock/tmp`.
|
||||
|
@ -33,8 +33,8 @@ This application will create a new file to process once every 10 seconds. The fi
|
|||
In order to properly demonstrate locking, this application will be run more than once with the same App ID. However, the applications do need different ports in order to properly receive bindings. Run them with the command below:
|
||||
|
||||
```bash
|
||||
dapr run --components-path ./Components --app-id worker --app-port 5000 -- dotnet run
|
||||
dapr run --components-path ./Components --app-id worker --app-port 5001 -- dotnet run
|
||||
dapr run --resources-path ./Components --app-id worker --app-port 5000 -- dotnet run
|
||||
dapr run --resources-path ./Components --app-id worker --app-port 5001 -- dotnet run
|
||||
```
|
||||
|
||||
After running the applications, they will attempt to process files. You should see output such as:
|
||||
|
|
|
@ -0,0 +1,123 @@
|
|||
// ------------------------------------------------------------------------
|
||||
// Copyright 2025 The Dapr Authors
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
// ------------------------------------------------------------------------
|
||||
|
||||
using System;
|
||||
using System.Globalization;
|
||||
using System.Linq;
|
||||
using System.Text.RegularExpressions;
|
||||
|
||||
namespace Dapr.Actors.Extensions;
|
||||
|
||||
internal static class DurationExtensions
|
||||
{
|
||||
/// <summary>
|
||||
/// Used to parse the duration string accompanying an @every expression.
|
||||
/// </summary>
|
||||
private static readonly Regex durationRegex = new(@"(?<value>\d+(\.\d+)?)(?<unit>ns|us|µs|ms|s|m|h)", RegexOptions.Compiled | RegexOptions.IgnoreCase);
|
||||
/// <summary>
|
||||
/// A regular expression used to evaluate whether a given prefix period embodies an @every statement.
|
||||
/// </summary>
|
||||
private static readonly Regex isEveryExpression = new(@"^@every (\d+(\.\d+)?(ns|us|µs|ms|s|m|h))+$");
|
||||
/// <summary>
|
||||
/// The various acceptable duration values for a period expression.
|
||||
/// </summary>
|
||||
private static readonly string[] acceptablePeriodValues =
|
||||
{
|
||||
"yearly", "monthly", "weekly", "daily", "midnight", "hourly"
|
||||
};
|
||||
|
||||
private const string YearlyPrefixPeriod = "@yearly";
|
||||
private const string MonthlyPrefixPeriod = "@monthly";
|
||||
private const string WeeklyPrefixPeriod = "@weekly";
|
||||
private const string DailyPrefixPeriod = "@daily";
|
||||
private const string MidnightPrefixPeriod = "@midnight";
|
||||
private const string HourlyPrefixPeriod = "@hourly";
|
||||
private const string EveryPrefixPeriod = "@every";
|
||||
|
||||
/// <summary>
|
||||
/// Indicates that the schedule represents a prefixed period expression.
|
||||
/// </summary>
|
||||
/// <param name="expression"></param>
|
||||
/// <returns></returns>
|
||||
public static bool IsDurationExpression(this string expression) => expression.StartsWith('@') &&
|
||||
(isEveryExpression.IsMatch(expression) ||
|
||||
expression.EndsWithAny(acceptablePeriodValues, StringComparison.InvariantCulture));
|
||||
|
||||
/// <summary>
|
||||
/// Creates a TimeSpan value from the prefixed period value.
|
||||
/// </summary>
|
||||
/// <param name="period">The prefixed period value to parse.</param>
|
||||
/// <returns>A TimeSpan value matching the provided period.</returns>
|
||||
public static TimeSpan FromPrefixedPeriod(this string period)
|
||||
{
|
||||
if (period.StartsWith(YearlyPrefixPeriod))
|
||||
{
|
||||
var dateTime = DateTime.UtcNow;
|
||||
return dateTime.AddYears(1) - dateTime;
|
||||
}
|
||||
|
||||
if (period.StartsWith(MonthlyPrefixPeriod))
|
||||
{
|
||||
return TimeSpan.FromDays(30);
|
||||
}
|
||||
|
||||
if (period.StartsWith(MidnightPrefixPeriod))
|
||||
{
|
||||
return TimeSpan.Zero;
|
||||
}
|
||||
|
||||
if (period.StartsWith(WeeklyPrefixPeriod))
|
||||
{
|
||||
return TimeSpan.FromDays(7);
|
||||
}
|
||||
|
||||
if (period.StartsWith(DailyPrefixPeriod) || period.StartsWith(MidnightPrefixPeriod))
|
||||
{
|
||||
return TimeSpan.FromDays(1);
|
||||
}
|
||||
|
||||
if (period.StartsWith(HourlyPrefixPeriod))
|
||||
{
|
||||
return TimeSpan.FromHours(1);
|
||||
}
|
||||
|
||||
if (period.StartsWith(EveryPrefixPeriod))
|
||||
{
|
||||
//A sequence of decimal numbers each with an optional fraction and unit suffix
|
||||
//Valid time units are: 'ns', 'us'/'µs', 'ms', 's', 'm', and 'h'
|
||||
double totalMilliseconds = 0;
|
||||
var durationString = period.Split(' ').Last().Trim();
|
||||
|
||||
foreach (Match match in durationRegex.Matches(durationString))
|
||||
{
|
||||
var value = double.Parse(match.Groups["value"].Value, CultureInfo.InvariantCulture);
|
||||
var unit = match.Groups["unit"].Value.ToLower();
|
||||
|
||||
totalMilliseconds += unit switch
|
||||
{
|
||||
"ns" => value / 1_000_000,
|
||||
"us" or "µs" => value / 1_000,
|
||||
"ms" => value,
|
||||
"s" => value * 1_000,
|
||||
"m" => value * 1_000 * 60,
|
||||
"h" => value * 1_000 * 60 * 60,
|
||||
_ => throw new ArgumentException($"Unknown duration unit: {unit}")
|
||||
};
|
||||
}
|
||||
|
||||
return TimeSpan.FromMilliseconds(totalMilliseconds);
|
||||
}
|
||||
|
||||
throw new ArgumentException($"Unknown prefix period expression: {period}");
|
||||
}
|
||||
}
|
|
@ -0,0 +1,32 @@
|
|||
// ------------------------------------------------------------------------
|
||||
// Copyright 2025 The Dapr Authors
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
// ------------------------------------------------------------------------
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text.RegularExpressions;
|
||||
|
||||
namespace Dapr.Actors.Extensions;
|
||||
|
||||
internal static class StringExtensions
|
||||
{
|
||||
/// <summary>
|
||||
/// Extension method that validates a string against a list of possible matches.
|
||||
/// </summary>
|
||||
/// <param name="value">The string value to evaluate.</param>
|
||||
/// <param name="possibleValues">The possible values to look for a match within.</param>
|
||||
/// <param name="comparisonType">The type of string comparison to perform.</param>
|
||||
/// <returns>True if the value ends with any of the possible values; otherwise false.</returns>
|
||||
public static bool EndsWithAny(this string value, IReadOnlyList<string> possibleValues, StringComparison comparisonType )
|
||||
=> possibleValues.Any(val => value.EndsWith(val, comparisonType));
|
||||
}
|
|
@ -223,7 +223,7 @@ namespace Dapr.Actors.Runtime
|
|||
internal async Task FireTimerAsync(ActorId actorId, Stream requestBodyStream, CancellationToken cancellationToken = default)
|
||||
{
|
||||
#pragma warning disable 0618
|
||||
var timerData = await JsonSerializer.DeserializeAsync<TimerInfo>(requestBodyStream);
|
||||
var timerData = await DeserializeAsync(requestBodyStream);
|
||||
#pragma warning restore 0618
|
||||
|
||||
// Create a Func to be invoked by common method.
|
||||
|
@ -243,6 +243,62 @@ namespace Dapr.Actors.Runtime
|
|||
await this.DispatchInternalAsync(actorId, this.timerMethodContext, RequestFunc, cancellationToken);
|
||||
}
|
||||
|
||||
#pragma warning disable 0618
|
||||
internal static async Task<TimerInfo> DeserializeAsync(Stream stream)
|
||||
{
|
||||
var json = await JsonSerializer.DeserializeAsync<JsonElement>(stream);
|
||||
if (json.ValueKind == JsonValueKind.Null)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
var setAnyProperties = false; // Used to determine if anything was actually deserialized
|
||||
var dueTime = TimeSpan.Zero;
|
||||
var callback = "";
|
||||
var period = TimeSpan.Zero;
|
||||
var data = Array.Empty<byte>();
|
||||
TimeSpan? ttl = null;
|
||||
if (json.TryGetProperty("callback", out var callbackProperty))
|
||||
{
|
||||
setAnyProperties = true;
|
||||
callback = callbackProperty.GetString();
|
||||
}
|
||||
if (json.TryGetProperty("dueTime", out var dueTimeProperty))
|
||||
{
|
||||
setAnyProperties = true;
|
||||
var dueTimeString = dueTimeProperty.GetString();
|
||||
dueTime = ConverterUtils.ConvertTimeSpanFromDaprFormat(dueTimeString);
|
||||
}
|
||||
|
||||
if (json.TryGetProperty("period", out var periodProperty))
|
||||
{
|
||||
setAnyProperties = true;
|
||||
var periodString = periodProperty.GetString();
|
||||
(period, _) = ConverterUtils.ConvertTimeSpanValueFromISO8601Format(periodString);
|
||||
}
|
||||
|
||||
if (json.TryGetProperty("data", out var dataProperty) && dataProperty.ValueKind != JsonValueKind.Null)
|
||||
{
|
||||
setAnyProperties = true;
|
||||
data = dataProperty.GetBytesFromBase64();
|
||||
}
|
||||
|
||||
if (json.TryGetProperty("ttl", out var ttlProperty))
|
||||
{
|
||||
setAnyProperties = true;
|
||||
var ttlString = ttlProperty.GetString();
|
||||
ttl = ConverterUtils.ConvertTimeSpanFromDaprFormat(ttlString);
|
||||
}
|
||||
|
||||
if (!setAnyProperties)
|
||||
{
|
||||
return null; //No properties were ever deserialized, so return null instead of default values
|
||||
}
|
||||
|
||||
return new TimerInfo(callback, data, dueTime, period, ttl);
|
||||
}
|
||||
#pragma warning restore 0618
|
||||
|
||||
internal async Task ActivateActorAsync(ActorId actorId)
|
||||
{
|
||||
// An actor is activated by "Dapr" runtime when a call is to be made for an actor.
|
||||
|
|
|
@ -11,138 +11,148 @@
|
|||
// limitations under the License.
|
||||
// ------------------------------------------------------------------------
|
||||
|
||||
namespace Dapr.Actors.Runtime
|
||||
using Dapr.Actors.Extensions;
|
||||
|
||||
namespace Dapr.Actors.Runtime;
|
||||
|
||||
using System;
|
||||
using System.Text;
|
||||
using System.Text.RegularExpressions;
|
||||
|
||||
internal static class ConverterUtils
|
||||
{
|
||||
using System;
|
||||
using System.Text;
|
||||
using System.Text.RegularExpressions;
|
||||
|
||||
internal class ConverterUtils
|
||||
private static Regex regex = new("^(R(?<repetition>\\d+)/)?P((?<year>\\d+)Y)?((?<month>\\d+)M)?((?<week>\\d+)W)?((?<day>\\d+)D)?(T((?<hour>\\d+)H)?((?<minute>\\d+)M)?((?<second>\\d+)S)?)?$", RegexOptions.Compiled);
|
||||
public static TimeSpan ConvertTimeSpanFromDaprFormat(string valueString)
|
||||
{
|
||||
private static Regex regex = new Regex("^(R(?<repetition>\\d+)/)?P((?<year>\\d+)Y)?((?<month>\\d+)M)?((?<week>\\d+)W)?((?<day>\\d+)D)?(T((?<hour>\\d+)H)?((?<minute>\\d+)M)?((?<second>\\d+)S)?)?$", RegexOptions.Compiled);
|
||||
public static TimeSpan ConvertTimeSpanFromDaprFormat(string valueString)
|
||||
if (string.IsNullOrEmpty(valueString))
|
||||
{
|
||||
if (string.IsNullOrEmpty(valueString))
|
||||
{
|
||||
var never = TimeSpan.FromMilliseconds(-1);
|
||||
return never;
|
||||
}
|
||||
|
||||
// TimeSpan is a string. Format returned by Dapr is: 1h4m5s4ms4us4ns
|
||||
// acceptable values are: m, s, ms, us(micro), ns
|
||||
var spanOfValue = valueString.AsSpan();
|
||||
|
||||
// Change the value returned by Dapr runtime, so that it can be parsed with TimeSpan.
|
||||
// Format returned by Dapr runtime: 4h15m50s60ms. It doesnt have days.
|
||||
// Dapr runtime should handle timespans in ISO 8601 format.
|
||||
// Replace ms before m & s. Also append 0 days for parsing correctly with TimeSpan
|
||||
int hIndex = spanOfValue.IndexOf('h');
|
||||
int mIndex = spanOfValue.IndexOf('m');
|
||||
int sIndex = spanOfValue.IndexOf('s');
|
||||
int msIndex = spanOfValue.IndexOf("ms");
|
||||
|
||||
// handle days from hours.
|
||||
var hoursSpan = spanOfValue.Slice(0, hIndex);
|
||||
var hours = int.Parse(hoursSpan);
|
||||
var days = hours / 24;
|
||||
hours %= 24;
|
||||
|
||||
var minutesSpan = spanOfValue[(hIndex + 1)..mIndex];
|
||||
var minutes = int.Parse(minutesSpan);
|
||||
|
||||
var secondsSpan = spanOfValue[(mIndex + 1)..sIndex];
|
||||
var seconds = int.Parse(secondsSpan);
|
||||
|
||||
var millisecondsSpan = spanOfValue[(sIndex + 1)..msIndex];
|
||||
var milliseconds = int.Parse(millisecondsSpan);
|
||||
|
||||
return new TimeSpan(days, hours, minutes, seconds, milliseconds);
|
||||
var never = TimeSpan.FromMilliseconds(-1);
|
||||
return never;
|
||||
}
|
||||
|
||||
if (valueString.IsDurationExpression())
|
||||
{
|
||||
return valueString.FromPrefixedPeriod();
|
||||
}
|
||||
|
||||
public static string ConvertTimeSpanValueInDaprFormat(TimeSpan? value)
|
||||
{
|
||||
// write in format expected by Dapr, it only accepts h, m, s, ms, us(micro), ns
|
||||
var stringValue = string.Empty;
|
||||
if (value.Value >= TimeSpan.Zero)
|
||||
{
|
||||
var hours = (value.Value.Days * 24) + value.Value.Hours;
|
||||
stringValue = FormattableString.Invariant($"{hours}h{value.Value.Minutes}m{value.Value.Seconds}s{value.Value.Milliseconds}ms");
|
||||
}
|
||||
// TimeSpan is a string. Format returned by Dapr is: 1h4m5s4ms4us4ns
|
||||
// acceptable values are: m, s, ms, us(micro), ns
|
||||
var spanOfValue = valueString.AsSpan();
|
||||
|
||||
// Change the value returned by Dapr runtime, so that it can be parsed with TimeSpan.
|
||||
// Format returned by Dapr runtime: 4h15m50s60ms. It doesnt have days.
|
||||
// Dapr runtime should handle timespans in ISO 8601 format.
|
||||
// Replace ms before m & s. Also append 0 days for parsing correctly with TimeSpan
|
||||
int hIndex = spanOfValue.IndexOf('h');
|
||||
int mIndex = spanOfValue.IndexOf('m');
|
||||
int sIndex = spanOfValue.IndexOf('s');
|
||||
int msIndex = spanOfValue.IndexOf("ms");
|
||||
|
||||
// handle days from hours.
|
||||
var hoursSpan = spanOfValue[..hIndex];
|
||||
var hours = int.Parse(hoursSpan);
|
||||
var days = hours / 24;
|
||||
hours %= 24;
|
||||
|
||||
var minutesSpan = spanOfValue[(hIndex + 1)..mIndex];
|
||||
var minutes = int.Parse(minutesSpan);
|
||||
|
||||
var secondsSpan = spanOfValue[(mIndex + 1)..sIndex];
|
||||
var seconds = int.Parse(secondsSpan);
|
||||
|
||||
var millisecondsSpan = spanOfValue[(sIndex + 1)..msIndex];
|
||||
var milliseconds = int.Parse(millisecondsSpan);
|
||||
|
||||
return new TimeSpan(days, hours, minutes, seconds, milliseconds);
|
||||
}
|
||||
|
||||
public static string ConvertTimeSpanValueInDaprFormat(TimeSpan? value)
|
||||
{
|
||||
// write in format expected by Dapr, it only accepts h, m, s, ms, us(micro), ns
|
||||
var stringValue = string.Empty;
|
||||
if (value is null)
|
||||
return stringValue;
|
||||
}
|
||||
|
||||
public static string ConvertTimeSpanValueInISO8601Format(TimeSpan value, int? repetitions)
|
||||
|
||||
if (value.Value >= TimeSpan.Zero)
|
||||
{
|
||||
StringBuilder builder = new StringBuilder();
|
||||
|
||||
if (repetitions == null)
|
||||
{
|
||||
return ConvertTimeSpanValueInDaprFormat(value);
|
||||
}
|
||||
|
||||
if (value.Milliseconds > 0)
|
||||
{
|
||||
throw new ArgumentException("The TimeSpan value, combined with repetition cannot be in milliseconds.", nameof(value));
|
||||
}
|
||||
|
||||
builder.AppendFormat("R{0}/P", repetitions);
|
||||
|
||||
if(value.Days > 0)
|
||||
{
|
||||
builder.AppendFormat("{0}D", value.Days);
|
||||
}
|
||||
|
||||
builder.Append("T");
|
||||
|
||||
if(value.Hours > 0)
|
||||
{
|
||||
builder.AppendFormat("{0}H", value.Hours);
|
||||
}
|
||||
|
||||
if(value.Minutes > 0)
|
||||
{
|
||||
builder.AppendFormat("{0}M", value.Minutes);
|
||||
}
|
||||
|
||||
if(value.Seconds > 0)
|
||||
{
|
||||
builder.AppendFormat("{0}S", value.Seconds);
|
||||
}
|
||||
return builder.ToString();
|
||||
var hours = (value.Value.Days * 24) + value.Value.Hours;
|
||||
stringValue = FormattableString.Invariant($"{hours}h{value.Value.Minutes}m{value.Value.Seconds}s{value.Value.Milliseconds}ms");
|
||||
}
|
||||
|
||||
return stringValue;
|
||||
}
|
||||
|
||||
public static (TimeSpan, int?) ConvertTimeSpanValueFromISO8601Format(string valueString)
|
||||
public static string ConvertTimeSpanValueInISO8601Format(TimeSpan value, int? repetitions)
|
||||
{
|
||||
StringBuilder builder = new StringBuilder();
|
||||
|
||||
if (repetitions == null)
|
||||
{
|
||||
// ISO 8601 format can be Rn/PaYbMcHTdHeMfS or PaYbMcHTdHeMfS so if it does
|
||||
// not start with R or P then assuming it to default Dapr format without repetition
|
||||
if (!(valueString.StartsWith('R') || valueString.StartsWith('P')))
|
||||
{
|
||||
return (ConvertTimeSpanFromDaprFormat(valueString), -1);
|
||||
}
|
||||
|
||||
var matches = regex.Match(valueString);
|
||||
|
||||
var repetition = matches.Groups["repetition"].Success ? int.Parse(matches.Groups["repetition"].Value) : (int?)null;
|
||||
|
||||
var days = 0;
|
||||
var year = matches.Groups["year"].Success ? int.Parse(matches.Groups["year"].Value) : 0;
|
||||
days = year * 365;
|
||||
|
||||
var month = matches.Groups["month"].Success ? int.Parse(matches.Groups["month"].Value) : 0;
|
||||
days += month * 30;
|
||||
|
||||
var week = matches.Groups["week"].Success ? int.Parse(matches.Groups["week"].Value) : 0;
|
||||
days += week * 7;
|
||||
|
||||
var day = matches.Groups["day"].Success ? int.Parse(matches.Groups["day"].Value) : 0;
|
||||
days += day;
|
||||
|
||||
var hour = matches.Groups["hour"].Success ? int.Parse(matches.Groups["hour"].Value) : 0;
|
||||
var minute = matches.Groups["minute"].Success ? int.Parse(matches.Groups["minute"].Value) : 0;
|
||||
var second = matches.Groups["second"].Success ? int.Parse(matches.Groups["second"].Value) : 0;
|
||||
|
||||
return (new TimeSpan(days, hour, minute, second), repetition);
|
||||
return ConvertTimeSpanValueInDaprFormat(value);
|
||||
}
|
||||
|
||||
if (value.Milliseconds > 0)
|
||||
{
|
||||
throw new ArgumentException("The TimeSpan value, combined with repetition cannot be in milliseconds.", nameof(value));
|
||||
}
|
||||
|
||||
builder.Append($"R{repetitions}/P");
|
||||
|
||||
if(value.Days > 0)
|
||||
{
|
||||
builder.Append($"{value.Days}D");
|
||||
}
|
||||
|
||||
builder.Append('T');
|
||||
|
||||
if(value.Hours > 0)
|
||||
{
|
||||
builder.Append($"{value.Hours}H");
|
||||
}
|
||||
|
||||
if(value.Minutes > 0)
|
||||
{
|
||||
builder.Append($"{value.Minutes}M");
|
||||
}
|
||||
|
||||
if(value.Seconds > 0)
|
||||
{
|
||||
builder.Append($"{value.Seconds}S");
|
||||
}
|
||||
return builder.ToString();
|
||||
}
|
||||
|
||||
public static (TimeSpan, int?) ConvertTimeSpanValueFromISO8601Format(string valueString)
|
||||
{
|
||||
// ISO 8601 format can be Rn/PaYbMcHTdHeMfS or PaYbMcHTdHeMfS so if it does
|
||||
// not start with R or P then assuming it to default Dapr format without repetition
|
||||
if (!(valueString.StartsWith('R') || valueString.StartsWith('P')))
|
||||
{
|
||||
return (ConvertTimeSpanFromDaprFormat(valueString), -1);
|
||||
}
|
||||
|
||||
var matches = regex.Match(valueString);
|
||||
|
||||
var repetition = matches.Groups["repetition"].Success ? int.Parse(matches.Groups["repetition"].Value) : (int?)null;
|
||||
|
||||
|
||||
var days = 0;
|
||||
var year = matches.Groups["year"].Success ? int.Parse(matches.Groups["year"].Value) : 0;
|
||||
days = year * 365;
|
||||
|
||||
var month = matches.Groups["month"].Success ? int.Parse(matches.Groups["month"].Value) : 0;
|
||||
days += month * 30;
|
||||
|
||||
var week = matches.Groups["week"].Success ? int.Parse(matches.Groups["week"].Value) : 0;
|
||||
days += week * 7;
|
||||
|
||||
var day = matches.Groups["day"].Success ? int.Parse(matches.Groups["day"].Value) : 0;
|
||||
days += day;
|
||||
|
||||
var hour = matches.Groups["hour"].Success ? int.Parse(matches.Groups["hour"].Value) : 0;
|
||||
var minute = matches.Groups["minute"].Success ? int.Parse(matches.Groups["minute"].Value) : 0;
|
||||
var second = matches.Groups["second"].Success ? int.Parse(matches.Groups["second"].Value) : 0;
|
||||
|
||||
return (new TimeSpan(days, hour, minute, second), repetition);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -12,6 +12,7 @@
|
|||
// ------------------------------------------------------------------------
|
||||
|
||||
#nullable enable
|
||||
|
||||
namespace Dapr.Actors.Runtime;
|
||||
|
||||
using System;
|
||||
|
|
|
@ -0,0 +1,153 @@
|
|||
// ------------------------------------------------------------------------
|
||||
// Copyright 2025 The Dapr Authors
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
// ------------------------------------------------------------------------
|
||||
|
||||
#nullable enable
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Runtime.CompilerServices;
|
||||
using System.Threading;
|
||||
using System.Threading.Channels;
|
||||
using System.Threading.Tasks;
|
||||
using Google.Protobuf;
|
||||
using Grpc.Core;
|
||||
using Autogenerated = Dapr.Client.Autogen.Grpc.v1;
|
||||
#pragma warning disable CS1998 // Async method lacks 'await' operators and will run synchronously
|
||||
|
||||
namespace Dapr.Client.Crypto;
|
||||
|
||||
/// <summary>
|
||||
/// Provides the implementation to decrypt a stream of plaintext data with the Dapr runtime.
|
||||
/// </summary>
|
||||
internal sealed class DecryptionStreamProcessor : IDisposable
|
||||
{
|
||||
private bool disposed;
|
||||
private readonly Channel<ReadOnlyMemory<byte>> outputChannel = Channel.CreateUnbounded<ReadOnlyMemory<byte>>();
|
||||
|
||||
/// <summary>
|
||||
/// Surfaces any exceptions encountered while asynchronously processing the inbound and outbound streams.
|
||||
/// </summary>
|
||||
internal event EventHandler<Exception>? OnException;
|
||||
|
||||
/// <summary>
|
||||
/// Sends the provided bytes in chunks to the sidecar for the encryption operation.
|
||||
/// </summary>
|
||||
/// <param name="inputStream">The stream containing the bytes to decrypt.</param>
|
||||
/// <param name="call">The call to make to the sidecar to process the encryption operation.</param>
|
||||
/// <param name="streamingBlockSizeInBytes">The size, in bytes, of the streaming blocks.</param>
|
||||
/// <param name="options">The decryption options.</param>
|
||||
/// <param name="cancellationToken">Token used to cancel the ongoing request.</param>
|
||||
public async Task ProcessStreamAsync(
|
||||
Stream inputStream,
|
||||
AsyncDuplexStreamingCall<Autogenerated.DecryptRequest, Autogenerated.DecryptResponse> call,
|
||||
int streamingBlockSizeInBytes,
|
||||
Autogenerated.DecryptRequestOptions options,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
//Read from the input stream and write to the gRPC call
|
||||
_ = Task.Run(async () =>
|
||||
{
|
||||
try
|
||||
{
|
||||
await using var bufferedStream = new BufferedStream(inputStream, streamingBlockSizeInBytes);
|
||||
var buffer = new byte[streamingBlockSizeInBytes];
|
||||
int bytesRead;
|
||||
ulong sequenceNumber = 0;
|
||||
|
||||
while ((bytesRead = await bufferedStream.ReadAsync(buffer, cancellationToken)) > 0)
|
||||
{
|
||||
var request = new Autogenerated.DecryptRequest
|
||||
{
|
||||
Payload = new Autogenerated.StreamPayload
|
||||
{
|
||||
Data = ByteString.CopyFrom(buffer, 0, bytesRead), Seq = sequenceNumber
|
||||
}
|
||||
};
|
||||
|
||||
//Only include the options in the first message
|
||||
if (sequenceNumber == 0)
|
||||
{
|
||||
request.Options = options;
|
||||
}
|
||||
|
||||
await call.RequestStream.WriteAsync(request, cancellationToken);
|
||||
|
||||
//Increment the sequence number
|
||||
sequenceNumber++;
|
||||
}
|
||||
}
|
||||
catch (OperationCanceledException) when (cancellationToken.IsCancellationRequested)
|
||||
{
|
||||
// Expected cancellation exception
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
OnException?.Invoke(this, ex);
|
||||
}
|
||||
finally
|
||||
{
|
||||
await call.RequestStream.CompleteAsync();
|
||||
}
|
||||
}, cancellationToken);
|
||||
|
||||
//Start reading from the gRPC call and writing to the output channel
|
||||
_ = Task.Run(async () =>
|
||||
{
|
||||
try
|
||||
{
|
||||
await foreach (var response in call.ResponseStream.ReadAllAsync(cancellationToken))
|
||||
{
|
||||
await outputChannel.Writer.WriteAsync(response.Payload.Data.Memory, cancellationToken);
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
OnException?.Invoke(this, ex);
|
||||
}
|
||||
finally
|
||||
{
|
||||
outputChannel.Writer.Complete();
|
||||
}
|
||||
}, cancellationToken);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Retrieves the processed bytes from the operation from the sidecar and
|
||||
/// returns as an enumerable stream.
|
||||
/// </summary>
|
||||
public async IAsyncEnumerable<ReadOnlyMemory<byte>> GetProcessedDataAsync([EnumeratorCancellation] CancellationToken cancellationToken)
|
||||
{
|
||||
await foreach (var data in outputChannel.Reader.ReadAllAsync(cancellationToken))
|
||||
{
|
||||
yield return data;
|
||||
}
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
Dispose(true);
|
||||
}
|
||||
|
||||
private void Dispose(bool disposing)
|
||||
{
|
||||
if (!disposed)
|
||||
{
|
||||
if (disposing)
|
||||
{
|
||||
outputChannel.Writer.TryComplete();
|
||||
}
|
||||
|
||||
disposed = true;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,154 @@
|
|||
// ------------------------------------------------------------------------
|
||||
// Copyright 2025 The Dapr Authors
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
// ------------------------------------------------------------------------
|
||||
|
||||
#nullable enable
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Runtime.CompilerServices;
|
||||
using System.Threading;
|
||||
using System.Threading.Channels;
|
||||
using System.Threading.Tasks;
|
||||
using Google.Protobuf;
|
||||
using Grpc.Core;
|
||||
using Autogenerated = Dapr.Client.Autogen.Grpc.v1;
|
||||
#pragma warning disable CS1998 // Async method lacks 'await' operators and will run synchronously
|
||||
|
||||
namespace Dapr.Client.Crypto;
|
||||
|
||||
/// <summary>
|
||||
/// Provides the implementation to encrypt a stream of plaintext data with the Dapr runtime.
|
||||
/// </summary>
|
||||
internal sealed class EncryptionStreamProcessor : IDisposable
|
||||
{
|
||||
private bool disposed;
|
||||
private readonly Channel<ReadOnlyMemory<byte>> outputChannel = Channel.CreateUnbounded<ReadOnlyMemory<byte>>();
|
||||
|
||||
/// <summary>
|
||||
/// Surfaces any exceptions encountered while asynchronously processing the inbound and outbound streams.
|
||||
/// </summary>
|
||||
internal event EventHandler<Exception>? OnException;
|
||||
|
||||
/// <summary>
|
||||
/// Sends the provided bytes in chunks to the sidecar for the encryption operation.
|
||||
/// </summary>
|
||||
/// <param name="inputStream">The stream containing the bytes to encrypt.</param>
|
||||
/// <param name="call">The call to make to the sidecar to process the encryption operation.</param>
|
||||
/// <param name="options">The encryption options.</param>
|
||||
/// <param name="streamingBlockSizeInBytes">The size, in bytes, of the streaming blocks.</param>
|
||||
/// <param name="cancellationToken">Token used to cancel the ongoing request.</param>
|
||||
public async Task ProcessStreamAsync(
|
||||
Stream inputStream,
|
||||
AsyncDuplexStreamingCall<Autogenerated.EncryptRequest, Autogenerated.EncryptResponse> call,
|
||||
Autogenerated.EncryptRequestOptions options,
|
||||
int streamingBlockSizeInBytes,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
//Read from the input stream and write to the gRPC call
|
||||
_ = Task.Run(async () =>
|
||||
{
|
||||
try
|
||||
{
|
||||
await using var bufferedStream = new BufferedStream(inputStream, streamingBlockSizeInBytes);
|
||||
var buffer = new byte[streamingBlockSizeInBytes];
|
||||
int bytesRead;
|
||||
ulong sequenceNumber = 0;
|
||||
|
||||
while ((bytesRead = await bufferedStream.ReadAsync(buffer, cancellationToken)) > 0)
|
||||
{
|
||||
var request = new Autogenerated.EncryptRequest
|
||||
{
|
||||
Payload = new Autogenerated.StreamPayload
|
||||
{
|
||||
Data = ByteString.CopyFrom(buffer, 0, bytesRead), Seq = sequenceNumber
|
||||
}
|
||||
};
|
||||
|
||||
//Only include the options in the first message
|
||||
if (sequenceNumber == 0)
|
||||
{
|
||||
request.Options = options;
|
||||
}
|
||||
|
||||
await call.RequestStream.WriteAsync(request, cancellationToken);
|
||||
|
||||
//Increment the sequence number
|
||||
sequenceNumber++;
|
||||
}
|
||||
}
|
||||
catch (OperationCanceledException) when (cancellationToken.IsCancellationRequested)
|
||||
{
|
||||
// Expected cancellation exception
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
OnException?.Invoke(this, ex);
|
||||
}
|
||||
finally
|
||||
{
|
||||
await call.RequestStream.CompleteAsync();
|
||||
}
|
||||
}, cancellationToken);
|
||||
|
||||
//Start reading from the gRPC call and writing to the output channel
|
||||
_ = Task.Run(async () =>
|
||||
{
|
||||
try
|
||||
{
|
||||
await foreach (var response in call.ResponseStream.ReadAllAsync(cancellationToken))
|
||||
{
|
||||
await outputChannel.Writer.WriteAsync(response.Payload.Data.Memory, cancellationToken);
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
OnException?.Invoke(this, ex);
|
||||
}
|
||||
finally
|
||||
{
|
||||
outputChannel.Writer.Complete();
|
||||
}
|
||||
}, cancellationToken);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Retrieves the processed bytes from the operation from the sidecar and
|
||||
/// returns as an enumerable stream.
|
||||
/// </summary>
|
||||
public async IAsyncEnumerable<ReadOnlyMemory<byte>> GetProcessedDataAsync([EnumeratorCancellation] CancellationToken cancellationToken)
|
||||
{
|
||||
await foreach (var data in outputChannel.Reader.ReadAllAsync(cancellationToken))
|
||||
{
|
||||
yield return data;
|
||||
}
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
Dispose(true);
|
||||
GC.SuppressFinalize(this);
|
||||
}
|
||||
|
||||
private void Dispose(bool disposing)
|
||||
{
|
||||
if (!disposed)
|
||||
{
|
||||
if (disposing)
|
||||
{
|
||||
outputChannel.Writer.TryComplete();
|
||||
}
|
||||
|
||||
disposed = true;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1105,7 +1105,7 @@ namespace Dapr.Client
|
|||
/// <param name="cancellationToken">A <see cref="CancellationToken"/> that can be used to cancel the operation.</param>
|
||||
/// <returns>An array of encrypted bytes.</returns>
|
||||
[Obsolete("The API is currently not stable as it is in the Alpha stage. This attribute will be removed once it is stable.")]
|
||||
public abstract Task<IAsyncEnumerable<ReadOnlyMemory<byte>>> EncryptAsync(string vaultResourceName, Stream plaintextStream, string keyName,
|
||||
public abstract IAsyncEnumerable<ReadOnlyMemory<byte>> EncryptAsync(string vaultResourceName, Stream plaintextStream, string keyName,
|
||||
EncryptionOptions encryptionOptions, CancellationToken cancellationToken = default);
|
||||
|
||||
/// <summary>
|
||||
|
@ -1144,7 +1144,7 @@ namespace Dapr.Client
|
|||
/// <returns>An asynchronously enumerable array of decrypted bytes.</returns>
|
||||
[Obsolete(
|
||||
"The API is currently not stable as it is in the Alpha stage. This attribute will be removed once it is stable.")]
|
||||
public abstract Task<IAsyncEnumerable<ReadOnlyMemory<byte>>> DecryptAsync(string vaultResourceName, Stream ciphertextStream,
|
||||
public abstract IAsyncEnumerable<ReadOnlyMemory<byte>> DecryptAsync(string vaultResourceName, Stream ciphertextStream,
|
||||
string keyName, DecryptionOptions options, CancellationToken cancellationToken = default);
|
||||
|
||||
/// <summary>
|
||||
|
@ -1157,7 +1157,7 @@ namespace Dapr.Client
|
|||
/// <returns>An asynchronously enumerable array of decrypted bytes.</returns>
|
||||
[Obsolete(
|
||||
"The API is currently not stable as it is in the Alpha stage. This attribute will be removed once it is stable.")]
|
||||
public abstract Task<IAsyncEnumerable<ReadOnlyMemory<byte>>> DecryptAsync(string vaultResourceName, Stream ciphertextStream,
|
||||
public abstract IAsyncEnumerable<ReadOnlyMemory<byte>> DecryptAsync(string vaultResourceName, Stream ciphertextStream,
|
||||
string keyName, CancellationToken cancellationToken = default);
|
||||
|
||||
#endregion
|
||||
|
|
|
@ -11,10 +11,10 @@
|
|||
// limitations under the License.
|
||||
// ------------------------------------------------------------------------
|
||||
|
||||
using Dapr.Common.Extensions;
|
||||
|
||||
namespace Dapr.Client;
|
||||
|
||||
using Crypto;
|
||||
using System;
|
||||
using System.Buffers;
|
||||
using System.Collections.Generic;
|
||||
|
@ -23,7 +23,6 @@ using System.Linq;
|
|||
using System.Net.Http;
|
||||
using System.Net.Http.Json;
|
||||
using System.Runtime.CompilerServices;
|
||||
using System.Runtime.InteropServices;
|
||||
using System.Text.Json;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
|
@ -1665,18 +1664,17 @@ internal class DaprClientGrpc : DaprClient
|
|||
|
||||
/// <inheritdoc />
|
||||
[Obsolete(
|
||||
"The API is currently not stable as it is in the Alpha stage. This attribute will be removed once it is stable.")]
|
||||
"As of Dapr v1.17, this method will be removed and should be used via the Dapr.Cryptography package on NuGet")]
|
||||
public override async Task<ReadOnlyMemory<byte>> EncryptAsync(string vaultResourceName,
|
||||
ReadOnlyMemory<byte> plaintextBytes, string keyName, EncryptionOptions encryptionOptions,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
using var memoryStream = plaintextBytes.CreateMemoryStream(true);
|
||||
|
||||
var encryptionResult =
|
||||
await EncryptAsync(vaultResourceName, memoryStream, keyName, encryptionOptions, cancellationToken);
|
||||
var encryptionResult = EncryptAsync(vaultResourceName, memoryStream, keyName, encryptionOptions, cancellationToken);
|
||||
|
||||
var bufferedResult = new ArrayBufferWriter<byte>();
|
||||
await foreach (var item in encryptionResult.WithCancellation(cancellationToken))
|
||||
await foreach (var item in encryptionResult)
|
||||
{
|
||||
bufferedResult.Write(item.Span);
|
||||
}
|
||||
|
@ -1686,15 +1684,18 @@ internal class DaprClientGrpc : DaprClient
|
|||
|
||||
/// <inheritdoc />
|
||||
[Obsolete(
|
||||
"The API is currently not stable as it is in the Alpha stage. This attribute will be removed once it is stable.")]
|
||||
public override async Task<IAsyncEnumerable<ReadOnlyMemory<byte>>> EncryptAsync(string vaultResourceName,
|
||||
"As of Dapr v1.17, this method will be removed and should be used via the Dapr.Cryptography package on NuGet")]
|
||||
public override async IAsyncEnumerable<ReadOnlyMemory<byte>> EncryptAsync(string vaultResourceName,
|
||||
Stream plaintextStream,
|
||||
string keyName, EncryptionOptions encryptionOptions, CancellationToken cancellationToken = default)
|
||||
string keyName, EncryptionOptions encryptionOptions,
|
||||
[EnumeratorCancellation] CancellationToken cancellationToken = default)
|
||||
{
|
||||
ArgumentVerifier.ThrowIfNullOrEmpty(vaultResourceName, nameof(vaultResourceName));
|
||||
ArgumentVerifier.ThrowIfNullOrEmpty(keyName, nameof(keyName));
|
||||
ArgumentVerifier.ThrowIfNull(plaintextStream, nameof(plaintextStream));
|
||||
ArgumentVerifier.ThrowIfNull(encryptionOptions, nameof(encryptionOptions));
|
||||
|
||||
EventHandler<Exception> exceptionHandler = (_, ex) => throw ex;
|
||||
|
||||
var shouldOmitDecryptionKeyName =
|
||||
string.IsNullOrWhiteSpace(encryptionOptions
|
||||
|
@ -1717,185 +1718,91 @@ internal class DaprClientGrpc : DaprClient
|
|||
}
|
||||
|
||||
var options = CreateCallOptions(headers: null, cancellationToken);
|
||||
var duplexStream = client.EncryptAlpha1(options);
|
||||
var duplexStream = Client.EncryptAlpha1(options);
|
||||
|
||||
//Run both operations at the same time, but return the output of the streaming values coming from the operation
|
||||
var receiveResult = Task.FromResult(RetrieveEncryptedStreamAsync(duplexStream, cancellationToken));
|
||||
return await Task.WhenAll(
|
||||
//Stream the plaintext data to the sidecar in chunks
|
||||
SendPlaintextStreamAsync(plaintextStream, encryptionOptions.StreamingBlockSizeInBytes,
|
||||
duplexStream, encryptRequestOptions, cancellationToken),
|
||||
//At the same time, retrieve the encrypted response from the sidecar
|
||||
receiveResult).ContinueWith(_ => receiveResult.Result, cancellationToken);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Sends the plaintext bytes in chunks to the sidecar to be encrypted.
|
||||
/// </summary>
|
||||
private async Task SendPlaintextStreamAsync(Stream plaintextStream,
|
||||
int streamingBlockSizeInBytes,
|
||||
AsyncDuplexStreamingCall<Autogenerated.EncryptRequest, Autogenerated.EncryptResponse> duplexStream,
|
||||
Autogenerated.EncryptRequestOptions encryptRequestOptions,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
//Start with passing the metadata about the encryption request itself in the first message
|
||||
await duplexStream.RequestStream.WriteAsync(
|
||||
new Autogenerated.EncryptRequest { Options = encryptRequestOptions }, cancellationToken);
|
||||
|
||||
//Send the plaintext bytes in blocks in subsequent messages
|
||||
await using (var bufferedStream = new BufferedStream(plaintextStream, streamingBlockSizeInBytes))
|
||||
using var streamProcessor = new EncryptionStreamProcessor();
|
||||
try
|
||||
{
|
||||
var buffer = new byte[streamingBlockSizeInBytes];
|
||||
int bytesRead;
|
||||
ulong sequenceNumber = 0;
|
||||
streamProcessor.OnException += exceptionHandler;
|
||||
await streamProcessor.ProcessStreamAsync(plaintextStream, duplexStream, encryptRequestOptions,
|
||||
encryptionOptions.StreamingBlockSizeInBytes,
|
||||
cancellationToken);
|
||||
|
||||
while ((bytesRead =
|
||||
await bufferedStream.ReadAsync(buffer.AsMemory(0, streamingBlockSizeInBytes),
|
||||
cancellationToken)) !=
|
||||
0)
|
||||
await foreach (var value in streamProcessor.GetProcessedDataAsync(cancellationToken))
|
||||
{
|
||||
await duplexStream.RequestStream.WriteAsync(
|
||||
new Autogenerated.EncryptRequest
|
||||
{
|
||||
Payload = new Autogenerated.StreamPayload
|
||||
{
|
||||
Data = ByteString.CopyFrom(buffer, 0, bytesRead), Seq = sequenceNumber
|
||||
}
|
||||
}, cancellationToken);
|
||||
|
||||
//Increment the sequence number
|
||||
sequenceNumber++;
|
||||
yield return value;
|
||||
}
|
||||
}
|
||||
|
||||
//Send the completion message
|
||||
await duplexStream.RequestStream.CompleteAsync();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Retrieves the encrypted bytes from the encryption operation on the sidecar and returns as an enumerable stream.
|
||||
/// </summary>
|
||||
private async IAsyncEnumerable<ReadOnlyMemory<byte>> RetrieveEncryptedStreamAsync(
|
||||
AsyncDuplexStreamingCall<Autogenerated.EncryptRequest, Autogenerated.EncryptResponse> duplexStream,
|
||||
[EnumeratorCancellation] CancellationToken cancellationToken)
|
||||
{
|
||||
await foreach (var encryptResponse in duplexStream.ResponseStream.ReadAllAsync(cancellationToken)
|
||||
.ConfigureAwait(false))
|
||||
finally
|
||||
{
|
||||
yield return encryptResponse.Payload.Data.Memory;
|
||||
streamProcessor.OnException -= exceptionHandler;
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
[Obsolete(
|
||||
"The API is currently not stable as it is in the Alpha stage. This attribute will be removed once it is stable.")]
|
||||
public override async Task<IAsyncEnumerable<ReadOnlyMemory<byte>>> DecryptAsync(string vaultResourceName,
|
||||
"As of Dapr v1.17, this method will be removed and should be used via the Dapr.Cryptography package on NuGet")]
|
||||
public override async IAsyncEnumerable<ReadOnlyMemory<byte>> DecryptAsync(string vaultResourceName,
|
||||
Stream ciphertextStream, string keyName,
|
||||
DecryptionOptions decryptionOptions, CancellationToken cancellationToken = default)
|
||||
DecryptionOptions decryptionOptions,
|
||||
[EnumeratorCancellation] CancellationToken cancellationToken = default)
|
||||
{
|
||||
ArgumentVerifier.ThrowIfNullOrEmpty(vaultResourceName, nameof(vaultResourceName));
|
||||
ArgumentVerifier.ThrowIfNullOrEmpty(keyName, nameof(keyName));
|
||||
ArgumentVerifier.ThrowIfNull(ciphertextStream, nameof(ciphertextStream));
|
||||
ArgumentVerifier.ThrowIfNull(decryptionOptions, nameof(decryptionOptions));
|
||||
decryptionOptions ??= new DecryptionOptions();
|
||||
|
||||
EventHandler<Exception> exceptionHandler = (_, ex) => throw ex;
|
||||
|
||||
var decryptRequestOptions = new Autogenerated.DecryptRequestOptions
|
||||
{
|
||||
ComponentName = vaultResourceName, KeyName = keyName
|
||||
ComponentName = vaultResourceName,
|
||||
KeyName = keyName
|
||||
};
|
||||
|
||||
var options = CreateCallOptions(headers: null, cancellationToken);
|
||||
var duplexStream = client.DecryptAlpha1(options);
|
||||
|
||||
//Run both operations at the same time, but return the output of the streaming values coming from the operation
|
||||
var receiveResult = Task.FromResult(RetrieveDecryptedStreamAsync(duplexStream, cancellationToken));
|
||||
return await Task.WhenAll(
|
||||
//Stream the ciphertext data to the sidecar in chunks
|
||||
SendCiphertextStreamAsync(ciphertextStream, decryptionOptions.StreamingBlockSizeInBytes,
|
||||
duplexStream, decryptRequestOptions, cancellationToken),
|
||||
//At the same time, retrieve the decrypted response from the sidecar
|
||||
receiveResult)
|
||||
//Return only the result of the `RetrieveEncryptedStreamAsync` method
|
||||
.ContinueWith(t => receiveResult.Result, cancellationToken);
|
||||
using var streamProcessor = new DecryptionStreamProcessor();
|
||||
try
|
||||
{
|
||||
streamProcessor.OnException += exceptionHandler;
|
||||
await streamProcessor.ProcessStreamAsync(ciphertextStream, duplexStream, decryptionOptions.StreamingBlockSizeInBytes,
|
||||
decryptRequestOptions,
|
||||
cancellationToken);
|
||||
|
||||
await foreach (var value in streamProcessor.GetProcessedDataAsync(cancellationToken))
|
||||
{
|
||||
yield return value;
|
||||
}
|
||||
}
|
||||
finally
|
||||
{
|
||||
streamProcessor.OnException -= exceptionHandler;
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
[Obsolete(
|
||||
"The API is currently not stable as it is in the Alpha stage. This attribute will be removed once it is stable.")]
|
||||
public override Task<IAsyncEnumerable<ReadOnlyMemory<byte>>> DecryptAsync(string vaultResourceName,
|
||||
"As of Dapr v1.17, this method will be removed and should be used via the Dapr.Cryptography package on NuGet")]
|
||||
public override IAsyncEnumerable<ReadOnlyMemory<byte>> DecryptAsync(string vaultResourceName,
|
||||
Stream ciphertextStream, string keyName, CancellationToken cancellationToken = default) =>
|
||||
DecryptAsync(vaultResourceName, ciphertextStream, keyName, new DecryptionOptions(),
|
||||
cancellationToken);
|
||||
|
||||
/// <summary>
|
||||
/// Sends the ciphertext bytes in chunks to the sidecar to be decrypted.
|
||||
/// </summary>
|
||||
private async Task SendCiphertextStreamAsync(Stream ciphertextStream,
|
||||
int streamingBlockSizeInBytes,
|
||||
AsyncDuplexStreamingCall<Autogenerated.DecryptRequest, Autogenerated.DecryptResponse> duplexStream,
|
||||
Autogenerated.DecryptRequestOptions decryptRequestOptions,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
//Start with passing the metadata about the decryption request itself in the first message
|
||||
await duplexStream.RequestStream.WriteAsync(
|
||||
new Autogenerated.DecryptRequest { Options = decryptRequestOptions }, cancellationToken);
|
||||
|
||||
//Send the ciphertext bytes in blocks in subsequent messages
|
||||
await using (var bufferedStream = new BufferedStream(ciphertextStream, streamingBlockSizeInBytes))
|
||||
{
|
||||
var buffer = new byte[streamingBlockSizeInBytes];
|
||||
int bytesRead;
|
||||
ulong sequenceNumber = 0;
|
||||
|
||||
while ((bytesRead =
|
||||
await bufferedStream.ReadAsync(buffer.AsMemory(0, streamingBlockSizeInBytes),
|
||||
cancellationToken)) != 0)
|
||||
{
|
||||
await duplexStream.RequestStream.WriteAsync(
|
||||
new Autogenerated.DecryptRequest
|
||||
{
|
||||
Payload = new Autogenerated.StreamPayload
|
||||
{
|
||||
Data = ByteString.CopyFrom(buffer, 0, bytesRead), Seq = sequenceNumber
|
||||
}
|
||||
}, cancellationToken);
|
||||
|
||||
//Increment the sequence number
|
||||
sequenceNumber++;
|
||||
}
|
||||
}
|
||||
|
||||
//Send the completion message
|
||||
await duplexStream.RequestStream.CompleteAsync();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Retrieves the decrypted bytes from the decryption operation on the sidecar and returns as an enumerable stream.
|
||||
/// </summary>
|
||||
private async IAsyncEnumerable<ReadOnlyMemory<byte>> RetrieveDecryptedStreamAsync(
|
||||
AsyncDuplexStreamingCall<Autogenerated.DecryptRequest, Autogenerated.DecryptResponse> duplexStream,
|
||||
[EnumeratorCancellation] CancellationToken cancellationToken)
|
||||
{
|
||||
await foreach (var decryptResponse in duplexStream.ResponseStream.ReadAllAsync(cancellationToken)
|
||||
.ConfigureAwait(false))
|
||||
{
|
||||
yield return decryptResponse.Payload.Data.Memory;
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
[Obsolete(
|
||||
"The API is currently not stable as it is in the Alpha stage. This attribute will be removed once it is stable.")]
|
||||
"As of Dapr v1.17, this method will be removed and should be used via the Dapr.Cryptography package on NuGet")]
|
||||
public override async Task<ReadOnlyMemory<byte>> DecryptAsync(string vaultResourceName,
|
||||
ReadOnlyMemory<byte> ciphertextBytes, string keyName, DecryptionOptions decryptionOptions,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
using var memoryStream = ciphertextBytes.CreateMemoryStream(true);
|
||||
|
||||
var decryptionResult =
|
||||
await DecryptAsync(vaultResourceName, memoryStream, keyName, decryptionOptions, cancellationToken);
|
||||
var decryptionResult = DecryptAsync(vaultResourceName, memoryStream, keyName, decryptionOptions, cancellationToken);
|
||||
|
||||
var bufferedResult = new ArrayBufferWriter<byte>();
|
||||
await foreach (var item in decryptionResult.WithCancellation(cancellationToken))
|
||||
await foreach (var item in decryptionResult)
|
||||
{
|
||||
bufferedResult.Write(item.Span);
|
||||
}
|
||||
|
@ -1905,7 +1812,7 @@ internal class DaprClientGrpc : DaprClient
|
|||
|
||||
/// <inheritdoc />
|
||||
[Obsolete(
|
||||
"The API is currently not stable as it is in the Alpha stage. This attribute will be removed once it is stable.")]
|
||||
"As of Dapr v1.17, this method will be removed and should be used via the Dapr.Cryptography package on NuGet")]
|
||||
public override async Task<ReadOnlyMemory<byte>> DecryptAsync(string vaultResourceName,
|
||||
ReadOnlyMemory<byte> ciphertextBytes, string keyName, CancellationToken cancellationToken = default) =>
|
||||
await DecryptAsync(vaultResourceName, ciphertextBytes, keyName,
|
||||
|
|
|
@ -163,17 +163,7 @@ internal sealed class DaprJobsGrpcClient : DaprJobsClient
|
|||
var envelope = new Autogenerated.GetJobRequest { Name = jobName };
|
||||
var grpcCallOptions = DaprClientUtilities.ConfigureGrpcCallOptions(typeof(DaprJobsClient).Assembly, this.DaprApiToken, cancellationToken);
|
||||
var response = await Client.GetJobAlpha1Async(envelope, grpcCallOptions);
|
||||
var schedule = DateTime.TryParse(response.Job.DueTime, out var dueTime)
|
||||
? DaprJobSchedule.FromDateTime(dueTime)
|
||||
: new DaprJobSchedule(response.Job.Schedule);
|
||||
|
||||
return new DaprJobDetails(schedule)
|
||||
{
|
||||
DueTime = !string.IsNullOrWhiteSpace(response.Job.DueTime) ? DateTime.Parse(response.Job.DueTime) : null,
|
||||
Ttl = !string.IsNullOrWhiteSpace(response.Job.Ttl) ? DateTime.Parse(response.Job.Ttl) : null,
|
||||
RepeatCount = (int?)response.Job.Repeats,
|
||||
Payload = response.Job.Data.ToByteArray()
|
||||
};
|
||||
return DeserializeJobResponse(response);
|
||||
}
|
||||
catch (OperationCanceledException) when (cancellationToken.IsCancellationRequested)
|
||||
{
|
||||
|
@ -192,6 +182,29 @@ internal sealed class DaprJobsGrpcClient : DaprJobsClient
|
|||
throw new DaprException("Get job operation failed: the Dapr endpoint did not return the expected value.");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Testable method for performing job response deserialization.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// This is exposed strictly for testing purposes.
|
||||
/// </remarks>
|
||||
/// <param name="response">The job response to deserialize.</param>
|
||||
/// <returns>The deserialized job response.</returns>
|
||||
internal static DaprJobDetails DeserializeJobResponse(Autogenerated.GetJobResponse response)
|
||||
{
|
||||
var schedule = DateTime.TryParse(response.Job.DueTime, out var dueTime)
|
||||
? DaprJobSchedule.FromDateTime(dueTime)
|
||||
: new DaprJobSchedule(response.Job.Schedule);
|
||||
|
||||
return new DaprJobDetails(schedule)
|
||||
{
|
||||
DueTime = !string.IsNullOrWhiteSpace(response.Job.DueTime) ? DateTime.Parse(response.Job.DueTime) : null,
|
||||
Ttl = !string.IsNullOrWhiteSpace(response.Job.Ttl) ? DateTime.Parse(response.Job.Ttl) : null,
|
||||
RepeatCount = (int?)response.Job.Repeats ?? 0,
|
||||
Payload = response.Job.Data?.ToByteArray() ?? null
|
||||
};
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Deletes the specified job.
|
||||
/// </summary>
|
||||
|
|
|
@ -0,0 +1,35 @@
|
|||
// ------------------------------------------------------------------------
|
||||
// Copyright 2025 The Dapr Authors
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
// ------------------------------------------------------------------------
|
||||
|
||||
using System;
|
||||
using Dapr.Actors.Runtime;
|
||||
using Xunit;
|
||||
|
||||
namespace Dapr.Actors;
|
||||
|
||||
public class ConverterUtilsTests
|
||||
{
|
||||
[Fact]
|
||||
public void Deserialize_Period_Duration1()
|
||||
{
|
||||
var result = ConverterUtils.ConvertTimeSpanValueFromISO8601Format("@every 15m");
|
||||
Assert.Equal(TimeSpan.FromMinutes(15), result.Item1);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Deserialize_Period_Duration2()
|
||||
{
|
||||
var result = ConverterUtils.ConvertTimeSpanValueFromISO8601Format("@hourly");
|
||||
Assert.Equal(TimeSpan.FromHours(1), result.Item1);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,90 @@
|
|||
// ------------------------------------------------------------------------
|
||||
// Copyright 2025 The Dapr Authors
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
// ------------------------------------------------------------------------
|
||||
|
||||
using System;
|
||||
using Xunit;
|
||||
|
||||
namespace Dapr.Actors.Extensions;
|
||||
|
||||
public sealed class DurationExtensionsTests
|
||||
{
|
||||
[Theory]
|
||||
[InlineData("@yearly", 364, 0, 0, 0, 0)]
|
||||
[InlineData("@monthly", 28, 0, 0, 0, 0 )]
|
||||
[InlineData("@weekly", 7, 0, 0, 0, 0 )]
|
||||
[InlineData("@daily", 1, 0, 0, 0, 0)]
|
||||
[InlineData("@midnight", 0, 0, 0, 0, 0 )]
|
||||
[InlineData("@hourly", 0, 1, 0, 0, 0)]
|
||||
[InlineData("@every 1h", 0, 1, 0, 0, 0)]
|
||||
[InlineData("@every 30m", 0, 0, 30, 0, 0)]
|
||||
[InlineData("@every 45s", 0, 0, 0, 45, 0)]
|
||||
[InlineData("@every 1.5h", 0, 1, 30, 0, 0)]
|
||||
[InlineData("@every 1h30m", 0, 1, 30, 0, 0)]
|
||||
[InlineData("@every 1h30m45s", 0, 1, 30, 45, 0)]
|
||||
[InlineData("@every 1h30m45.3s", 0, 1, 30, 45, 300)]
|
||||
[InlineData("@every 100ms", 0, 0, 0, 0, 100)]
|
||||
[InlineData("@every 1s500ms", 0, 0, 0, 1, 500)]
|
||||
[InlineData("@every 1m1s", 0, 0, 1, 1, 0)]
|
||||
[InlineData("@every 1.1m", 0, 0, 1, 6, 0)]
|
||||
[InlineData("@every 1.5h30m45s100ms", 0, 2, 0, 45, 100)]
|
||||
public void ValidatePrefixedPeriodParsing(string input, int expectedDays, int expectedHours, int expectedMinutes, int expectedSeconds, int expectedMilliseconds)
|
||||
{
|
||||
var result = input.FromPrefixedPeriod();
|
||||
|
||||
if (input is "@yearly" or "@monthly")
|
||||
{
|
||||
Assert.True(result.Days >= expectedDays);
|
||||
return;
|
||||
}
|
||||
|
||||
Assert.Equal(expectedDays, result.Days);
|
||||
Assert.Equal(expectedHours, result.Hours);
|
||||
Assert.Equal(expectedMinutes, result.Minutes);
|
||||
Assert.Equal(expectedSeconds, result.Seconds);
|
||||
Assert.Equal(expectedMilliseconds, result.Milliseconds);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData("@yearly", true)]
|
||||
[InlineData("@monthly", true)]
|
||||
[InlineData("@weekly", true)]
|
||||
[InlineData("@daily", true)]
|
||||
[InlineData("@midnight", true)]
|
||||
[InlineData("@hourly", true)]
|
||||
[InlineData("@every 1h", true)]
|
||||
[InlineData("@every 30m", true)]
|
||||
[InlineData("@every 45s", true)]
|
||||
[InlineData("@every 1.5h", true)]
|
||||
[InlineData("@every 1h30m", true)]
|
||||
[InlineData("@every 1h30m45s", true)]
|
||||
[InlineData("@every 1h30m45.3s", true)]
|
||||
[InlineData("@every 100ms", true)]
|
||||
[InlineData("@every 1s500ms", true)]
|
||||
[InlineData("@every 1m1s", true)]
|
||||
[InlineData("@every 1.1m", true)]
|
||||
[InlineData("@every 1.5h30m45s100ms", true)]
|
||||
public void TestIsDurationExpression(string input, bool expectedResult)
|
||||
{
|
||||
var actualResult = input.IsDurationExpression();
|
||||
Assert.Equal(expectedResult, actualResult);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ValidateExceptionForUnknownExpression()
|
||||
{
|
||||
Assert.Throws<ArgumentException>(() =>
|
||||
{
|
||||
var result = "every 100s".FromPrefixedPeriod();
|
||||
});
|
||||
}
|
||||
}
|
|
@ -0,0 +1,41 @@
|
|||
// ------------------------------------------------------------------------
|
||||
// Copyright 2025 The Dapr Authors
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
// ------------------------------------------------------------------------
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using Xunit;
|
||||
|
||||
namespace Dapr.Actors.Extensions;
|
||||
|
||||
public sealed class StringExtensionsTests
|
||||
{
|
||||
[Fact]
|
||||
public void ValidateMatchesValue()
|
||||
{
|
||||
var matchingValues = new List<string> { "apples", "bananas", "cherries", };
|
||||
const string value = "I have four cherries";
|
||||
|
||||
var result = value.EndsWithAny(matchingValues, StringComparison.InvariantCulture);
|
||||
Assert.True(result);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ValidateDoesNotMatchValue()
|
||||
{
|
||||
var matchingValues = new List<string> { "apples", "bananas", "cherries", };
|
||||
const string value = "I have four grapes";
|
||||
|
||||
var result = value.EndsWithAny(matchingValues, StringComparison.InvariantCulture);
|
||||
Assert.False(result);
|
||||
}
|
||||
}
|
|
@ -12,6 +12,8 @@
|
|||
// ------------------------------------------------------------------------
|
||||
|
||||
using System;
|
||||
using System.IO;
|
||||
using System.Text;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Dapr.Actors.Client;
|
||||
|
@ -175,6 +177,160 @@ namespace Dapr.Actors.Runtime
|
|||
Assert.Equal(1, activator.DeleteCallCount);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task DeserializeTimer_Period_Iso8601_Time()
|
||||
{
|
||||
const string timerJson = "{\"callback\": \"TimerCallback\", \"period\": \"0h0m7s10ms\"}";
|
||||
await using var stream = new MemoryStream(Encoding.UTF8.GetBytes(timerJson));
|
||||
var result = await ActorManager.DeserializeAsync(stream);
|
||||
|
||||
Assert.Equal("TimerCallback", result.Callback);
|
||||
Assert.Equal(Array.Empty<byte>(), result.Data);
|
||||
Assert.Null(result.Ttl);
|
||||
Assert.Equal(TimeSpan.Zero, result.DueTime);
|
||||
Assert.Equal(TimeSpan.FromSeconds(7).Add(TimeSpan.FromMilliseconds(10)), result.Period);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task DeserializeTimer_Period_DaprFormat_Every()
|
||||
{
|
||||
const string timerJson = "{\"callback\": \"TimerCallback\", \"period\": \"@every 15s\"}";
|
||||
await using var stream = new MemoryStream(Encoding.UTF8.GetBytes(timerJson));
|
||||
var result = await ActorManager.DeserializeAsync(stream);
|
||||
|
||||
Assert.Equal("TimerCallback", result.Callback);
|
||||
Assert.Equal(Array.Empty<byte>(), result.Data);
|
||||
Assert.Null(result.Ttl);
|
||||
Assert.Equal(TimeSpan.Zero, result.DueTime);
|
||||
Assert.Equal(TimeSpan.FromSeconds(15), result.Period);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task DeserializeTimer_Period_DaprFormat_Every2()
|
||||
{
|
||||
const string timerJson = "{\"callback\": \"TimerCallback\", \"period\": \"@every 3h2m15s\"}";
|
||||
await using var stream = new MemoryStream(Encoding.UTF8.GetBytes(timerJson));
|
||||
var result = await ActorManager.DeserializeAsync(stream);
|
||||
|
||||
Assert.Equal("TimerCallback", result.Callback);
|
||||
Assert.Equal(Array.Empty<byte>(), result.Data);
|
||||
Assert.Null(result.Ttl);
|
||||
Assert.Equal(TimeSpan.Zero, result.DueTime);
|
||||
Assert.Equal(TimeSpan.FromHours(3).Add(TimeSpan.FromMinutes(2)).Add(TimeSpan.FromSeconds(15)), result.Period);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task DeserializeTimer_Period_DaprFormat_Monthly()
|
||||
{
|
||||
const string timerJson = "{\"callback\": \"TimerCallback\", \"period\": \"@monthly\"}";
|
||||
await using var stream = new MemoryStream(Encoding.UTF8.GetBytes(timerJson));
|
||||
var result = await ActorManager.DeserializeAsync(stream);
|
||||
|
||||
Assert.Equal("TimerCallback", result.Callback);
|
||||
Assert.Equal(Array.Empty<byte>(), result.Data);
|
||||
Assert.Null(result.Ttl);
|
||||
Assert.Equal(TimeSpan.Zero, result.DueTime);
|
||||
Assert.Equal(TimeSpan.FromDays(30), result.Period);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task DeserializeTimer_Period_DaprFormat_Weekly()
|
||||
{
|
||||
const string timerJson = "{\"callback\": \"TimerCallback\", \"period\": \"@weekly\"}";
|
||||
await using var stream = new MemoryStream(Encoding.UTF8.GetBytes(timerJson));
|
||||
var result = await ActorManager.DeserializeAsync(stream);
|
||||
|
||||
Assert.Equal("TimerCallback", result.Callback);
|
||||
Assert.Equal(Array.Empty<byte>(), result.Data);
|
||||
Assert.Null(result.Ttl);
|
||||
Assert.Equal(TimeSpan.Zero, result.DueTime);
|
||||
Assert.Equal(TimeSpan.FromDays(7), result.Period);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task DeserializeTimer_Period_DaprFormat_Daily()
|
||||
{
|
||||
const string timerJson = "{\"callback\": \"TimerCallback\", \"period\": \"@daily\"}";
|
||||
await using var stream = new MemoryStream(Encoding.UTF8.GetBytes(timerJson));
|
||||
var result = await ActorManager.DeserializeAsync(stream);
|
||||
|
||||
Assert.Equal("TimerCallback", result.Callback);
|
||||
Assert.Equal(Array.Empty<byte>(), result.Data);
|
||||
Assert.Null(result.Ttl);
|
||||
Assert.Equal(TimeSpan.Zero, result.DueTime);
|
||||
Assert.Equal(TimeSpan.FromDays(1), result.Period);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task DeserializeTimer_Period_DaprFormat_Hourly()
|
||||
{
|
||||
const string timerJson = "{\"callback\": \"TimerCallback\", \"period\": \"@hourly\"}";
|
||||
await using var stream = new MemoryStream(Encoding.UTF8.GetBytes(timerJson));
|
||||
var result = await ActorManager.DeserializeAsync(stream);
|
||||
|
||||
Assert.Equal("TimerCallback", result.Callback);
|
||||
Assert.Equal(Array.Empty<byte>(), result.Data);
|
||||
Assert.Null(result.Ttl);
|
||||
Assert.Equal(TimeSpan.Zero, result.DueTime);
|
||||
Assert.Equal(TimeSpan.FromHours(1), result.Period);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task DeserializeTimer_DueTime_DaprFormat_Hourly()
|
||||
{
|
||||
const string timerJson = "{\"callback\": \"TimerCallback\", \"dueTime\": \"@hourly\"}";
|
||||
await using var stream = new MemoryStream(Encoding.UTF8.GetBytes(timerJson));
|
||||
var result = await ActorManager.DeserializeAsync(stream);
|
||||
|
||||
Assert.Equal("TimerCallback", result.Callback);
|
||||
Assert.Equal(Array.Empty<byte>(), result.Data);
|
||||
Assert.Null(result.Ttl);
|
||||
Assert.Equal(TimeSpan.FromHours(1), result.DueTime);
|
||||
Assert.Equal(TimeSpan.Zero, result.Period);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task DeserializeTimer_DueTime_Iso8601Times()
|
||||
{
|
||||
const string timerJson = "{\"callback\": \"TimerCallback\", \"dueTime\": \"0h0m7s10ms\"}";
|
||||
await using var stream = new MemoryStream(Encoding.UTF8.GetBytes(timerJson));
|
||||
var result = await ActorManager.DeserializeAsync(stream);
|
||||
|
||||
Assert.Equal("TimerCallback", result.Callback);
|
||||
Assert.Equal(Array.Empty<byte>(), result.Data);
|
||||
Assert.Null(result.Ttl);
|
||||
Assert.Equal(TimeSpan.Zero, result.Period);
|
||||
Assert.Equal(TimeSpan.FromSeconds(7).Add(TimeSpan.FromMilliseconds(10)), result.DueTime);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task DeserializeTimer_Ttl_DaprFormat_Hourly()
|
||||
{
|
||||
const string timerJson = "{\"callback\": \"TimerCallback\", \"ttl\": \"@hourly\"}";
|
||||
await using var stream = new MemoryStream(Encoding.UTF8.GetBytes(timerJson));
|
||||
var result = await ActorManager.DeserializeAsync(stream);
|
||||
|
||||
Assert.Equal("TimerCallback", result.Callback);
|
||||
Assert.Equal(Array.Empty<byte>(), result.Data);
|
||||
Assert.Equal(TimeSpan.Zero, result.DueTime);
|
||||
Assert.Equal(TimeSpan.Zero, result.Period);
|
||||
Assert.Equal(TimeSpan.FromHours(1), result.Ttl);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task DeserializeTimer_Ttl_Iso8601Times()
|
||||
{
|
||||
const string timerJson = "{\"callback\": \"TimerCallback\", \"ttl\": \"0h0m7s10ms\"}";
|
||||
await using var stream = new MemoryStream(Encoding.UTF8.GetBytes(timerJson));
|
||||
var result = await ActorManager.DeserializeAsync(stream);
|
||||
|
||||
Assert.Equal("TimerCallback", result.Callback);
|
||||
Assert.Equal(Array.Empty<byte>(), result.Data);
|
||||
Assert.Equal(TimeSpan.Zero, result.DueTime);
|
||||
Assert.Equal(TimeSpan.Zero, result.Period);
|
||||
Assert.Equal(TimeSpan.FromSeconds(7).Add(TimeSpan.FromMilliseconds(10)), result.Ttl);
|
||||
}
|
||||
|
||||
private interface ITestActor : IActor { }
|
||||
|
||||
private class TestActor : Actor, ITestActor, IDisposable
|
||||
|
|
|
@ -1,5 +1,4 @@
|
|||
using System;
|
||||
using System.IO;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Xunit;
|
||||
|
@ -30,28 +29,6 @@ namespace Dapr.Client.Test
|
|||
(ReadOnlyMemory<byte>) Array.Empty<byte>(), keyName, new EncryptionOptions(KeyWrapAlgorithm.Rsa), CancellationToken.None));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task EncryptAsync_Stream_VaultResourceName_ArgumentVerifierException()
|
||||
{
|
||||
var client = new DaprClientBuilder().Build();
|
||||
const string vaultResourceName = "";
|
||||
//Get response and validate
|
||||
await Assert.ThrowsAsync<ArgumentException>(async () => await client.EncryptAsync(vaultResourceName,
|
||||
new MemoryStream(), "MyKey", new EncryptionOptions(KeyWrapAlgorithm.Rsa),
|
||||
CancellationToken.None));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task EncryptAsync_Stream_KeyName_ArgumentVerifierException()
|
||||
{
|
||||
var client = new DaprClientBuilder().Build();
|
||||
const string keyName = "";
|
||||
//Get response and validate
|
||||
await Assert.ThrowsAsync<ArgumentException>(async () => await client.EncryptAsync("myVault",
|
||||
(Stream) new MemoryStream(), keyName, new EncryptionOptions(KeyWrapAlgorithm.Rsa),
|
||||
CancellationToken.None));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task DecryptAsync_ByteArray_VaultResourceName_ArgumentVerifierException()
|
||||
{
|
||||
|
@ -71,25 +48,5 @@ namespace Dapr.Client.Test
|
|||
await Assert.ThrowsAsync<ArgumentException>(async () => await client.DecryptAsync("myVault",
|
||||
Array.Empty<byte>(), keyName, new DecryptionOptions(), CancellationToken.None));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task DecryptAsync_Stream_VaultResourceName_ArgumentVerifierException()
|
||||
{
|
||||
var client = new DaprClientBuilder().Build();
|
||||
const string vaultResourceName = "";
|
||||
//Get response and validate
|
||||
await Assert.ThrowsAsync<ArgumentException>(async () => await client.DecryptAsync(vaultResourceName,
|
||||
new MemoryStream(), "MyKey", new DecryptionOptions(), CancellationToken.None));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task DecryptAsync_Stream_KeyName_ArgumentVerifierException()
|
||||
{
|
||||
var client = new DaprClientBuilder().Build();
|
||||
const string keyName = "";
|
||||
//Get response and validate
|
||||
await Assert.ThrowsAsync<ArgumentException>(async () => await client.DecryptAsync("myVault",
|
||||
new MemoryStream(), keyName, new DecryptionOptions(), CancellationToken.None));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -97,7 +97,7 @@ namespace Dapr.E2E.Test
|
|||
/// <see cref="ISerializationActor"/>. (it's defined in the base of it.)
|
||||
/// That why <see cref="ISerializationActor.AnotherMethod(DateTime)"/> was created,
|
||||
/// so there are now more then one method.
|
||||
/// </remark>
|
||||
/// </remarks>
|
||||
[Fact]
|
||||
public async Task ActorCanSupportCustomSerializerAndCallMoreThenOneDefinedMethod()
|
||||
{
|
||||
|
|
|
@ -45,7 +45,7 @@ namespace Dapr.E2E.Test
|
|||
{
|
||||
var (appPort, httpPort, grpcPort, metricsPort) = GetFreePorts();
|
||||
|
||||
var componentsPath = Combine(".", "..", "..", "..", "..", "..", "test", "Dapr.E2E.Test", "components");
|
||||
var resourcesPath = Combine(".", "..", "..", "..", "..", "..", "test", "Dapr.E2E.Test", "components");
|
||||
var configPath = Combine(".", "..", "..", "..", "..", "..", "test", "Dapr.E2E.Test", "configuration", "featureconfig.yaml");
|
||||
var arguments = new List<string>()
|
||||
{
|
||||
|
@ -55,11 +55,10 @@ namespace Dapr.E2E.Test
|
|||
"--dapr-http-port", httpPort.ToString(CultureInfo.InvariantCulture),
|
||||
"--dapr-grpc-port", grpcPort.ToString(CultureInfo.InvariantCulture),
|
||||
"--metrics-port", metricsPort.ToString(CultureInfo.InvariantCulture),
|
||||
"--components-path", componentsPath,
|
||||
"--resources-path", resourcesPath,
|
||||
"--config", configPath,
|
||||
"--log-level", "debug",
|
||||
"--dapr-http-max-request-size", "32",
|
||||
|
||||
"--max-body-size", "8Mi"
|
||||
};
|
||||
|
||||
if (configuration.UseAppPort)
|
||||
|
|
|
@ -13,6 +13,7 @@
|
|||
|
||||
using System;
|
||||
using System.Net.Http;
|
||||
using Dapr.Client.Autogen.Grpc.v1;
|
||||
using Dapr.Jobs.Models;
|
||||
using Moq;
|
||||
using Xunit;
|
||||
|
@ -167,6 +168,21 @@ public sealed class DaprJobsGrpcClientTests
|
|||
});
|
||||
#pragma warning restore CS0618 // Type or member is obsolete
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ShouldDeserialize_EveryExpression()
|
||||
{
|
||||
const string scheduleText = "@every 1m";
|
||||
var response = new GetJobResponse { Job = new Job { Name = "test", Schedule = scheduleText } };
|
||||
var schedule = DaprJobSchedule.FromExpression(scheduleText);
|
||||
|
||||
var jobDetails = DaprJobsGrpcClient.DeserializeJobResponse(response);
|
||||
Assert.Null(jobDetails.Payload);
|
||||
Assert.Equal(0, jobDetails.RepeatCount);
|
||||
Assert.Null(jobDetails.Ttl);
|
||||
Assert.Null(jobDetails.DueTime);
|
||||
Assert.Equal(jobDetails.Schedule.ExpressionValue, schedule.ExpressionValue);
|
||||
}
|
||||
|
||||
private sealed record TestPayload(string Name, string Color);
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue