diff --git a/.devcontainer/localinit.sh b/.devcontainer/localinit.sh index 80b27e4f4..69c4c1274 100644 --- a/.devcontainer/localinit.sh +++ b/.devcontainer/localinit.sh @@ -6,4 +6,4 @@ az extension add --name containerapp --yes nvm install v18.12.1 # initialize Dapr -dapr init --runtime-version=1.10.0-rc.2 \ No newline at end of file +dapr init --runtime-version=1.14.0 \ No newline at end of file diff --git a/.github/workflows/itests.yml b/.github/workflows/itests.yml index 2c97343ed..36741ce7c 100644 --- a/.github/workflows/itests.yml +++ b/.github/workflows/itests.yml @@ -42,9 +42,9 @@ jobs: GOOS: linux GOARCH: amd64 GOPROXY: https://proxy.golang.org - DAPR_CLI_VER: 1.13.0 - DAPR_RUNTIME_VER: 1.13.0 - DAPR_INSTALL_URL: https://raw.githubusercontent.com/dapr/cli/release-1.13/install/install.sh + DAPR_CLI_VER: 1.14.0 + DAPR_RUNTIME_VER: 1.14.0 + DAPR_INSTALL_URL: https://raw.githubusercontent.com/dapr/cli/release-1.14/install/install.sh DAPR_CLI_REF: '' steps: - name: Set up Dapr CLI diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index f47877cbd..7712340a5 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -123,3 +123,13 @@ A non-exclusive list of code that must be places in `vendor/`: ## Code of Conduct This project has adopted the [Contributor Covenant Code of Conduct](https://github.com/dapr/community/blob/master/CODE-OF-CONDUCT.md) + + + +## GitHub Dapr Bot Commands + +Checkout the [daprbot documentation](https://docs.dapr.io/contributing/daprbot/) for Github commands you can run in this repo for common tasks. For example, you can comment `/assign` on an issue to assign it to yourself. + + + + diff --git a/Directory.Packages.props b/Directory.Packages.props new file mode 100644 index 000000000..3c1459b5d --- /dev/null +++ b/Directory.Packages.props @@ -0,0 +1,47 @@ + + + true + true + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/all.sln b/all.sln index 0b95478f3..1dd0ab3c5 100644 --- a/all.sln +++ b/all.sln @@ -1,5 +1,5 @@ Microsoft Visual Studio Solution File, Format Version 12.00 -# Visual Studio Version 17 +# 17 VisualStudioVersion = 17.3.32929.385 MinimumVisualStudioVersion = 10.0.40219.1 Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Dapr.Actors", "src\Dapr.Actors\Dapr.Actors.csproj", "{C2DB4B64-B7C3-4FED-8753-C040F677C69A}" @@ -14,11 +14,6 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Dapr.Client", "src\Dapr.Cli EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Dapr.AspNetCore", "src\Dapr.AspNetCore\Dapr.AspNetCore.csproj", "{08D602F6-7C11-4653-B70B-B56333BF6FD2}" EndProject -Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "samples", "samples", "{B2DB41EE-45F5-447B-95E8-38E1E8B70C4E}" - ProjectSection(SolutionItems) = preProject - samples\.editorconfig = samples\.editorconfig - EndProjectSection -EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "test", "test", "{DD020B34-460F-455F-8D17-CF4A949F100B}" EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Dapr.Client.Test", "test\Dapr.Client.Test\Dapr.Client.Test.csproj", "{383609C1-F43F-49EB-85E4-1964EE7F0F14}" @@ -32,6 +27,7 @@ EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{1BD1276E-D28A-45EA-89B1-6AD48471500D}" ProjectSection(SolutionItems) = preProject .editorconfig = .editorconfig + README.md = README.md EndProjectSection EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Dapr.Actors.AspNetCore.Test", "test\Dapr.Actors.AspNetCore.Test\Dapr.Actors.AspNetCore.Test.csproj", "{9C1D6ABA-5EDE-4FA0-A8A9-0AB98CB74737}" @@ -117,6 +113,12 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Dapr.E2E.Test.Actors.Genera EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Cryptography", "examples\Client\Cryptography\Cryptography.csproj", "{C74FBA78-13E8-407F-A173-4555AEE41FF3}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Dapr.Protos", "src\Dapr.Protos\Dapr.Protos.csproj", "{DFBABB04-50E9-42F6-B470-310E1B545638}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Dapr.Common", "src\Dapr.Common\Dapr.Common.csproj", "{B445B19C-A925-4873-8CB7-8317898B6970}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Dapr.Common.Test", "test\Dapr.Common.Test\Dapr.Common.Test.csproj", "{CDB47863-BEBD-4841-A807-46D868962521}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -289,6 +291,18 @@ Global {C74FBA78-13E8-407F-A173-4555AEE41FF3}.Debug|Any CPU.Build.0 = Debug|Any CPU {C74FBA78-13E8-407F-A173-4555AEE41FF3}.Release|Any CPU.ActiveCfg = Release|Any CPU {C74FBA78-13E8-407F-A173-4555AEE41FF3}.Release|Any CPU.Build.0 = Release|Any CPU + {DFBABB04-50E9-42F6-B470-310E1B545638}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {DFBABB04-50E9-42F6-B470-310E1B545638}.Debug|Any CPU.Build.0 = Debug|Any CPU + {DFBABB04-50E9-42F6-B470-310E1B545638}.Release|Any CPU.ActiveCfg = Release|Any CPU + {DFBABB04-50E9-42F6-B470-310E1B545638}.Release|Any CPU.Build.0 = Release|Any CPU + {B445B19C-A925-4873-8CB7-8317898B6970}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {B445B19C-A925-4873-8CB7-8317898B6970}.Debug|Any CPU.Build.0 = Debug|Any CPU + {B445B19C-A925-4873-8CB7-8317898B6970}.Release|Any CPU.ActiveCfg = Release|Any CPU + {B445B19C-A925-4873-8CB7-8317898B6970}.Release|Any CPU.Build.0 = Release|Any CPU + {CDB47863-BEBD-4841-A807-46D868962521}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {CDB47863-BEBD-4841-A807-46D868962521}.Debug|Any CPU.Build.0 = Debug|Any CPU + {CDB47863-BEBD-4841-A807-46D868962521}.Release|Any CPU.ActiveCfg = Release|Any CPU + {CDB47863-BEBD-4841-A807-46D868962521}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -342,19 +356,9 @@ Global {AF89083D-4715-42E6-93E9-38497D12A8A6} = {DD020B34-460F-455F-8D17-CF4A949F100B} {B5CDB0DC-B26D-48F1-B934-FE5C1C991940} = {DD020B34-460F-455F-8D17-CF4A949F100B} {C74FBA78-13E8-407F-A173-4555AEE41FF3} = {A7F41094-8648-446B-AECD-DCC2CC871F73} - EndGlobalSection - GlobalSection(ExtensibilityGlobals) = postSolution - SolutionGuid = {65220BF2-EAE1-4CB2-AA58-EBE80768CB40} - EndGlobalSection -EndGlobal -8-446B-AECD-DCC2CC871F73} - EndGlobalSection - GlobalSection(ExtensibilityGlobals) = postSolution - SolutionGuid = {65220BF2-EAE1-4CB2-AA58-EBE80768CB40} - EndGlobalSection -EndGlobal -C991940} = {DD020B34-460F-455F-8D17-CF4A949F100B} - {C74FBA78-13E8-407F-A173-4555AEE41FF3} = {A7F41094-8648-446B-AECD-DCC2CC871F73} + {DFBABB04-50E9-42F6-B470-310E1B545638} = {27C5D71D-0721-4221-9286-B94AB07B58CF} + {B445B19C-A925-4873-8CB7-8317898B6970} = {27C5D71D-0721-4221-9286-B94AB07B58CF} + {CDB47863-BEBD-4841-A807-46D868962521} = {DD020B34-460F-455F-8D17-CF4A949F100B} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {65220BF2-EAE1-4CB2-AA58-EBE80768CB40} diff --git a/daprdocs/content/en/dotnet-sdk-contributing/dotnet-contributing.md b/daprdocs/content/en/dotnet-sdk-contributing/dotnet-contributing.md index a4e546ffa..6664191d6 100644 --- a/daprdocs/content/en/dotnet-sdk-contributing/dotnet-contributing.md +++ b/daprdocs/content/en/dotnet-sdk-contributing/dotnet-contributing.md @@ -21,3 +21,7 @@ The `daprdocs` directory contains the markdown files that are rendered into the - All rules in the [docs guide]({{< ref contributing-docs.md >}}) should be followed in addition to these. - All files and directories should be prefixed with `dotnet-` to ensure all file/directory names are globally unique across all Dapr documentation. + +## GitHub Dapr Bot Commands + +Checkout the [daprbot documentation](https://docs.dapr.io/contributing/daprbot/) for Github commands you can run in this repo for common tasks. For example, you can comment `/assign` on an issue to assign it to yourself. diff --git a/daprdocs/content/en/dotnet-sdk-docs/_index.md b/daprdocs/content/en/dotnet-sdk-docs/_index.md index e823ca29f..121dde310 100644 --- a/daprdocs/content/en/dotnet-sdk-docs/_index.md +++ b/daprdocs/content/en/dotnet-sdk-docs/_index.md @@ -18,7 +18,7 @@ Dapr offers a variety of packages to help with the development of .NET applicati - [Dapr CLI]({{< ref install-dapr-cli.md >}}) installed - Initialized [Dapr environment]({{< ref install-dapr-selfhost.md >}}) -- [.NET Core 3.1 or .NET 5+](https://dotnet.microsoft.com/download) installed +- [.NET 6+](https://dotnet.microsoft.com/download) installed ## Installation diff --git a/daprdocs/content/en/dotnet-sdk-docs/dotnet-actors/dotnet-actors-howto.md b/daprdocs/content/en/dotnet-sdk-docs/dotnet-actors/dotnet-actors-howto.md index 8229d6820..eaa13625d 100644 --- a/daprdocs/content/en/dotnet-sdk-docs/dotnet-actors/dotnet-actors-howto.md +++ b/daprdocs/content/en/dotnet-sdk-docs/dotnet-actors/dotnet-actors-howto.md @@ -45,7 +45,7 @@ This project contains the implementation of the actor client which calls MyActor - [Dapr CLI]({{< ref install-dapr-cli.md >}}) installed. - Initialized [Dapr environment]({{< ref install-dapr-selfhost.md >}}). -- [.NET Core 3.1 or .NET 6+](https://dotnet.microsoft.com/download) installed. Dapr .NET SDK uses [ASP.NET Core](https://docs.microsoft.com/aspnet/core/introduction-to-aspnet-core?view=aspnetcore-6.0). +- [.NET 6+](https://dotnet.microsoft.com/download) installed. Dapr .NET SDK uses [ASP.NET Core](https://docs.microsoft.com/aspnet/core/introduction-to-aspnet-core?view=aspnetcore-6.0). ## Step 0: Prepare diff --git a/daprdocs/content/en/dotnet-sdk-docs/dotnet-actors/dotnet-actors-serialization.md b/daprdocs/content/en/dotnet-sdk-docs/dotnet-actors/dotnet-actors-serialization.md index abbeb437d..787a7e41f 100644 --- a/daprdocs/content/en/dotnet-sdk-docs/dotnet-actors/dotnet-actors-serialization.md +++ b/daprdocs/content/en/dotnet-sdk-docs/dotnet-actors/dotnet-actors-serialization.md @@ -5,15 +5,263 @@ linkTitle: "Actor serialization" weight: 300000 description: Necessary steps to serialize your types using remoted Actors in .NET --- +# Actor Serialization -The Dapr actor package enables you to use Dapr virtual actors within a .NET application with strongly-typed remoting, but if you intend to send and receive strongly-typed data from your methods, there are a few key ground rules to understand. In this guide, you will learn how to configure your classes and records so they are properly serialized and deserialized at runtime. +The Dapr actor package enables you to use Dapr virtual actors within a .NET application with either a weakly- or strongly-typed client. Each utilizes a different serialization approach. This document will review the differences and convey a few key ground rules to understand in either scenario. -# Data Contract Serialization -When Dapr's virtual actors are invoked via the remoting proxy, your data is serialized using a serialization engine called the [Data Contract Serializer](https://learn.microsoft.com/en-us/dotnet/framework/wcf/feature-details/serializable-types) implemented by the [DataContractSerializer](https://learn.microsoft.com/en-us/dotnet/api/system.runtime.serialization.datacontractserializer) class, which converts your C# types to and from XML documents. When sending or receiving primitives (like strings or ints), this serialization happens transparently and there's no requisite preparation needed on your part. However, when working with complex types such as those you create, there are some important rules to take into consideration so this process works smoothly. +Please be advised that it is not a supported scenario to use the weakly- or strongly typed actor clients interchangeably because of these different serialization approaches. The data persisted using one Actor client will not be accessible using the other Actor client, so it is important to pick one and use it consistently throughout your application. -This serialization framework is not specific to Dapr and is separately maintained by the .NET team within the [.NET Github repository](https://github.com/dotnet/runtime/blob/main/src/libraries/System.Private.DataContractSerialization/src/System/Runtime/Serialization/DataContractSerializer.cs). +## Weakly-typed Dapr Actor client +In this section, you will learn how to configure your C# types so they are properly serialized and deserialized at runtime when using a weakly-typed actor client. These clients use string-based names of methods with request and response payloads that are serialized using the System.Text.Json serializer. Please note that this serialization framework is not specific to Dapr and is separately maintained by the .NET team within the [.NET GitHub repository](https://github.com/dotnet/runtime/tree/main/src/libraries/System.Text.Json). -## Serializable Types +When using the weakly-typed Dapr Actor client to invoke methods from your various actors, it's not necessary to independently serialize or deserialize the method payloads as this will happen transparently on your behalf by the SDK. + +The client will use the latest version of System.Text.Json available for the version of .NET you're building against and serialization is subject to all the inherent capabilities provided in the [associated .NET documentation](https://learn.microsoft.com/en-us/dotnet/standard/serialization/system-text-json/overview). + +The serializer will be configured to use the `JsonSerializerOptions.Web` [default options](https://learn.microsoft.com/en-us/dotnet/standard/serialization/system-text-json/configure-options?pivots=dotnet-8-0#web-defaults-for-jsonserializeroptions) unless overridden with a custom options configuration which means the following are applied: +- Deserialization of the property name is performed in a case-insensitive manner +- Serialization of the property name is performed using [camel casing](https://en.wikipedia.org/wiki/Camel_case) unless the property is overridden with a `[JsonPropertyName]` attribute +- Deserialization will read numeric values from number and/or string values + +### Basic Serialization +In the following example, we present a simple class named Doodad though it could just as well be a record as well. + +```csharp +public class Doodad +{ + public Guid Id { get; set; } + public string Name { get; set; } + public int Count { get; set; } +} +``` + +By default, this will serialize using the names of the members as used in the type and whatever values it was instantiated with: + +```json +{"id": "a06ced64-4f42-48ad-84dd-46ae6a7e333d", "name": "DoodadName", "count": 5} +``` + +### Override Serialized Property Name +The default property names can be overridden by applying the `[JsonPropertyName]` attribute to desired properties. + +Generally, this isn't going to be necessary for types you're persisting to the actor state as you're not intended to read or write them independent of Dapr-associated functionality, but +the following is provided just to clearly illustrate that it's possible. + +#### Override Property Names on Classes +Here's an example demonstrating the use of `JsonPropertyName` to change the name for the first property following serialization. Note that the last usage of `JsonPropertyName` on the `Count` property +matches what it would be expected to serialize to. This is largely just to demonstrate that applying this attribute won't negatively impact anything - in fact, it might be preferable if you later +decide to change the default serialization options but still need to consistently access the properties previously serialized before that change as `JsonPropertyName` will override those options. + +```csharp +public class Doodad +{ + [JsonPropertyName("identifier")] + public Guid Id { get; set; } + public string Name { get; set; } + [JsonPropertyName("count")] + public int Count { get; set; } +} +``` + +This would serialize to the following: + +```json +{"identifier": "a06ced64-4f42-48ad-84dd-46ae6a7e333d", "name": "DoodadName", "count": 5} +``` + +#### Override Property Names on Records +Let's try doing the same thing with a record from C# 12 or later: + +```csharp +public record Thingy(string Name, [JsonPropertyName("count")] int Count); +``` + +Because the argument passed in a primary constructor (introduced in C# 12) can be applied to either a property or field within a record, using the `[JsonPropertyName]` attribute may +require specifying that you intend the attribute to apply to a property and not a field in some ambiguous cases. Should this be necessary, you'd indicate as much in the primary constructor with: + +```csharp +public record Thingy(string Name, [property: JsonPropertyName("count")] int Count); +``` + +If `[property: ]` is applied to the `[JsonPropertyName]` attribute where it's not necessary, it will not negatively impact serialization or deserialization as the operation will +proceed normally as though it were a property (as it typically would if not marked as such). + +### Enumeration types +Enumerations, including flat enumerations are serializable to JSON, but the value persisted may surprise you. Again, it's not expected that the developer should ever engage +with the serialized data independently of Dapr, but the following information may at least help in diagnosing why a seemingly mild version migration isn't working as expected. + +Take the following `enum` type providing the various seasons in the year: + +```csharp +public enum Season +{ + Spring, + Summer, + Fall, + Winter +} +``` + +We'll go ahead and use a separate demonstration type that references our `Season` and simultaneously illustrate how this works with records: + +```csharp +public record Engagement(string Name, Season TimeOfYear); +``` + +Given the following initialized instance: + +```csharp +var myEngagement = new Engagement("Ski Trip", Season.Winter); +``` + +This would serialize to the following JSON: +```json +{"name": "Ski Trip", "season": 3} +``` + +That might be unexpected that our `Season.Winter` value was represented as a `3`, but this is because the serializer is going to automatically use numeric representations +of the enum values starting with zero for the first value and incrementing the numeric value for each additional value available. Again, if a migration were taking place and +a developer had flipped the order of the enums, this would affect a breaking change in your solution as the serialized numeric values would point to different values when deserialized. + +Rather, there is a `JsonConverter` available with `System.Text.Json` that will instead opt to use a string-based value instead of the numeric value. The `[JsonConverter]` attribute needs +to be applied to be enum type itself to enable this, but will then be realized in any downstream serialization or deserialization operation that references the enum. + +```csharp +[JsonConverter(typeof(JsonStringEnumConverter))] +public enum Season +{ + Spring, + Summer, + Fall, + Winter +} +``` + +Using the same values from our `myEngagement` instance above, this would produce the following JSON instead: + +```json +{"name": "Ski Trip", "season": "Winter"} +``` + +As a result, the enum members can be shifted around without fear of introducing errors during deserialization. + +#### Custom Enumeration Values + +The System.Text.Json serialization platform doesn't, out of the box, support the use of `[EnumMember]` to allow you to change the value of enum that's used during serialization or deserialization, but +there are scenarios where this could be useful. Again, assume that you're tasking with refactoring the solution to apply some better names to your various +enums. You're using the `JsonStringEnumConverter` detailed above so you're saving the name of the enum to value instead of a numeric value, but if you change +the enum name, that will introduce a breaking change as the name will no longer match what's in state. + +Do note that if you opt into using this approach, you should decorate all your enum members with the `[EnumMeber]` attribute so that the values are consistently applied for each enum value instead +of haphazardly. Nothing will validate this at build or runtime, but it is considered a best practice operation. + +How can you specify the precise value persisted while still changing the name of the enum member in this scenario? Use a custom `JsonConverter` with an extension method that can pull the value +out of the attached `[EnumMember]` attributes where provided. Add the following to your solution: + +```csharp +public sealed class EnumMemberJsonConverter : JsonConverter where T : struct, Enum +{ + /// Reads and converts the JSON to type . + /// The reader. + /// The type to convert. + /// An object that specifies serialization options to use. + /// The converted value. + public override T Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + { + // Get the string value from the JSON reader + var value = reader.GetString(); + + // Loop through all the enum values + foreach (var enumValue in Enum.GetValues()) + { + // Get the value from the EnumMember attribute, if any + var enumMemberValue = GetValueFromEnumMember(enumValue); + + // If the values match, return the enum value + if (value == enumMemberValue) + { + return enumValue; + } + } + + // If no match found, throw an exception + throw new JsonException($"Invalid value for {typeToConvert.Name}: {value}"); + } + + /// Writes a specified value as JSON. + /// The writer to write to. + /// The value to convert to JSON. + /// An object that specifies serialization options to use. + public override void Write(Utf8JsonWriter writer, T value, JsonSerializerOptions options) + { + // Get the value from the EnumMember attribute, if any + var enumMemberValue = GetValueFromEnumMember(value); + + // Write the value to the JSON writer + writer.WriteStringValue(enumMemberValue); + } + + private static string GetValueFromEnumMember(T value) + { + MemberInfo[] member = typeof(T).GetMember(value.ToString(), BindingFlags.DeclaredOnly | BindingFlags.Static | BindingFlags.Public); + if (member.Length == 0) + return value.ToString(); + object[] customAttributes = member.GetCustomAttributes(typeof(EnumMemberAttribute), false); + if (customAttributes.Length != 0) + { + EnumMemberAttribute enumMemberAttribute = (EnumMemberAttribute)customAttributes; + if (enumMemberAttribute != null && enumMemberAttribute.Value != null) + return enumMemberAttribute.Value; + } + return value.ToString(); + } +} +``` + +Now let's add a sample enumerator. We'll set a value that uses the lower-case version of each enum member to demonstrate this. Don't forget to decorate the enum with the `JsonConverter` +attribute and reference our custom converter in place of the numeral-to-string converter used in the last section. + +```csharp +[JsonConverter(typeof(EnumMemberJsonConverter))] +public enum Season +{ + [EnumMember(Value="spring")] + Spring, + [EnumMember(Value="summer")] + Summer, + [EnumMember(Value="fall")] + Fall, + [EnumMember(Value="winter")] + Winter +} +``` + +Let's use our sample record from before. We'll also add a `[JsonPropertyName]` attribute just to augment the demonstration: +```csharp +public record Engagement([property: JsonPropertyName("event")] string Name, Season TimeOfYear); +``` + +And finally, let's initialize a new instance of this: + +```csharp +var myEngagement = new Engagement("Conference", Season.Fall); +``` + +This time, serialization will take into account the values from the attached `[EnumMember]` attribute providing us a mechanism to refactor our application without necessitating +a complex versioning scheme for our existing enum values in the state. + +```json +{"event": "Conference", "season": "fall"} +``` + +## Strongly-typed Dapr Actor client +In this section, you will learn how to configure your classes and records so they are properly serialized and deserialized at runtime when using a strongly-typed actor client. These clients are implemented using .NET interfaces and are not compatible with Dapr Actors written using other languages. + +This actor client serializes data using an engine called the [Data Contract Serializer](https://learn.microsoft.com/en-us/dotnet/framework/wcf/feature-details/serializable-types) which converts your C# types to and from XML documents. This serialization framework is not specific to Dapr and is separately maintained by the .NET team within the [.NET GitHub repository](https://github.com/dotnet/runtime/blob/main/src/libraries/System.Private.DataContractSerialization/src/System/Runtime/Serialization/DataContractSerializer.cs). + +When sending or receiving primitives (like strings or ints), this serialization happens transparently and there's no requisite preparation needed on your part. However, when working with complex types such as those you create, there are some important rules to take into consideration so this process works smoothly. + +### Serializable Types There are several important considerations to keep in mind when using the Data Contract Serializer: - By default, all types, read/write properties (after construction) and fields marked as publicly visible are serialized @@ -23,14 +271,14 @@ There are several important considerations to keep in mind when using the Data C - Serialization is supported for types that use other complex types that are not themselves marked with the DataContractAttribute attribute through the use of the KnownTypesAttribute attribute - If a type is marked with the DataContractAttribute attribute, all members you wish to serialize and deserialize must be decorated with the DataMemberAttribute attribute as well or they'll be set to their default values -## How does deserialization work? +### How does deserialization work? The approach used for deserialization depends on whether or not the type is decorated with the [DataContractAttribute](https://learn.microsoft.com/en-us/dotnet/api/system.runtime.serialization.datacontractattribute) attribute. If this attribute isn't present, an instance of the type is created using the parameterless constructor. Each of the properties and fields are then mapped into the type using their respective setters and the instance is returned to the caller. If the type _is_ marked with `[DataContract]`, the serializer instead uses reflection to read the metadata of the type and determine which properties or fields should be included based on whether or not they're marked with the DataMemberAttribute attribute as it's performed on an opt-in basis. It then allocates an uninitialized object in memory (avoiding the use of any constructors, parameterless or not) and then sets the value directly on each mapped property or field, even if private or uses init-only setters. Serialization callbacks are invoked as applicable throughout this process and then the object is returned to the caller. Use of the serialization attributes is highly recommended as they grant more flexibility to override names and namespaces and generally use more of the modern C# functionality. While the default serializer can be relied on for primitive types, it's not recommended for any of your own types, whether they be classes, structs or records. It's recommended that if you decorate a type with the DataContractAttribute attribute, you also explicitly decorate each of the members you want to serialize or deserialize with the DataMemberAttribute attribute as well. -### .NET Classes +#### .NET Classes Classes are fully supported in the Data Contract Serializer provided that that other rules detailed on this page and the [Data Contract Serializer](https://learn.microsoft.com/en-us/dotnet/framework/wcf/feature-details/serializable-types) documentation are also followed. The most important thing to remember here is that you must either have a public parameterless constructor or you must decorate it with the appropriate attributes. Let's review some examples to really clarify what will and won't work. @@ -153,7 +401,7 @@ When this is serialized, because we're changing the names of the serialized memb ``` -#### Classes in C# 12 - Primary Constructors +##### Classes in C# 12 - Primary Constructors C# 12 brought us primary constructors on classes. Use of a primary constructor means the compiler will be prevented from creating the default implicit parameterless constructor. While a primary constructor on a class doesn't generate any public properties, it does mean that if you pass this primary constructor any arguments or have non-primitive types in your class, you'll either need to specify your own parameterless constructor or use the serialization attributes. Here's an example where we're using the primary constructor to inject an ILogger to a field and add our own parameterless constructor without the need for any attributes. @@ -198,7 +446,7 @@ public class Doodad(ILogger _logger) } ``` -### .NET Structs +#### .NET Structs Structs are supported by the Data Contract serializer provided that they are marked with the DataContractAttribute attribute and the members you wish to serialize are marked with the DataMemberAttribute attribute. Further, to support deserialization, the struct will also need to have a parameterless constructor. This works even if you define your own parameterless constructor as enabled in C# 10. ```csharp @@ -210,7 +458,7 @@ public struct Doodad } ``` -### .NET Records +#### .NET Records Records were introduced in C# 9 and follow precisely the same rules as classes when it comes to serialization. We recommend that you should decorate all your records with the DataContractAttribute attribute and members you wish to serialize with DataMemberAttribute attributes so you don't experience any deserialization issues using this or other newer C# functionalities. Because record classes use init-only setters for properties by default and encourage the use of the primary constructor, applying these attributes to your types ensures that the serializer can properly otherwise accommodate your types as-is. Typically records are presented as a simple one-line statement using the new primary constructor concept: @@ -238,7 +486,7 @@ public record Doodad( [property: DataMember] int Count) ``` -### Supported Primitive Types +#### Supported Primitive Types There are several types built into .NET that are considered primitive and eligible for serialization without additional effort on the part of the developer: - [Byte](https://learn.microsoft.com/en-us/dotnet/api/system.byte) @@ -267,7 +515,7 @@ There are additional types that aren't actually primitives but have similar buil Again, if you want to pass these types around via your actor methods, no additional consideration is necessary as they'll be serialized and deserialized without issue. Further, types that are themselves marked with the (SerializeableAttribute)[https://learn.microsoft.com/en-us/dotnet/api/system.serializableattribute] attribute will be serialized. -### Enumeration Types +#### Enumeration Types Enumerations, including flag enumerations are serializable if appropriately marked. The enum members you wish to be serialized must be marked with the [EnumMemberAttribute](https://learn.microsoft.com/en-us/dotnet/api/system.runtime.serialization.enummemberattribute) attribute in order to be serialized. Passing a custom value into the optional Value argument on this attribute will allow you to specify the value used for the member in the serialized document instead of having the serializer derive it from the name of the member. The enum type does not require that the type be decorated with the `DataContractAttribute` attribute - only that the members you wish to serialize be decorated with the `EnumMemberAttribute` attributes. @@ -283,15 +531,15 @@ public enum Colors } ``` -### Collection Types +#### Collection Types With regards to the data contact serializer, all collection types that implement the [IEnumerable](https://learn.microsoft.com/en-us/dotnet/api/system.collections.ienumerable) interface including arays and generic collections are considered collections. Those types that implement [IDictionary](https://learn.microsoft.com/en-us/dotnet/api/system.collections.idictionary) or the generic [IDictionary](https://learn.microsoft.com/en-us/dotnet/api/system.collections.generic.idictionary-2) are considered dictionary collections; all others are list collections. Not unlike other complex types, collection types must have a parameterless constructor available. Further, they must also have a method called Add so they can be properly serialized and deserialized. The types used by these collection types must themselves be marked with the `DataContractAttribute` attribute or otherwise be serializable as described throughout this document. -### Data Contract Versioning +#### Data Contract Versioning As the data contract serializer is only used in Dapr with respect to serializing the values in the .NET SDK to and from the Dapr actor instances via the proxy methods, there's little need to consider versioning of data contracts as the data isn't being persisted between application versions using the same serializer. For those interested in learning more about data contract versioning visit [here](https://learn.microsoft.com/en-us/dotnet/framework/wcf/feature-details/data-contract-versioning). -### Known Types +#### Known Types Nesting your own complex types is easily accommodated by marking each of the types with the [DataContractAttribute](https://learn.microsoft.com/en-us/dotnet/api/system.runtime.serialization.datacontractattribute) attribute. This informs the serializer as to how deserialization should be performed. But what if you're working with polymorphic types and one of your members is a base class or interface with derived classes or other implementations? Here, you'll use the [KnownTypeAttribute](https://learn.microsoft.com/en-us/dotnet/api/system.runtime.serialization.knowntypeattribute) attribute to give a hint to the serializer about how to proceed. diff --git a/daprdocs/content/en/dotnet-sdk-docs/dotnet-client/_index.md b/daprdocs/content/en/dotnet-sdk-docs/dotnet-client/_index.md index f608cd07d..cab44468b 100644 --- a/daprdocs/content/en/dotnet-sdk-docs/dotnet-client/_index.md +++ b/daprdocs/content/en/dotnet-sdk-docs/dotnet-client/_index.md @@ -24,6 +24,12 @@ The .NET SDK allows you to interface with all of the [Dapr building blocks]({{< #### HTTP You can either use the `DaprClient` or `System.Net.Http.HttpClient` to invoke your services. +{{% alert title="Note" color="primary" %}} + You can also [invoke a non-Dapr endpoint using either a named `HTTPEndpoint` or an FQDN URL to the non-Dapr environment]({{< ref "howto-invoke-non-dapr-endpoints.md#using-an-httpendpoint-resource-or-fqdn-url-for-non-dapr-endpoints" >}}). + +{{% /alert %}} + + {{< tabs SDK HTTP>}} {{% codetab %}} @@ -56,7 +62,7 @@ Console.WriteLine("Returned: id:{0} | Balance:{1}", account.Id, account.Balance) #### gRPC You can use the `DaprClient` to invoke your services over gRPC. -{{% codetab %}} + ```csharp using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(20)); var invoker = DaprClient.CreateInvocationInvoker(appId: myAppId, daprEndpoint: serviceEndpoint); @@ -67,8 +73,6 @@ await client.MyMethodAsync(new Empty(), options); Assert.Equal(StatusCode.DeadlineExceeded, ex.StatusCode); ``` -{{% /codetab %}} - - For a full guide on service invocation visit [How-To: Invoke a service]({{< ref howto-invoke-discover-services.md >}}). @@ -162,7 +166,7 @@ var secrets = await client.GetSecretAsync("mysecretstore", "key-value-pair-secre Console.WriteLine($"Got secret keys: {string.Join(", ", secrets.Keys)}"); ``` -{{% / codetab %}} +{{% /codetab %}} {{% codetab %}} diff --git a/examples/Actor/ActorClient/Program.cs b/examples/Actor/ActorClient/Program.cs index bae5d2ec2..950869b2b 100644 --- a/examples/Actor/ActorClient/Program.cs +++ b/examples/Actor/ActorClient/Program.cs @@ -11,6 +11,9 @@ // limitations under the License. // ------------------------------------------------------------------------ +using Dapr.Actors.Communication; +using IDemoActor; + namespace ActorClient { using System; @@ -18,7 +21,6 @@ namespace ActorClient using System.Threading.Tasks; using Dapr.Actors; using Dapr.Actors.Client; - using IDemoActorInterface; /// /// Actor Client class. @@ -43,7 +45,7 @@ public static async Task Main(string[] args) // Make strongly typed Actor calls with Remoting. // DemoActor is the type registered with Dapr runtime in the service. - var proxy = ActorProxy.Create(actorId, "DemoActor"); + var proxy = ActorProxy.Create(actorId, "DemoActor"); Console.WriteLine("Making call using actor proxy to save data."); await proxy.SaveData(data, TimeSpan.FromMinutes(10)); @@ -83,7 +85,7 @@ public static async Task Main(string[] args) var nonRemotingProxy = ActorProxy.Create(actorId, "DemoActor"); await nonRemotingProxy.InvokeMethodAsync("TestNoArgumentNoReturnType"); await nonRemotingProxy.InvokeMethodAsync("SaveData", data); - var res = await nonRemotingProxy.InvokeMethodAsync("GetData"); + await nonRemotingProxy.InvokeMethodAsync("GetData"); Console.WriteLine("Registering the timer and reminder"); await proxy.RegisterTimer(); diff --git a/examples/Actor/DemoActor/BankService.cs b/examples/Actor/DemoActor/BankService.cs index 0a164183f..a24eadedb 100644 --- a/examples/Actor/DemoActor/BankService.cs +++ b/examples/Actor/DemoActor/BankService.cs @@ -11,9 +11,9 @@ // limitations under the License. // ------------------------------------------------------------------------ -using IDemoActorInterface; +using IDemoActor; -namespace DaprDemoActor +namespace DemoActor { public class BankService { diff --git a/examples/Actor/DemoActor/DemoActor.cs b/examples/Actor/DemoActor/DemoActor.cs index da780d517..b5ef53e93 100644 --- a/examples/Actor/DemoActor/DemoActor.cs +++ b/examples/Actor/DemoActor/DemoActor.cs @@ -11,14 +11,14 @@ // limitations under the License. // ------------------------------------------------------------------------ -namespace DaprDemoActor -{ - using System; - using System.Text.Json; - using System.Threading.Tasks; - using Dapr.Actors.Runtime; - using IDemoActorInterface; +using System; +using System.Text.Json; +using System.Threading.Tasks; +using Dapr.Actors.Runtime; +using IDemoActor; +namespace DemoActor +{ // The following example showcases a few features of Actors // // Every actor should inherit from the Actor type, and must implement one or more actor interfaces. @@ -27,7 +27,7 @@ namespace DaprDemoActor // For Actors to use Reminders, it must derive from IRemindable. // If you don't intend to use Reminder feature, you can skip implementing IRemindable and reminder // specific methods which are shown in the code below. - public class DemoActor : Actor, IDemoActor, IBankActor, IRemindable + public class DemoActor : Actor, IDemoActor.IDemoActor, IBankActor, IRemindable { private const string StateName = "my_data"; diff --git a/examples/Actor/DemoActor/DemoActor.csproj b/examples/Actor/DemoActor/DemoActor.csproj index 1ee37fdbe..24a42ee0e 100644 --- a/examples/Actor/DemoActor/DemoActor.csproj +++ b/examples/Actor/DemoActor/DemoActor.csproj @@ -1,13 +1,24 @@  - - net6 - - - - - - - + + net6 + + + + true + true + demo-actor + + + + + + + + + + + + diff --git a/examples/Actor/DemoActor/Program.cs b/examples/Actor/DemoActor/Program.cs index a56681fdb..1d538b471 100644 --- a/examples/Actor/DemoActor/Program.cs +++ b/examples/Actor/DemoActor/Program.cs @@ -11,11 +11,11 @@ // limitations under the License. // ------------------------------------------------------------------------ -namespace DaprDemoActor -{ - using Microsoft.AspNetCore.Hosting; - using Microsoft.Extensions.Hosting; +using Microsoft.AspNetCore.Hosting; +using Microsoft.Extensions.Hosting; +namespace DemoActor +{ public class Program { public static void Main(string[] args) diff --git a/examples/Actor/DemoActor/Startup.cs b/examples/Actor/DemoActor/Startup.cs index c04dfdcba..f1165e3c7 100644 --- a/examples/Actor/DemoActor/Startup.cs +++ b/examples/Actor/DemoActor/Startup.cs @@ -1,4 +1,4 @@ -// ------------------------------------------------------------------------ +// ------------------------------------------------------------------------ // Copyright 2021 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. @@ -11,14 +11,14 @@ // limitations under the License. // ------------------------------------------------------------------------ -namespace DaprDemoActor -{ - using Microsoft.AspNetCore.Builder; - using Microsoft.AspNetCore.Hosting; - using Microsoft.Extensions.Configuration; - using Microsoft.Extensions.DependencyInjection; - using Microsoft.Extensions.Hosting; +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Hosting; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Hosting; +namespace DemoActor +{ public class Startup { public Startup(IConfiguration configuration) diff --git a/examples/Actor/DemoActor/demo-actor.yaml b/examples/Actor/DemoActor/demo-actor.yaml new file mode 100644 index 000000000..99a8abd34 --- /dev/null +++ b/examples/Actor/DemoActor/demo-actor.yaml @@ -0,0 +1,67 @@ +apiVersion: dapr.io/v1alpha1 +kind: Component +metadata: + name: statestore +spec: + type: state.in-memory + version: v1 + metadata: + - name: actorStateStore + value: "true" +--- +kind: Service +apiVersion: v1 +metadata: + name: demoactor + labels: + app: demoactor +spec: + selector: + app: demoactor + ports: + - name: app-port + protocol: TCP + port: 5010 + targetPort: app-port + - name: dapr-http + protocol: TCP + port: 3500 + targetPort: 3500 + type: LoadBalancer +--- +apiVersion: apps/v1 +kind: Deployment +metadata: + name: demoactor + labels: + app: demoactor +spec: + replicas: 1 + selector: + matchLabels: + app: demoactor + template: + metadata: + labels: + app: demoactor + annotations: + dapr.io/enabled: "true" + dapr.io/app-id: "demoactor" + dapr.io/app-port: "5010" + dapr.io/enable-api-logging: "true" + dapr.io/sidecar-listen-addresses: "0.0.0.0" + spec: + containers: + - name: demoactor + # image: /demo-actor:latest + image: demo-actor:latest + # if you are using docker desktop, you can use imagePullPolicy: Never to use local image + imagePullPolicy: Never + env: + - name: APP_PORT + value: "5010" + - name: ASPNETCORE_URLS + value: "http://+:5010" + ports: + - name: app-port + containerPort: 5010 diff --git a/examples/Actor/IDemoActor/IBankActor.cs b/examples/Actor/IDemoActor/IBankActor.cs index 95ac23844..c495f027b 100644 --- a/examples/Actor/IDemoActor/IBankActor.cs +++ b/examples/Actor/IDemoActor/IBankActor.cs @@ -11,12 +11,12 @@ // limitations under the License. // ------------------------------------------------------------------------ -namespace IDemoActorInterface -{ - using System; - using System.Threading.Tasks; - using Dapr.Actors; +using System; +using System.Threading.Tasks; +using Dapr.Actors; +namespace IDemoActor +{ public interface IBankActor : IActor { Task GetAccountBalance(); diff --git a/examples/Actor/IDemoActor/IDemoActor.cs b/examples/Actor/IDemoActor/IDemoActor.cs index 579386c68..6f2d32801 100644 --- a/examples/Actor/IDemoActor/IDemoActor.cs +++ b/examples/Actor/IDemoActor/IDemoActor.cs @@ -11,12 +11,12 @@ // limitations under the License. // ------------------------------------------------------------------------ -namespace IDemoActorInterface -{ - using System; - using System.Threading.Tasks; - using Dapr.Actors; +using System; +using System.Threading.Tasks; +using Dapr.Actors; +namespace IDemoActor +{ /// /// Interface for Actor method. /// diff --git a/examples/Actor/README.md b/examples/Actor/README.md index ddc42cecf..89b6bf0bb 100644 --- a/examples/Actor/README.md +++ b/examples/Actor/README.md @@ -4,7 +4,7 @@ The Actor example shows how to create a virtual actor (`DemoActor`) and invoke i ## Prerequisites -- [.NET Core 3.1 or .NET 5+](https://dotnet.microsoft.com/download) installed +- [.NET 6+](https://dotnet.microsoft.com/download) installed - [Dapr CLI](https://docs.dapr.io/getting-started/install-dapr-cli/) - [Initialized Dapr environment](https://docs.dapr.io/getting-started/install-dapr-selfhost/) - [Dapr .NET SDK](https://github.com/dapr/dotnet-sdk/) @@ -80,3 +80,80 @@ On Windows: ```sh curl -X POST http://127.0.0.1:3500/v1.0/actors/DemoActor/abc/method/GetData ``` + +### Build and push Docker image +You can build the docker image of `DemoActor` service by running the following commands in the `DemoActor` project directory: + +``` Bash +dotnet publish --os linux --arch x64 /t:PublishContainer -p ContainerImageTags='"latest"' --self-contained +``` + +The build produce and image with tag `demo-actor:latest` and load it in the local registry. +Now the image can be pushed to your remote Docker registry by running the following commands: + +``` Bash +# Replace with your Docker registry +docker tag demo-actor:latest /demo-actor:latest + +# Push the image to your Docker registry +docker push /demo-actor:latest +``` + +### Deploy the Actor service to Kubernetes +#### Prerequisites +- A Kubernetes cluster with `kubectl` configured to access it. +- Dapr v1.14+ installed on the Kubernetes cluster. Follow the instructions [here](https://docs.dapr.io/getting-started/install-dapr-kubernetes/). +- A Docker registry where you pushed the `DemoActor` image. + +#### Deploy the Actor service +For quick deployment you can install dapr in dev mode using the following command: + +``` Bash +dapr init -k --dev +``` + +To deploy the `DemoActor` service to Kubernetes, you can use the provided Kubernetes manifest file `demo-actor.yaml` in the `DemoActor` project directory. +Before applying the manifest file, replace the image name in the manifest file with the image name you pushed to your Docker registry. + +Part to update in `demo-actor.yaml`: +``` YAML +image: /demoactor:latest +``` + +To install the application in `default` namespace, run the following command: + +``` Bash +kubectl apply -f demo-actor.yaml +``` + +This will deploy the `DemoActor` service to Kubernetes. You can check the status of the deployment by running: + +``` Bash +kubectl get pods -n default --watch +``` + +The manifest create 2 services: + +- `demoactor` service: The service that hosts the `DemoActor` actor. +- `demoactor-dapr` service: The service that hosts the Dapr sidecar for the `DemoActor` actor. + +### Make client calls to the deployed Actor service +To make client calls to the deployed `DemoActor` service, you can use the `ActorClient` project. +Before running the client, update the `DAPR_HTTP_PORT` environment variable in the `ActorClient` project directory to the port on which Dapr is running in the Kubernetes cluster. + +On Linux, MacOS: +``` Bash +export DAPR_HTTP_PORT=3500 +``` + +Than port-forward the `DemoActor` service to your local machine: + +``` Bash +kubectl port-forward svc/demoactor 3500:3500 +``` + +Now you can run the client project from the `ActorClient` directory: + +``` Bash +dotnet run +``` \ No newline at end of file diff --git a/examples/AspNetCore/ControllerSample/Controllers/SampleController.cs b/examples/AspNetCore/ControllerSample/Controllers/SampleController.cs index 485614150..5b339288c 100644 --- a/examples/AspNetCore/ControllerSample/Controllers/SampleController.cs +++ b/examples/AspNetCore/ControllerSample/Controllers/SampleController.cs @@ -11,6 +11,8 @@ // limitations under the License. // ------------------------------------------------------------------------ +using System.Linq; + namespace ControllerSample.Controllers { using System; @@ -43,6 +45,7 @@ public SampleController(ILogger logger) /// State store name. /// public const string StoreName = "statestore"; + private readonly ILogger logger; /// @@ -72,6 +75,11 @@ public ActionResult Get([FromState(StoreName)] StateEntry acco [HttpPost("deposit")] public async Task> Deposit(Transaction transaction, [FromServices] DaprClient daprClient) { + // Example reading cloudevent properties from the headers + var headerEntries = Request.Headers.Aggregate("", (current, header) => current + ($"------- Header: {header.Key} : {header.Value}" + Environment.NewLine)); + + logger.LogInformation(headerEntries); + logger.LogInformation("Enter deposit"); var state = await daprClient.GetStateEntryAsync(StoreName, transaction.Id); state.Value ??= new Account() { Id = transaction.Id, }; @@ -83,7 +91,7 @@ public async Task> Deposit(Transaction transaction, [FromS } state.Value.Balance += transaction.Amount; - logger.LogInformation("Balance for Id {0} is {1}",state.Value.Id, state.Value.Balance); + logger.LogInformation("Balance for Id {0} is {1}", state.Value.Id, state.Value.Balance); await state.SaveAsync(); return state.Value; } @@ -98,22 +106,23 @@ public async Task> Deposit(Transaction transaction, [FromS [Topic("pubsub", "multideposit", "amountDeadLetterTopic", false)] [BulkSubscribe("multideposit", 500, 2000)] [HttpPost("multideposit")] - public async Task> MultiDeposit([FromBody] BulkSubscribeMessage> - bulkMessage, [FromServices] DaprClient daprClient) + public async Task> MultiDeposit([FromBody] + BulkSubscribeMessage> + bulkMessage, [FromServices] DaprClient daprClient) { logger.LogInformation("Enter bulk deposit"); - + List entries = new List(); foreach (var entry in bulkMessage.Entries) - { + { try { var transaction = entry.Event.Data; var state = await daprClient.GetStateEntryAsync(StoreName, transaction.Id); state.Value ??= new Account() { Id = transaction.Id, }; - logger.LogInformation("Id is {0}, the amount to be deposited is {1}", + logger.LogInformation("Id is {0}, the amount to be deposited is {1}", transaction.Id, transaction.Amount); if (transaction.Amount < 0m) @@ -124,12 +133,16 @@ public async Task> MultiDeposit([FromBody state.Value.Balance += transaction.Amount; logger.LogInformation("Balance is {0}", state.Value.Balance); await state.SaveAsync(); - entries.Add(new BulkSubscribeAppResponseEntry(entry.EntryId, BulkSubscribeAppResponseStatus.SUCCESS)); - } catch (Exception e) { + entries.Add( + new BulkSubscribeAppResponseEntry(entry.EntryId, BulkSubscribeAppResponseStatus.SUCCESS)); + } + catch (Exception e) + { logger.LogError(e.Message); entries.Add(new BulkSubscribeAppResponseEntry(entry.EntryId, BulkSubscribeAppResponseStatus.RETRY)); } } + return new BulkSubscribeAppResponse(entries); } @@ -165,6 +178,7 @@ public async Task> Withdraw(Transaction transaction, [From { return this.NotFound(); } + if (transaction.Amount < 0m) { return BadRequest(new { statusCode = 400, message = "bad request" }); @@ -185,7 +199,8 @@ public async Task> Withdraw(Transaction transaction, [From /// "pubsub", the first parameter into the Topic attribute, is name of the default pub/sub configured by the Dapr CLI. [Topic("pubsub", "withdraw", "event.type ==\"withdraw.v2\"", 1)] [HttpPost("withdraw.v2")] - public async Task> WithdrawV2(TransactionV2 transaction, [FromServices] DaprClient daprClient) + public async Task> WithdrawV2(TransactionV2 transaction, + [FromServices] DaprClient daprClient) { logger.LogInformation("Enter withdraw.v2"); if (transaction.Channel == "mobile" && transaction.Amount > 10000) @@ -214,12 +229,15 @@ public async Task> WithdrawV2(TransactionV2 transaction, [ /// "pubsub", the first parameter into the Topic attribute, is name of the default pub/sub configured by the Dapr CLI. [Topic("pubsub", "rawDeposit", true)] [HttpPost("rawDeposit")] - public async Task> RawDeposit([FromBody] JsonDocument rawTransaction, [FromServices] DaprClient daprClient) + public async Task> RawDeposit([FromBody] JsonDocument rawTransaction, + [FromServices] DaprClient daprClient) { var transactionString = rawTransaction.RootElement.GetProperty("data_base64").GetString(); - logger.LogInformation($"Enter deposit: {transactionString} - {Encoding.UTF8.GetString(Convert.FromBase64String(transactionString))}"); + logger.LogInformation( + $"Enter deposit: {transactionString} - {Encoding.UTF8.GetString(Convert.FromBase64String(transactionString))}"); var transactionJson = JsonSerializer.Deserialize(Convert.FromBase64String(transactionString)); - var transaction = JsonSerializer.Deserialize(transactionJson.RootElement.GetProperty("data").GetRawText()); + var transaction = + JsonSerializer.Deserialize(transactionJson.RootElement.GetProperty("data").GetRawText()); var state = await daprClient.GetStateEntryAsync(StoreName, transaction.Id); state.Value ??= new Account() { Id = transaction.Id, }; logger.LogInformation("Id is {0}, the amount to be deposited is {1}", transaction.Id, transaction.Amount); @@ -239,7 +257,8 @@ public async Task> RawDeposit([FromBody] JsonDocument rawT /// Method for returning a BadRequest result which will cause Dapr sidecar to throw an RpcException /// [HttpPost("throwException")] - public async Task> ThrowException(Transaction transaction, [FromServices] DaprClient daprClient) + public async Task> ThrowException(Transaction transaction, + [FromServices] DaprClient daprClient) { logger.LogInformation("Enter ThrowException"); var task = Task.Delay(10); diff --git a/examples/AspNetCore/ControllerSample/CustomTopicAttribute.cs b/examples/AspNetCore/ControllerSample/CustomTopicAttribute.cs index 96eb918fb..5c9996aea 100644 --- a/examples/AspNetCore/ControllerSample/CustomTopicAttribute.cs +++ b/examples/AspNetCore/ControllerSample/CustomTopicAttribute.cs @@ -11,6 +11,8 @@ // limitations under the License. // ------------------------------------------------------------------------ +using Dapr.AspNetCore; + namespace ControllerSample { using System; diff --git a/examples/AspNetCore/ControllerSample/Startup.cs b/examples/AspNetCore/ControllerSample/Startup.cs index 11b81d8b3..ddc6d1c5f 100644 --- a/examples/AspNetCore/ControllerSample/Startup.cs +++ b/examples/AspNetCore/ControllerSample/Startup.cs @@ -11,6 +11,11 @@ // limitations under the License. // ------------------------------------------------------------------------ + +using Dapr; +using Dapr.AspNetCore; + + namespace ControllerSample { using Microsoft.AspNetCore.Builder; @@ -61,7 +66,10 @@ public void Configure(IApplicationBuilder app, IWebHostEnvironment env) app.UseRouting(); - app.UseCloudEvents(); + app.UseCloudEvents(new CloudEventsMiddlewareOptions + { + ForwardCloudEventPropertiesAsHeaders = true + }); app.UseAuthorization(); diff --git a/examples/AspNetCore/GrpcServiceSample/GrpcServiceSample.csproj b/examples/AspNetCore/GrpcServiceSample/GrpcServiceSample.csproj index 123763489..2319f6a56 100644 --- a/examples/AspNetCore/GrpcServiceSample/GrpcServiceSample.csproj +++ b/examples/AspNetCore/GrpcServiceSample/GrpcServiceSample.csproj @@ -10,15 +10,16 @@ - - - - - + + + + + + diff --git a/examples/AspNetCore/GrpcServiceSample/Services/BankingService.cs b/examples/AspNetCore/GrpcServiceSample/Services/BankingService.cs index 56b80cad6..9518fd610 100644 --- a/examples/AspNetCore/GrpcServiceSample/Services/BankingService.cs +++ b/examples/AspNetCore/GrpcServiceSample/Services/BankingService.cs @@ -22,7 +22,7 @@ using GrpcServiceSample.Generated; using Microsoft.Extensions.Logging; -namespace GrpcServiceSample +namespace GrpcServiceSample.Services { /// /// BankAccount gRPC service diff --git a/examples/AspNetCore/GrpcServiceSample/Startup.cs b/examples/AspNetCore/GrpcServiceSample/Startup.cs index 752d62448..4aa5ac7d3 100644 --- a/examples/AspNetCore/GrpcServiceSample/Startup.cs +++ b/examples/AspNetCore/GrpcServiceSample/Startup.cs @@ -11,6 +11,8 @@ // limitations under the License. // ------------------------------------------------------------------------ +using Dapr.AspNetCore; +using GrpcServiceSample.Services; using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Hosting; using Microsoft.AspNetCore.Http; diff --git a/examples/AspNetCore/RoutingSample/Startup.cs b/examples/AspNetCore/RoutingSample/Startup.cs index 29bb488fe..3d71e9e1f 100644 --- a/examples/AspNetCore/RoutingSample/Startup.cs +++ b/examples/AspNetCore/RoutingSample/Startup.cs @@ -221,7 +221,6 @@ await JsonSerializer.SerializeAsync(context.Response.Body, async Task ViewErrorMessage(HttpContext context) { - var client = context.RequestServices.GetRequiredService(); var transaction = await JsonSerializer.DeserializeAsync(context.Request.Body, serializerOptions); logger.LogInformation("The amount cannot be negative: {0}", transaction.Amount); diff --git a/examples/Client/ConfigurationApi/ConfigurationApi.csproj b/examples/Client/ConfigurationApi/ConfigurationApi.csproj index dee6a9878..761ebb38f 100644 --- a/examples/Client/ConfigurationApi/ConfigurationApi.csproj +++ b/examples/Client/ConfigurationApi/ConfigurationApi.csproj @@ -8,6 +8,7 @@ + diff --git a/examples/Client/Cryptography/Program.cs b/examples/Client/Cryptography/Program.cs index 74e3c7f48..da81bef8f 100644 --- a/examples/Client/Cryptography/Program.cs +++ b/examples/Client/Cryptography/Program.cs @@ -11,10 +11,9 @@ // limitations under the License. // ------------------------------------------------------------------------ -using Cryptography; using Cryptography.Examples; -namespace Samples.Client +namespace Cryptography { class Program { diff --git a/examples/Client/DistributedLock/DistributedLock.csproj b/examples/Client/DistributedLock/DistributedLock.csproj index 9c3272e6e..4c04fb907 100644 --- a/examples/Client/DistributedLock/DistributedLock.csproj +++ b/examples/Client/DistributedLock/DistributedLock.csproj @@ -3,6 +3,7 @@ + diff --git a/examples/Client/DistributedLock/Startup.cs b/examples/Client/DistributedLock/Startup.cs index 0309af0f5..9f40e4752 100644 --- a/examples/Client/DistributedLock/Startup.cs +++ b/examples/Client/DistributedLock/Startup.cs @@ -1,4 +1,5 @@ -using DistributedLock.Services; +using Dapr.AspNetCore; +using DistributedLock.Services; using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Hosting; using Microsoft.Extensions.Configuration; diff --git a/examples/Client/PublishSubscribe/BulkPublishEventExample/BulkPublishEventExample.csproj b/examples/Client/PublishSubscribe/BulkPublishEventExample/BulkPublishEventExample.csproj index 3f22acaf8..b1e7647c7 100644 --- a/examples/Client/PublishSubscribe/BulkPublishEventExample/BulkPublishEventExample.csproj +++ b/examples/Client/PublishSubscribe/BulkPublishEventExample/BulkPublishEventExample.csproj @@ -16,9 +16,9 @@ - - - + + + diff --git a/examples/Client/PublishSubscribe/BulkPublishEventExample/README.md b/examples/Client/PublishSubscribe/BulkPublishEventExample/README.md index dfcc99ca6..39d206fa2 100644 --- a/examples/Client/PublishSubscribe/BulkPublishEventExample/README.md +++ b/examples/Client/PublishSubscribe/BulkPublishEventExample/README.md @@ -2,7 +2,7 @@ ## Prerequisites -- [.NET Core 3.1 or .NET 5+](https://dotnet.microsoft.com/download) installed +- [.NET 6+](https://dotnet.microsoft.com/download) installed - [Dapr CLI](https://docs.dapr.io/getting-started/install-dapr-cli/) - [Initialized Dapr environment](https://docs.dapr.io/getting-started/install-dapr-selfhost/) - [Dapr .NET SDK](https://docs.dapr.io/developing-applications/sdks/dotnet/) diff --git a/examples/Client/PublishSubscribe/PublishEventExample/PublishEventExample.csproj b/examples/Client/PublishSubscribe/PublishEventExample/PublishEventExample.csproj index 2df4ec967..52b77a3e5 100644 --- a/examples/Client/PublishSubscribe/PublishEventExample/PublishEventExample.csproj +++ b/examples/Client/PublishSubscribe/PublishEventExample/PublishEventExample.csproj @@ -16,9 +16,9 @@ - - - + + + diff --git a/examples/Client/PublishSubscribe/PublishEventExample/README.md b/examples/Client/PublishSubscribe/PublishEventExample/README.md index 455fc2537..9f3af565f 100644 --- a/examples/Client/PublishSubscribe/PublishEventExample/README.md +++ b/examples/Client/PublishSubscribe/PublishEventExample/README.md @@ -2,7 +2,7 @@ ## Prerequisites -- [.NET Core 3.1 or .NET 5+](https://dotnet.microsoft.com/download) installed +- [.NET 6+](https://dotnet.microsoft.com/download) installed - [Dapr CLI](https://docs.dapr.io/getting-started/install-dapr-cli/) - [Initialized Dapr environment](https://docs.dapr.io/getting-started/install-dapr-selfhost/) - [Dapr .NET SDK](https://docs.dapr.io/developing-applications/sdks/dotnet/) diff --git a/examples/Client/ServiceInvocation/ServiceInvocation.csproj b/examples/Client/ServiceInvocation/ServiceInvocation.csproj index e3df962a1..7b165835e 100644 --- a/examples/Client/ServiceInvocation/ServiceInvocation.csproj +++ b/examples/Client/ServiceInvocation/ServiceInvocation.csproj @@ -16,9 +16,9 @@ - - - + + + diff --git a/examples/Client/StateManagement/StateManagement.csproj b/examples/Client/StateManagement/StateManagement.csproj index e3df962a1..7b165835e 100644 --- a/examples/Client/StateManagement/StateManagement.csproj +++ b/examples/Client/StateManagement/StateManagement.csproj @@ -16,9 +16,9 @@ - - - + + + diff --git a/examples/GeneratedActor/ActorClient/Program.cs b/examples/GeneratedActor/ActorClient/Program.cs index 87f714907..0b858214a 100644 --- a/examples/GeneratedActor/ActorClient/Program.cs +++ b/examples/GeneratedActor/ActorClient/Program.cs @@ -23,7 +23,7 @@ var client = new ClientActorClient(proxy); -var state = await client.GetStateAsync(cancellationTokenSource.Token); +await client.GetStateAsync(cancellationTokenSource.Token); await client.SetStateAsync(new ClientState("Hello, World!"), cancellationTokenSource.Token); diff --git a/examples/Workflow/WorkflowConsoleApp/Activities/ProcessPaymentActivity.cs b/examples/Workflow/WorkflowConsoleApp/Activities/ProcessPaymentActivity.cs index b66c85255..4ca39f0ae 100644 --- a/examples/Workflow/WorkflowConsoleApp/Activities/ProcessPaymentActivity.cs +++ b/examples/Workflow/WorkflowConsoleApp/Activities/ProcessPaymentActivity.cs @@ -1,6 +1,5 @@ using Dapr.Workflow; using Microsoft.Extensions.Logging; -using WorkflowConsoleApp.Models; namespace WorkflowConsoleApp.Activities { diff --git a/examples/Workflow/WorkflowConsoleApp/Activities/RequestApprovalActivity.cs b/examples/Workflow/WorkflowConsoleApp/Activities/RequestApprovalActivity.cs index af0b1fa13..d40078fc8 100644 --- a/examples/Workflow/WorkflowConsoleApp/Activities/RequestApprovalActivity.cs +++ b/examples/Workflow/WorkflowConsoleApp/Activities/RequestApprovalActivity.cs @@ -1,6 +1,5 @@ using Dapr.Workflow; using Microsoft.Extensions.Logging; -using WorkflowConsoleApp.Models; namespace WorkflowConsoleApp.Activities { diff --git a/examples/Workflow/WorkflowConsoleApp/Activities/ReserveInventoryActivity.cs b/examples/Workflow/WorkflowConsoleApp/Activities/ReserveInventoryActivity.cs index fc6c48921..cdae1c6ed 100644 --- a/examples/Workflow/WorkflowConsoleApp/Activities/ReserveInventoryActivity.cs +++ b/examples/Workflow/WorkflowConsoleApp/Activities/ReserveInventoryActivity.cs @@ -1,7 +1,6 @@ using Dapr.Client; using Dapr.Workflow; using Microsoft.Extensions.Logging; -using WorkflowConsoleApp.Models; namespace WorkflowConsoleApp.Activities { diff --git a/examples/Workflow/WorkflowConsoleApp/Activities/UpdateInventoryActivity.cs b/examples/Workflow/WorkflowConsoleApp/Activities/UpdateInventoryActivity.cs index 947dab6cb..c035aadde 100644 --- a/examples/Workflow/WorkflowConsoleApp/Activities/UpdateInventoryActivity.cs +++ b/examples/Workflow/WorkflowConsoleApp/Activities/UpdateInventoryActivity.cs @@ -1,6 +1,5 @@ using Dapr.Client; using Dapr.Workflow; -using WorkflowConsoleApp.Models; using Microsoft.Extensions.Logging; namespace WorkflowConsoleApp.Activities diff --git a/examples/Workflow/WorkflowConsoleApp/Models.cs b/examples/Workflow/WorkflowConsoleApp/Models.cs index 6c9583d84..7892c7525 100644 --- a/examples/Workflow/WorkflowConsoleApp/Models.cs +++ b/examples/Workflow/WorkflowConsoleApp/Models.cs @@ -1,4 +1,4 @@ -namespace WorkflowConsoleApp.Models +namespace WorkflowConsoleApp { public record OrderPayload(string Name, double TotalCost, int Quantity = 1); public record InventoryRequest(string RequestId, string ItemName, int Quantity); diff --git a/examples/Workflow/WorkflowConsoleApp/Program.cs b/examples/Workflow/WorkflowConsoleApp/Program.cs index 2b8213887..26d34615d 100644 --- a/examples/Workflow/WorkflowConsoleApp/Program.cs +++ b/examples/Workflow/WorkflowConsoleApp/Program.cs @@ -1,10 +1,10 @@ using Dapr.Client; using Dapr.Workflow; using WorkflowConsoleApp.Activities; -using WorkflowConsoleApp.Models; using WorkflowConsoleApp.Workflows; using Microsoft.Extensions.Hosting; using Microsoft.Extensions.DependencyInjection; +using WorkflowConsoleApp; const string StoreName = "statestore"; diff --git a/examples/Workflow/WorkflowConsoleApp/Workflows/OrderProcessingWorkflow.cs b/examples/Workflow/WorkflowConsoleApp/Workflows/OrderProcessingWorkflow.cs index bd2a710b6..3b8af5951 100644 --- a/examples/Workflow/WorkflowConsoleApp/Workflows/OrderProcessingWorkflow.cs +++ b/examples/Workflow/WorkflowConsoleApp/Workflows/OrderProcessingWorkflow.cs @@ -1,6 +1,5 @@ using Dapr.Workflow; using WorkflowConsoleApp.Activities; -using WorkflowConsoleApp.Models; namespace WorkflowConsoleApp.Workflows { diff --git a/examples/Workflow/WorkflowUnitTest/OrderProcessingTests.cs b/examples/Workflow/WorkflowUnitTest/OrderProcessingTests.cs index ac53c4081..3093e5c7a 100644 --- a/examples/Workflow/WorkflowUnitTest/OrderProcessingTests.cs +++ b/examples/Workflow/WorkflowUnitTest/OrderProcessingTests.cs @@ -1,8 +1,8 @@ using System.Threading.Tasks; using Dapr.Workflow; using Moq; +using WorkflowConsoleApp; using WorkflowConsoleApp.Activities; -using WorkflowConsoleApp.Models; using WorkflowConsoleApp.Workflows; using Xunit; @@ -64,7 +64,7 @@ public async Task TestInsufficientInventory() .Returns(Task.FromResult(inventoryResult)); // Run the workflow directly - OrderResult result = await new OrderProcessingWorkflow().RunAsync(mockContext.Object, order); + await new OrderProcessingWorkflow().RunAsync(mockContext.Object, order); // Verify that ReserveInventoryActivity was called with a specific input mockContext.Verify( diff --git a/examples/Workflow/WorkflowUnitTest/WorkflowUnitTest.csproj b/examples/Workflow/WorkflowUnitTest/WorkflowUnitTest.csproj index 4ce0c9801..dec14a713 100644 --- a/examples/Workflow/WorkflowUnitTest/WorkflowUnitTest.csproj +++ b/examples/Workflow/WorkflowUnitTest/WorkflowUnitTest.csproj @@ -7,14 +7,14 @@ - - - - + + + + runtime; build; native; contentfiles; analyzers; buildtransitive all - + runtime; build; native; contentfiles; analyzers; buildtransitive all diff --git a/global.json b/global.json index 980f4652d..fe53f92ae 100644 --- a/global.json +++ b/global.json @@ -1,7 +1,7 @@ { - "_comment": "This policy allows the 7.0.101 SDK or patches in that family.", + "_comment": "This policy allows the 8.0.100 SDK or patches in that family.", "sdk": { - "version": "7.0.101", - "rollForward": "latestMajor" + "version": "8.0.100", + "rollForward": "minor" } } \ No newline at end of file diff --git a/properties/dapr_managed_netcore.props b/properties/dapr_managed_netcore.props index 3bafcb50c..6e8c01bfe 100644 --- a/properties/dapr_managed_netcore.props +++ b/properties/dapr_managed_netcore.props @@ -53,7 +53,7 @@ - + all runtime; build; native; contentfiles; analyzers; buildtransitive diff --git a/samples/Client/README.md b/samples/Client/README.md deleted file mode 100644 index 2501bfadc..000000000 --- a/samples/Client/README.md +++ /dev/null @@ -1,56 +0,0 @@ -# Dapr Client examples - -The following examples will show you how to: - -- Invoke services -- Publish events -- Use the state store to get, set, and delete data - -## Prerequisites - -* [.Net Core 3.1 or .NET 5+](https://dotnet.microsoft.com/download) -* [Dapr CLI](https://github.com/dapr/cli) -* [Dapr DotNet SDK](https://github.com/dapr/dotnet-sdk) - -## Running the Sample - -To run the sample locally run this command in DaprClient directory: -```sh -dapr run --app-id DaprClient -- dotnet run -``` - -Running the following command will output a list of the samples included. -```sh -dapr run --app-id DaprClient -- dotnet run -``` - -Press Ctrl+C to exit, and then run the command again and provide a sample number to run the samples. - -For example run this command to run the 0th sample from the list produced earlier. -```sh -dapr run --app-id DaprClient -- dotnet run 0 -``` - -Samples that use HTTP-based service invocation will require running the [RoutingService](../../AspNetCore/RoutingSample). - -Samples that use gRPC-based service invocation will require running [GrpcService](../../AspNetCore/GrpcServiceSample). - -## Invoking Services - -See: `InvokeServiceHttpClientExample.cs` for an example of using `HttpClient` to invoke another service through Dapr. - -See: `InvokeServiceHttpExample.cs` for an example using the `DaprClient` to invoke another service through Dapr. - -See: `InvokeServiceGrpcExample.cs` for an example using the `DaprClient` to invoke a service using gRPC through Dapr. - -## Publishing Pub/Sub Events - -See: `PublishEventExample.cs` for an example using the `DaprClient` to publish a pub/sub event. - -## Working with the State Store - -See: `StateStoreExample.cs` for an example of using `DaprClient` for basic state store operations like get, set, and delete. - -See: `StateStoreETagsExample.cs` for an example of using `DaprClient` for optimistic concurrency control with the state store. - -See: `StateStoreTransactionsExample.cs` for an example of using `DaprClient` for transactional state store operations that affect multiple keys. diff --git a/src/Dapr.Actors.AspNetCore/ActorsServiceCollectionExtensions.cs b/src/Dapr.Actors.AspNetCore/ActorsServiceCollectionExtensions.cs index a7f5d04a2..11f05f4c1 100644 --- a/src/Dapr.Actors.AspNetCore/ActorsServiceCollectionExtensions.cs +++ b/src/Dapr.Actors.AspNetCore/ActorsServiceCollectionExtensions.cs @@ -1,4 +1,4 @@ -// ------------------------------------------------------------------------ +// ------------------------------------------------------------------------ // Copyright 2021 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. @@ -11,9 +11,13 @@ // limitations under the License. // ------------------------------------------------------------------------ +#nullable enable + using System; +using Dapr; using Dapr.Actors.Client; using Dapr.Actors.Runtime; +using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection.Extensions; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; @@ -30,12 +34,9 @@ public static class ActorsServiceCollectionExtensions /// /// The . /// A delegate used to configure actor options and register actor types. - public static void AddActors(this IServiceCollection services, Action configure) + public static void AddActors(this IServiceCollection? services, Action? configure) { - if (services is null) - { - throw new ArgumentNullException(nameof(services)); - } + ArgumentNullException.ThrowIfNull(services, nameof(services)); // Routing and health checks are required dependencies. services.AddRouting(); @@ -45,6 +46,8 @@ public static void AddActors(this IServiceCollection services, Action(s => { var options = s.GetRequiredService>().Value; + ConfigureActorOptions(s, options); + var loggerFactory = s.GetRequiredService(); var activatorFactory = s.GetRequiredService(); var proxyFactory = s.GetRequiredService(); @@ -54,6 +57,8 @@ public static void AddActors(this IServiceCollection services, Action(s => { var options = s.GetRequiredService>().Value; + ConfigureActorOptions(s, options); + var factory = new ActorProxyFactory() { DefaultOptions = @@ -72,5 +77,16 @@ public static void AddActors(this IServiceCollection services, Action(configure); } } + + private static void ConfigureActorOptions(IServiceProvider serviceProvider, ActorRuntimeOptions options) + { + var configuration = serviceProvider.GetService(); + options.DaprApiToken = !string.IsNullOrWhiteSpace(options.DaprApiToken) + ? options.DaprApiToken + : DaprDefaults.GetDefaultDaprApiToken(configuration); + options.HttpEndpoint = !string.IsNullOrWhiteSpace(options.HttpEndpoint) + ? options.HttpEndpoint + : DaprDefaults.GetDefaultHttpEndpoint(); + } } } diff --git a/src/Dapr.Actors.AspNetCore/Dapr.Actors.AspNetCore.csproj b/src/Dapr.Actors.AspNetCore/Dapr.Actors.AspNetCore.csproj index 82c5863db..ef57e76fe 100644 --- a/src/Dapr.Actors.AspNetCore/Dapr.Actors.AspNetCore.csproj +++ b/src/Dapr.Actors.AspNetCore/Dapr.Actors.AspNetCore.csproj @@ -11,5 +11,6 @@ + diff --git a/src/Dapr.Actors.Generators/ActorClientGenerator.cs b/src/Dapr.Actors.Generators/ActorClientGenerator.cs index 349d80188..f95fc4224 100644 --- a/src/Dapr.Actors.Generators/ActorClientGenerator.cs +++ b/src/Dapr.Actors.Generators/ActorClientGenerator.cs @@ -112,7 +112,6 @@ public void Execute(GeneratorExecutionContext context) { try { - var actorInterfaceTypeName = interfaceSymbol.Name; var fullyQualifiedActorInterfaceTypeName = interfaceSymbol.ToString(); var attributeData = interfaceSymbol.GetAttributes().Single(a => a.AttributeClass?.Equals(generateActorClientAttributeSymbol, SymbolEqualityComparer.Default) == true); diff --git a/src/Dapr.Actors.Generators/Dapr.Actors.Generators.csproj b/src/Dapr.Actors.Generators/Dapr.Actors.Generators.csproj index a69f2d1a0..370d422f1 100644 --- a/src/Dapr.Actors.Generators/Dapr.Actors.Generators.csproj +++ b/src/Dapr.Actors.Generators/Dapr.Actors.Generators.csproj @@ -10,8 +10,8 @@ - - + + This package contains the reference assemblies for developing services using Dapr. + - - - - + + + + - + - + + - + diff --git a/src/Dapr.Client/DaprClient.cs b/src/Dapr.Client/DaprClient.cs index 21777105b..43c640a69 100644 --- a/src/Dapr.Client/DaprClient.cs +++ b/src/Dapr.Client/DaprClient.cs @@ -58,7 +58,7 @@ public abstract class DaprClient : IDisposable /// The client will read the property, and /// interpret the hostname as the destination app-id. The /// property will be replaced with a new URI with the authority section replaced by - /// and the path portion of the URI rewitten to follow the format of a Dapr service invocation request. + /// and the path portion of the URI rewritten to follow the format of a Dapr service invocation request. /// /// /// @@ -139,17 +139,14 @@ public static HttpClient CreateInvokeHttpClient(string appId = null, string dapr public static CallInvoker CreateInvocationInvoker(string appId, string daprEndpoint = null, string daprApiToken = null) { var channel = GrpcChannel.ForAddress(daprEndpoint ?? DaprDefaults.GetDefaultGrpcEndpoint()); - return channel.Intercept(new InvocationInterceptor(appId, daprApiToken ?? DaprDefaults.GetDefaultDaprApiToken())); + return channel.Intercept(new InvocationInterceptor(appId, daprApiToken ?? DaprDefaults.GetDefaultDaprApiToken(null))); } internal static KeyValuePair? GetDaprApiTokenHeader(string apiToken) { - if (string.IsNullOrWhiteSpace(apiToken)) - { - return null; - } - - return new KeyValuePair("dapr-api-token", apiToken); + return string.IsNullOrWhiteSpace(apiToken) + ? null + : new KeyValuePair("dapr-api-token", apiToken); } /// @@ -306,6 +303,20 @@ public HttpRequestMessage CreateInvokeMethodRequest(string appId, string methodN return CreateInvokeMethodRequest(HttpMethod.Post, appId, methodName); } + /// + /// Creates an that can be used to perform service invocation for the + /// application identified by and invokes the method specified by + /// with the POST HTTP method. + /// + /// The Dapr application id to invoke the method on. + /// The name of the method to invoke. + /// A collection of key/value pairs to populate the query string from. + /// An for use with SendInvokeMethodRequestAsync. + public HttpRequestMessage CreateInvokeMethodRequest(string appId, string methodName, IReadOnlyCollection> queryStringParameters) + { + return CreateInvokeMethodRequest(HttpMethod.Post, appId, methodName, queryStringParameters); + } + /// /// Creates an that can be used to perform service invocation for the /// application identified by and invokes the method specified by @@ -317,6 +328,19 @@ public HttpRequestMessage CreateInvokeMethodRequest(string appId, string methodN /// An for use with SendInvokeMethodRequestAsync. public abstract HttpRequestMessage CreateInvokeMethodRequest(HttpMethod httpMethod, string appId, string methodName); + /// + /// Creates an that can be used to perform service invocation for the + /// application identified by and invokes the method specified by + /// with the HTTP method specified by . + /// + /// The to use for the invocation request. + /// The Dapr application id to invoke the method on. + /// The name of the method to invoke. + /// A collection of key/value pairs to populate the query string from. + /// An for use with SendInvokeMethodRequestAsync. + public abstract HttpRequestMessage CreateInvokeMethodRequest(HttpMethod httpMethod, string appId, + string methodName, IReadOnlyCollection> queryStringParameters); + /// /// Creates an that can be used to perform service invocation for the /// application identified by and invokes the method specified by @@ -329,9 +353,9 @@ public HttpRequestMessage CreateInvokeMethodRequest(string appId, string methodN /// An for use with SendInvokeMethodRequestAsync. public HttpRequestMessage CreateInvokeMethodRequest(string appId, string methodName, TRequest data) { - return CreateInvokeMethodRequest(HttpMethod.Post, appId, methodName, data); + return CreateInvokeMethodRequest(HttpMethod.Post, appId, methodName, new List>(), data); } - + /// /// Creates an that can be used to perform service invocation for the /// application identified by and invokes the method specified by @@ -343,9 +367,10 @@ public HttpRequestMessage CreateInvokeMethodRequest(string appId, stri /// The Dapr application id to invoke the method on. /// The name of the method to invoke. /// The data that will be JSON serialized and provided as the request body. + /// A collection of key/value pairs to populate the query string from. /// An for use with SendInvokeMethodRequestAsync. - public abstract HttpRequestMessage CreateInvokeMethodRequest(HttpMethod httpMethod, string appId, string methodName, TRequest data); - + public abstract HttpRequestMessage CreateInvokeMethodRequest(HttpMethod httpMethod, string appId, string methodName, IReadOnlyCollection> queryStringParameters, TRequest data); + /// /// Perform health-check of Dapr sidecar. Return 'true' if sidecar is healthy. Otherwise 'false'. /// CheckHealthAsync handle and will return 'false' if error will occur on transport level @@ -420,6 +445,30 @@ public HttpRequestMessage CreateInvokeMethodRequest(string appId, stri /// A that will return the value when the operation has completed. public abstract Task InvokeMethodWithResponseAsync(HttpRequestMessage request, CancellationToken cancellationToken = default); +#nullable enable + /// + /// + /// Creates an that can be used to perform Dapr service invocation using + /// objects. + /// + /// + /// The client will read the property, and + /// interpret the hostname as the destination app-id. The + /// property will be replaced with a new URI with the authority section replaced by the HTTP endpoint value + /// and the path portion of the URI rewritten to follow the format of a Dapr service invocation request. + /// + /// + /// + /// An optional app-id. If specified, the app-id will be configured as the value of + /// so that relative URIs can be used. It is mandatory to set this parameter if your app-id contains at least one upper letter. + /// If some requests use absolute URL with an app-id which contains at least one upper letter, it will not work, the workaround is to create one HttpClient for each app-id with the app-ip parameter set. + /// + /// An that can be used to perform service invocation requests. + /// + /// + public abstract HttpClient CreateInvokableHttpClient(string? appId = null); +#nullable disable + /// /// Perform service invocation using the request provided by . If the response has a non-success /// status an exception will be thrown. @@ -526,7 +575,7 @@ public Task InvokeMethodAsync( TRequest data, CancellationToken cancellationToken = default) { - var request = CreateInvokeMethodRequest(httpMethod, appId, methodName, data); + var request = CreateInvokeMethodRequest(httpMethod, appId, methodName, new List>(), data); return InvokeMethodAsync(request, cancellationToken); } @@ -620,7 +669,7 @@ public Task InvokeMethodAsync( TRequest data, CancellationToken cancellationToken = default) { - var request = CreateInvokeMethodRequest(httpMethod, appId, methodName, data); + var request = CreateInvokeMethodRequest(httpMethod, appId, methodName, new List>(), data); return InvokeMethodAsync(request, cancellationToken); } @@ -1268,192 +1317,7 @@ public abstract Task Unlock( string resourceId, string lockOwner, CancellationToken cancellationToken = default); - - /// - /// Attempt to start the given workflow with response indicating success. - /// - /// The component to interface with. - /// Name of the workflow to run. - /// Identifier of the specific run. - /// The JSON-serializeable input for the given workflow. - /// The list of options that are potentially needed to start a workflow. - /// A that can be used to cancel the operation. - /// A containing a - [Obsolete("This API is currently not stable as it is in the Alpha stage. This attribute will be removed once it is stable.")] - public abstract Task StartWorkflowAsync( - string workflowComponent, - string workflowName, - string instanceId = null, - object input = null, - IReadOnlyDictionary workflowOptions = default, - CancellationToken cancellationToken = default); - - /// - /// Waits for a workflow to start running and returns a object that contains metadata - /// about the started workflow. - /// - /// - /// - /// A "started" workflow instance is any instance not in the state. - /// - /// This method will return a completed task if the workflow has already started running or has already completed. - /// - /// - /// The unique ID of the workflow instance to wait for. - /// The component to interface with. - /// A that can be used to cancel the wait operation. - /// - /// Returns a record that describes the workflow instance and its execution status. - /// - /// - /// Thrown if is canceled before the workflow starts running. - /// - [Obsolete("This API is currently not stable as it is in the Alpha stage. This attribute will be removed once it is stable.")] - public virtual async Task WaitForWorkflowStartAsync( - string instanceId, - string workflowComponent, - CancellationToken cancellationToken = default) - { - while (true) - { - var response = await this.GetWorkflowAsync(instanceId, workflowComponent, cancellationToken); - if (response.RuntimeStatus != WorkflowRuntimeStatus.Pending) - { - return response; - } - - await Task.Delay(TimeSpan.FromMilliseconds(500), cancellationToken); - } - } - - /// - /// Waits for a workflow to complete and returns a - /// object that contains metadata about the started instance. - /// - /// - /// - /// A "completed" workflow instance is any instance in one of the terminal states. For example, the - /// , , or - /// states. - /// - /// Workflows are long-running and could take hours, days, or months before completing. - /// Workflows can also be eternal, in which case they'll never complete unless terminated. - /// In such cases, this call may block indefinitely, so care must be taken to ensure appropriate timeouts are - /// enforced using the parameter. - /// - /// If a workflow instance is already complete when this method is called, the method will return immediately. - /// - /// - /// - /// Returns a record that describes the workflow instance and its execution status. - /// - /// - /// Thrown if is canceled before the workflow completes. - /// - [Obsolete("This API is currently not stable as it is in the Alpha stage. This attribute will be removed once it is stable.")] - public virtual async Task WaitForWorkflowCompletionAsync( - string instanceId, - string workflowComponent, - CancellationToken cancellationToken = default) - { - while (true) - { - var response = await this.GetWorkflowAsync(instanceId, workflowComponent, cancellationToken); - if (response.RuntimeStatus == WorkflowRuntimeStatus.Completed || - response.RuntimeStatus == WorkflowRuntimeStatus.Failed || - response.RuntimeStatus == WorkflowRuntimeStatus.Terminated) - { - return response; - } - - await Task.Delay(TimeSpan.FromMilliseconds(500), cancellationToken); - } - } - - /// - /// Attempt to get information about the given workflow. - /// - /// The unique ID of the target workflow instance. - /// The component to interface with. - /// A that can be used to cancel the operation. - /// A containing a - [Obsolete("This API is currently not stable as it is in the Alpha stage. This attribute will be removed once it is stable.")] - public abstract Task GetWorkflowAsync( - string instanceId, - string workflowComponent, - CancellationToken cancellationToken = default); - - /// - /// Attempt to get terminate the given workflow. - /// - /// The unique ID of the target workflow instance. - /// The component to interface with. - /// A that can be used to cancel the operation. - /// A that will complete when the terminate operation has been scheduled. If the wrapped value is true the operation suceeded. - [Obsolete("This API is currently not stable as it is in the Alpha stage. This attribute will be removed once it is stable.")] - public abstract Task TerminateWorkflowAsync( - string instanceId, - string workflowComponent, - CancellationToken cancellationToken = default); - - /// - /// Attempt to raise an event the given workflow with response indicating success. - /// - /// Identifier of the specific run. - /// The component to interface with. - /// Name of the event to raise. - /// The JSON-serializable event payload to include in the raised event. - /// A that can be used to cancel the operation. - /// A that will complete when the raise event operation has been scheduled. If the wrapped value is true the operation suceeded. - [Obsolete("This API is currently not stable as it is in the Alpha stage. This attribute will be removed once it is stable.")] - public abstract Task RaiseWorkflowEventAsync( - string instanceId, - string workflowComponent, - string eventName, - object eventData = null, - CancellationToken cancellationToken = default); - - - /// - /// Pauses the specified workflow instance. - /// - /// The unique ID of the target workflow instance. - /// The component to interface with. - /// A that can be used to cancel the operation. - /// A that will complete when the pause operation has been scheduled. If the wrapped value is true the operation suceeded. - [Obsolete("This API is currently not stable as it is in the Alpha stage. This attribute will be removed once it is stable.")] - public abstract Task PauseWorkflowAsync( - string instanceId, - string workflowComponent, - CancellationToken cancellationToken = default); - - - /// - /// Resumes a paused workflow instance. - /// - /// The unique ID of the target workflow instance. - /// The component to interface with. - /// A that can be used to cancel the operation. - /// A that will complete when the resume operation has been scheduled. If the wrapped value is true the operation suceeded. - [Obsolete("This API is currently not stable as it is in the Alpha stage. This attribute will be removed once it is stable.")] - public abstract Task ResumeWorkflowAsync( - string instanceId, - string workflowComponent, - CancellationToken cancellationToken = default); - - /// - /// Delete all state associated with the specified workflow instance. The workflow must be in a non-running state to be purged. - /// - /// The unique ID of the target workflow instance. - /// The component to interface with. - /// A that can be used to cancel the operation. - /// A that will complete when the purge operation has been scheduled. If the wrapped value is true the operation suceeded. - [Obsolete("This API is currently not stable as it is in the Alpha stage. This attribute will be removed once it is stable.")] - public abstract Task PurgeWorkflowAsync( - string instanceId, - string workflowComponent, - CancellationToken cancellationToken = default); - + /// public void Dispose() { diff --git a/src/Dapr.Client/DaprClientBuilder.cs b/src/Dapr.Client/DaprClientBuilder.cs index 50a4979d1..68315c45b 100644 --- a/src/Dapr.Client/DaprClientBuilder.cs +++ b/src/Dapr.Client/DaprClientBuilder.cs @@ -40,7 +40,7 @@ public DaprClientBuilder() }; this.JsonSerializerOptions = new JsonSerializerOptions(JsonSerializerDefaults.Web); - this.DaprApiToken = DaprDefaults.GetDefaultDaprApiToken(); + this.DaprApiToken = DaprDefaults.GetDefaultDaprApiToken(null); } // property exposed for testing purposes diff --git a/src/Dapr.Client/DaprClientGrpc.cs b/src/Dapr.Client/DaprClientGrpc.cs index 3cd7de526..c70aef77b 100644 --- a/src/Dapr.Client/DaprClientGrpc.cs +++ b/src/Dapr.Client/DaprClientGrpc.cs @@ -345,7 +345,32 @@ public override async Task InvokeBindingAsync(BindingRequest re #region InvokeMethod Apis + /// + /// Creates an that can be used to perform service invocation for the + /// application identified by and invokes the method specified by + /// with the HTTP method specified by . + /// + /// The to use for the invocation request. + /// The Dapr application id to invoke the method on. + /// The name of the method to invoke. + /// An for use with SendInvokeMethodRequestAsync. public override HttpRequestMessage CreateInvokeMethodRequest(HttpMethod httpMethod, string appId, string methodName) + { + return CreateInvokeMethodRequest(httpMethod, appId, methodName, new List>()); + } + + /// + /// Creates an that can be used to perform service invocation for the + /// application identified by and invokes the method specified by + /// with the HTTP method specified by . + /// + /// The to use for the invocation request. + /// The Dapr application id to invoke the method on. + /// The name of the method to invoke. + /// A collection of key/value pairs to populate the query string from. + /// An for use with SendInvokeMethodRequestAsync. + public override HttpRequestMessage CreateInvokeMethodRequest(HttpMethod httpMethod, string appId, string methodName, + IReadOnlyCollection> queryStringParameters) { ArgumentVerifier.ThrowIfNull(httpMethod, nameof(httpMethod)); ArgumentVerifier.ThrowIfNullOrEmpty(appId, nameof(appId)); @@ -356,7 +381,8 @@ public override HttpRequestMessage CreateInvokeMethodRequest(HttpMethod httpMeth // // This approach avoids some common pitfalls that could lead to undesired encoding. var path = $"/v1.0/invoke/{appId}/method/{methodName.TrimStart('/')}"; - var request = new HttpRequestMessage(httpMethod, new Uri(this.httpEndpoint, path)); + var requestUri = new Uri(this.httpEndpoint, path).AddQueryParameters(queryStringParameters); + var request = new HttpRequestMessage(httpMethod, requestUri); request.Options.Set(new HttpRequestOptionsKey(AppIdKey), appId); request.Options.Set(new HttpRequestOptionsKey(MethodNameKey), methodName); @@ -369,13 +395,27 @@ public override HttpRequestMessage CreateInvokeMethodRequest(HttpMethod httpMeth return request; } - public override HttpRequestMessage CreateInvokeMethodRequest(HttpMethod httpMethod, string appId, string methodName, TRequest data) + /// + /// Creates an that can be used to perform service invocation for the + /// application identified by and invokes the method specified by + /// with the HTTP method specified by and a JSON serialized request body specified by + /// . + /// + /// The type of the data that will be JSON serialized and provided as the request body. + /// The to use for the invocation request. + /// The Dapr application id to invoke the method on. + /// The name of the method to invoke. + /// The data that will be JSON serialized and provided as the request body. + /// A collection of key/value pairs to populate the query string from. + /// An for use with SendInvokeMethodRequestAsync. + public override HttpRequestMessage CreateInvokeMethodRequest(HttpMethod httpMethod, string appId, string methodName, + IReadOnlyCollection> queryStringParameters, TRequest data) { ArgumentVerifier.ThrowIfNull(httpMethod, nameof(httpMethod)); ArgumentVerifier.ThrowIfNullOrEmpty(appId, nameof(appId)); ArgumentVerifier.ThrowIfNull(methodName, nameof(methodName)); - var request = CreateInvokeMethodRequest(httpMethod, appId, methodName); + var request = CreateInvokeMethodRequest(httpMethod, appId, methodName, queryStringParameters); request.Content = JsonContent.Create(data, options: this.JsonSerializerOptions); return request; } @@ -410,6 +450,31 @@ public override async Task InvokeMethodWithResponseAsync(Ht } } + /// + /// + /// Creates an that can be used to perform Dapr service invocation using + /// objects. + /// + /// + /// The client will read the property, and + /// interpret the hostname as the destination app-id. The + /// property will be replaced with a new URI with the authority section replaced by the instance's value + /// and the path portion of the URI rewritten to follow the format of a Dapr service invocation request. + /// + /// + /// + /// An optional app-id. If specified, the app-id will be configured as the value of + /// so that relative URIs can be used. It is mandatory to set this parameter if your app-id contains at least one upper letter. + /// If some requests use absolute URL with an app-id which contains at least one upper letter, it will not work, the workaround is to create one HttpClient for each app-id with the app-ip parameter set. + /// + /// An that can be used to perform service invocation requests. + /// + /// +#nullable enable + public override HttpClient CreateInvokableHttpClient(string? appId = null) => + DaprClient.CreateInvokeHttpClient(appId, this.httpEndpoint?.AbsoluteUri, this.apiTokenHeader?.Value); + #nullable disable + public async override Task InvokeMethodAsync(HttpRequestMessage request, CancellationToken cancellationToken = default) { ArgumentVerifier.ThrowIfNull(request, nameof(request)); @@ -1996,287 +2061,6 @@ public async override Task Unlock( #endregion - - #region Workflow API - /// - [Obsolete] - public async override Task StartWorkflowAsync( - string workflowComponent, - string workflowName, - string instanceId = null, - object input = null, - IReadOnlyDictionary workflowOptions = default, - CancellationToken cancellationToken = default) - { - ArgumentVerifier.ThrowIfNullOrEmpty(instanceId, nameof(instanceId)); - ArgumentVerifier.ThrowIfNullOrEmpty(workflowComponent, nameof(workflowComponent)); - ArgumentVerifier.ThrowIfNullOrEmpty(workflowName, nameof(workflowName)); - ArgumentVerifier.ThrowIfNull(input, nameof(input)); - - // Serialize json data. Converts input object to bytes and then bytestring inside the request. - byte[] jsonUtf8Bytes = null; - if (input is not null) - { - jsonUtf8Bytes = JsonSerializer.SerializeToUtf8Bytes(input); - } - - var request = new Autogenerated.StartWorkflowRequest() - { - InstanceId = instanceId, - WorkflowComponent = workflowComponent, - WorkflowName = workflowName, - Input = jsonUtf8Bytes is not null ? ByteString.CopyFrom(jsonUtf8Bytes) : null, - }; - - if (workflowOptions?.Count > 0) - { - foreach (var item in workflowOptions) - { - request.Options[item.Key] = item.Value; - } - } - - try - { - var options = CreateCallOptions(headers: null, cancellationToken); - var response = await client.StartWorkflowAlpha1Async(request, options); - return new StartWorkflowResponse(response.InstanceId); - - } - catch (RpcException ex) - { - throw new DaprException("Start Workflow operation failed: the Dapr endpoint indicated a failure. See InnerException for details.", ex); - } - } - - /// - [Obsolete] - public async override Task GetWorkflowAsync( - string instanceId, - string workflowComponent, - CancellationToken cancellationToken = default) - { - ArgumentVerifier.ThrowIfNullOrEmpty(instanceId, nameof(instanceId)); - ArgumentVerifier.ThrowIfNullOrEmpty(workflowComponent, nameof(workflowComponent)); - - var request = new Autogenerated.GetWorkflowRequest() - { - InstanceId = instanceId, - WorkflowComponent = workflowComponent - }; - - try - { - var options = CreateCallOptions(headers: null, cancellationToken); - var response = await client.GetWorkflowAlpha1Async(request, options); - if (response == null) - { - throw new DaprException("Get workflow operation failed: the Dapr endpoint returned an empty result."); - } - - response.CreatedAt ??= new Timestamp(); - response.LastUpdatedAt ??= response.CreatedAt; - - return new GetWorkflowResponse - { - InstanceId = response.InstanceId, - WorkflowName = response.WorkflowName, - WorkflowComponentName = workflowComponent, - CreatedAt = response.CreatedAt.ToDateTime(), - LastUpdatedAt = response.LastUpdatedAt.ToDateTime(), - RuntimeStatus = GetWorkflowRuntimeStatus(response.RuntimeStatus), - Properties = response.Properties, - FailureDetails = GetWorkflowFailureDetails(response, workflowComponent), - }; - } - catch (RpcException ex) - { - throw new DaprException("Get workflow operation failed: the Dapr endpoint indicated a failure. See InnerException for details.", ex); - } - } - - private static WorkflowRuntimeStatus GetWorkflowRuntimeStatus(string runtimeStatus) - { - if (!System.Enum.TryParse(runtimeStatus, true /* ignoreCase */, out WorkflowRuntimeStatus status)) - { - status = WorkflowRuntimeStatus.Unknown; - } - - return status; - } - - private static WorkflowFailureDetails GetWorkflowFailureDetails(Autogenerated.GetWorkflowResponse response, string componentName) - { - // FUTURE: Make this part of the protobuf contract instead of getting it from properties - // NOTE: The use of | instead of || is intentional. We want to get all the values. - if (response.Properties.TryGetValue($"{componentName}.workflow.failure.error_type", out string errorType) | - response.Properties.TryGetValue($"{componentName}.workflow.failure.error_message", out string errorMessage) | - response.Properties.TryGetValue($"{componentName}.workflow.failure.stack_trace", out string stackTrace)) - { - return new WorkflowFailureDetails(errorMessage, errorType, stackTrace); - } - - return null; - } - - /// - [Obsolete] - public async override Task TerminateWorkflowAsync( - string instanceId, - string workflowComponent, - CancellationToken cancellationToken = default) - { - ArgumentVerifier.ThrowIfNullOrEmpty(instanceId, nameof(instanceId)); - ArgumentVerifier.ThrowIfNullOrEmpty(workflowComponent, nameof(workflowComponent)); - - var request = new Autogenerated.TerminateWorkflowRequest() - { - InstanceId = instanceId, - WorkflowComponent = workflowComponent - }; - - var options = CreateCallOptions(headers: null, cancellationToken); - - try - { - await client.TerminateWorkflowAlpha1Async(request, options); - } - catch (RpcException ex) - { - throw new DaprException("Terminate workflow operation failed: the Dapr endpoint indicated a failure. See InnerException for details.", ex); - } - - } - - /// - [Obsolete] - public async override Task RaiseWorkflowEventAsync( - string instanceId, - string workflowComponent, - string eventName, - Object eventData, - CancellationToken cancellationToken = default) - { - ArgumentVerifier.ThrowIfNullOrEmpty(instanceId, nameof(instanceId)); - ArgumentVerifier.ThrowIfNullOrEmpty(workflowComponent, nameof(workflowComponent)); - ArgumentVerifier.ThrowIfNullOrEmpty(eventName, nameof(eventName)); - - byte[] jsonUtf8Bytes = new byte[0]; - // Serialize json data. Converts eventData object to bytes and then bytestring inside the request. - if (eventData != null) - { - jsonUtf8Bytes = JsonSerializer.SerializeToUtf8Bytes(eventData); - } - - var request = new Autogenerated.RaiseEventWorkflowRequest() - { - InstanceId = instanceId, - WorkflowComponent = workflowComponent, - EventName = eventName, - EventData = ByteString.CopyFrom(jsonUtf8Bytes), - }; - - var options = CreateCallOptions(headers: null, cancellationToken); - - try - { - await client.RaiseEventWorkflowAlpha1Async(request, options); - } - catch (RpcException ex) - { - throw new DaprException("Start Workflow operation failed: the Dapr endpoint indicated a failure. See InnerException for details.", ex); - } - } - - - /// - [Obsolete] - public async override Task PauseWorkflowAsync( - string instanceId, - string workflowComponent, - CancellationToken cancellationToken = default) - { - ArgumentVerifier.ThrowIfNullOrEmpty(instanceId, nameof(instanceId)); - ArgumentVerifier.ThrowIfNullOrEmpty(workflowComponent, nameof(workflowComponent)); - - var request = new Autogenerated.PauseWorkflowRequest() - { - InstanceId = instanceId, - WorkflowComponent = workflowComponent - }; - - var options = CreateCallOptions(headers: null, cancellationToken); - - try - { - await client.PauseWorkflowAlpha1Async(request, options); - } - catch (RpcException ex) - { - throw new DaprException("Pause workflow operation failed: the Dapr endpoint indicated a failure. See InnerException for details.", ex); - } - } - - /// - [Obsolete] - public async override Task ResumeWorkflowAsync( - string instanceId, - string workflowComponent, - CancellationToken cancellationToken = default) - { - ArgumentVerifier.ThrowIfNullOrEmpty(instanceId, nameof(instanceId)); - ArgumentVerifier.ThrowIfNullOrEmpty(workflowComponent, nameof(workflowComponent)); - - var request = new Autogenerated.ResumeWorkflowRequest() - { - InstanceId = instanceId, - WorkflowComponent = workflowComponent - }; - - var options = CreateCallOptions(headers: null, cancellationToken); - - try - { - await client.ResumeWorkflowAlpha1Async(request, options); - } - catch (RpcException ex) - { - throw new DaprException("Resume workflow operation failed: the Dapr endpoint indicated a failure. See InnerException for details.", ex); - } - - } - - /// - [Obsolete] - public async override Task PurgeWorkflowAsync( - string instanceId, - string workflowComponent, - CancellationToken cancellationToken = default) - { - ArgumentVerifier.ThrowIfNullOrEmpty(instanceId, nameof(instanceId)); - ArgumentVerifier.ThrowIfNullOrEmpty(workflowComponent, nameof(workflowComponent)); - - var request = new Autogenerated.PurgeWorkflowRequest() - { - InstanceId = instanceId, - WorkflowComponent = workflowComponent - }; - - var options = CreateCallOptions(headers: null, cancellationToken); - - try - { - await client.PurgeWorkflowAlpha1Async(request, options); - } - catch (RpcException ex) - { - throw new DaprException("Purge workflow operation failed: the Dapr endpoint indicated a failure. See InnerException for details.", ex); - } - - } - #endregion - - #region Dapr Sidecar Methods /// @@ -2350,10 +2134,14 @@ public override async Task GetMetadataAsync(CancellationToken canc try { var response = await client.GetMetadataAsync(new Autogenerated.GetMetadataRequest(), options); - return new DaprMetadata(response.Id, - response.ActorRuntime.ActiveActors.Select(c => new DaprActorMetadata(c.Type, c.Count)).ToList(), - response.ExtendedMetadata.ToDictionary(c => c.Key, c => c.Value), - response.RegisteredComponents.Select(c => new DaprComponentsMetadata(c.Name, c.Type, c.Version, c.Capabilities.ToArray())).ToList()); + return new DaprMetadata(response.Id ?? "", + response.ActorRuntime?.ActiveActors?.Select(c => new DaprActorMetadata(c.Type, c.Count)).ToList() ?? + new List(), + response.ExtendedMetadata?.ToDictionary(c => c.Key, c => c.Value) ?? + new Dictionary(), + response.RegisteredComponents?.Select(c => + new DaprComponentsMetadata(c.Name, c.Type, c.Version, c.Capabilities.ToArray())).ToList() ?? + new List()); } catch (RpcException ex) { diff --git a/src/Dapr.Client/EnumExtensions.cs b/src/Dapr.Client/Extensions/EnumExtensions.cs similarity index 88% rename from src/Dapr.Client/EnumExtensions.cs rename to src/Dapr.Client/Extensions/EnumExtensions.cs index 6b058ca77..df9c9ad33 100644 --- a/src/Dapr.Client/EnumExtensions.cs +++ b/src/Dapr.Client/Extensions/EnumExtensions.cs @@ -11,6 +11,7 @@ // limitations under the License. // ------------------------------------------------------------------------ +#nullable enable using System; using System.Reflection; using System.Runtime.Serialization; @@ -27,12 +28,14 @@ internal static class EnumExtensions /// public static string GetValueFromEnumMember(this T value) where T : Enum { + ArgumentNullException.ThrowIfNull(value, nameof(value)); + var memberInfo = typeof(T).GetMember(value.ToString(), BindingFlags.Static | BindingFlags.Public | BindingFlags.DeclaredOnly); if (memberInfo.Length <= 0) return value.ToString(); var attributes = memberInfo[0].GetCustomAttributes(typeof(EnumMemberAttribute), false); - return attributes.Length > 0 ? ((EnumMemberAttribute)attributes[0]).Value : value.ToString(); + return (attributes.Length > 0 ? ((EnumMemberAttribute)attributes[0]).Value : value.ToString()) ?? value.ToString(); } } } diff --git a/src/Dapr.Client/Extensions/HttpExtensions.cs b/src/Dapr.Client/Extensions/HttpExtensions.cs new file mode 100644 index 000000000..259d2747d --- /dev/null +++ b/src/Dapr.Client/Extensions/HttpExtensions.cs @@ -0,0 +1,51 @@ +// ------------------------------------------------------------------------ +// Copyright 2024 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.Text; + +namespace Dapr.Client +{ + /// + /// Provides extensions specific to HTTP types. + /// + internal static class HttpExtensions + { + /// + /// Appends key/value pairs to the query string on an HttpRequestMessage. + /// + /// The uri to append the query string parameters to. + /// The key/value pairs to populate the query string with. + public static Uri AddQueryParameters(this Uri? uri, + IReadOnlyCollection>? queryStringParameters) + { + ArgumentNullException.ThrowIfNull(uri, nameof(uri)); + if (queryStringParameters is null) + return uri; + + var uriBuilder = new UriBuilder(uri); + var qsBuilder = new StringBuilder(uriBuilder.Query); + foreach (var kvParam in queryStringParameters) + { + if (qsBuilder.Length > 0) + qsBuilder.Append('&'); + qsBuilder.Append($"{Uri.EscapeDataString(kvParam.Key)}={Uri.EscapeDataString(kvParam.Value)}"); + } + + uriBuilder.Query = qsBuilder.ToString(); + return uriBuilder.Uri; + } + } +} diff --git a/src/Dapr.Client/GetWorkflowResponse.cs b/src/Dapr.Client/GetWorkflowResponse.cs deleted file mode 100644 index 11fc253ac..000000000 --- a/src/Dapr.Client/GetWorkflowResponse.cs +++ /dev/null @@ -1,100 +0,0 @@ -// ------------------------------------------------------------------------ -// Copyright 2021 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.Text.Json; - -namespace Dapr.Client -{ - /// - /// The response type for the API. - /// - public class GetWorkflowResponse - { - /// - /// Gets the instance ID of the workflow. - /// - public string InstanceId { get; init; } - - /// - /// Gets the name of the workflow. - /// - public string WorkflowName { get; init; } - - /// - /// Gets the name of the workflow component. - /// - public string WorkflowComponentName { get; init; } - - /// - /// Gets the time at which the workflow was created. - /// - public DateTime CreatedAt { get; init; } - - /// - /// Gets the time at which the workflow was last updated. - /// - public DateTime LastUpdatedAt { get; init; } - - /// - /// Gets the runtime status of the workflow. - /// - public WorkflowRuntimeStatus RuntimeStatus { get; init; } - - /// - /// Gets the component-specific workflow properties. - /// - public IReadOnlyDictionary Properties { get; init; } - - /// - /// Gets the details associated with the workflow failure, if any. - /// - public WorkflowFailureDetails FailureDetails { get; init; } - - /// - /// Deserializes the workflow input into using . - /// - /// The type to deserialize the workflow input into. - /// Options to control the behavior during parsing. - /// Returns the input as , or returns a default value if the workflow doesn't have an input. - public T ReadInputAs(JsonSerializerOptions options = null) - { - // FUTURE: Make this part of the protobuf contract instead of properties - string defaultInputKey = $"{this.WorkflowComponentName}.workflow.input"; - if (!this.Properties.TryGetValue(defaultInputKey, out string serializedInput)) - { - return default; - } - - return JsonSerializer.Deserialize(serializedInput, options); - } - - /// - /// Deserializes the workflow output into using . - /// - /// The type to deserialize the workflow output into. - /// Options to control the behavior during parsing. - /// Returns the output as , or returns a default value if the workflow doesn't have an output. - public T ReadOutputAs(JsonSerializerOptions options = null) - { - // FUTURE: Make this part of the protobuf contract instead of properties - string defaultOutputKey = $"{this.WorkflowComponentName}.workflow.output"; - if (!this.Properties.TryGetValue(defaultOutputKey, out string serializedOutput)) - { - return default; - } - - return JsonSerializer.Deserialize(serializedOutput, options); - } - } -} diff --git a/src/Dapr.Client/InvocationHandler.cs b/src/Dapr.Client/InvocationHandler.cs index 1b55436aa..36fd6b77f 100644 --- a/src/Dapr.Client/InvocationHandler.cs +++ b/src/Dapr.Client/InvocationHandler.cs @@ -51,7 +51,7 @@ public class InvocationHandler : DelegatingHandler public InvocationHandler() { this.parsedEndpoint = new Uri(DaprDefaults.GetDefaultHttpEndpoint(), UriKind.Absolute); - this.apiToken = DaprDefaults.GetDefaultDaprApiToken(); + this.apiToken = DaprDefaults.GetDefaultDaprApiToken(null); } /// diff --git a/src/Dapr.Client/WorkflowFailureDetails.cs b/src/Dapr.Client/WorkflowFailureDetails.cs deleted file mode 100644 index a61754ff1..000000000 --- a/src/Dapr.Client/WorkflowFailureDetails.cs +++ /dev/null @@ -1,35 +0,0 @@ -// ------------------------------------------------------------------------ -// Copyright 2021 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. -// ------------------------------------------------------------------------ - -namespace Dapr.Client -{ - /// - /// Represents workflow failure details. - /// - /// A summary description of the failure, which is typically an exception message. - /// The error type, which is defined by the workflow component implementation. - /// The stack trace of the failure. - public record WorkflowFailureDetails( - string ErrorMessage, - string ErrorType, - string StackTrace = null) - { - /// - /// Creates a user-friendly string representation of the failure information. - /// - public override string ToString() - { - return $"{this.ErrorType}: {this.ErrorMessage}"; - } - } -} diff --git a/src/Shared/ArgumentVerifier.cs b/src/Dapr.Common/ArgumentVerifier.cs similarity index 96% rename from src/Shared/ArgumentVerifier.cs rename to src/Dapr.Common/ArgumentVerifier.cs index 907543f01..62ae98b54 100644 --- a/src/Shared/ArgumentVerifier.cs +++ b/src/Dapr.Common/ArgumentVerifier.cs @@ -1,4 +1,4 @@ -// ------------------------------------------------------------------------ +// ------------------------------------------------------------------------ // Copyright 2021 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. diff --git a/src/Dapr.Common/AssemblyInfo.cs b/src/Dapr.Common/AssemblyInfo.cs new file mode 100644 index 000000000..a18d03bbc --- /dev/null +++ b/src/Dapr.Common/AssemblyInfo.cs @@ -0,0 +1,40 @@ +// ------------------------------------------------------------------------ +// Copyright 2024 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.Runtime.CompilerServices; + +[assembly: InternalsVisibleTo("Dapr.Actors, PublicKey=0024000004800000940000000602000000240000525341310004000001000100b1f597635c44597fcecb493e2b1327033b29b1a98ac956a1a538664b68f87d45fbaada0438a15a6265e62864947cc067d8da3a7d93c5eb2fcbb850e396c8684dba74ea477d82a1bbb18932c0efb30b64ff1677f85ae833818707ac8b49ad8062ca01d2c89d8ab1843ae73e8ba9649cd28666b539444dcdee3639f95e2a099bb2")] +[assembly: InternalsVisibleTo("Dapr.Actors.Generators, PublicKey=0024000004800000940000000602000000240000525341310004000001000100b1f597635c44597fcecb493e2b1327033b29b1a98ac956a1a538664b68f87d45fbaada0438a15a6265e62864947cc067d8da3a7d93c5eb2fcbb850e396c8684dba74ea477d82a1bbb18932c0efb30b64ff1677f85ae833818707ac8b49ad8062ca01d2c89d8ab1843ae73e8ba9649cd28666b539444dcdee3639f95e2a099bb2")] +[assembly: InternalsVisibleTo("Dapr.Actors.AspNetCore, PublicKey=0024000004800000940000000602000000240000525341310004000001000100b1f597635c44597fcecb493e2b1327033b29b1a98ac956a1a538664b68f87d45fbaada0438a15a6265e62864947cc067d8da3a7d93c5eb2fcbb850e396c8684dba74ea477d82a1bbb18932c0efb30b64ff1677f85ae833818707ac8b49ad8062ca01d2c89d8ab1843ae73e8ba9649cd28666b539444dcdee3639f95e2a099bb2")] +[assembly: InternalsVisibleTo("Dapr.Client, PublicKey=0024000004800000940000000602000000240000525341310004000001000100b1f597635c44597fcecb493e2b1327033b29b1a98ac956a1a538664b68f87d45fbaada0438a15a6265e62864947cc067d8da3a7d93c5eb2fcbb850e396c8684dba74ea477d82a1bbb18932c0efb30b64ff1677f85ae833818707ac8b49ad8062ca01d2c89d8ab1843ae73e8ba9649cd28666b539444dcdee3639f95e2a099bb2")] +[assembly: InternalsVisibleTo("Dapr.AspNetCore, PublicKey=0024000004800000940000000602000000240000525341310004000001000100b1f597635c44597fcecb493e2b1327033b29b1a98ac956a1a538664b68f87d45fbaada0438a15a6265e62864947cc067d8da3a7d93c5eb2fcbb850e396c8684dba74ea477d82a1bbb18932c0efb30b64ff1677f85ae833818707ac8b49ad8062ca01d2c89d8ab1843ae73e8ba9649cd28666b539444dcdee3639f95e2a099bb2")] +[assembly: InternalsVisibleTo("Dapr.Extensions.Configuration, PublicKey=0024000004800000940000000602000000240000525341310004000001000100b1f597635c44597fcecb493e2b1327033b29b1a98ac956a1a538664b68f87d45fbaada0438a15a6265e62864947cc067d8da3a7d93c5eb2fcbb850e396c8684dba74ea477d82a1bbb18932c0efb30b64ff1677f85ae833818707ac8b49ad8062ca01d2c89d8ab1843ae73e8ba9649cd28666b539444dcdee3639f95e2a099bb2")] +[assembly: InternalsVisibleTo("Dapr.Workflow, PublicKey=0024000004800000940000000602000000240000525341310004000001000100b1f597635c44597fcecb493e2b1327033b29b1a98ac956a1a538664b68f87d45fbaada0438a15a6265e62864947cc067d8da3a7d93c5eb2fcbb850e396c8684dba74ea477d82a1bbb18932c0efb30b64ff1677f85ae833818707ac8b49ad8062ca01d2c89d8ab1843ae73e8ba9649cd28666b539444dcdee3639f95e2a099bb2")] + +[assembly: InternalsVisibleTo("Dapr.Actors.AspNetCore.IntegrationTest, PublicKey=0024000004800000940000000602000000240000525341310004000001000100b1f597635c44597fcecb493e2b1327033b29b1a98ac956a1a538664b68f87d45fbaada0438a15a6265e62864947cc067d8da3a7d93c5eb2fcbb850e396c8684dba74ea477d82a1bbb18932c0efb30b64ff1677f85ae833818707ac8b49ad8062ca01d2c89d8ab1843ae73e8ba9649cd28666b539444dcdee3639f95e2a099bb2")] +[assembly: InternalsVisibleTo("Dapr.Actors.AspNetCore.IntegrationTest.App, PublicKey=0024000004800000940000000602000000240000525341310004000001000100b1f597635c44597fcecb493e2b1327033b29b1a98ac956a1a538664b68f87d45fbaada0438a15a6265e62864947cc067d8da3a7d93c5eb2fcbb850e396c8684dba74ea477d82a1bbb18932c0efb30b64ff1677f85ae833818707ac8b49ad8062ca01d2c89d8ab1843ae73e8ba9649cd28666b539444dcdee3639f95e2a099bb2")] +[assembly: InternalsVisibleTo("Dapr.Actors.AspNetCore.Test, PublicKey=0024000004800000940000000602000000240000525341310004000001000100b1f597635c44597fcecb493e2b1327033b29b1a98ac956a1a538664b68f87d45fbaada0438a15a6265e62864947cc067d8da3a7d93c5eb2fcbb850e396c8684dba74ea477d82a1bbb18932c0efb30b64ff1677f85ae833818707ac8b49ad8062ca01d2c89d8ab1843ae73e8ba9649cd28666b539444dcdee3639f95e2a099bb2")] +[assembly: InternalsVisibleTo("Dapr.Actors.Generators.Test, PublicKey=0024000004800000940000000602000000240000525341310004000001000100b1f597635c44597fcecb493e2b1327033b29b1a98ac956a1a538664b68f87d45fbaada0438a15a6265e62864947cc067d8da3a7d93c5eb2fcbb850e396c8684dba74ea477d82a1bbb18932c0efb30b64ff1677f85ae833818707ac8b49ad8062ca01d2c89d8ab1843ae73e8ba9649cd28666b539444dcdee3639f95e2a099bb2")] +[assembly: InternalsVisibleTo("Dapr.Actors.Test, PublicKey=0024000004800000940000000602000000240000525341310004000001000100b1f597635c44597fcecb493e2b1327033b29b1a98ac956a1a538664b68f87d45fbaada0438a15a6265e62864947cc067d8da3a7d93c5eb2fcbb850e396c8684dba74ea477d82a1bbb18932c0efb30b64ff1677f85ae833818707ac8b49ad8062ca01d2c89d8ab1843ae73e8ba9649cd28666b539444dcdee3639f95e2a099bb2")] +[assembly: InternalsVisibleTo("Dapr.AspNetCore.IntegrationTest, PublicKey=0024000004800000940000000602000000240000525341310004000001000100b1f597635c44597fcecb493e2b1327033b29b1a98ac956a1a538664b68f87d45fbaada0438a15a6265e62864947cc067d8da3a7d93c5eb2fcbb850e396c8684dba74ea477d82a1bbb18932c0efb30b64ff1677f85ae833818707ac8b49ad8062ca01d2c89d8ab1843ae73e8ba9649cd28666b539444dcdee3639f95e2a099bb2")] +[assembly: InternalsVisibleTo("Dapr.AspNetCore.IntegrationTest.App, PublicKey=0024000004800000940000000602000000240000525341310004000001000100b1f597635c44597fcecb493e2b1327033b29b1a98ac956a1a538664b68f87d45fbaada0438a15a6265e62864947cc067d8da3a7d93c5eb2fcbb850e396c8684dba74ea477d82a1bbb18932c0efb30b64ff1677f85ae833818707ac8b49ad8062ca01d2c89d8ab1843ae73e8ba9649cd28666b539444dcdee3639f95e2a099bb2")] +[assembly: InternalsVisibleTo("Dapr.AspNetCore.Test, PublicKey=0024000004800000940000000602000000240000525341310004000001000100b1f597635c44597fcecb493e2b1327033b29b1a98ac956a1a538664b68f87d45fbaada0438a15a6265e62864947cc067d8da3a7d93c5eb2fcbb850e396c8684dba74ea477d82a1bbb18932c0efb30b64ff1677f85ae833818707ac8b49ad8062ca01d2c89d8ab1843ae73e8ba9649cd28666b539444dcdee3639f95e2a099bb2")] +[assembly: InternalsVisibleTo("Dapr.Client.Test, PublicKey=0024000004800000940000000602000000240000525341310004000001000100b1f597635c44597fcecb493e2b1327033b29b1a98ac956a1a538664b68f87d45fbaada0438a15a6265e62864947cc067d8da3a7d93c5eb2fcbb850e396c8684dba74ea477d82a1bbb18932c0efb30b64ff1677f85ae833818707ac8b49ad8062ca01d2c89d8ab1843ae73e8ba9649cd28666b539444dcdee3639f95e2a099bb2")] +[assembly: InternalsVisibleTo("Dapr.Common.Test, PublicKey=0024000004800000940000000602000000240000525341310004000001000100b1f597635c44597fcecb493e2b1327033b29b1a98ac956a1a538664b68f87d45fbaada0438a15a6265e62864947cc067d8da3a7d93c5eb2fcbb850e396c8684dba74ea477d82a1bbb18932c0efb30b64ff1677f85ae833818707ac8b49ad8062ca01d2c89d8ab1843ae73e8ba9649cd28666b539444dcdee3639f95e2a099bb2")] +[assembly: InternalsVisibleTo("Dapr.E2E.Test, PublicKey=0024000004800000940000000602000000240000525341310004000001000100b1f597635c44597fcecb493e2b1327033b29b1a98ac956a1a538664b68f87d45fbaada0438a15a6265e62864947cc067d8da3a7d93c5eb2fcbb850e396c8684dba74ea477d82a1bbb18932c0efb30b64ff1677f85ae833818707ac8b49ad8062ca01d2c89d8ab1843ae73e8ba9649cd28666b539444dcdee3639f95e2a099bb2")] +[assembly: InternalsVisibleTo("Dapr.E2E.Test.Actors, PublicKey=0024000004800000940000000602000000240000525341310004000001000100b1f597635c44597fcecb493e2b1327033b29b1a98ac956a1a538664b68f87d45fbaada0438a15a6265e62864947cc067d8da3a7d93c5eb2fcbb850e396c8684dba74ea477d82a1bbb18932c0efb30b64ff1677f85ae833818707ac8b49ad8062ca01d2c89d8ab1843ae73e8ba9649cd28666b539444dcdee3639f95e2a099bb2")] +[assembly: InternalsVisibleTo("Dapr.E2E.Test.Actors.Generators, PublicKey=0024000004800000940000000602000000240000525341310004000001000100b1f597635c44597fcecb493e2b1327033b29b1a98ac956a1a538664b68f87d45fbaada0438a15a6265e62864947cc067d8da3a7d93c5eb2fcbb850e396c8684dba74ea477d82a1bbb18932c0efb30b64ff1677f85ae833818707ac8b49ad8062ca01d2c89d8ab1843ae73e8ba9649cd28666b539444dcdee3639f95e2a099bb2")] +[assembly: InternalsVisibleTo("Dapr.E2E.Test.App, PublicKey=0024000004800000940000000602000000240000525341310004000001000100b1f597635c44597fcecb493e2b1327033b29b1a98ac956a1a538664b68f87d45fbaada0438a15a6265e62864947cc067d8da3a7d93c5eb2fcbb850e396c8684dba74ea477d82a1bbb18932c0efb30b64ff1677f85ae833818707ac8b49ad8062ca01d2c89d8ab1843ae73e8ba9649cd28666b539444dcdee3639f95e2a099bb2")] +[assembly: InternalsVisibleTo("Dapr.E2E.Test.App.Grpc, PublicKey=0024000004800000940000000602000000240000525341310004000001000100b1f597635c44597fcecb493e2b1327033b29b1a98ac956a1a538664b68f87d45fbaada0438a15a6265e62864947cc067d8da3a7d93c5eb2fcbb850e396c8684dba74ea477d82a1bbb18932c0efb30b64ff1677f85ae833818707ac8b49ad8062ca01d2c89d8ab1843ae73e8ba9649cd28666b539444dcdee3639f95e2a099bb2")] +[assembly: InternalsVisibleTo("Dapr.E2E.Test.App.ReentrantActors, PublicKey=0024000004800000940000000602000000240000525341310004000001000100b1f597635c44597fcecb493e2b1327033b29b1a98ac956a1a538664b68f87d45fbaada0438a15a6265e62864947cc067d8da3a7d93c5eb2fcbb850e396c8684dba74ea477d82a1bbb18932c0efb30b64ff1677f85ae833818707ac8b49ad8062ca01d2c89d8ab1843ae73e8ba9649cd28666b539444dcdee3639f95e2a099bb2")] +[assembly: InternalsVisibleTo("Dapr.Extensions.Configuration.Test, PublicKey=0024000004800000940000000602000000240000525341310004000001000100b1f597635c44597fcecb493e2b1327033b29b1a98ac956a1a538664b68f87d45fbaada0438a15a6265e62864947cc067d8da3a7d93c5eb2fcbb850e396c8684dba74ea477d82a1bbb18932c0efb30b64ff1677f85ae833818707ac8b49ad8062ca01d2c89d8ab1843ae73e8ba9649cd28666b539444dcdee3639f95e2a099bb2")] diff --git a/src/Dapr.Common/Dapr.Common.csproj b/src/Dapr.Common/Dapr.Common.csproj new file mode 100644 index 000000000..31af3952c --- /dev/null +++ b/src/Dapr.Common/Dapr.Common.csproj @@ -0,0 +1,16 @@ + + + + net6;net7;net8 + enable + enable + + + + + + + + + + diff --git a/src/Dapr.Common/DaprDefaults.cs b/src/Dapr.Common/DaprDefaults.cs new file mode 100644 index 000000000..85a4b18c8 --- /dev/null +++ b/src/Dapr.Common/DaprDefaults.cs @@ -0,0 +1,132 @@ +// ------------------------------------------------------------------------ +// Copyright 2021 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 Microsoft.Extensions.Configuration; + +namespace Dapr +{ + internal static class DaprDefaults + { + private static string httpEndpoint = string.Empty; + private static string grpcEndpoint = string.Empty; + private static string daprApiToken = string.Empty; + private static string appApiToken = string.Empty; + + public const string DaprApiTokenName = "DAPR_API_TOKEN"; + public const string AppApiTokenName = "APP_API_TOKEN"; + public const string DaprHttpEndpointName = "DAPR_HTTP_ENDPOINT"; + public const string DaprHttpPortName = "DAPR_HTTP_PORT"; + public const string DaprGrpcEndpointName = "DAPR_GRPC_ENDPOINT"; + public const string DaprGrpcPortName = "DAPR_GRPC_PORT"; + + public const string DefaultDaprScheme = "http"; + public const string DefaultDaprHost = "localhost"; + public const int DefaultHttpPort = 3500; + public const int DefaultGrpcPort = 50001; + + /// + /// Get the value of environment variable DAPR_API_TOKEN + /// + /// The optional to pull the value from. + /// The value of environment variable DAPR_API_TOKEN + public static string GetDefaultDaprApiToken(IConfiguration? configuration) => + GetResourceValue(configuration, DaprApiTokenName) ?? string.Empty; + + /// + /// Get the value of environment variable APP_API_TOKEN + /// + /// The optional to pull the value from. + /// The value of environment variable APP_API_TOKEN + public static string GetDefaultAppApiToken(IConfiguration? configuration) => + GetResourceValue(configuration, AppApiTokenName) ?? string.Empty; + + /// + /// Get the value of HTTP endpoint based off environment variables + /// + /// The optional to pull the value from. + /// The value of HTTP endpoint based off environment variables + public static string GetDefaultHttpEndpoint(IConfiguration? configuration = null) + { + //Prioritize pulling from the IConfiguration and fallback to the environment variable if not populated + var endpoint = GetResourceValue(configuration, DaprHttpEndpointName); + var port = GetResourceValue(configuration, DaprHttpPortName); + + //Use the default HTTP port if we're unable to retrieve/parse the provided port + int? parsedGrpcPort = string.IsNullOrWhiteSpace(port) ? DefaultHttpPort : int.Parse(port); + + return BuildEndpoint(endpoint, parsedGrpcPort.Value); + } + + /// + /// Get the value of gRPC endpoint based off environment variables + /// + /// The optional to pull the value from. + /// The value of gRPC endpoint based off environment variables + public static string GetDefaultGrpcEndpoint(IConfiguration? configuration = null) + { + //Prioritize pulling from the IConfiguration and fallback to the environment variable if not populated + var endpoint = GetResourceValue(configuration, DaprGrpcEndpointName); + var port = GetResourceValue(configuration, DaprGrpcPortName); + + //Use the default gRPC port if we're unable to retrieve/parse the provided port + int? parsedGrpcPort = string.IsNullOrWhiteSpace(port) ? DefaultGrpcPort : int.Parse(port); + + return BuildEndpoint(endpoint, parsedGrpcPort.Value); + } + + /// + /// Builds the Dapr endpoint. + /// + /// The endpoint value. + /// The endpoint port value, whether pulled from configuration/envvar or the default. + /// A constructed endpoint value. + private static string BuildEndpoint(string? endpoint, int endpointPort) + { + var endpointBuilder = new UriBuilder { Scheme = DefaultDaprScheme, Host = DefaultDaprHost }; //Port depends on endpoint + + if (!string.IsNullOrWhiteSpace(endpoint)) //If the endpoint is set, it doesn't matter if the port is + { + //Extract the scheme, host and port from the endpoint and replace defaults + var uri = new Uri(endpoint); + endpointBuilder.Scheme = uri.Scheme; + endpointBuilder.Host = uri.Host; + endpointBuilder.Port = uri.Port; + } + else + { + //Should only set the port if the endpoint isn't populated + endpointBuilder.Port = endpointPort; + } + + return endpointBuilder.ToString(); + } + + /// + /// Retrieves the specified value prioritizing pulling it from , falling back + /// to an environment variable, and using an empty string as a default. + /// + /// An instance of an . + /// The name of the value to retrieve. + /// The value of the resource. + private static string? GetResourceValue(IConfiguration? configuration, string name) + { + //Attempt to retrieve first from the configuration + var configurationValue = configuration?[name]; + if (configurationValue is not null) + return configurationValue; + + //Fall back to the environment variable with the same name or default to an empty string + return Environment.GetEnvironmentVariable(name); + } + } +} diff --git a/src/Dapr.Client/DaprException.cs b/src/Dapr.Common/DaprException.cs similarity index 96% rename from src/Dapr.Client/DaprException.cs rename to src/Dapr.Common/DaprException.cs index e7b1efaba..2b600ef3a 100644 --- a/src/Dapr.Client/DaprException.cs +++ b/src/Dapr.Common/DaprException.cs @@ -1,4 +1,4 @@ -// ------------------------------------------------------------------------ +// ------------------------------------------------------------------------ // Copyright 2021 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. @@ -11,7 +11,6 @@ // limitations under the License. // ------------------------------------------------------------------------ -using System; using System.Runtime.Serialization; namespace Dapr diff --git a/src/Dapr.Extensions.Configuration/Dapr.Extensions.Configuration.csproj b/src/Dapr.Extensions.Configuration/Dapr.Extensions.Configuration.csproj index 71fd0153e..5cc1043d3 100644 --- a/src/Dapr.Extensions.Configuration/Dapr.Extensions.Configuration.csproj +++ b/src/Dapr.Extensions.Configuration/Dapr.Extensions.Configuration.csproj @@ -4,10 +4,6 @@ enable - - - - Dapr Secret Store configuration provider implementation for Microsoft.Extensions.Configuration. @@ -15,10 +11,11 @@ + - + \ No newline at end of file diff --git a/src/Dapr.Extensions.Configuration/DaprSecretStoreConfigurationProvider.cs b/src/Dapr.Extensions.Configuration/DaprSecretStoreConfigurationProvider.cs index 5991a7dad..ecd0ac91b 100644 --- a/src/Dapr.Extensions.Configuration/DaprSecretStoreConfigurationProvider.cs +++ b/src/Dapr.Extensions.Configuration/DaprSecretStoreConfigurationProvider.cs @@ -227,8 +227,15 @@ private async Task LoadAsync() $"A duplicate key '{key}' was found in the secret store '{store}'. Please remove any duplicates from your secret store."); } - data.Add(normalizeKey ? NormalizeKey(secretDescriptor.SecretName) : secretDescriptor.SecretName, - result[key]); + // The name of the key "as desired" by the user based on the descriptor. + // + // NOTE: This should vary only if a single secret of the same name is returned. + string desiredKey = StringComparer.Ordinal.Equals(key, secretDescriptor.SecretKey) ? secretDescriptor.SecretName : key; + + // The name of the key normalized based on the configured delimiters. + string normalizedKey = normalizeKey ? NormalizeKey(desiredKey) : desiredKey; + + data.Add(normalizedKey, result[key]); } } diff --git a/src/Dapr.Protos/Dapr.Protos.csproj b/src/Dapr.Protos/Dapr.Protos.csproj new file mode 100644 index 000000000..8a8804b22 --- /dev/null +++ b/src/Dapr.Protos/Dapr.Protos.csproj @@ -0,0 +1,22 @@ + + + + enable + enable + This package contains the reference protos used by develop services using Dapr. + + + + + + + + + + + + + + + + diff --git a/src/Dapr.Client/Protos/dapr/proto/common/v1/common.proto b/src/Dapr.Protos/Protos/dapr/proto/common/v1/common.proto similarity index 98% rename from src/Dapr.Client/Protos/dapr/proto/common/v1/common.proto rename to src/Dapr.Protos/Protos/dapr/proto/common/v1/common.proto index 1e63b885d..0eb882b89 100644 --- a/src/Dapr.Client/Protos/dapr/proto/common/v1/common.proto +++ b/src/Dapr.Protos/Protos/dapr/proto/common/v1/common.proto @@ -1,4 +1,4 @@ -/* +/* Copyright 2021 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. @@ -26,7 +26,7 @@ option go_package = "github.com/dapr/dapr/pkg/proto/common/v1;common"; // when Dapr runtime delivers HTTP content. // // For example, when callers calls http invoke api -// POST http://localhost:3500/v1.0/invoke//method/?query1=value1&query2=value2 +// `POST http://localhost:3500/v1.0/invoke//method/?query1=value1&query2=value2` // // Dapr runtime will parse POST as a verb and extract querystring to quersytring map. message HTTPExtension { @@ -157,4 +157,4 @@ message ConfigurationItem { // the metadata which will be passed to/from configuration store component. map metadata = 3; -} +} \ No newline at end of file diff --git a/src/Dapr.Client/Protos/dapr/proto/dapr/v1/appcallback.proto b/src/Dapr.Protos/Protos/dapr/proto/runtime/v1/appcallback.proto similarity index 90% rename from src/Dapr.Client/Protos/dapr/proto/dapr/v1/appcallback.proto rename to src/Dapr.Protos/Protos/dapr/proto/runtime/v1/appcallback.proto index 823c0aae4..51dee5539 100644 --- a/src/Dapr.Client/Protos/dapr/proto/dapr/v1/appcallback.proto +++ b/src/Dapr.Protos/Protos/dapr/proto/runtime/v1/appcallback.proto @@ -1,4 +1,4 @@ -/* +/* Copyright 2021 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. @@ -15,6 +15,7 @@ syntax = "proto3"; package dapr.proto.runtime.v1; +import "google/protobuf/any.proto"; import "google/protobuf/empty.proto"; import "dapr/proto/common/v1/common.proto"; import "google/protobuf/struct.proto"; @@ -57,10 +58,39 @@ service AppCallbackHealthCheck { // AppCallbackAlpha V1 is an optional extension to AppCallback V1 to opt // for Alpha RPCs. service AppCallbackAlpha { - // Subscribes bulk events from Pubsub + // Subscribes bulk events from Pubsub rpc OnBulkTopicEventAlpha1(TopicEventBulkRequest) returns (TopicEventBulkResponse) {} + + // Sends job back to the app's endpoint at trigger time. + rpc OnJobEventAlpha1 (JobEventRequest) returns (JobEventResponse); } +message JobEventRequest { + // Job name. + string name = 1; + + // Job data to be sent back to app. + google.protobuf.Any data = 2; + + // Required. method is a method name which will be invoked by caller. + string method = 3; + + // The type of data content. + // + // This field is required if data delivers http request body + // Otherwise, this is optional. + string content_type = 4; + + // HTTP specific fields if request conveys http-compatible request. + // + // This field is required for http-compatible request. Otherwise, + // this field is optional. + common.v1.HTTPExtension http_extension = 5; +} + +// JobEventResponse is the response from the app when a job is triggered. +message JobEventResponse {} + // TopicEventRequest message is compatible with CloudEvent spec v1.0 // https://github.com/cloudevents/spec/blob/v1.0/spec.md message TopicEventRequest { @@ -155,14 +185,14 @@ message TopicEventBulkRequestEntry { // content type of the event contained. string content_type = 4; - + // The metadata associated with the event. map metadata = 5; } // TopicEventBulkRequest represents request for bulk message message TopicEventBulkRequest { - // Unique identifier for the bulk request. + // Unique identifier for the bulk request. string id = 1; // The list of items inside this bulk request. @@ -173,10 +203,10 @@ message TopicEventBulkRequest { // The pubsub topic which publisher sent to. string topic = 4; - + // The name of the pubsub the publisher sent to. string pubsub_name = 5; - + // The type of event related to the originating occurrence. string type = 6; @@ -280,8 +310,8 @@ message TopicRoutes { message TopicRule { // The optional CEL expression used to match the event. - // If the match is not specified, then the route is considered - // the default. + // If the match is not specified, then the route is considered + // the default. string match = 1; // The path used to identify matches for this subscription. @@ -310,4 +340,4 @@ message ListInputBindingsResponse { // HealthCheckResponse is the message with the response to the health check. // This message is currently empty as used as placeholder. -message HealthCheckResponse {} +message HealthCheckResponse {} \ No newline at end of file diff --git a/src/Dapr.Client/Protos/dapr/proto/dapr/v1/dapr.proto b/src/Dapr.Protos/Protos/dapr/proto/runtime/v1/dapr.proto similarity index 84% rename from src/Dapr.Client/Protos/dapr/proto/dapr/v1/dapr.proto rename to src/Dapr.Protos/Protos/dapr/proto/runtime/v1/dapr.proto index 5ec1cc9d8..ecf0f76f7 100644 --- a/src/Dapr.Client/Protos/dapr/proto/dapr/v1/dapr.proto +++ b/src/Dapr.Protos/Protos/dapr/proto/runtime/v1/dapr.proto @@ -1,4 +1,4 @@ -/* +/* Copyright 2021 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. @@ -19,6 +19,7 @@ import "google/protobuf/any.proto"; import "google/protobuf/empty.proto"; import "google/protobuf/timestamp.proto"; import "dapr/proto/common/v1/common.proto"; +import "dapr/proto/runtime/v1/appcallback.proto"; option csharp_namespace = "Dapr.Client.Autogen.Grpc.v1"; option java_outer_classname = "DaprProtos"; @@ -58,6 +59,10 @@ service Dapr { // Bulk Publishes multiple events to the specified topic. rpc BulkPublishEventAlpha1(BulkPublishRequest) returns (BulkPublishResponse) {} + // SubscribeTopicEventsAlpha1 subscribes to a PubSub topic and receives topic + // events from it. + rpc SubscribeTopicEventsAlpha1(stream SubscribeTopicEventsRequestAlpha1) returns (stream SubscribeTopicEventsResponseAlpha1) {} + // Invokes binding data to specific output bindings rpc InvokeBinding(InvokeBindingRequest) returns (InvokeBindingResponse) {} @@ -188,6 +193,15 @@ service Dapr { rpc RaiseEventWorkflowBeta1 (RaiseEventWorkflowRequest) returns (google.protobuf.Empty) {} // Shutdown the sidecar rpc Shutdown (ShutdownRequest) returns (google.protobuf.Empty) {} + + // Create and schedule a job + rpc ScheduleJobAlpha1(ScheduleJobRequest) returns (ScheduleJobResponse) {} + + // Gets a scheduled job + rpc GetJobAlpha1(GetJobRequest) returns (GetJobResponse) {} + + // Delete a job + rpc DeleteJobAlpha1(DeleteJobRequest) returns (DeleteJobResponse) {} } // InvokeServiceRequest represents the request message for Service invocation. @@ -411,6 +425,62 @@ message BulkPublishResponseFailedEntry { string error = 2; } +// SubscribeTopicEventsRequestAlpha1 is a message containing the details for +// subscribing to a topic via streaming. +// The first message must always be the initial request. All subsequent +// messages must be event processed responses. +message SubscribeTopicEventsRequestAlpha1 { + oneof subscribe_topic_events_request_type { + SubscribeTopicEventsRequestInitialAlpha1 initial_request = 1; + SubscribeTopicEventsRequestProcessedAlpha1 event_processed = 2; + } +} + +// SubscribeTopicEventsRequestInitialAlpha1 is the initial message containing +// the details for subscribing to a topic via streaming. +message SubscribeTopicEventsRequestInitialAlpha1 { + // The name of the pubsub component + string pubsub_name = 1; + + // The pubsub topic + string topic = 2; + + // The metadata passing to pub components + // + // metadata property: + // - key : the key of the message. + map metadata = 3; + + // dead_letter_topic is the topic to which messages that fail to be processed + // are sent. + optional string dead_letter_topic = 4; +} + +// SubscribeTopicEventsRequestProcessedAlpha1 is the message containing the +// subscription to a topic. +message SubscribeTopicEventsRequestProcessedAlpha1 { + // id is the unique identifier for the subscription request. + string id = 1; + + // status is the result of the subscription request. + TopicEventResponse status = 2; +} + + +// SubscribeTopicEventsResponseAlpha1 is a message returned from daprd +// when subscribing to a topic via streaming. +message SubscribeTopicEventsResponseAlpha1 { + oneof subscribe_topic_events_response_type { + SubscribeTopicEventsResponseInitialAlpha1 initial_response = 1; + TopicEventRequest event_message = 2; + } +} + +// SubscribeTopicEventsResponseInitialAlpha1 is the initial response from daprd +// when subscribing to a topic. +message SubscribeTopicEventsResponseInitialAlpha1 {} + + // InvokeBindingRequest is the message to send data to output bindings message InvokeBindingRequest { // The name of the output binding to invoke. @@ -423,6 +493,7 @@ message InvokeBindingRequest { // // Common metadata property: // - ttlInSeconds : the time to live in seconds for the message. + // // If set in the binding definition will cause all messages to // have a default time to live. The message ttl overrides any value // in the binding definition. @@ -504,45 +575,45 @@ message ExecuteStateTransactionRequest { // RegisterActorTimerRequest is the message to register a timer for an actor of a given type and id. message RegisterActorTimerRequest { - string actor_type = 1; - string actor_id = 2; + string actor_type = 1 [json_name = "actorType"]; + string actor_id = 2 [json_name = "actorId"]; string name = 3; - string due_time = 4; + string due_time = 4 [json_name = "dueTime"]; string period = 5; string callback = 6; - bytes data = 7; + bytes data = 7; string ttl = 8; } // UnregisterActorTimerRequest is the message to unregister an actor timer message UnregisterActorTimerRequest { - string actor_type = 1; - string actor_id = 2; + string actor_type = 1 [json_name = "actorType"]; + string actor_id = 2 [json_name = "actorId"]; string name = 3; } // RegisterActorReminderRequest is the message to register a reminder for an actor of a given type and id. message RegisterActorReminderRequest { - string actor_type = 1; - string actor_id = 2; + string actor_type = 1 [json_name = "actorType"]; + string actor_id = 2 [json_name = "actorId"]; string name = 3; - string due_time = 4; + string due_time = 4 [json_name = "dueTime"]; string period = 5; - bytes data = 6; + bytes data = 6; string ttl = 7; } // UnregisterActorReminderRequest is the message to unregister an actor reminder. message UnregisterActorReminderRequest { - string actor_type = 1; - string actor_id = 2; + string actor_type = 1 [json_name = "actorType"]; + string actor_id = 2 [json_name = "actorId"]; string name = 3; } // GetActorStateRequest is the message to get key-value states from specific actor. message GetActorStateRequest { - string actor_type = 1; - string actor_id = 2; + string actor_type = 1 [json_name = "actorType"]; + string actor_id = 2 [json_name = "actorId"]; string key = 3; } @@ -556,8 +627,8 @@ message GetActorStateResponse { // ExecuteActorStateTransactionRequest is the message to execute multiple operations on a specified actor. message ExecuteActorStateTransactionRequest { - string actor_type = 1; - string actor_id = 2; + string actor_type = 1 [json_name = "actorType"]; + string actor_id = 2 [json_name = "actorId"]; repeated TransactionalActorStateOperation operations = 3; } @@ -575,8 +646,8 @@ message TransactionalActorStateOperation { // InvokeActorRequest is the message to call an actor. message InvokeActorRequest { - string actor_type = 1; - string actor_id = 2; + string actor_type = 1 [json_name = "actorType"]; + string actor_id = 2 [json_name = "actorId"]; string method = 3; bytes data = 4; map metadata = 5; @@ -605,6 +676,7 @@ message GetMetadataResponse { string runtime_version = 8 [json_name = "runtimeVersion"]; repeated string enabled_features = 9 [json_name = "enabledFeatures"]; ActorRuntime actor_runtime = 10 [json_name = "actorRuntime"]; + //TODO: Cassie: probably add scheduler runtime status } message ActorRuntime { @@ -665,6 +737,19 @@ message PubsubSubscription { map metadata = 3 [json_name = "metadata"]; PubsubSubscriptionRules rules = 4 [json_name = "rules"]; string dead_letter_topic = 5 [json_name = "deadLetterTopic"]; + PubsubSubscriptionType type = 6 [json_name = "type"]; +} + +// PubsubSubscriptionType indicates the type of subscription +enum PubsubSubscriptionType { + // UNKNOWN is the default value for the subscription type. + UNKNOWN = 0; + // Declarative subscription (k8s CRD) + DECLARATIVE = 1; + // Programmatically created subscription + PROGRAMMATIC = 2; + // Bidirectional Streaming subscription + STREAMING = 3; } message PubsubSubscriptionRules { @@ -755,11 +840,11 @@ message TryLockRequest { // // The reason why we don't make it automatically generated is: // 1. If it is automatically generated,there must be a 'my_lock_owner_id' field in the response. - // This name is so weird that we think it is inappropriate to put it into the api spec + // This name is so weird that we think it is inappropriate to put it into the api spec // 2. If we change the field 'my_lock_owner_id' in the response to 'lock_owner',which means the current lock owner of this lock, - // we find that in some lock services users can't get the current lock owner.Actually users don't need it at all. + // we find that in some lock services users can't get the current lock owner.Actually users don't need it at all. // 3. When reentrant lock is needed,the existing lock_owner is required to identify client and check "whether this client can reenter this lock". - // So this field in the request shouldn't be removed. + // So this field in the request shouldn't be removed. string lock_owner = 3 [json_name = "lockOwner"]; // Required. The time before expiry.The time unit is second. @@ -796,7 +881,7 @@ message SubtleGetKeyRequest { // JSON (JSON Web Key) as string JSON = 1; } - + // Name of the component string component_name = 1 [json_name="componentName"]; // Name (or name/version) of the key to use in the key vault @@ -978,7 +1063,7 @@ message EncryptRequestOptions { // If true, the encrypted document does not contain a key reference. // In that case, calls to the Decrypt method must provide a key reference (name or name/version). // Defaults to false. - bool omit_decryption_key_name = 11 [json_name="omitDecryptionKeyName"]; + bool omit_decryption_key_name = 11 [json_name="omitDecryptionKeyName"]; // Key reference to embed in the encrypted document (name or name/version). // This is helpful if the reference of the key used to decrypt the document is different from the one used to encrypt it. // If unset, uses the reference of the key used to encrypt the document (this is the default behavior). @@ -1108,3 +1193,85 @@ message PurgeWorkflowRequest { message ShutdownRequest { // Empty } + +// Job is the definition of a job. At least one of schedule or due_time must be +// provided but can also be provided together. +message Job { + // The unique name for the job. + string name = 1 [json_name = "name"]; + + // schedule is an optional schedule at which the job is to be run. + // Accepts both systemd timer style cron expressions, as well as human + // readable '@' prefixed period strings as defined below. + // + // Systemd timer style cron accepts 6 fields: + // seconds | minutes | hours | day of month | month | day of week + // 0-59 | 0-59 | 0-23 | 1-31 | 1-12/jan-dec | 0-7/sun-sat + // + // "0 30 * * * *" - every hour on the half hour + // "0 15 3 * * *" - every day at 03:15 + // + // Period string expressions: + // Entry | Description | Equivalent To + // ----- | ----------- | ------------- + // @every `` | Run every `` (e.g. '@every 1h30m') | N/A + // @yearly (or @annually) | Run once a year, midnight, Jan. 1st | 0 0 0 1 1 * + // @monthly | Run once a month, midnight, first of month | 0 0 0 1 * * + // @weekly | Run once a week, midnight on Sunday | 0 0 0 * * 0 + // @daily (or @midnight) | Run once a day, midnight | 0 0 0 * * * + // @hourly | Run once an hour, beginning of hour | 0 0 * * * * + optional string schedule = 2 [json_name = "schedule"]; + + // repeats is the optional number of times in which the job should be + // triggered. If not set, the job will run indefinitely or until expiration. + optional uint32 repeats = 3 [json_name = "repeats"]; + + // due_time is the optional time at which the job should be active, or the + // "one shot" time if other scheduling type fields are not provided. Accepts + // a "point in time" string in the format of RFC3339, Go duration string + // (calculated from job creation time), or non-repeating ISO8601. + optional string due_time = 4 [json_name = "dueTime"]; + + // ttl is the optional time to live or expiration of the job. Accepts a + // "point in time" string in the format of RFC3339, Go duration string + // (calculated from job creation time), or non-repeating ISO8601. + optional string ttl = 5 [json_name = "ttl"]; + + // payload is the serialized job payload that will be sent to the recipient + // when the job is triggered. + google.protobuf.Any data = 6 [json_name = "data"]; +} + +// ScheduleJobRequest is the message to create/schedule the job. +message ScheduleJobRequest { + // The job details. + Job job = 1; +} + +// ScheduleJobResponse is the message response to create/schedule the job. +message ScheduleJobResponse { + // Empty +} + +// GetJobRequest is the message to retrieve a job. +message GetJobRequest { + // The name of the job. + string name = 1; +} + +// GetJobResponse is the message's response for a job retrieved. +message GetJobResponse { + // The job details. + Job job = 1; +} + +// DeleteJobRequest is the message to delete the job by name. +message DeleteJobRequest { + // The name of the job. + string name = 1; +} + +// DeleteJobResponse is the message response to delete the job by name. +message DeleteJobResponse { + // Empty +} \ No newline at end of file diff --git a/src/Dapr.Workflow/Dapr.Workflow.csproj b/src/Dapr.Workflow/Dapr.Workflow.csproj index 9092b101a..992baee73 100644 --- a/src/Dapr.Workflow/Dapr.Workflow.csproj +++ b/src/Dapr.Workflow/Dapr.Workflow.csproj @@ -13,17 +13,15 @@ - - - - - - + + + + \ No newline at end of file diff --git a/src/Dapr.Workflow/DaprWorkflowClientBuilderFactory.cs b/src/Dapr.Workflow/DaprWorkflowClientBuilderFactory.cs new file mode 100644 index 000000000..7a854cf05 --- /dev/null +++ b/src/Dapr.Workflow/DaprWorkflowClientBuilderFactory.cs @@ -0,0 +1,95 @@ +// ------------------------------------------------------------------------ +// Copyright 2024 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.Net.Http; +using Grpc.Net.Client; +using Microsoft.DurableTask.Client; +using Microsoft.DurableTask.Worker; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; + +#nullable enable + +namespace Dapr.Workflow; + +/// +/// A factory for building a . +/// +internal sealed class DaprWorkflowClientBuilderFactory +{ + private readonly IConfiguration _configuration; + private readonly IHttpClientFactory _httpClientFactory; + private readonly IServiceCollection _services; + + /// + /// Constructor used to inject the required types into the factory. + /// + public DaprWorkflowClientBuilderFactory(IConfiguration configuration, IHttpClientFactory httpClientFactory, IServiceCollection services) + { + _configuration = configuration; + _httpClientFactory = httpClientFactory; + _services = services; + } + + /// + /// Responsible for building the client itself. + /// + /// + public void CreateClientBuilder(Action configure) + { + _services.AddDurableTaskClient(builder => + { + var apiToken = DaprDefaults.GetDefaultDaprApiToken(_configuration); + var grpcEndpoint = DaprDefaults.GetDefaultGrpcEndpoint(_configuration); + + var httpClient = _httpClientFactory.CreateClient(); + + if (!string.IsNullOrWhiteSpace(apiToken)) + { + httpClient.DefaultRequestHeaders.Add( "Dapr-Api-Token", apiToken); + } + + builder.UseGrpc(GrpcChannel.ForAddress(grpcEndpoint, new GrpcChannelOptions { HttpClient = httpClient })); + builder.RegisterDirectly(); + }); + + _services.AddDurableTaskWorker(builder => + { + WorkflowRuntimeOptions options = new(); + configure?.Invoke(options); + + var apiToken = DaprDefaults.GetDefaultDaprApiToken(_configuration); + var grpcEndpoint = DaprDefaults.GetDefaultGrpcEndpoint(_configuration); + + if (!string.IsNullOrEmpty(grpcEndpoint)) + { + var httpClient = _httpClientFactory.CreateClient(); + + if (!string.IsNullOrWhiteSpace(apiToken)) + { + httpClient.DefaultRequestHeaders.Add("Dapr-Api-Token", apiToken); + } + + builder.UseGrpc( + GrpcChannel.ForAddress(grpcEndpoint, new GrpcChannelOptions { HttpClient = httpClient })); + } + else + { + builder.UseGrpc(); + } + + builder.AddTasks(registry => options.AddWorkflowsAndActivitiesToRegistry(registry)); + }); + } +} diff --git a/src/Dapr.Client/WorkflowRuntimeStatus.cs b/src/Dapr.Workflow/WorkflowRuntimeStatus.cs similarity index 98% rename from src/Dapr.Client/WorkflowRuntimeStatus.cs rename to src/Dapr.Workflow/WorkflowRuntimeStatus.cs index dc652630e..24024cd63 100644 --- a/src/Dapr.Client/WorkflowRuntimeStatus.cs +++ b/src/Dapr.Workflow/WorkflowRuntimeStatus.cs @@ -11,7 +11,7 @@ // limitations under the License. // ------------------------------------------------------------------------ -namespace Dapr.Client +namespace Dapr.Workflow { /// /// Enum describing the runtime status of a workflow. diff --git a/src/Dapr.Workflow/WorkflowServiceCollectionExtensions.cs b/src/Dapr.Workflow/WorkflowServiceCollectionExtensions.cs index ca514f221..3c19583aa 100644 --- a/src/Dapr.Workflow/WorkflowServiceCollectionExtensions.cs +++ b/src/Dapr.Workflow/WorkflowServiceCollectionExtensions.cs @@ -14,20 +14,14 @@ namespace Dapr.Workflow { using System; - using Grpc.Net.Client; - using Microsoft.DurableTask.Client; - using Microsoft.DurableTask.Worker; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.DependencyInjection.Extensions; - using System.Net.Http; - using Dapr; /// /// Contains extension methods for using Dapr Workflow with dependency injection. /// public static class WorkflowServiceCollectionExtensions { - /// /// Adds Dapr Workflow support to the service collection. /// @@ -43,6 +37,7 @@ public static IServiceCollection AddDaprWorkflow( } serviceCollection.TryAddSingleton(); + serviceCollection.AddHttpClient(); #pragma warning disable CS0618 // Type or member is obsolete - keeping around temporarily - replaced by DaprWorkflowClient serviceCollection.TryAddSingleton(); @@ -50,135 +45,17 @@ public static IServiceCollection AddDaprWorkflow( serviceCollection.AddHostedService(); serviceCollection.TryAddSingleton(); serviceCollection.AddDaprClient(); - serviceCollection.AddDaprWorkflowClient(); - + serviceCollection.AddOptions().Configure(configure); - serviceCollection.AddDurableTaskWorker(builder => + serviceCollection.AddSingleton(c => { - WorkflowRuntimeOptions options = new(); - configure?.Invoke(options); - - if (TryGetGrpcAddress(out string address)) - { - var daprApiToken = DaprDefaults.GetDefaultDaprApiToken(); - if (!string.IsNullOrEmpty(daprApiToken)) - { - var client = new HttpClient(); - client.DefaultRequestHeaders.Add("Dapr-Api-Token", daprApiToken); - builder.UseGrpc(CreateChannel(address, client)); - } - else - { - builder.UseGrpc(address); - } - - } - else - { - builder.UseGrpc(); - } - - builder.AddTasks(registry => options.AddWorkflowsAndActivitiesToRegistry(registry)); + var factory = c.GetRequiredService(); + factory.CreateClientBuilder(configure); + return new object(); //Placeholder as actual registration is performed inside factory }); return serviceCollection; } - - /// - /// Adds Dapr Workflow client support to the service collection. - /// - /// - /// Use this extension method if you want to use in your app - /// but don't wish to define any workflows or activities. - /// - /// The . - public static IServiceCollection AddDaprWorkflowClient(this IServiceCollection services) - { - services.TryAddSingleton(); - services.AddDurableTaskClient(builder => - { - if (TryGetGrpcAddress(out string address)) - { - var daprApiToken = DaprDefaults.GetDefaultDaprApiToken(); - if (!string.IsNullOrEmpty(daprApiToken)) - { - var client = new HttpClient(); - client.DefaultRequestHeaders.Add("Dapr-Api-Token", daprApiToken); - builder.UseGrpc(CreateChannel(address, client)); - } - else - { - builder.UseGrpc(address); - } - - } - else - { - builder.UseGrpc(); - } - - builder.RegisterDirectly(); - }); - - return services; - } - - static bool TryGetGrpcAddress(out string address) - { - // TODO: Ideally we should be using DaprDefaults.cs for this. However, there are two blockers: - // 1. DaprDefaults.cs uses 127.0.0.1 instead of localhost, which prevents testing with Dapr on WSL2 and the app on Windows - // 2. DaprDefaults.cs doesn't compile when the project has C# nullable reference types enabled. - // If the above issues are fixed (ensuring we don't regress anything) we should switch to using the logic in DaprDefaults.cs. - var daprEndpoint = DaprDefaults.GetDefaultGrpcEndpoint(); - if (!String.IsNullOrEmpty(daprEndpoint)) { - address = daprEndpoint; - return true; - } - - var daprPortStr = Environment.GetEnvironmentVariable("DAPR_GRPC_PORT"); - if (int.TryParse(daprPortStr, out int daprGrpcPort)) - { - // There is a bug in the Durable Task SDK that requires us to change the format of the address - // depending on the version of .NET that we're targeting. For now, we work around this manually. -#if NET6_0_OR_GREATER - address = $"http://localhost:{daprGrpcPort}"; -#else - address = $"localhost:{daprGrpcPort}"; -#endif - return true; - } - - address = string.Empty; - return false; - } - - static GrpcChannel CreateChannel(string address, HttpClient client) - { - - GrpcChannelOptions options = new() { HttpClient = client}; - var daprEndpoint = DaprDefaults.GetDefaultGrpcEndpoint(); - if (!String.IsNullOrEmpty(daprEndpoint)) { - return GrpcChannel.ForAddress(daprEndpoint, options); - } - - var daprPortStr = Environment.GetEnvironmentVariable("DAPR_GRPC_PORT"); - if (int.TryParse(daprPortStr, out int daprGrpcPort)) - { - // If there is no address passed in, we default to localhost - if (String.IsNullOrEmpty(address)) - { - // There is a bug in the Durable Task SDK that requires us to change the format of the address - // depending on the version of .NET that we're targeting. For now, we work around this manually. - #if NET6_0_OR_GREATER - address = $"http://localhost:{daprGrpcPort}"; - #else - address = $"localhost:{daprGrpcPort}"; - #endif - } - - } - return GrpcChannel.ForAddress(address, options); - } } } diff --git a/src/Dapr.Workflow/WorkflowState.cs b/src/Dapr.Workflow/WorkflowState.cs index aefe4be6b..ea1ffae22 100644 --- a/src/Dapr.Workflow/WorkflowState.cs +++ b/src/Dapr.Workflow/WorkflowState.cs @@ -84,6 +84,8 @@ public WorkflowRuntimeStatus RuntimeStatus return WorkflowRuntimeStatus.Terminated; case OrchestrationRuntimeStatus.Pending: return WorkflowRuntimeStatus.Pending; + case OrchestrationRuntimeStatus.Suspended: + return WorkflowRuntimeStatus.Suspended; default: return WorkflowRuntimeStatus.Unknown; } diff --git a/src/Directory.Build.props b/src/Directory.Build.props index 2794f1b1f..35f0fbf7c 100644 --- a/src/Directory.Build.props +++ b/src/Directory.Build.props @@ -15,7 +15,7 @@ true - + diff --git a/src/Shared/DaprDefaults.cs b/src/Shared/DaprDefaults.cs deleted file mode 100644 index b738de921..000000000 --- a/src/Shared/DaprDefaults.cs +++ /dev/null @@ -1,103 +0,0 @@ -// ------------------------------------------------------------------------ -// Copyright 2021 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; - -namespace Dapr -{ - internal static class DaprDefaults - { - private static string httpEndpoint = string.Empty; - private static string grpcEndpoint = string.Empty; - private static string daprApiToken = string.Empty; - private static string appApiToken = string.Empty; - - /// - /// Get the value of environment variable DAPR_API_TOKEN - /// - /// The value of environment variable DAPR_API_TOKEN - public static string GetDefaultDaprApiToken() - { - // Lazy-init is safe because this is just populating the default - // We don't plan to support the case where the user changes environment variables - // for a running process. - if (string.IsNullOrEmpty(daprApiToken)) - { - // Treat empty the same as null since it's an environment variable - var value = Environment.GetEnvironmentVariable("DAPR_API_TOKEN"); - daprApiToken = string.IsNullOrEmpty(value) ? string.Empty : value; - } - - return daprApiToken; - } - - /// - /// Get the value of environment variable APP_API_TOKEN - /// - /// The value of environment variable APP_API_TOKEN - public static string GetDefaultAppApiToken() - { - if (string.IsNullOrEmpty(appApiToken)) - { - var value = Environment.GetEnvironmentVariable("APP_API_TOKEN"); - appApiToken = string.IsNullOrEmpty(value) ? string.Empty : value; - } - - return appApiToken; - } - - /// - /// Get the value of HTTP endpoint based off environment variables - /// - /// The value of HTTP endpoint based off environment variables - public static string GetDefaultHttpEndpoint() - { - if (string.IsNullOrEmpty(httpEndpoint)) - { - var endpoint = Environment.GetEnvironmentVariable("DAPR_HTTP_ENDPOINT"); - if (!string.IsNullOrEmpty(endpoint)) { - httpEndpoint = endpoint; - return httpEndpoint; - } - - var port = Environment.GetEnvironmentVariable("DAPR_HTTP_PORT"); - port = string.IsNullOrEmpty(port) ? "3500" : port; - httpEndpoint = $"http://127.0.0.1:{port}"; - } - - return httpEndpoint; - } - - /// - /// Get the value of gRPC endpoint based off environment variables - /// - /// The value of gRPC endpoint based off environment variables - public static string GetDefaultGrpcEndpoint() - { - if (string.IsNullOrEmpty(grpcEndpoint)) - { - var endpoint = Environment.GetEnvironmentVariable("DAPR_GRPC_ENDPOINT"); - if (!string.IsNullOrEmpty(endpoint)) { - grpcEndpoint = endpoint; - return grpcEndpoint; - } - - var port = Environment.GetEnvironmentVariable("DAPR_GRPC_PORT"); - port = string.IsNullOrEmpty(port) ? "50001" : port; - grpcEndpoint = $"http://127.0.0.1:{port}"; - } - - return grpcEndpoint; - } - } -} diff --git a/test/Dapr.Actors.AspNetCore.IntegrationTest/Dapr.Actors.AspNetCore.IntegrationTest.csproj b/test/Dapr.Actors.AspNetCore.IntegrationTest/Dapr.Actors.AspNetCore.IntegrationTest.csproj index deccfc1e6..921e2dda4 100644 --- a/test/Dapr.Actors.AspNetCore.IntegrationTest/Dapr.Actors.AspNetCore.IntegrationTest.csproj +++ b/test/Dapr.Actors.AspNetCore.IntegrationTest/Dapr.Actors.AspNetCore.IntegrationTest.csproj @@ -1,14 +1,14 @@ - + runtime; build; native; contentfiles; analyzers; buildtransitive all - - - - - + + + + + all runtime; build; native; contentfiles; analyzers; buildtransitive diff --git a/test/Dapr.Actors.AspNetCore.IntegrationTest/HostingTests.cs b/test/Dapr.Actors.AspNetCore.IntegrationTest/HostingTests.cs index 7f96c66e6..bf3757ce1 100644 --- a/test/Dapr.Actors.AspNetCore.IntegrationTest/HostingTests.cs +++ b/test/Dapr.Actors.AspNetCore.IntegrationTest/HostingTests.cs @@ -42,7 +42,7 @@ public void MapActorsHandlers_WithoutAddActors_Throws() // NOTE: in 3.1 TestServer.CreateClient triggers the failure, in 5.0 it's Host.Start using var host = CreateHost(); var server = host.GetTestServer(); - var client = server.CreateClient(); + server.CreateClient(); }); Assert.Equal( diff --git a/test/Dapr.Actors.AspNetCore.Test/Dapr.Actors.AspNetCore.Test.csproj b/test/Dapr.Actors.AspNetCore.Test/Dapr.Actors.AspNetCore.Test.csproj index 83ea494a8..c448e915c 100644 --- a/test/Dapr.Actors.AspNetCore.Test/Dapr.Actors.AspNetCore.Test.csproj +++ b/test/Dapr.Actors.AspNetCore.Test/Dapr.Actors.AspNetCore.Test.csproj @@ -5,16 +5,16 @@ - + runtime; build; native; contentfiles; analyzers; buildtransitive all - - - - - - + + + + + + all runtime; build; native; contentfiles; analyzers; buildtransitive diff --git a/test/Dapr.Actors.Generators.Test/CSharpSourceGeneratorVerifier.cs b/test/Dapr.Actors.Generators.Test/CSharpSourceGeneratorVerifier.cs index 435488c2c..2b1046e1a 100644 --- a/test/Dapr.Actors.Generators.Test/CSharpSourceGeneratorVerifier.cs +++ b/test/Dapr.Actors.Generators.Test/CSharpSourceGeneratorVerifier.cs @@ -24,7 +24,9 @@ internal static class CSharpSourceGeneratorVerifier where TSourceGenerator : ISourceGenerator, new() { +#pragma warning disable CS0618 // Type or member is obsolete public class Test : CSharpSourceGeneratorTest +#pragma warning restore CS0618 // Type or member is obsolete { public Test() { @@ -78,4 +80,4 @@ protected override ParseOptions CreateParseOptions() return ((CSharpParseOptions)base.CreateParseOptions()).WithLanguageVersion(LanguageVersion); } } -} \ No newline at end of file +} diff --git a/test/Dapr.Actors.Generators.Test/Dapr.Actors.Generators.Test.csproj b/test/Dapr.Actors.Generators.Test/Dapr.Actors.Generators.Test.csproj index 212faed2d..80a79cafe 100644 --- a/test/Dapr.Actors.Generators.Test/Dapr.Actors.Generators.Test.csproj +++ b/test/Dapr.Actors.Generators.Test/Dapr.Actors.Generators.Test.csproj @@ -13,20 +13,21 @@ - - - - - - - + + + + + + + runtime; build; native; contentfiles; analyzers; buildtransitive all - + runtime; build; native; contentfiles; analyzers; buildtransitive all + diff --git a/test/Dapr.Actors.Test/ActorCodeBuilderTests.cs b/test/Dapr.Actors.Test/ActorCodeBuilderTests.cs index 9a84f4f62..2f2bb67db 100644 --- a/test/Dapr.Actors.Test/ActorCodeBuilderTests.cs +++ b/test/Dapr.Actors.Test/ActorCodeBuilderTests.cs @@ -31,7 +31,7 @@ public class ActorCodeBuilderTests [Fact] public void TestBuildActorProxyGenerator() { - ActorProxyGenerator proxyGenerator = ActorCodeBuilder.GetOrCreateProxyGenerator(typeof(ITestActor)); + ActorCodeBuilder.GetOrCreateProxyGenerator(typeof(ITestActor)); } [Fact] diff --git a/test/Dapr.Actors.Test/ActorIdTests.cs b/test/Dapr.Actors.Test/ActorIdTests.cs index 668bd0fbf..c54f4c351 100644 --- a/test/Dapr.Actors.Test/ActorIdTests.cs +++ b/test/Dapr.Actors.Test/ActorIdTests.cs @@ -115,10 +115,7 @@ public class ActorIdTests [InlineData(" ")] public void Initialize_New_ActorId_Object_With_Null_Or_Whitespace_Id(string id) { - Assert.Throws(() => - { - ActorId actorId = new ActorId(id); - }); + Assert.Throws(() => new ActorId(id)); } [Theory] diff --git a/test/Dapr.Actors.Test/ActorReferenceTests.cs b/test/Dapr.Actors.Test/ActorReferenceTests.cs new file mode 100644 index 000000000..7450f616c --- /dev/null +++ b/test/Dapr.Actors.Test/ActorReferenceTests.cs @@ -0,0 +1,93 @@ +using System; +using System.Threading.Tasks; +using Dapr.Actors.Client; +using Dapr.Actors.Runtime; +using Dapr.Actors.Test; +using Xunit; + +namespace Dapr.Actors +{ + public class ActorReferenceTests + { + [Fact] + public void Get_WhenActorIsNull_ReturnsNull() + { + // Arrange + object actor = null; + + // Act + var result = ActorReference.Get(actor); + + // Assert + Assert.Null(result); + } + + [Fact] + public void Get_FromActorProxy_ReturnsActorReference() + { + // Arrange + var expectedActorId = new ActorId("abc"); + var expectedActorType = "TestActor"; + var proxy = ActorProxy.Create(expectedActorId, typeof(ITestActor), expectedActorType); + + // Act + var actorReference = ActorReference.Get(proxy); + + // Assert + Assert.NotNull(actorReference); + Assert.Equal(expectedActorId, actorReference.ActorId); + Assert.Equal(expectedActorType, actorReference.ActorType); + } + + [Fact] + public async Task Get_FromActorImplementation_ReturnsActorReference() + { + // Arrange + var expectedActorId = new ActorId("abc"); + var expectedActorType = nameof(ActorReferenceTestActor); + var host = ActorHost.CreateForTest(new ActorTestOptions() { ActorId = expectedActorId }); + var actor = new ActorReferenceTestActor(host); + + // Act + var actorReference = await actor.GetActorReference(); + + // Assert + Assert.NotNull(actorReference); + Assert.Equal(expectedActorId, actorReference.ActorId); + Assert.Equal(expectedActorType, actorReference.ActorType); + } + + [Fact] + public void Get_WithInvalidObjectType_ThrowArgumentOutOfRangeException() + { + // Arrange + var actor = new object(); + + // Act + var act = () => ActorReference.Get(actor); + + // Assert + var exception = Assert.Throws(act); + Assert.Equal("actor", exception.ParamName); + Assert.Equal("Invalid actor object type. (Parameter 'actor')", exception.Message); + } + } + + public interface IActorReferenceTestActor : IActor + { + Task GetActorReference(); + } + + public class ActorReferenceTestActor : Actor, IActorReferenceTestActor + { + public ActorReferenceTestActor(ActorHost host) + : base(host) + { + } + + public Task GetActorReference() + { + return Task.FromResult(ActorReference.Get(this)); + } + } +} diff --git a/test/Dapr.Actors.Test/Dapr.Actors.Test.csproj b/test/Dapr.Actors.Test/Dapr.Actors.Test.csproj index 8852dd465..9ef26cd13 100644 --- a/test/Dapr.Actors.Test/Dapr.Actors.Test.csproj +++ b/test/Dapr.Actors.Test/Dapr.Actors.Test.csproj @@ -5,16 +5,16 @@ - + runtime; build; native; contentfiles; analyzers; buildtransitive all - - - - - - + + + + + + all runtime; build; native; contentfiles; analyzers; buildtransitive diff --git a/test/Dapr.Actors.Test/Runtime/ActorManagerTests.cs b/test/Dapr.Actors.Test/Runtime/ActorManagerTests.cs index 6b92c7e18..b27e9afe3 100644 --- a/test/Dapr.Actors.Test/Runtime/ActorManagerTests.cs +++ b/test/Dapr.Actors.Test/Runtime/ActorManagerTests.cs @@ -164,7 +164,7 @@ public async Task DeactivateActorAsync_ExceptionDuringDeactivation_ActorIsRemove var id = ActorId.CreateRandom(); await manager.ActivateActorAsync(id); - Assert.True(manager.TryGetActorAsync(id, out var actor)); + Assert.True(manager.TryGetActorAsync(id, out _)); await Assert.ThrowsAsync(async () => { diff --git a/test/Dapr.Actors.Test/Runtime/ActorRuntimeOptionsTests.cs b/test/Dapr.Actors.Test/Runtime/ActorRuntimeOptionsTests.cs index 4fe991970..b68eda5ce 100644 --- a/test/Dapr.Actors.Test/Runtime/ActorRuntimeOptionsTests.cs +++ b/test/Dapr.Actors.Test/Runtime/ActorRuntimeOptionsTests.cs @@ -26,8 +26,6 @@ public void TestRegisterActor_SavesActivator() { var actorType = typeof(TestActor); var actorTypeInformation = ActorTypeInformation.Get(actorType, actorTypeName: null); - var host = ActorHost.CreateForTest(); - var actor = new TestActor(host); var activator = Mock.Of(); diff --git a/test/Dapr.Actors.Test/Serialization/ActorIdJsonConverterTest.cs b/test/Dapr.Actors.Test/Serialization/ActorIdJsonConverterTest.cs index fe33eca54..9c4f22193 100644 --- a/test/Dapr.Actors.Test/Serialization/ActorIdJsonConverterTest.cs +++ b/test/Dapr.Actors.Test/Serialization/ActorIdJsonConverterTest.cs @@ -50,9 +50,9 @@ public void CanDeserializeActorId() { var id = ActorId.CreateRandom().GetId(); var document = $@" -{{ - ""actor"": ""{id}"" -}}"; + {{ + ""actor"": ""{id}"" + }}"; var deserialized = JsonSerializer.Deserialize(document); @@ -62,11 +62,10 @@ public void CanDeserializeActorId() [Fact] public void CanDeserializeNullActorId() { - var id = ActorId.CreateRandom().GetId(); - var document = $@" -{{ - ""actor"": null -}}"; + const string document = @" + { + ""actor"": null + }"; var deserialized = JsonSerializer.Deserialize(document); diff --git a/test/Dapr.AspNetCore.IntegrationTest/ControllerIntegrationTest.cs b/test/Dapr.AspNetCore.IntegrationTest/ControllerIntegrationTest.cs index 364cd8448..7735ec4fb 100644 --- a/test/Dapr.AspNetCore.IntegrationTest/ControllerIntegrationTest.cs +++ b/test/Dapr.AspNetCore.IntegrationTest/ControllerIntegrationTest.cs @@ -67,7 +67,6 @@ public async Task ModelBinder_GetFromStateEntryWithKeyNotInStateStore_ReturnsNul using (var factory = new AppWebApplicationFactory()) { var httpClient = factory.CreateClient(new Microsoft.AspNetCore.Mvc.Testing.WebApplicationFactoryClientOptions { HandleCookies = false }); - var daprClient = factory.DaprClient; var request = new HttpRequestMessage(HttpMethod.Get, "http://localhost/controllerwithoutstateentry/test"); var response = await httpClient.SendAsync(request); @@ -142,7 +141,6 @@ public async Task ModelBinder_GetFromStateEntryWithStateEntry_WithKeyNotInStateS using (var factory = new AppWebApplicationFactory()) { var httpClient = factory.CreateClient(new Microsoft.AspNetCore.Mvc.Testing.WebApplicationFactoryClientOptions { HandleCookies = false }); - var daprClient = factory.DaprClient; var request = new HttpRequestMessage(HttpMethod.Get, "http://localhost/controllerwithstateentry/test"); var response = await httpClient.SendAsync(request); @@ -159,7 +157,6 @@ public async Task ModelBinder_CanGetOutOfTheWayWhenTheresNoBinding() using (var factory = new AppWebApplicationFactory()) { var httpClient = factory.CreateClient(new Microsoft.AspNetCore.Mvc.Testing.WebApplicationFactoryClientOptions { HandleCookies = false }); - var daprClient = factory.DaprClient; var request = new HttpRequestMessage(HttpMethod.Post, "http://localhost/echo-user?name=jimmy"); var response = await httpClient.SendAsync(request); diff --git a/test/Dapr.AspNetCore.IntegrationTest/Dapr.AspNetCore.IntegrationTest.csproj b/test/Dapr.AspNetCore.IntegrationTest/Dapr.AspNetCore.IntegrationTest.csproj index 3cd79d908..d51dc70e8 100644 --- a/test/Dapr.AspNetCore.IntegrationTest/Dapr.AspNetCore.IntegrationTest.csproj +++ b/test/Dapr.AspNetCore.IntegrationTest/Dapr.AspNetCore.IntegrationTest.csproj @@ -1,22 +1,24 @@  - - - runtime; build; native; contentfiles; analyzers; buildtransitive - all - - - - - - - all - runtime; build; native; contentfiles; analyzers; buildtransitive - - - + + + runtime; build; native; contentfiles; analyzers; buildtransitive + all + + + + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + + diff --git a/test/Dapr.AspNetCore.Test/CloudEventsMiddlewareTest.cs b/test/Dapr.AspNetCore.Test/CloudEventsMiddlewareTest.cs index 881e2f3da..9c1f1e005 100644 --- a/test/Dapr.AspNetCore.Test/CloudEventsMiddlewareTest.cs +++ b/test/Dapr.AspNetCore.Test/CloudEventsMiddlewareTest.cs @@ -83,7 +83,151 @@ public async Task InvokeAsync_ReplacesBodyJson(string dataContentType, string ch await pipeline.Invoke(context); } + + [Theory] + [InlineData(null, null)] // assumes application/json + utf8 + [InlineData("application/json", null)] // assumes utf8 + [InlineData("application/json", "utf-8")] + [InlineData("application/json", "UTF-8")] + [InlineData("application/person+json", "UTF-16")] // arbitrary content type and charset + public async Task InvokeAsync_ReplacesPascalCasedBodyJson(string dataContentType, string charSet) + { + var encoding = charSet == null ? null : Encoding.GetEncoding(charSet); + var app = new ApplicationBuilder(null); + app.UseCloudEvents(); + // Do verification in the scope of the middleware + app.Run(httpContext => + { + httpContext.Request.ContentType.Should().Be(dataContentType ?? "application/json"); + ReadBody(httpContext.Request.Body).Should().Be("{\"name\":\"jimmy\"}"); + return Task.CompletedTask; + }); + + var pipeline = app.Build(); + + var context = new DefaultHttpContext(); + context.Request.ContentType = charSet == null ? "application/cloudevents+json" : $"application/cloudevents+json;charset={charSet}"; + context.Request.Body = dataContentType == null ? + MakeBody("{ \"Data\": { \"name\":\"jimmy\" } }", encoding) : + MakeBody($"{{ \"DataContentType\": \"{dataContentType}\", \"Data\": {{ \"name\":\"jimmy\" }} }}", encoding); + + await pipeline.Invoke(context); + } + + [Theory] + [InlineData(null, null)] // assumes application/json + utf8 + [InlineData("application/json", null)] // assumes utf8 + [InlineData("application/json", "utf-8")] + [InlineData("application/json", "UTF-8")] + [InlineData("application/person+json", "UTF-16")] // arbitrary content type and charset + public async Task InvokeAsync_ForwardsJsonPropertiesAsHeaders(string dataContentType, string charSet) + { + var encoding = charSet == null ? null : Encoding.GetEncoding(charSet); + var app = new ApplicationBuilder(null); + app.UseCloudEvents(new CloudEventsMiddlewareOptions + { + ForwardCloudEventPropertiesAsHeaders = true + }); + + // Do verification in the scope of the middleware + app.Run(httpContext => + { + httpContext.Request.ContentType.Should().Be(dataContentType ?? "application/json"); + ReadBody(httpContext.Request.Body).Should().Be("{\"name\":\"jimmy\"}"); + + httpContext.Request.Headers.Should().ContainKey("Cloudevent.type").WhichValue.Should().BeEquivalentTo("Test.Type"); + httpContext.Request.Headers.Should().ContainKey("Cloudevent.subject").WhichValue.Should().BeEquivalentTo("Test.Subject"); + return Task.CompletedTask; + }); + + var pipeline = app.Build(); + + var context = new DefaultHttpContext(); + context.Request.ContentType = charSet == null ? "application/cloudevents+json" : $"application/cloudevents+json;charset={charSet}"; + context.Request.Body = dataContentType == null ? + MakeBody("{ \"type\": \"Test.Type\", \"subject\": \"Test.Subject\", \"data\": { \"name\":\"jimmy\" } }", encoding) : + MakeBody($"{{ \"datacontenttype\": \"{dataContentType}\", \"type\":\"Test.Type\", \"subject\": \"Test.Subject\", \"data\": {{ \"name\":\"jimmy\" }} }}", encoding); + + await pipeline.Invoke(context); + } + + [Theory] + [InlineData(null, null)] // assumes application/json + utf8 + [InlineData("application/json", null)] // assumes utf8 + [InlineData("application/json", "utf-8")] + [InlineData("application/json", "UTF-8")] + [InlineData("application/person+json", "UTF-16")] // arbitrary content type and charset + public async Task InvokeAsync_ForwardsIncludedJsonPropertiesAsHeaders(string dataContentType, string charSet) + { + var encoding = charSet == null ? null : Encoding.GetEncoding(charSet); + var app = new ApplicationBuilder(null); + app.UseCloudEvents(new CloudEventsMiddlewareOptions + { + ForwardCloudEventPropertiesAsHeaders = true, + IncludedCloudEventPropertiesAsHeaders = new []{"type"} + }); + + // Do verification in the scope of the middleware + app.Run(httpContext => + { + httpContext.Request.ContentType.Should().Be(dataContentType ?? "application/json"); + ReadBody(httpContext.Request.Body).Should().Be("{\"name\":\"jimmy\"}"); + + httpContext.Request.Headers.Should().ContainKey("Cloudevent.type").WhichValue.Should().BeEquivalentTo("Test.Type"); + httpContext.Request.Headers.Should().NotContainKey("Cloudevent.subject"); + return Task.CompletedTask; + }); + + var pipeline = app.Build(); + + var context = new DefaultHttpContext(); + context.Request.ContentType = charSet == null ? "application/cloudevents+json" : $"application/cloudevents+json;charset={charSet}"; + context.Request.Body = dataContentType == null ? + MakeBody("{ \"type\": \"Test.Type\", \"subject\": \"Test.Subject\", \"data\": { \"name\":\"jimmy\" } }", encoding) : + MakeBody($"{{ \"datacontenttype\": \"{dataContentType}\", \"type\":\"Test.Type\", \"subject\": \"Test.Subject\", \"data\": {{ \"name\":\"jimmy\" }} }}", encoding); + + await pipeline.Invoke(context); + } + + [Theory] + [InlineData(null, null)] // assumes application/json + utf8 + [InlineData("application/json", null)] // assumes utf8 + [InlineData("application/json", "utf-8")] + [InlineData("application/json", "UTF-8")] + [InlineData("application/person+json", "UTF-16")] // arbitrary content type and charset + public async Task InvokeAsync_DoesNotForwardExcludedJsonPropertiesAsHeaders(string dataContentType, string charSet) + { + var encoding = charSet == null ? null : Encoding.GetEncoding(charSet); + var app = new ApplicationBuilder(null); + app.UseCloudEvents(new CloudEventsMiddlewareOptions + { + ForwardCloudEventPropertiesAsHeaders = true, + ExcludedCloudEventPropertiesFromHeaders = new []{"type"} + }); + + // Do verification in the scope of the middleware + app.Run(httpContext => + { + httpContext.Request.ContentType.Should().Be(dataContentType ?? "application/json"); + ReadBody(httpContext.Request.Body).Should().Be("{\"name\":\"jimmy\"}"); + + httpContext.Request.Headers.Should().NotContainKey("Cloudevent.type"); + httpContext.Request.Headers.Should().ContainKey("Cloudevent.subject").WhichValue.Should().BeEquivalentTo("Test.Subject"); + return Task.CompletedTask; + }); + + var pipeline = app.Build(); + + var context = new DefaultHttpContext(); + context.Request.ContentType = charSet == null ? "application/cloudevents+json" : $"application/cloudevents+json;charset={charSet}"; + context.Request.Body = dataContentType == null ? + MakeBody("{ \"type\": \"Test.Type\", \"subject\": \"Test.Subject\", \"data\": { \"name\":\"jimmy\" } }", encoding) : + MakeBody($"{{ \"datacontenttype\": \"{dataContentType}\", \"type\":\"Test.Type\", \"subject\": \"Test.Subject\", \"data\": {{ \"name\":\"jimmy\" }} }}", encoding); + + await pipeline.Invoke(context); + } + [Fact] public async Task InvokeAsync_ReplacesBodyNonJsonData() { diff --git a/test/Dapr.AspNetCore.Test/Dapr.AspNetCore.Test.csproj b/test/Dapr.AspNetCore.Test/Dapr.AspNetCore.Test.csproj index aa463be98..9135e63d4 100644 --- a/test/Dapr.AspNetCore.Test/Dapr.AspNetCore.Test.csproj +++ b/test/Dapr.AspNetCore.Test/Dapr.AspNetCore.Test.csproj @@ -1,14 +1,14 @@  - + runtime; build; native; contentfiles; analyzers; buildtransitive all - - - - + + + + all runtime; build; native; contentfiles; analyzers; buildtransitive @@ -21,6 +21,7 @@ + \ No newline at end of file diff --git a/test/Dapr.AspNetCore.Test/DaprClientBuilderTest.cs b/test/Dapr.AspNetCore.Test/DaprClientBuilderTest.cs index 43d69295b..45cbcc152 100644 --- a/test/Dapr.AspNetCore.Test/DaprClientBuilderTest.cs +++ b/test/Dapr.AspNetCore.Test/DaprClientBuilderTest.cs @@ -11,7 +11,7 @@ // limitations under the License. // ------------------------------------------------------------------------ -using System; +using System; using System.Text.Json; using Dapr.Client; using Grpc.Net.Client; @@ -43,7 +43,7 @@ public void DaprClientBuilder_UsesPropertyNameCaseHandlingAsSpecified() public void DaprClientBuilder_UsesThrowOperationCanceledOnCancellation_ByDefault() { var builder = new DaprClientBuilder(); - var daprClient = builder.Build(); + Assert.True(builder.GrpcChannelOptions.ThrowOperationCanceledOnCancellation); } @@ -51,7 +51,7 @@ public void DaprClientBuilder_UsesThrowOperationCanceledOnCancellation_ByDefault public void DaprClientBuilder_DoesNotOverrideUserGrpcChannelOptions() { var builder = new DaprClientBuilder(); - var daprClient = builder.UseGrpcChannelOptions(new GrpcChannelOptions()).Build(); + builder.UseGrpcChannelOptions(new GrpcChannelOptions()).Build(); Assert.False(builder.GrpcChannelOptions.ThrowOperationCanceledOnCancellation); } diff --git a/test/Dapr.AspNetCore.Test/DaprServiceCollectionExtensionsTest.cs b/test/Dapr.AspNetCore.Test/DaprServiceCollectionExtensionsTest.cs index 614faf5e4..4a340e22a 100644 --- a/test/Dapr.AspNetCore.Test/DaprServiceCollectionExtensionsTest.cs +++ b/test/Dapr.AspNetCore.Test/DaprServiceCollectionExtensionsTest.cs @@ -1,4 +1,4 @@ -// ------------------------------------------------------------------------ +// ------------------------------------------------------------------------ // Copyright 2021 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. @@ -11,7 +11,9 @@ // limitations under the License. // ------------------------------------------------------------------------ -using System; +#nullable enable + +using System; using System.Text.Json; using Dapr.Client; using Microsoft.Extensions.DependencyInjection; @@ -43,11 +45,39 @@ public void AddDaprClient_RegistersDaprClientOnlyOnce() var serviceProvider = services.BuildServiceProvider(); - DaprClientGrpc daprClient = serviceProvider.GetService() as DaprClientGrpc; + DaprClientGrpc? daprClient = serviceProvider.GetService() as DaprClientGrpc; - Assert.True(daprClient.JsonSerializerOptions.PropertyNameCaseInsensitive); + Assert.NotNull(daprClient); + Assert.True(daprClient?.JsonSerializerOptions.PropertyNameCaseInsensitive); } + [Fact] + public void AddDaprClient_RegistersUsingDependencyFromIServiceProvider() + { + + var services = new ServiceCollection(); + services.AddSingleton(); + services.AddDaprClient((provider, builder) => + { + var configProvider = provider.GetRequiredService(); + var caseSensitivity = configProvider.GetCaseSensitivity(); + + builder.UseJsonSerializationOptions(new JsonSerializerOptions + { + PropertyNameCaseInsensitive = caseSensitivity + }); + }); + + var serviceProvider = services.BuildServiceProvider(); + + DaprClientGrpc? client = serviceProvider.GetRequiredService() as DaprClientGrpc; + + //Registers with case-insensitive as true by default, but we set as false above + Assert.NotNull(client); + Assert.False(client?.JsonSerializerOptions.PropertyNameCaseInsensitive); + } + + #if NET8_0_OR_GREATER [Fact] public void AddDaprClient_WithKeyedServices() @@ -65,5 +95,10 @@ public void AddDaprClient_WithKeyedServices() Assert.NotNull(daprClient); } #endif + + private class TestConfigurationProvider + { + public bool GetCaseSensitivity() => false; + } } } diff --git a/test/Dapr.Client.Test/Dapr.Client.Test.csproj b/test/Dapr.Client.Test/Dapr.Client.Test.csproj index aef5b4113..f0bea601f 100644 --- a/test/Dapr.Client.Test/Dapr.Client.Test.csproj +++ b/test/Dapr.Client.Test/Dapr.Client.Test.csproj @@ -1,21 +1,21 @@  - + runtime; build; native; contentfiles; analyzers; buildtransitive all - - - - - - - - - - - + + + + + + + + + + + all runtime; build; native; contentfiles; analyzers; buildtransitive @@ -33,6 +33,8 @@ + + diff --git a/test/Dapr.Client.Test/DaprApiTokenTest.cs b/test/Dapr.Client.Test/DaprApiTokenTest.cs index 3726e0321..cf9f422bc 100644 --- a/test/Dapr.Client.Test/DaprApiTokenTest.cs +++ b/test/Dapr.Client.Test/DaprApiTokenTest.cs @@ -35,7 +35,7 @@ public async Task DaprCall_WithApiTokenSet() request.Dismiss(); // Get Request and validate - var envelope = await request.GetRequestEnvelopeAsync(); + await request.GetRequestEnvelopeAsync(); request.Request.Headers.TryGetValues("dapr-api-token", out var headerValues); headerValues.Count().Should().Be(1); @@ -56,7 +56,7 @@ public async Task DaprCall_WithoutApiToken() request.Dismiss(); // Get Request and validate - var envelope = await request.GetRequestEnvelopeAsync(); + await request.GetRequestEnvelopeAsync(); request.Request.Headers.TryGetValues("dapr-api-token", out var headerValues); headerValues.Should().BeNull(); diff --git a/test/Dapr.Client.Test/DaprClientTest.CreateInvokableHttpClientTest.cs b/test/Dapr.Client.Test/DaprClientTest.CreateInvokableHttpClientTest.cs new file mode 100644 index 000000000..99fbd4972 --- /dev/null +++ b/test/Dapr.Client.Test/DaprClientTest.CreateInvokableHttpClientTest.cs @@ -0,0 +1,52 @@ +// ------------------------------------------------------------------------ +// Copyright 2024 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.Client +{ + public partial class DaprClientTest + { + [Fact] + public void CreateInvokableHttpClient_WithAppId_FromDaprClient() + { + var daprClient = new MockClient().DaprClient; + var client = daprClient.CreateInvokableHttpClient(appId: "bank"); + Assert.Equal("http://bank/", client.BaseAddress.AbsoluteUri); + } + + [Fact] + public void CreateInvokableHttpClient_InvalidAppId_FromDaprClient() + { + var daprClient = new MockClient().DaprClient; + var ex = Assert.Throws(() => + { + // The appId needs to be something that can be used as hostname in a URI. + _ = daprClient.CreateInvokableHttpClient(appId: ""); + }); + + Assert.Contains("The appId must be a valid hostname.", ex.Message); + Assert.IsType(ex.InnerException); + } + + [Fact] + public void CreateInvokableHttpClient_WithoutAppId_FromDaprClient() + { + var daprClient = new MockClient().DaprClient; + + var client = daprClient.CreateInvokableHttpClient(); + Assert.Null(client.BaseAddress); + } + } +} diff --git a/test/Dapr.Client.Test/DaprClientTest.InvokeMethodAsync.cs b/test/Dapr.Client.Test/DaprClientTest.InvokeMethodAsync.cs index d529e0055..58663227c 100644 --- a/test/Dapr.Client.Test/DaprClientTest.InvokeMethodAsync.cs +++ b/test/Dapr.Client.Test/DaprClientTest.InvokeMethodAsync.cs @@ -11,6 +11,10 @@ // limitations under the License. // ------------------------------------------------------------------------ +using System.Collections.Generic; +using System.Linq; +using System.Net.Http.Headers; + namespace Dapr.Client.Test { using System; @@ -516,6 +520,18 @@ public async Task CreateInvokeMethodRequest_TransformsUrlCorrectly(string method Assert.Equal(new Uri(expected).AbsoluteUri, request.RequestUri.AbsoluteUri); } + [Fact] + public async Task CreateInvokeMethodRequest_AppendQueryStringValuesCorrectly() + { + await using var client = TestClient.CreateForDaprClient(c => + { + c.UseGrpcEndpoint("http://localhost").UseHttpEndpoint("https://test-endpoint:3501").UseJsonSerializationOptions(this.jsonSerializerOptions); + }); + + var request = client.InnerClient.CreateInvokeMethodRequest("test-app", "mymethod", (IReadOnlyCollection>)new List> { new("a", "0"), new("b", "1") }); + Assert.Equal(new Uri("https://test-endpoint:3501/v1.0/invoke/test-app/method/mymethod?a=0&b=1").AbsoluteUri, request.RequestUri.AbsoluteUri); + } + [Fact] public async Task CreateInvokeMethodRequest_WithoutApiToken_CreatesHttpRequestWithoutApiTokenHeader() { @@ -615,6 +631,60 @@ public async Task CreateInvokeMethodRequest_WithData_CreatesJsonContent() Assert.Equal(data.Color, actual.Color); } + [Fact] + public async Task CreateInvokeMethodRequest_WithData_CreatesJsonContentWithQueryString() + { + await using var client = TestClient.CreateForDaprClient(c => + { + c.UseGrpcEndpoint("http://localhost").UseHttpEndpoint("https://test-endpoint:3501").UseJsonSerializationOptions(this.jsonSerializerOptions); + }); + + var data = new Widget + { + Color = "red", + }; + + var request = client.InnerClient.CreateInvokeMethodRequest(HttpMethod.Post, "test-app", "test", new List> { new("a", "0"), new("b", "1") }, data); + + Assert.Equal(new Uri("https://test-endpoint:3501/v1.0/invoke/test-app/method/test?a=0&b=1").AbsoluteUri, request.RequestUri.AbsoluteUri); + + var content = Assert.IsType(request.Content); + Assert.Equal(typeof(Widget), content.ObjectType); + Assert.Same(data, content.Value); + + // the best way to verify the usage of the correct settings object + var actual = await content.ReadFromJsonAsync(this.jsonSerializerOptions); + Assert.Equal(data.Color, actual.Color); + } + + [Fact] + public async Task InvokeMethodWithoutResponse_WithExtraneousHeaders() + { + await using var client = TestClient.CreateForDaprClient(c => + { + c.UseGrpcEndpoint("http://localhost").UseHttpEndpoint("https://test-endpoint:3501").UseJsonSerializationOptions(this.jsonSerializerOptions); + }); + + var req = await client.CaptureHttpRequestAsync(async DaprClient => + { + var request = client.InnerClient.CreateInvokeMethodRequest(HttpMethod.Get, "test-app", "mymethod"); + request.Headers.Add("test-api-key", "test"); + request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", "abc123"); + request.Headers.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json")); + + await DaprClient.InvokeMethodAsync(request); + }); + + req.Dismiss(); + + Assert.NotNull(req); + Assert.True(req.Request.Headers.Contains("test-api-key")); + Assert.Equal("test", req.Request.Headers.GetValues("test-api-key").First()); + Assert.True(req.Request.Headers.Contains("Authorization")); + Assert.Equal("Bearer abc123", req.Request.Headers.GetValues("Authorization").First()); + Assert.Equal("application/json", req.Request.Headers.GetValues("Accept").First()); + } + [Fact] public async Task InvokeMethodWithResponseAsync_ReturnsMessageWithoutCheckingStatus() { diff --git a/test/Dapr.Client.Test/DaprClientTest.InvokeMethodGrpcAsync.cs b/test/Dapr.Client.Test/DaprClientTest.InvokeMethodGrpcAsync.cs index 4001e4b06..6ccbbe4c4 100644 --- a/test/Dapr.Client.Test/DaprClientTest.InvokeMethodGrpcAsync.cs +++ b/test/Dapr.Client.Test/DaprClientTest.InvokeMethodGrpcAsync.cs @@ -88,8 +88,7 @@ public async Task InvokeMethodGrpcAsync_CanInvokeMethodWithReturnTypeAndData_Thr Data = Any.Pack(data), }; - var response = - client.Call() + await client.Call() .SetResponse(invokeResponse) .Build(); @@ -153,8 +152,7 @@ public async Task InvokeMethodGrpcAsync_CanInvokeMethodWithReturnTypeNoData_Thro Data = Any.Pack(data), }; - var response = - client.Call() + await client.Call() .SetResponse(invokeResponse) .Build(); @@ -197,7 +195,7 @@ public void InvokeMethodGrpcAsync_CanInvokeMethodWithNoReturnTypeAndData() .Setup(m => m.InvokeServiceAsync(It.IsAny(), It.IsAny())) .Returns(response); - FluentActions.Awaiting(async () => await client.DaprClient.InvokeMethodGrpcAsync("test", "test", request)).Should().NotThrow(); + FluentActions.Awaiting(async () => await client.DaprClient.InvokeMethodGrpcAsync("test", "test", request)).Should().NotThrowAsync(); } [Fact] @@ -210,8 +208,7 @@ public async Task InvokeMethodGrpcAsync_CanInvokeMethodWithNoReturnTypeAndData_T Data = Any.Pack(data), }; - var response = - client.Call() + await client.Call() .SetResponse(invokeResponse) .Build(); @@ -294,7 +291,7 @@ public async Task InvokeMethodGrpcAsync_WithReturnTypeAndData() // Validate Response var invokedResponse = await request.CompleteWithMessageAsync(response); - invokeResponse.Name.Should().Be(invokeResponse.Name); + invokedResponse.Name.Should().Be(invokeResponse.Name); } [Fact] diff --git a/test/Dapr.Client.Test/DistributedLockApiTest.cs b/test/Dapr.Client.Test/DistributedLockApiTest.cs index def7d8f8f..a030586c5 100644 --- a/test/Dapr.Client.Test/DistributedLockApiTest.cs +++ b/test/Dapr.Client.Test/DistributedLockApiTest.cs @@ -60,7 +60,6 @@ public async Task TryLockAsync_WithAllValues_ArgumentVerifierException() string lockOwner = "owner1"; Int32 expiryInSeconds = 0; // Get response and validate - var invokeResponse = new ArgumentException(); await Assert.ThrowsAsync(async () => await client.Lock(storeName, resourceId, lockOwner, expiryInSeconds)); } diff --git a/test/Dapr.Client.Test/EnumExtensionTest.cs b/test/Dapr.Client.Test/Extensions/EnumExtensionTest.cs similarity index 87% rename from test/Dapr.Client.Test/EnumExtensionTest.cs rename to test/Dapr.Client.Test/Extensions/EnumExtensionTest.cs index be78c3861..83c4354f9 100644 --- a/test/Dapr.Client.Test/EnumExtensionTest.cs +++ b/test/Dapr.Client.Test/Extensions/EnumExtensionTest.cs @@ -1,7 +1,7 @@ using System.Runtime.Serialization; using Xunit; -namespace Dapr.Client.Test +namespace Dapr.Client.Test.Extensions { public class EnumExtensionTest { @@ -29,9 +29,9 @@ public void GetValueFromEnumMember_BlueResolvesAsExpected() public enum TestEnum { - [EnumMember(Value="red")] + [EnumMember(Value = "red")] Red, - [EnumMember(Value="YELLOW")] + [EnumMember(Value = "YELLOW")] Yellow, Blue } diff --git a/test/Dapr.Client.Test/Extensions/HttpExtensionTest.cs b/test/Dapr.Client.Test/Extensions/HttpExtensionTest.cs new file mode 100644 index 000000000..7b93c1c91 --- /dev/null +++ b/test/Dapr.Client.Test/Extensions/HttpExtensionTest.cs @@ -0,0 +1,63 @@ +using System.Collections.Generic; +using System.Net.Http; +using Xunit; + +namespace Dapr.Client.Test.Extensions +{ + public class HttpExtensionTest + { + [Fact] + public void AddQueryParameters_ReturnsEmptyQueryStringWithNullParameters() + { + const string uri = "https://localhost/mypath"; + var httpRq = new HttpRequestMessage(HttpMethod.Get, uri); + var updatedUri = httpRq.RequestUri.AddQueryParameters(null); + Assert.Equal(uri, updatedUri.AbsoluteUri); + } + + [Fact] + public void AddQueryParameters_ReturnsOriginalQueryStringWithNullParameters() + { + const string uri = "https://localhost/mypath?a=0&b=1"; + var httpRq = new HttpRequestMessage(HttpMethod.Get, uri); + var updatedUri = httpRq.RequestUri.AddQueryParameters(null); + Assert.Equal(uri, updatedUri.AbsoluteUri); + } + + [Fact] + public void AddQueryParameters_BuildsQueryString() + { + var httpRq = new HttpRequestMessage(HttpMethod.Get, "https://localhost/mypath?a=0"); + var updatedUri = httpRq.RequestUri.AddQueryParameters(new List> + { + new("test", "value") + }); + Assert.Equal("https://localhost/mypath?a=0&test=value", updatedUri.AbsoluteUri); + } + + [Fact] + public void AddQueryParameters_BuildQueryStringWithDuplicateKeys() + { + var httpRq = new HttpRequestMessage(HttpMethod.Get, "https://localhost/mypath"); + var updatedUri = httpRq.RequestUri.AddQueryParameters(new List> + { + new("test", "1"), + new("test", "2"), + new("test", "3") + }); + Assert.Equal("https://localhost/mypath?test=1&test=2&test=3", updatedUri.AbsoluteUri); + } + + [Fact] + public void AddQueryParameters_EscapeSpacesInValues() + { + var httpRq = new HttpRequestMessage(HttpMethod.Get, "https://localhost/mypath"); + var updatedUri = httpRq.RequestUri.AddQueryParameters(new List> + { + new("name1", "John Doe"), + new("name2", "Jane Doe") + }); + Assert.Equal("https://localhost/mypath?name1=John%20Doe&name2=Jane%20Doe", updatedUri.AbsoluteUri); + } + } +} diff --git a/test/Dapr.Client.Test/InvocationHandlerTests.cs b/test/Dapr.Client.Test/InvocationHandlerTests.cs index 3dac84113..b171ca399 100644 --- a/test/Dapr.Client.Test/InvocationHandlerTests.cs +++ b/test/Dapr.Client.Test/InvocationHandlerTests.cs @@ -156,7 +156,7 @@ public async Task SendAsync_RewritesUri() }; var request = new HttpRequestMessage(HttpMethod.Post, uri); - var response = await CallSendAsync(handler, request); + await CallSendAsync(handler, request); Assert.Equal("https://localhost:5000/v1.0/invoke/bank/method/accounts/17?", capture.RequestUri?.OriginalString); Assert.Null(capture.DaprApiToken); @@ -181,7 +181,7 @@ public async Task SendAsync_RewritesUri_AndAppId() }; var request = new HttpRequestMessage(HttpMethod.Post, uri); - var response = await CallSendAsync(handler, request); + await CallSendAsync(handler, request); Assert.Equal("https://localhost:5000/v1.0/invoke/Bank/method/accounts/17?", capture.RequestUri?.OriginalString); Assert.Null(capture.DaprApiToken); @@ -205,7 +205,7 @@ public async Task SendAsync_RewritesUri_AndAddsApiToken() }; var request = new HttpRequestMessage(HttpMethod.Post, uri); - var response = await CallSendAsync(handler, request); + await CallSendAsync(handler, request); Assert.Equal("https://localhost:5000/v1.0/invoke/bank/method/accounts/17?", capture.RequestUri?.OriginalString); Assert.Equal("super-duper-secure", capture.DaprApiToken); diff --git a/test/Dapr.Client.Test/InvokeBindingApiTest.cs b/test/Dapr.Client.Test/InvokeBindingApiTest.cs index 30c9d48ba..c2ada7a11 100644 --- a/test/Dapr.Client.Test/InvokeBindingApiTest.cs +++ b/test/Dapr.Client.Test/InvokeBindingApiTest.cs @@ -209,7 +209,7 @@ public async Task InvokeBindingAsync_WrapsJsonException() return await daprClient.InvokeBindingAsync("test", "test", new InvokeRequest() { RequestParameter = "Hello " }); }); - var envelope = await request.GetRequestEnvelopeAsync(); + await request.GetRequestEnvelopeAsync(); var ex = await Assert.ThrowsAsync(async () => { await request.CompleteWithMessageAsync(response); diff --git a/test/Dapr.Client.Test/PublishEventApiTest.cs b/test/Dapr.Client.Test/PublishEventApiTest.cs index cf32e55e4..6ba774e52 100644 --- a/test/Dapr.Client.Test/PublishEventApiTest.cs +++ b/test/Dapr.Client.Test/PublishEventApiTest.cs @@ -141,7 +141,6 @@ public async Task PublishEventAsync_CanPublishTopicWithNoContent() request.Dismiss(); var envelope = await request.GetRequestEnvelopeAsync(); - var jsonFromRequest = envelope.Data.ToStringUtf8(); envelope.PubsubName.Should().Be(TestPubsubName); envelope.Topic.Should().Be("test"); @@ -213,7 +212,6 @@ public async Task PublishEventAsync_CanPublishCloudEventWithNoContent() { await using var client = TestClient.CreateForDaprClient(); - var publishData = new PublishData() { PublishObjectParameter = "testparam" }; var cloudEvent = new CloudEvent { Source = new Uri("urn:testsource"), diff --git a/test/Dapr.Client.Test/SecretApiTest.cs b/test/Dapr.Client.Test/SecretApiTest.cs index c94c82844..26048e2a4 100644 --- a/test/Dapr.Client.Test/SecretApiTest.cs +++ b/test/Dapr.Client.Test/SecretApiTest.cs @@ -1,4 +1,4 @@ -// ------------------------------------------------------------------------ +// ------------------------------------------------------------------------ // Copyright 2021 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. @@ -93,6 +93,32 @@ public async Task GetSecretAsync_ReturnSingleSecret() secretsResponse["redis_secret"].Should().Be("Guess_Redis"); } + [Fact] + public async Task GetSecretAsync_WithSlashesInName() + { + await using var client = TestClient.CreateForDaprClient(); + + var request = await client.CaptureGrpcRequestAsync(async DaprClient => + { + return await DaprClient.GetSecretAsync("testStore", "us-west-1/org/xpto/secretabc"); + }); + + request.Dismiss(); + + //Get Request and validate + var envelope = await request.GetRequestEnvelopeAsync(); + envelope.StoreName.Should().Be("testStore"); + envelope.Key.Should().Be("us-west-1/org/xpto/secretabc"); + + var secrets = new Dictionary { { "us-west-1/org/xpto/secretabc", "abc123" } }; + var secretsResponse = await SendResponseWithSecrets(secrets, request); + + //Get response and validate + secretsResponse.Count.Should().Be(1); + secretsResponse.ContainsKey("us-west-1/org/xpto/secretabc").Should().BeTrue(); + secretsResponse["us-west-1/org/xpto/secretabc"].Should().Be("abc123"); + } + [Fact] public async Task GetSecretAsync_ReturnMultipleSecrets() { diff --git a/test/Dapr.Client.Test/StateApiTest.cs b/test/Dapr.Client.Test/StateApiTest.cs index 8c96f3d5d..f6ecb5d80 100644 --- a/test/Dapr.Client.Test/StateApiTest.cs +++ b/test/Dapr.Client.Test/StateApiTest.cs @@ -503,7 +503,7 @@ public async Task ExecuteStateTransactionAsync_CanSaveState() req1.Request.Etag.Value.Should().Be("testEtag"); req1.Request.Metadata.Count.Should().Be(1); req1.Request.Metadata["a"].Should().Be("b"); - req1.Request.Options.Concurrency.Should().Be(2); + req1.Request.Options.Concurrency.Should().Be(StateConcurrency.ConcurrencyLastWrite); var req2 = envelope.Operations[1]; req2.Request.Key.Should().Be("stateKey2"); @@ -837,8 +837,7 @@ public async Task TrySaveStateAsync_ValidateNonETagErrorThrowsException() { var client = new MockClient(); - var response = client.CallStateApi() - .Build(); + await client.CallStateApi().Build(); var rpcException = new RpcException(new Status(StatusCode.Internal, "Network Error")); @@ -859,8 +858,8 @@ public async Task TrySaveStateAsync_ValidateETagRelatedExceptionReturnsFalse() { var client = new MockClient(); - var response = client.CallStateApi() - .Build(); + await client.CallStateApi() + .Build(); var rpcException = new RpcException(new Status(StatusCode.Aborted, $"failed saving state in state store testStore")); // Setup the mock client to throw an Rpc Exception with the expected details info @@ -877,8 +876,8 @@ public async Task TrySaveStateAsync_NullEtagThrowsArgumentException() { var client = new MockClient(); - var response = client.CallStateApi() - .Build(); + await client.CallStateApi() + .Build(); await FluentActions.Awaiting(async () => await client.DaprClient.TrySaveStateAsync("test", "test", "testValue", null)) .Should().ThrowAsync(); @@ -905,8 +904,8 @@ public async Task TryDeleteStateAsync_ValidateNonETagErrorThrowsException() { var client = new MockClient(); - var response = client.CallStateApi() - .Build(); + await client.CallStateApi() + .Build(); var rpcException = new RpcException(new Status(StatusCode.Internal, "Network Error")); @@ -927,8 +926,8 @@ public async Task TryDeleteStateAsync_NullEtagThrowsArgumentException() { var client = new MockClient(); - var response = client.CallStateApi() - .Build(); + await client.CallStateApi() + .Build(); await FluentActions.Awaiting(async () => await client.DaprClient.TryDeleteStateAsync("test", "test", null)) .Should().ThrowAsync(); @@ -955,8 +954,8 @@ public async Task TryDeleteStateAsync_ValidateETagRelatedExceptionReturnsFalse() { var client = new MockClient(); - var response = client.CallStateApi() - .Build(); + await client.CallStateApi() + .Build(); var rpcException = new RpcException(new Status(StatusCode.Aborted, $"failed deleting state with key test")); // Setup the mock client to throw an Rpc Exception with the expected details info diff --git a/test/Dapr.Client.Test/TryLockResponseTest.cs b/test/Dapr.Client.Test/TryLockResponseTest.cs index f94f949db..3420dc233 100644 --- a/test/Dapr.Client.Test/TryLockResponseTest.cs +++ b/test/Dapr.Client.Test/TryLockResponseTest.cs @@ -47,7 +47,7 @@ public async Task TryLockAsync_WithAllValues_ValidateRequest() Success = true }; - var domainResponse = await request.CompleteWithMessageAsync(invokeResponse); + await request.CompleteWithMessageAsync(invokeResponse); //testing unlocking diff --git a/test/Dapr.Common.Test/Dapr.Common.Test.csproj b/test/Dapr.Common.Test/Dapr.Common.Test.csproj new file mode 100644 index 000000000..6e34c3a7a --- /dev/null +++ b/test/Dapr.Common.Test/Dapr.Common.Test.csproj @@ -0,0 +1,23 @@ + + + + + runtime; build; native; contentfiles; analyzers; buildtransitive + all + + + + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + + + + + + diff --git a/test/Dapr.Common.Test/DaprDefaultTest.cs b/test/Dapr.Common.Test/DaprDefaultTest.cs new file mode 100644 index 000000000..ef4d0da3c --- /dev/null +++ b/test/Dapr.Common.Test/DaprDefaultTest.cs @@ -0,0 +1,582 @@ +using System; +using Microsoft.Extensions.Configuration; +using Xunit; + +namespace Dapr.Common.Test; + +public class DaprDefaultTest +{ + [Fact] + public void ShouldBuildHttpEndpointAndPortUsingPrefixedConfiguration() + { + const string endpointVarName = "test_DAPR_HTTP_ENDPOINT"; + const string portVarName = "test_DAPR_HTTP_PORT"; + var original_HttpEndpoint = Environment.GetEnvironmentVariable(endpointVarName); + var original_HttpPort = Environment.GetEnvironmentVariable(portVarName); + + try + { + const string prefix = "test_"; + + Environment.SetEnvironmentVariable(endpointVarName, "https://dapr.io"); + Environment.SetEnvironmentVariable(portVarName, null); //Will use 443 from the endpoint instead + + var configurationBuilder = new ConfigurationBuilder(); + configurationBuilder.AddEnvironmentVariables(prefix); + var configuration = configurationBuilder.Build(); + + var httpEndpoint = DaprDefaults.GetDefaultHttpEndpoint(configuration); + Assert.Equal("https://dapr.io:443/", httpEndpoint); + } + finally + { + //Restore + Environment.SetEnvironmentVariable(endpointVarName, original_HttpEndpoint); + Environment.SetEnvironmentVariable(portVarName, original_HttpPort); + } + } + + [Fact] + public void ShouldBuildHttpEndpointAndPortUsingConfiguration() + { + const string endpointVarName = "DAPR_HTTP_ENDPOINT"; + const string portVarName = "DAPR_HTTP_PORT"; + var original_HttpEndpoint = Environment.GetEnvironmentVariable(endpointVarName); + var original_HttpPort = Environment.GetEnvironmentVariable(portVarName); + + try + { + Environment.SetEnvironmentVariable(endpointVarName, "https://dapr.io"); + Environment.SetEnvironmentVariable(portVarName, null); //Will use 443 from the endpoint instead + + var configurationBuilder = new ConfigurationBuilder(); + configurationBuilder.AddEnvironmentVariables(); + var configuration = configurationBuilder.Build(); + + var httpEndpoint = DaprDefaults.GetDefaultHttpEndpoint(configuration); + Assert.Equal("https://dapr.io:443/", httpEndpoint); + } + finally + { + //Restore + Environment.SetEnvironmentVariable(endpointVarName, original_HttpEndpoint); + Environment.SetEnvironmentVariable(portVarName, original_HttpPort); + } + } + + [Fact] + public void ShouldBuildHttpEndpointUsingPrefixedConfiguration() + { + const string endpointVarName = "test_DAPR_HTTP_ENDPOINT"; + const string portVarName = "test_DAPR_HTTP_PORT"; + var original_HttpEndpoint = Environment.GetEnvironmentVariable(endpointVarName); + var original_HttpPort = Environment.GetEnvironmentVariable(portVarName); + + try + { + const string prefix = "test_"; + + Environment.SetEnvironmentVariable(endpointVarName, "https://dapr.io"); + Environment.SetEnvironmentVariable(portVarName, "2569"); + + var configurationBuilder = new ConfigurationBuilder(); + configurationBuilder.AddEnvironmentVariables(prefix); + var configuration = configurationBuilder.Build(); + + var httpEndpoint = DaprDefaults.GetDefaultHttpEndpoint(configuration); + Assert.Equal("https://dapr.io:443/", httpEndpoint); + } + finally + { + //Restore + Environment.SetEnvironmentVariable(endpointVarName, original_HttpEndpoint); + Environment.SetEnvironmentVariable(portVarName, original_HttpPort); + } + } + + [Fact] + public void ShouldBuildHttpEndpointUsingConfiguration() + { + const string endpointVarName = "DAPR_HTTP_ENDPOINT"; + const string portVarName = "DAPR_HTTP_PORT"; + var original_HttpEndpoint = Environment.GetEnvironmentVariable(endpointVarName); + var original_HttpPort = Environment.GetEnvironmentVariable(portVarName); + + try + { + Environment.SetEnvironmentVariable(endpointVarName, "https://dapr.io"); + Environment.SetEnvironmentVariable(portVarName, "2569"); + + var configurationBuilder = new ConfigurationBuilder(); + configurationBuilder.AddEnvironmentVariables(); + var configuration = configurationBuilder.Build(); + + var httpEndpoint = DaprDefaults.GetDefaultHttpEndpoint(configuration); + Assert.Equal("https://dapr.io:443/", httpEndpoint); + } + finally + { + //Restore + Environment.SetEnvironmentVariable(endpointVarName, original_HttpEndpoint); + Environment.SetEnvironmentVariable(portVarName, original_HttpPort); + } + } + + [Fact] + public void ShouldBuildHttpEndpointUsingOnlyPortConfiguration() + { + const string portVarName = "DAPR_HTTP_PORT"; + var original_HttpPort = Environment.GetEnvironmentVariable(portVarName); + + try + { + Environment.SetEnvironmentVariable(portVarName, "2569"); + + var configurationBuilder = new ConfigurationBuilder(); + configurationBuilder.AddEnvironmentVariables(); + var configuration = configurationBuilder.Build(); + + var httpEndpoint = DaprDefaults.GetDefaultHttpEndpoint(configuration); + Assert.Equal("http://localhost:2569/", httpEndpoint); + } + finally + { + //Restore + Environment.SetEnvironmentVariable(portVarName, original_HttpPort); + } + } + + [Fact] + public void ShouldBuildHttpEndpointUsingEnvVarValues() + { + const string endpointVarName = "DAPR_HTTP_ENDPOINT"; + const string portVarName = "DAPR_HTTP_PORT"; + var original_HttpEndpoint = Environment.GetEnvironmentVariable(endpointVarName); + var original_HttpPort = Environment.GetEnvironmentVariable(portVarName); + + try + { + Environment.SetEnvironmentVariable(endpointVarName, "http://dapr.io"); + Environment.SetEnvironmentVariable(portVarName, "2569"); + + var configurationBuilder = new ConfigurationBuilder(); + var configuration = configurationBuilder.Build(); + + var httpEndpoint = DaprDefaults.GetDefaultHttpEndpoint(configuration); + Assert.Equal("http://dapr.io:80/", httpEndpoint); + } + finally + { + //Restore + Environment.SetEnvironmentVariable(endpointVarName, original_HttpEndpoint); + Environment.SetEnvironmentVariable(portVarName, original_HttpPort); + } + } + + [Fact] + public void ShouldBuildHttpEndpointUsingMixedValues() + { + const string endpointVarName = "test_DAPR_HTTP_ENDPOINT"; + const string portVarName = "DAPR_HTTP_PORT"; + var original_HttpEndpoint = Environment.GetEnvironmentVariable(endpointVarName); + var original_HttpPort = Environment.GetEnvironmentVariable(portVarName); + + try + { + Environment.SetEnvironmentVariable(endpointVarName, "https://dapr.io"); + Environment.SetEnvironmentVariable(portVarName, "2569"); + + var configurationBuilder = new ConfigurationBuilder(); + configurationBuilder.AddEnvironmentVariables(); + configurationBuilder.AddEnvironmentVariables("test_"); + var configuration = configurationBuilder.Build(); + + var httpEndpoint = DaprDefaults.GetDefaultHttpEndpoint(configuration); + Assert.Equal("https://dapr.io:443/", httpEndpoint); + } + finally + { + //Restore + Environment.SetEnvironmentVariable(endpointVarName, original_HttpEndpoint); + Environment.SetEnvironmentVariable(portVarName, original_HttpPort); + } + } + + [Fact] + public void ShouldDefaultToEmptyHttpEndpoint() + { + const string endpointVarName = "DAPR_HTTP_ENDPOINT"; + const string portVarName = "DAPR_HTTP_PORT"; + var original_HttpEndpoint = Environment.GetEnvironmentVariable(endpointVarName); + var original_HttpPort = Environment.GetEnvironmentVariable(portVarName); + + try + { + Environment.SetEnvironmentVariable(endpointVarName, null); + Environment.SetEnvironmentVariable(portVarName, null); + + var configurationBuilder = new ConfigurationBuilder(); + var configuration = configurationBuilder.Build(); + + var httpEndpoint = DaprDefaults.GetDefaultHttpEndpoint(configuration); + Assert.Equal("http://localhost:3500/", httpEndpoint); + } + finally + { + //Restore + Environment.SetEnvironmentVariable(endpointVarName, original_HttpEndpoint); + Environment.SetEnvironmentVariable(portVarName, original_HttpPort); + } + } + + [Fact] + public void ShouldDefaultToLocalhostWithPort() + { + const string endpointVarName = "DAPR_HTTP_ENDPOINT"; + const string portVarName = "DAPR_HTTP_PORT"; + var original_HttpEndpoint = Environment.GetEnvironmentVariable(endpointVarName); + var original_HttpPort = Environment.GetEnvironmentVariable(portVarName); + + try + { + Environment.SetEnvironmentVariable(endpointVarName, null); + Environment.SetEnvironmentVariable(portVarName, "7256"); + + var configurationBuilder = new ConfigurationBuilder(); + var configuration = configurationBuilder.Build(); + + var httpEndpoint = DaprDefaults.GetDefaultHttpEndpoint(configuration); + Assert.Equal("http://localhost:7256/", httpEndpoint); + } + finally + { + //Restore + Environment.SetEnvironmentVariable(endpointVarName, original_HttpEndpoint); + Environment.SetEnvironmentVariable(portVarName, original_HttpPort); + } + } + + [Fact] + public void ShouldDefaultToLocalhostWithDefaultPort() + { + const string endpointVarName = "DAPR_HTTP_ENDPOINT"; + const string portVarName = "DAPR_HTTP_PORT"; + var original_HttpEndpoint = Environment.GetEnvironmentVariable(endpointVarName); + var original_HttpPort = Environment.GetEnvironmentVariable(portVarName); + + try + { + Environment.SetEnvironmentVariable(endpointVarName, null); + Environment.SetEnvironmentVariable(portVarName, null); + + var configurationBuilder = new ConfigurationBuilder(); + var configuration = configurationBuilder.Build(); + + var httpEndpoint = DaprDefaults.GetDefaultHttpEndpoint(configuration); + Assert.Equal("http://localhost:3500/", httpEndpoint); + } + finally + { + //Restore + Environment.SetEnvironmentVariable(endpointVarName, original_HttpEndpoint); + Environment.SetEnvironmentVariable(portVarName, original_HttpPort); + } + } + + [Fact] + public void ShouldBuildGrpcEndpointAndPortUsingPrefixedConfiguration() + { + const string endpointVarName = "test_DAPR_GRPC_ENDPOINT"; + const string portVarName = "test_DAPR_GRPC_PORT"; + var original_GrpcEndpoint = Environment.GetEnvironmentVariable(endpointVarName); + var original_GrpcPort = Environment.GetEnvironmentVariable(portVarName); + + try + { + const string prefix = "test_"; + + Environment.SetEnvironmentVariable(endpointVarName, "https://grpc.dapr.io"); + Environment.SetEnvironmentVariable(portVarName, "2570"); + + var configurationBuilder = new ConfigurationBuilder(); + configurationBuilder.AddEnvironmentVariables(prefix); + var configuration = configurationBuilder.Build(); + + var grpcEndpoint = DaprDefaults.GetDefaultGrpcEndpoint(configuration); + Assert.Equal("https://grpc.dapr.io:443/", grpcEndpoint); + } + finally + { + //Restore + Environment.SetEnvironmentVariable(endpointVarName, original_GrpcEndpoint); + Environment.SetEnvironmentVariable(portVarName, original_GrpcPort); + } + } + + [Fact] + public void ShouldBuildGrpcEndpointAndPortUsingConfiguration() + { + const string endpointVarName = DaprDefaults.DaprGrpcEndpointName; + const string portVarName = DaprDefaults.DaprGrpcPortName; + var original_GrpcEndpoint = Environment.GetEnvironmentVariable(endpointVarName); + var original_GrpcPort = Environment.GetEnvironmentVariable(portVarName); + + try + { + Environment.SetEnvironmentVariable(endpointVarName, "https://grpc.dapr.io", EnvironmentVariableTarget.Process); + Environment.SetEnvironmentVariable(portVarName, null, EnvironmentVariableTarget.Process); //Will use 443 from the endpoint value instead + + var configurationBuilder = new ConfigurationBuilder(); + configurationBuilder.AddEnvironmentVariables(); + var configuration = configurationBuilder.Build(); + + var grpcEndpoint = DaprDefaults.GetDefaultGrpcEndpoint(configuration); + Assert.Equal("https://grpc.dapr.io:443/", grpcEndpoint); + } + finally + { + //Restore + Environment.SetEnvironmentVariable(endpointVarName, original_GrpcEndpoint); + Environment.SetEnvironmentVariable(portVarName, original_GrpcPort); + } + } + + [Fact] + public void ShouldBuildGrpcEndpointUsingPrefixedConfiguration() + { + const string endpointVarName = "test_DAPR_GRPC_ENDPOINT"; + var original_GrpcEndpoint = Environment.GetEnvironmentVariable(endpointVarName); + + try + { + const string prefix = "test_"; + + Environment.SetEnvironmentVariable(endpointVarName, "https://grpc.dapr.io"); + + var configurationBuilder = new ConfigurationBuilder(); + configurationBuilder.AddEnvironmentVariables(prefix); + var configuration = configurationBuilder.Build(); + + var grpcEndpoint = DaprDefaults.GetDefaultGrpcEndpoint(configuration); + Assert.Equal("https://grpc.dapr.io:443/", grpcEndpoint); + } + finally + { + //Restore + Environment.SetEnvironmentVariable(endpointVarName, original_GrpcEndpoint); + } + } + + [Fact] + public void ShouldBuildGrpcEndpointUsingConfiguration() + { + const string endpointVarName = "DAPR_GRPC_ENDPOINT"; + const string portVarName = "DAPR_GRPC_PORT"; + var original_GrpcEndpoint = Environment.GetEnvironmentVariable(endpointVarName); + var original_GrpcPort = Environment.GetEnvironmentVariable(portVarName); + + try + { + Environment.SetEnvironmentVariable(endpointVarName, "https://grpc.dapr.io"); + Environment.SetEnvironmentVariable(portVarName, null); //Will use 443 from the endpoint value instead + + var configurationBuilder = new ConfigurationBuilder(); + configurationBuilder.AddEnvironmentVariables(); + var configuration = configurationBuilder.Build(); + + var grpcEndpoint = DaprDefaults.GetDefaultGrpcEndpoint(configuration); + Assert.Equal("https://grpc.dapr.io:443/", grpcEndpoint); + } + finally + { + //Restore + Environment.SetEnvironmentVariable(endpointVarName, original_GrpcEndpoint); + Environment.SetEnvironmentVariable(portVarName, original_GrpcPort); + } + } + + [Fact] + public void ShouldBuildGrpcEndpointAndPortUsingEnvVarValues() + { + const string endpointVarName = "DAPR_GRPC_ENDPOINT"; + const string portVarName = "DAPR_GRPC_PORT"; + var original_GrpcEndpoint = Environment.GetEnvironmentVariable(endpointVarName); + var original_GrpcPort = Environment.GetEnvironmentVariable(portVarName); + + try + { + Environment.SetEnvironmentVariable(endpointVarName, "https://grpc.dapr.io"); + Environment.SetEnvironmentVariable(portVarName, "4744"); //Will use 443 from the endpoint value instead + + var configurationBuilder = new ConfigurationBuilder(); + var configuration = configurationBuilder.Build(); + + var grpcEndpoint = DaprDefaults.GetDefaultGrpcEndpoint(configuration); + Assert.Equal("https://grpc.dapr.io:443/", grpcEndpoint); + } + finally + { + //Restore + Environment.SetEnvironmentVariable(endpointVarName, original_GrpcEndpoint); + Environment.SetEnvironmentVariable(portVarName, original_GrpcPort); + } + } + + [Fact] + public void ShouldBuildGrpcEndpointUsingEnvVarValues() + { + const string endpointVarName = "DAPR_GRPC_ENDPOINT"; + const string portVarName = "DAPR_GRPC_PORT"; + var original_GrpcEndpoint = Environment.GetEnvironmentVariable(endpointVarName); + var original_GrpcPort = Environment.GetEnvironmentVariable(portVarName); + + try + { + Environment.SetEnvironmentVariable(endpointVarName, "https://grpc.dapr.io"); + Environment.SetEnvironmentVariable(portVarName, null); //Will use 443 from the endpoint value instead + + var configurationBuilder = new ConfigurationBuilder(); + var configuration = configurationBuilder.Build(); + + var grpcEndpoint = DaprDefaults.GetDefaultGrpcEndpoint(configuration); + Assert.Equal("https://grpc.dapr.io:443/", grpcEndpoint); + } + finally + { + //Restore + Environment.SetEnvironmentVariable(endpointVarName, original_GrpcEndpoint); + Environment.SetEnvironmentVariable(portVarName, original_GrpcPort); + } + } + + [Fact] + public void ShouldBuildGrpcEndpointDefaultToLocalhostWithPort() + { + const string endpointVarName = DaprDefaults.DaprGrpcEndpointName; + const string portVarName = DaprDefaults.DaprGrpcPortName; + var original_grpcEndpoint = Environment.GetEnvironmentVariable(endpointVarName); + var original_grpcPort = Environment.GetEnvironmentVariable(portVarName); + + try + { + Environment.SetEnvironmentVariable(endpointVarName, null); + Environment.SetEnvironmentVariable(portVarName, "7256"); + + var configurationBuilder = new ConfigurationBuilder(); + var configuration = configurationBuilder.Build(); + + var grpcEndpoint = DaprDefaults.GetDefaultGrpcEndpoint(configuration); + Assert.Equal($"{DaprDefaults.DefaultDaprScheme}://{DaprDefaults.DefaultDaprHost}:7256/", grpcEndpoint); + } + finally + { + //Restore + Environment.SetEnvironmentVariable(endpointVarName, original_grpcEndpoint); + Environment.SetEnvironmentVariable(portVarName, original_grpcPort); + } + } + + [Fact] + public void ShouldBuildGrpcEndpointDefaultToLocalhostWithDefaultPort() + { + const string endpointVarName = DaprDefaults.DaprGrpcEndpointName; + const string portVarName = DaprDefaults.DaprGrpcPortName; + var original_grpcEndpoint = Environment.GetEnvironmentVariable(endpointVarName); + var original_grpcPort = Environment.GetEnvironmentVariable(portVarName); + + try + { + Environment.SetEnvironmentVariable(endpointVarName, null); + Environment.SetEnvironmentVariable(portVarName, null); + + var configurationBuilder = new ConfigurationBuilder(); + var configuration = configurationBuilder.Build(); + + var grpcEndpoint = DaprDefaults.GetDefaultGrpcEndpoint(configuration); + Assert.Equal($"{DaprDefaults.DefaultDaprScheme}://{DaprDefaults.DefaultDaprHost}:{DaprDefaults.DefaultGrpcPort}/", grpcEndpoint); + } + finally + { + //Restore + Environment.SetEnvironmentVariable(endpointVarName, original_grpcEndpoint); + Environment.SetEnvironmentVariable(portVarName, original_grpcPort); + } + } + + [Fact] + public void ShouldBuildApiTokenUsingConfiguration() + { + const string envVarName = DaprDefaults.DaprApiTokenName; + var original_ApiToken = Environment.GetEnvironmentVariable(envVarName); + + try + { + const string apiToken = "abc123"; + Environment.SetEnvironmentVariable(envVarName, apiToken); + + var configurationBuilder = new ConfigurationBuilder(); + configurationBuilder.AddEnvironmentVariables(); + var configuration = configurationBuilder.Build(); + + var testApiToken = DaprDefaults.GetDefaultDaprApiToken(configuration); + Assert.Equal(apiToken, testApiToken); + } + finally + { + //Restore + Environment.SetEnvironmentVariable(envVarName, original_ApiToken); + } + } + + [Fact] + public void ShouldBuildApiTokenUsingPrefixedConfiguration() + { + + const string envVarName = $"test_{DaprDefaults.DaprApiTokenName}"; + var original_ApiToken = Environment.GetEnvironmentVariable(envVarName); + + try + { + const string prefix = "test_"; + + const string apiToken = "abc123"; + Environment.SetEnvironmentVariable(envVarName, apiToken); + + var configurationBuilder = new ConfigurationBuilder(); + configurationBuilder.AddEnvironmentVariables(prefix); + var configuration = configurationBuilder.Build(); + + var testApiToken = DaprDefaults.GetDefaultDaprApiToken(configuration); + Assert.Equal(apiToken, testApiToken); + } + finally + { + //Restore + Environment.SetEnvironmentVariable(envVarName, original_ApiToken); + } + } + + [Fact] + public void ShouldBuildApiTokenWithEnvVarWhenConfigurationNotAvailable() + { + const string envVarName = DaprDefaults.DaprApiTokenName; + var original_ApiToken = Environment.GetEnvironmentVariable(envVarName); + const string apiToken = "abc123"; + Environment.SetEnvironmentVariable(envVarName, apiToken); + + try + { + var configurationBuilder = new ConfigurationBuilder(); + configurationBuilder.AddEnvironmentVariables(); + var configuration = configurationBuilder.Build(); + + var testApiToken = DaprDefaults.GetDefaultDaprApiToken(configuration); + Assert.Equal(apiToken, testApiToken); + } + finally + { + //Restore + Environment.SetEnvironmentVariable(envVarName, original_ApiToken); + } + } +} diff --git a/test/Dapr.E2E.Test.Actors.Generators/Clients/GeneratedClientTests.cs b/test/Dapr.E2E.Test.Actors.Generators/Clients/GeneratedClientTests.cs index 6079b5df7..a089a2e82 100644 --- a/test/Dapr.E2E.Test.Actors.Generators/Clients/GeneratedClientTests.cs +++ b/test/Dapr.E2E.Test.Actors.Generators/Clients/GeneratedClientTests.cs @@ -100,7 +100,7 @@ public async Task TestGeneratedClientAsync() var client = new ClientActorClient(actorProxy); - var result = await client.GetStateAsync(cancellationTokenSource.Token); + await client.GetStateAsync(cancellationTokenSource.Token); await client.SetStateAsync(new ClientState("updated state"), cancellationTokenSource.Token); } diff --git a/test/Dapr.E2E.Test.Actors.Generators/Dapr.E2E.Test.Actors.Generators.csproj b/test/Dapr.E2E.Test.Actors.Generators/Dapr.E2E.Test.Actors.Generators.csproj index 8618647cb..cb375af01 100644 --- a/test/Dapr.E2E.Test.Actors.Generators/Dapr.E2E.Test.Actors.Generators.csproj +++ b/test/Dapr.E2E.Test.Actors.Generators/Dapr.E2E.Test.Actors.Generators.csproj @@ -9,14 +9,14 @@ - - - - + + + + runtime; build; native; contentfiles; analyzers; buildtransitive all - + runtime; build; native; contentfiles; analyzers; buildtransitive all @@ -24,9 +24,7 @@ - + diff --git a/test/Dapr.E2E.Test.App.Grpc/Dapr.E2E.Test.App.Grpc.csproj b/test/Dapr.E2E.Test.App.Grpc/Dapr.E2E.Test.App.Grpc.csproj index 849870b98..9505df276 100644 --- a/test/Dapr.E2E.Test.App.Grpc/Dapr.E2E.Test.App.Grpc.csproj +++ b/test/Dapr.E2E.Test.App.Grpc/Dapr.E2E.Test.App.Grpc.csproj @@ -1,6 +1,6 @@ - + diff --git a/test/Dapr.E2E.Test.App/Dapr.E2E.Test.App.csproj b/test/Dapr.E2E.Test.App/Dapr.E2E.Test.App.csproj index e6ad11456..162a1306a 100644 --- a/test/Dapr.E2E.Test.App/Dapr.E2E.Test.App.csproj +++ b/test/Dapr.E2E.Test.App/Dapr.E2E.Test.App.csproj @@ -7,10 +7,9 @@ - - - - - + + + + diff --git a/test/Dapr.E2E.Test.App/Startup.cs b/test/Dapr.E2E.Test.App/Startup.cs index b1a437bf2..05c633000 100644 --- a/test/Dapr.E2E.Test.App/Startup.cs +++ b/test/Dapr.E2E.Test.App/Startup.cs @@ -118,7 +118,7 @@ public void Configure(IApplicationBuilder app, IWebHostEnvironment env) { var logger = new LoggerConfiguration().WriteTo.File("log.txt").CreateLogger(); - var loggerFactory = LoggerFactory.Create(builder => + LoggerFactory.Create(builder => { builder.AddSerilog(logger); }); diff --git a/test/Dapr.E2E.Test/Actors/E2ETests.ExceptionTests.cs b/test/Dapr.E2E.Test/Actors/E2ETests.ExceptionTests.cs index 986a2c4f0..fc036d15d 100644 --- a/test/Dapr.E2E.Test/Actors/E2ETests.ExceptionTests.cs +++ b/test/Dapr.E2E.Test/Actors/E2ETests.ExceptionTests.cs @@ -25,8 +25,7 @@ public partial class E2ETests : IAsyncLifetime public async Task ActorCanProvideExceptionDetails() { using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(60)); - var actorIds = new ActorId(Guid.NewGuid().ToString()); - + var proxy = this.ProxyFactory.CreateActorProxy(ActorId.CreateRandom(), "ExceptionActor"); await WaitForActorRuntimeAsync(proxy, cts.Token); ActorMethodInvocationException ex = await Assert.ThrowsAsync(async () => await proxy.ExceptionExample()); diff --git a/test/Dapr.E2E.Test/Dapr.E2E.Test.csproj b/test/Dapr.E2E.Test/Dapr.E2E.Test.csproj index f899167c4..fc92396a6 100644 --- a/test/Dapr.E2E.Test/Dapr.E2E.Test.csproj +++ b/test/Dapr.E2E.Test/Dapr.E2E.Test.csproj @@ -1,12 +1,12 @@  - - - - - - - + + + + + + + all runtime; build; native; contentfiles; analyzers; buildtransitive diff --git a/test/Dapr.E2E.Test/Workflows/WorkflowTest.cs b/test/Dapr.E2E.Test/Workflows/WorkflowTest.cs deleted file mode 100644 index 2ba5bfc3d..000000000 --- a/test/Dapr.E2E.Test/Workflows/WorkflowTest.cs +++ /dev/null @@ -1,171 +0,0 @@ -// ------------------------------------------------------------------------ -// Copyright 2022 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.IO; -using System.Collections.Generic; -using System.Threading; -using System.Threading.Tasks; -using Dapr.Client; -using FluentAssertions; -using Xunit; -using System.Linq; - -namespace Dapr.E2E.Test -{ - [Obsolete] - public partial class E2ETests - { - [Fact] - public async Task TestWorkflowLogging() - { - // This test starts the daprclient and searches through the logfile to ensure the - // workflow logger is correctly logging the registered workflow(s) and activity(s) - - Dictionary logStrings = new Dictionary(); - logStrings["PlaceOrder"] = false; - logStrings["ShipProduct"] = false; - var logFilePath = "../../../../../test/Dapr.E2E.Test.App/log.txt"; - var allLogsFound = false; - var timeout = 30; // 30s - using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(timeout)); - using var daprClient = new DaprClientBuilder().UseGrpcEndpoint(this.GrpcEndpoint).UseHttpEndpoint(this.HttpEndpoint).Build(); - var health = await daprClient.CheckHealthAsync(); - health.Should().Be(true, "DaprClient is not healthy"); - - var searchTask = Task.Run(async() => - { - using (StreamReader reader = new StreamReader(logFilePath)) - { - string line; - while ((line = await reader.ReadLineAsync().WaitAsync(cts.Token)) != null) - { - foreach (var entry in logStrings) - { - if (line.Contains(entry.Key)) - { - logStrings[entry.Key] = true; - } - } - allLogsFound = logStrings.All(k => k.Value); - if (allLogsFound) - { - break; - } - } - } - }, cts.Token); - - try - { - await searchTask; - } - finally - { - File.Delete(logFilePath); - } - if (!allLogsFound) - { - Assert.True(false, "The logs were not able to found within the timeout"); - } - } - [Fact] - public async Task TestWorkflows() - { - var instanceId = "testInstanceId"; - var instanceId2 = "EventRaiseId"; - var workflowComponent = "dapr"; - var workflowName = "PlaceOrder"; - object input = "paperclips"; - Dictionary workflowOptions = new Dictionary(); - workflowOptions.Add("task_queue", "testQueue"); - - using var daprClient = new DaprClientBuilder().UseGrpcEndpoint(this.GrpcEndpoint).UseHttpEndpoint(this.HttpEndpoint).Build(); - var health = await daprClient.CheckHealthAsync(); - health.Should().Be(true, "DaprClient is not healthy"); - - // START WORKFLOW TEST - var startResponse = await daprClient.StartWorkflowAsync( - instanceId: instanceId, - workflowComponent: workflowComponent, - workflowName: workflowName, - input: input, - workflowOptions: workflowOptions); - - startResponse.InstanceId.Should().Be("testInstanceId", $"Instance ID {startResponse.InstanceId} was not correct"); - - // GET INFO TEST - var getResponse = await daprClient.GetWorkflowAsync(instanceId, workflowComponent); - getResponse.InstanceId.Should().Be("testInstanceId"); - getResponse.RuntimeStatus.Should().Be(WorkflowRuntimeStatus.Running, $"Instance ID {getResponse.RuntimeStatus} was not correct"); - - // PAUSE TEST: - await daprClient.PauseWorkflowAsync(instanceId, workflowComponent); - getResponse = await daprClient.GetWorkflowAsync(instanceId, workflowComponent); - getResponse.RuntimeStatus.Should().Be(WorkflowRuntimeStatus.Suspended, $"Instance ID {getResponse.RuntimeStatus} was not correct"); - - // RESUME TEST: - await daprClient.ResumeWorkflowAsync(instanceId, workflowComponent); - getResponse = await daprClient.GetWorkflowAsync(instanceId, workflowComponent); - getResponse.RuntimeStatus.Should().Be(WorkflowRuntimeStatus.Running, $"Instance ID {getResponse.RuntimeStatus} was not correct"); - - // RAISE EVENT TEST - await daprClient.RaiseWorkflowEventAsync(instanceId, workflowComponent, "ChangePurchaseItem", "computers"); - getResponse = await daprClient.GetWorkflowAsync(instanceId, workflowComponent); - - // TERMINATE TEST: - await daprClient.TerminateWorkflowAsync(instanceId, workflowComponent); - getResponse = await daprClient.GetWorkflowAsync(instanceId, workflowComponent); - getResponse.RuntimeStatus.Should().Be(WorkflowRuntimeStatus.Terminated, $"Instance ID {getResponse.RuntimeStatus} was not correct"); - - // PURGE TEST - await daprClient.PurgeWorkflowAsync(instanceId, workflowComponent); - - try - { - getResponse = await daprClient.GetWorkflowAsync(instanceId, workflowComponent); - Assert.True(false, "The GetWorkflowAsync call should have failed since the instance was purged"); - } - catch (DaprException ex) - { - ex.InnerException.Message.Should().Contain("no such instance exists", $"Instance {instanceId} was not correctly purged"); - } - - // Start another workflow for event raising purposes - startResponse = await daprClient.StartWorkflowAsync( - instanceId: instanceId2, - workflowComponent: workflowComponent, - workflowName: workflowName, - input: input, - workflowOptions: workflowOptions); - - // PARALLEL RAISE EVENT TEST - var event1 = daprClient.RaiseWorkflowEventAsync(instanceId2, workflowComponent, "ChangePurchaseItem", "computers"); - var event2 = daprClient.RaiseWorkflowEventAsync(instanceId2, workflowComponent, "ChangePurchaseItem", "computers"); - var event3 = daprClient.RaiseWorkflowEventAsync(instanceId2, workflowComponent, "ChangePurchaseItem", "computers"); - var event4 = daprClient.RaiseWorkflowEventAsync(instanceId2, workflowComponent, "ChangePurchaseItem", "computers"); - var event5 = daprClient.RaiseWorkflowEventAsync(instanceId2, workflowComponent, "ChangePurchaseItem", "computers"); - - var externalEvents = Task.WhenAll(event1, event2, event3, event4, event5); - var winner = await Task.WhenAny(externalEvents, Task.Delay(TimeSpan.FromSeconds(30))); - externalEvents.IsCompletedSuccessfully.Should().BeTrue($"Unsuccessful at raising events. Status of events: {externalEvents.IsCompletedSuccessfully}"); - - // Wait up to 30 seconds for the workflow to complete and check the output - using var cts = new CancellationTokenSource(delay: TimeSpan.FromSeconds(30)); - getResponse = await daprClient.WaitForWorkflowCompletionAsync(instanceId2, workflowComponent, cts.Token); - var outputString = getResponse.Properties["dapr.workflow.output"]; - outputString.Should().Be("\"computers\"", $"Purchased item {outputString} was not correct"); - var deserializedOutput = getResponse.ReadOutputAs(); - deserializedOutput.Should().Be("computers", $"Deserialized output '{deserializedOutput}' was not expected"); - } - } -} diff --git a/test/Dapr.Extensions.Configuration.Test/Dapr.Extensions.Configuration.Test.csproj b/test/Dapr.Extensions.Configuration.Test/Dapr.Extensions.Configuration.Test.csproj index 7d11d5c40..ef6cfbcee 100644 --- a/test/Dapr.Extensions.Configuration.Test/Dapr.Extensions.Configuration.Test.csproj +++ b/test/Dapr.Extensions.Configuration.Test/Dapr.Extensions.Configuration.Test.csproj @@ -1,15 +1,15 @@  - + runtime; build; native; contentfiles; analyzers; buildtransitive all - - - - - + + + + + all runtime; build; native; contentfiles; analyzers; buildtransitive @@ -21,11 +21,13 @@ + + - + \ No newline at end of file diff --git a/test/Dapr.Extensions.Configuration.Test/DaprSecretStoreConfigurationProviderTest.cs b/test/Dapr.Extensions.Configuration.Test/DaprSecretStoreConfigurationProviderTest.cs index d35275dd1..fd323f989 100644 --- a/test/Dapr.Extensions.Configuration.Test/DaprSecretStoreConfigurationProviderTest.cs +++ b/test/Dapr.Extensions.Configuration.Test/DaprSecretStoreConfigurationProviderTest.cs @@ -16,6 +16,7 @@ using System.Net; using System.Threading.Tasks; using Dapr.Client; +using Dapr; using FluentAssertions; using Grpc.Net.Client; using Microsoft.Extensions.Configuration; @@ -40,11 +41,11 @@ public void AddDaprSecretStore_UsingDescriptors_WithoutStore_ReportsError() .Build(); var ex = Assert.Throws(() => - { - var config = CreateBuilder() - .AddDaprSecretStore(null, new DaprSecretDescriptor[] { new DaprSecretDescriptor("secretName") }, daprClient) - .Build(); - }); + { + CreateBuilder() + .AddDaprSecretStore(null, new DaprSecretDescriptor[] { new DaprSecretDescriptor("secretName") }, daprClient) + .Build(); + }); Assert.Contains("store", ex.Message); } @@ -57,11 +58,11 @@ public void AddDaprSecretStore_UsingDescriptors_WithEmptyStore_ReportsError() .Build(); var ex = Assert.Throws(() => - { - var config = CreateBuilder() - .AddDaprSecretStore(string.Empty, new DaprSecretDescriptor[] { new DaprSecretDescriptor("secretName") }, daprClient) - .Build(); - }); + { + CreateBuilder() + .AddDaprSecretStore(string.Empty, new DaprSecretDescriptor[] { new DaprSecretDescriptor("secretName") }, daprClient) + .Build(); + }); Assert.Contains("The value cannot be null or empty", ex.Message); } @@ -74,11 +75,11 @@ public void AddDaprSecretStore_UsingDescriptors_WithoutSecretDescriptors_Reports .Build(); var ex = Assert.Throws(() => - { - var config = CreateBuilder() - .AddDaprSecretStore("store", (DaprSecretDescriptor[])null, daprClient) - .Build(); - }); + { + CreateBuilder() + .AddDaprSecretStore("store", (DaprSecretDescriptor[])null, daprClient) + .Build(); + }); Assert.Contains("secretDescriptors", ex.Message); } @@ -87,11 +88,11 @@ public void AddDaprSecretStore_UsingDescriptors_WithoutSecretDescriptors_Reports public void AddDaprSecretStore_UsingDescriptors_WithoutClient_ReportsError() { var ex = Assert.Throws(() => - { - var config = CreateBuilder() - .AddDaprSecretStore("store", new DaprSecretDescriptor[] { new DaprSecretDescriptor("secretName") }, null) - .Build(); - }); + { + CreateBuilder() + .AddDaprSecretStore("store", new DaprSecretDescriptor[] { new DaprSecretDescriptor("secretName") }, null) + .Build(); + }); Assert.Contains("client", ex.Message); } @@ -104,11 +105,11 @@ public void AddDaprSecretStore_UsingDescriptors_WithZeroSecretDescriptors_Report .Build(); var ex = Assert.Throws(() => - { - var config = CreateBuilder() - .AddDaprSecretStore("store", new DaprSecretDescriptor[] { }, daprClient) - .Build(); - }); + { + CreateBuilder() + .AddDaprSecretStore("store", new DaprSecretDescriptor[] { }, daprClient) + .Build(); + }); Assert.Contains("No secret descriptor was provided", ex.Message); } @@ -131,11 +132,11 @@ public void AddDaprSecretStore_UsingDescriptors_DuplicateSecret_ReportsError() .Build(); var ex = Assert.Throws(() => - { - var config = CreateBuilder() - .AddDaprSecretStore("store", new DaprSecretDescriptor[] { new DaprSecretDescriptor("secretName") }, daprClient) - .Build(); - }); + { + CreateBuilder() + .AddDaprSecretStore("store", new DaprSecretDescriptor[] { new DaprSecretDescriptor("secretName") }, daprClient) + .Build(); + }); Assert.Contains("Please remove any duplicates from your secret store.", ex.Message); } @@ -198,6 +199,35 @@ public void LoadSecrets_FromSecretStoreThatCanReturnsMultipleValues() config[secondSecretKey].Should().Be(secondSecretValue); } + [Fact] + public void LoadSecrets_FromSecretStoreThatCanReturnsMultivaluedValues() + { + var storeName = "store"; + var parentSecretKey = "connectionStrings"; + var firstSecretKey = "first_secret"; + var secondSecretKey = "second_secret"; + var firstSecretValue = "secret1"; + var secondSecretValue = "secret2"; + + var secretDescriptors = new[] + { + new DaprSecretDescriptor(parentSecretKey) + }; + + var daprClient = new Mock(); + + daprClient.Setup(c => c.GetSecretAsync(storeName, parentSecretKey, + It.IsAny>(), default)) + .ReturnsAsync(new Dictionary { { firstSecretKey, firstSecretValue }, { secondSecretKey, secondSecretValue } }); + + var config = CreateBuilder() + .AddDaprSecretStore(storeName, secretDescriptors, daprClient.Object) + .Build(); + + config[firstSecretKey].Should().Be(firstSecretValue); + config[secondSecretKey].Should().Be(secondSecretValue); + } + [Fact] public void LoadSecrets_FromSecretStoreWithADifferentSecretKeyAndName() { @@ -280,7 +310,7 @@ public void LoadSecrets_FromSecretStoreRequiredAndDoesNotExist_ShouldThrowExcept var ex = Assert.Throws(() => { - var config = CreateBuilder() + CreateBuilder() .AddDaprSecretStore(storeName, secretDescriptors, daprClient) .Build(); }); @@ -297,11 +327,11 @@ public void AddDaprSecretStore_WithoutStore_ReportsError() .Build(); var ex = Assert.Throws(() => - { - var config = CreateBuilder() - .AddDaprSecretStore(null, daprClient) - .Build(); - }); + { + CreateBuilder() + .AddDaprSecretStore(null, daprClient) + .Build(); + }); Assert.Contains("store", ex.Message); } @@ -314,11 +344,11 @@ public void AddDaprSecretStore_WithEmptyStore_ReportsError() .Build(); var ex = Assert.Throws(() => - { - var config = CreateBuilder() - .AddDaprSecretStore(string.Empty, daprClient) - .Build(); - }); + { + CreateBuilder() + .AddDaprSecretStore(string.Empty, daprClient) + .Build(); + }); Assert.Contains("The value cannot be null or empty", ex.Message); } @@ -327,11 +357,11 @@ public void AddDaprSecretStore_WithEmptyStore_ReportsError() public void AddDaprSecretStore_WithoutClient_ReportsError() { var ex = Assert.Throws(() => - { - var config = CreateBuilder() - .AddDaprSecretStore("store", null) - .Build(); - }); + { + CreateBuilder() + .AddDaprSecretStore("store", null) + .Build(); + }); Assert.Contains("client", ex.Message); } @@ -354,11 +384,11 @@ public void AddDaprSecretStore_DuplicateSecret_ReportsError() .Build(); var ex = Assert.Throws(() => - { - var config = CreateBuilder() - .AddDaprSecretStore("store", daprClient) - .Build(); - }); + { + CreateBuilder() + .AddDaprSecretStore("store", daprClient) + .Build(); + }); Assert.Contains("Please remove any duplicates from your secret store.", ex.Message); } diff --git a/test/Directory.Build.props b/test/Directory.Build.props index 0ce23c19e..50b029a12 100644 --- a/test/Directory.Build.props +++ b/test/Directory.Build.props @@ -1,9 +1,9 @@ - + net6;net7;net8 - + $(RepoRoot)bin\$(Configuration)\test\$(MSBuildProjectName)\ @@ -12,6 +12,6 @@ - + \ No newline at end of file diff --git a/test/Shared/TestClient.cs b/test/Shared/TestClient.cs index 350c4c6e6..c84fe4c9e 100644 --- a/test/Shared/TestClient.cs +++ b/test/Shared/TestClient.cs @@ -1,4 +1,4 @@ -// ------------------------------------------------------------------------ +// ------------------------------------------------------------------------ // Copyright 2021 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. @@ -394,7 +394,7 @@ public async Task> CaptureGrpcRequestAsync(Func