Compare commits

...

7 Commits

Author SHA1 Message Date
Whit Waldo 6b49bed7f1
Re-enabled the test with a fix to use a fixed 30 days for @monthly in actor deserialization for now (#1530)
Signed-off-by: Whit Waldo <whit.waldo@innovian.net>
2025-05-02 17:35:08 -05:00
Whit Waldo faeeb8eaca
Fix for large file cryptography support (#1528)
* Porting the changes from Dapr.Cryptography 1.16 back to 1.15

Signed-off-by: Whit Waldo <whit.waldo@innovian.net>
2025-05-02 16:51:41 -05:00
Whit Waldo 0873c5ef6f
Updating gRPC and Microsoft.DurableTask.* packages (#1523)
* Updating gRPC, Google.Protobuf and Microsoft.DurableTask.* packages
2025-04-25 14:45:24 -05:00
Whit Waldo 32d06a7136
Tentative fix for timers deserializing error (#1512)
* Tentative fix for deserializing error
* Added unit tests to prove out timer deserialization for all supported formats

Signed-off-by: Whit Waldo <whit.waldo@innovian.net>
2025-04-08 00:17:49 -05:00
Whit Waldo 6f07643280
Refactored to make deserialization for GetJobAsync testable. Added unit test to validate reported customer issue. (#1497)
Signed-off-by: Whit Waldo <whit.waldo@innovian.net>
2025-04-01 17:32:17 -05:00
Whit Waldo 55895fa19d
Updated Dapr runtime/CLI version used in integration tests (#1485)
* Updated itests.yml to use latest 1.15 runtime and CLI versions over 1.14 versions
* Updated CLI argument name as `dapr-http-max-request-size` was changed in a recent update included in the CLI

Signed-off-by: Whit Waldo <whit.waldo@innovian.net>
2025-03-16 20:14:32 -05:00
Whit Waldo c14fcea0d4
Actor reminder deserialization bugfix (#1483)
Signed-off-by: Whit Waldo <whit.waldo@innovian.net>
2025-03-11 13:47:17 -05:00
24 changed files with 1091 additions and 349 deletions

View File

@ -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:

View File

@ -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" />

View File

@ -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

View File

@ -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

View File

@ -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);
}

View File

@ -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:

View File

@ -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}");
}
}

View File

@ -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));
}

View File

@ -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.

View File

@ -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);
}
}

View File

@ -12,6 +12,7 @@
// ------------------------------------------------------------------------
#nullable enable
namespace Dapr.Actors.Runtime;
using System;

View File

@ -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;
}
}
}

View File

@ -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;
}
}
}

View File

@ -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

View File

@ -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,

View File

@ -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>

View File

@ -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);
}
}

View File

@ -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();
});
}
}

View File

@ -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);
}
}

View File

@ -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

View File

@ -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));
}
}
}

View File

@ -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()
{

View File

@ -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)

View File

@ -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);
}