From 21b41c8fa868438049c3196251f4aa903a3d4aaa Mon Sep 17 00:00:00 2001 From: Aaron Powell Date: Wed, 15 Jan 2025 12:00:03 +1100 Subject: [PATCH 01/10] Dapr migration initial import (#378) * Dropping files from Aspire repo * Moving to the right namespace/folder naming * Adding to solution and getting it to compile * Adding in the Dapr tests from the Aspire repo Had to remove the DaprSchemaTests file as we can't do schema tests (missing a lot of infrastructure from the Aspire repo). Had to edit the DaprTests to not use EnvironmentVariableEvaluator, which we can't leverage as it uses some internal types from Aspire.Hosting. This means that our testing of the environment variables is slightly different, and the values we assert against are not the docker internal host endpoints, but the public endpoints --- CommunityToolkit.Aspire.sln | 14 + .../CommandLineBuilder.cs | 232 ++++++++ ...ommunityToolkit.Aspire.Hosting.Dapr.csproj | 15 + .../DaprComponentOptions.cs | 18 + .../DaprComponentReferenceAnnotation.cs | 14 + .../DaprComponentResource.cs | 28 + .../DaprConstants.cs | 14 + ...DaprDistributedApplicationLifecycleHook.cs | 498 ++++++++++++++++++ .../DaprOptions.cs | 23 + .../DaprSidecarAnnotation.cs | 13 + .../DaprSidecarOptions.cs | 176 +++++++ .../DaprSidecarOptionsAnnotation.cs | 13 + .../DaprSidecarResource.cs | 11 + .../IDaprComponentResource.cs | 25 + .../IDaprSidecarResource.cs | 13 + ...DistributedApplicationBuilderExtensions.cs | 94 ++++ ...edApplicationComponentBuilderExtensions.cs | 91 ++++ .../PublicAPI.Shipped.txt | 100 ++++ .../PublicAPI.Unshipped.txt | 3 + .../README.md | 39 ++ src/Shared/Utf8JsonWriterExtensions.cs | 100 ++++ ...tyToolkit.Aspire.Hosting.Dapr.Tests.csproj | 8 + .../DaprTests.cs | 172 ++++++ 23 files changed, 1714 insertions(+) create mode 100644 src/CommunityToolkit.Aspire.Hosting.Dapr/CommandLineBuilder.cs create mode 100644 src/CommunityToolkit.Aspire.Hosting.Dapr/CommunityToolkit.Aspire.Hosting.Dapr.csproj create mode 100644 src/CommunityToolkit.Aspire.Hosting.Dapr/DaprComponentOptions.cs create mode 100644 src/CommunityToolkit.Aspire.Hosting.Dapr/DaprComponentReferenceAnnotation.cs create mode 100644 src/CommunityToolkit.Aspire.Hosting.Dapr/DaprComponentResource.cs create mode 100644 src/CommunityToolkit.Aspire.Hosting.Dapr/DaprConstants.cs create mode 100644 src/CommunityToolkit.Aspire.Hosting.Dapr/DaprDistributedApplicationLifecycleHook.cs create mode 100644 src/CommunityToolkit.Aspire.Hosting.Dapr/DaprOptions.cs create mode 100644 src/CommunityToolkit.Aspire.Hosting.Dapr/DaprSidecarAnnotation.cs create mode 100644 src/CommunityToolkit.Aspire.Hosting.Dapr/DaprSidecarOptions.cs create mode 100644 src/CommunityToolkit.Aspire.Hosting.Dapr/DaprSidecarOptionsAnnotation.cs create mode 100644 src/CommunityToolkit.Aspire.Hosting.Dapr/DaprSidecarResource.cs create mode 100644 src/CommunityToolkit.Aspire.Hosting.Dapr/IDaprComponentResource.cs create mode 100644 src/CommunityToolkit.Aspire.Hosting.Dapr/IDaprSidecarResource.cs create mode 100644 src/CommunityToolkit.Aspire.Hosting.Dapr/IDistributedApplicationBuilderExtensions.cs create mode 100644 src/CommunityToolkit.Aspire.Hosting.Dapr/IDistributedApplicationComponentBuilderExtensions.cs create mode 100644 src/CommunityToolkit.Aspire.Hosting.Dapr/PublicAPI.Shipped.txt create mode 100644 src/CommunityToolkit.Aspire.Hosting.Dapr/PublicAPI.Unshipped.txt create mode 100644 src/CommunityToolkit.Aspire.Hosting.Dapr/README.md create mode 100644 src/Shared/Utf8JsonWriterExtensions.cs create mode 100644 tests/CommunityToolkit.Aspire.Hosting.Dapr.Tests/CommunityToolkit.Aspire.Hosting.Dapr.Tests.csproj create mode 100644 tests/CommunityToolkit.Aspire.Hosting.Dapr.Tests/DaprTests.cs diff --git a/CommunityToolkit.Aspire.sln b/CommunityToolkit.Aspire.sln index b79289ed..d4494f34 100644 --- a/CommunityToolkit.Aspire.sln +++ b/CommunityToolkit.Aspire.sln @@ -221,6 +221,10 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "CommunityToolkit.Aspire.Mic EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "CommunityToolkit.Aspire.Microsoft.EntityFrameworkCore.Sqlite.Tests", "tests\CommunityToolkit.Aspire.Microsoft.EntityFrameworkCore.Sqlite.Tests\CommunityToolkit.Aspire.Microsoft.EntityFrameworkCore.Sqlite.Tests.csproj", "{52846E18-99D1-4040-AF5F-17FC69198BCE}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "CommunityToolkit.Aspire.Hosting.Dapr", "src\CommunityToolkit.Aspire.Hosting.Dapr\CommunityToolkit.Aspire.Hosting.Dapr.csproj", "{2165F65B-83F2-4269-8781-86AB6ACF043D}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "CommunityToolkit.Aspire.Hosting.Dapr.Tests", "tests\CommunityToolkit.Aspire.Hosting.Dapr.Tests\CommunityToolkit.Aspire.Hosting.Dapr.Tests.csproj", "{B2384D1A-DD13-4D03-B8FE-B194DEF71A0C}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -579,6 +583,14 @@ Global {52846E18-99D1-4040-AF5F-17FC69198BCE}.Debug|Any CPU.Build.0 = Debug|Any CPU {52846E18-99D1-4040-AF5F-17FC69198BCE}.Release|Any CPU.ActiveCfg = Release|Any CPU {52846E18-99D1-4040-AF5F-17FC69198BCE}.Release|Any CPU.Build.0 = Release|Any CPU + {2165F65B-83F2-4269-8781-86AB6ACF043D}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {2165F65B-83F2-4269-8781-86AB6ACF043D}.Debug|Any CPU.Build.0 = Debug|Any CPU + {2165F65B-83F2-4269-8781-86AB6ACF043D}.Release|Any CPU.ActiveCfg = Release|Any CPU + {2165F65B-83F2-4269-8781-86AB6ACF043D}.Release|Any CPU.Build.0 = Release|Any CPU + {B2384D1A-DD13-4D03-B8FE-B194DEF71A0C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {B2384D1A-DD13-4D03-B8FE-B194DEF71A0C}.Debug|Any CPU.Build.0 = Debug|Any CPU + {B2384D1A-DD13-4D03-B8FE-B194DEF71A0C}.Release|Any CPU.ActiveCfg = Release|Any CPU + {B2384D1A-DD13-4D03-B8FE-B194DEF71A0C}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -700,6 +712,8 @@ Global {0E6EBCFB-DEF5-496C-95AF-00884826CFC8} = {899F0713-7FC6-4750-BAFC-AC650B35B453} {861FE61C-90EE-49B0-BCC8-8417C293CC21} = {899F0713-7FC6-4750-BAFC-AC650B35B453} {52846E18-99D1-4040-AF5F-17FC69198BCE} = {899F0713-7FC6-4750-BAFC-AC650B35B453} + {2165F65B-83F2-4269-8781-86AB6ACF043D} = {414151D4-7009-4E78-A5C6-D99EBD1E67D1} + {B2384D1A-DD13-4D03-B8FE-B194DEF71A0C} = {899F0713-7FC6-4750-BAFC-AC650B35B453} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {08B1D4B8-D2C5-4A64-BB8B-E1A2B29525F0} diff --git a/src/CommunityToolkit.Aspire.Hosting.Dapr/CommandLineBuilder.cs b/src/CommunityToolkit.Aspire.Hosting.Dapr/CommandLineBuilder.cs new file mode 100644 index 00000000..ad5a8b73 --- /dev/null +++ b/src/CommunityToolkit.Aspire.Hosting.Dapr/CommandLineBuilder.cs @@ -0,0 +1,232 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Globalization; +using System.Text; + +namespace CommunityToolkit.Aspire.Hosting.Dapr; + +internal delegate IEnumerable CommandLineArgBuilder(); + +internal sealed record CommandLine(string FileName, IEnumerable Arguments) +{ + public string ArgumentString + { + get + { + StringBuilder builder = new(); + + var args = this.Arguments.ToList(); + + for (int i = 0; i < args.Count; i++) + { + var arg = args[i]; + + if (arg is not null) + { + if (i > 0) + { + builder.Append(' '); + } + + builder.Append(arg); + } + } + + return builder.ToString(); + } + } +} + +internal static class CommandLineBuilder +{ + public static CommandLine Create(string fileName, params CommandLineArgBuilder[] argBuilders) + { + return new CommandLine(fileName, argBuilders.SelectMany(builder => builder())); + } +} + +internal static class CommandLineArgs +{ + public static CommandLineArgBuilder Args(params string[] args) + { + return Args((IEnumerable)args); + } + + public static CommandLineArgBuilder Args(IEnumerable? args) + { + return () => (args ?? Enumerable.Empty()); + } + + public static CommandLineArgBuilder Command(params string[] commands) + { + return () => commands; + } + + public static CommandLineArgBuilder Command(CommandLine commandLine) + { + return Command(new[] { commandLine.FileName, commandLine.ArgumentString }); + } + + public static CommandLineArgBuilder Flag(string name) + { + return Flag(name, true); + } + + public static CommandLineArgBuilder Flag(string name, bool? value) + { + return () => value == true ? new[] { name } : Enumerable.Empty(); + } + + public static CommandLineArgBuilder NamedArg(string name, T value, bool assignValue = false) where T : struct + { + return () => + { + string? stringValue = Convert.ToString(value, CultureInfo.InvariantCulture); + + return stringValue is not null + ? NamedStringArg(name, stringValue, assignValue)() + : Enumerable.Empty(); + }; + } + + public static CommandLineArgBuilder NamedArg(string name, T? value, bool assignValue = false) where T : struct + { + return () => + value.HasValue + ? NamedArg(name, value.Value, assignValue)() + : Enumerable.Empty(); + } + + public static CommandLineArgBuilder NamedArg(string name, string? value, bool assignValue = false) + { + return () => + { + return value is not null + ? NamedStringArg(name, value, assignValue)() + : Enumerable.Empty(); + }; + } + + public static CommandLineArgBuilder ModelNamedArg(string name, T value, bool assignValue = false) where T : struct + { + return () => + { + string? stringValue = Convert.ToString(value, CultureInfo.InvariantCulture); + + return stringValue is not null + ? ModelNamedStringArg(name, stringValue, assignValue)() + : Enumerable.Empty(); + }; + } + + public static CommandLineArgBuilder ModelNamedArg(string name, T? value, bool assignValue = false) where T : struct + { + return () => + value.HasValue + ? ModelNamedArg(name, value.Value, assignValue)() + : Enumerable.Empty(); + } + + public static CommandLineArgBuilder ModelNamedArg(string name, string? value, bool assignValue = false) + { + return () => + { + return value is not null + ? ModelNamedStringArg(name, value, assignValue)() + : Enumerable.Empty(); + }; + } + + public static CommandLineArgBuilder NamedArg(string name, IEnumerable? values, bool assignValue = false) + { + return () => + { + return (values ?? Enumerable.Empty()).SelectMany(value => NamedArg(name, value, assignValue)()); + }; + } + + public static CommandLineArgBuilder ModelNamedArg(string name, IEnumerable? values, bool assignValue = false) + { + return () => + { + return (values ?? Enumerable.Empty()).SelectMany(value => ModelNamedArg(name, value, assignValue)()); + }; + } + + private static readonly char[] s_reservedChars = new[] { ' ', '&', '|', '(', ')', '<', '>', '^' }; + + private static CommandLineArgBuilder NamedStringArg(string name, string value, bool assignValue) + { + bool hasReservedChars = value.Any(c => s_reservedChars.Contains(c)) == true; + + return () => + { + return assignValue + ? new[] { $"\"{name}={value}\"" } + : new[] { name, hasReservedChars ? $"\"{value}\"" : value }; + }; + } + + private static CommandLineArgBuilder ModelNamedStringArg(string name, string value, bool assignValue) + { + return () => + { + return assignValue + ? new[] { $"{name}={value}" } + : new[] { name, value }; + }; + } + + public static CommandLineArgBuilder ModelNamedObjectArg(string name, object value) + { + return () => + { + return [name, value]; + }; + } + + public static CommandLineArgBuilder ModelNamedObjectArg(string name, object value, bool assignValue = false) where T : struct + { + return () => + { + string? stringValue = Convert.ToString(value, CultureInfo.InvariantCulture); + + return stringValue is not null + ? ModelNamedStringArg(name, stringValue, assignValue)() + : Enumerable.Empty(); + }; + } + + public static CommandLineArgBuilder PostOptionsArgs(params CommandLineArgBuilder[] args) + { + return PostOptionsArgs(null, args); + } + + public static CommandLineArgBuilder PostOptionsArgs(string? separator, params CommandLineArgBuilder[] args) + { + return PostOptionsArgs(separator, (IEnumerable)args); + } + + public static CommandLineArgBuilder PostOptionsArgs(string? separator, IEnumerable args) + { + IEnumerable GeneratePostOptionsArgs() + { + bool postOptions = false; + + foreach (var arg in args.SelectMany(builder => builder())) + { + if (!postOptions) + { + postOptions = true; + + yield return separator ?? "--"; + } + + yield return arg; + } + } + + return GeneratePostOptionsArgs; + } +} diff --git a/src/CommunityToolkit.Aspire.Hosting.Dapr/CommunityToolkit.Aspire.Hosting.Dapr.csproj b/src/CommunityToolkit.Aspire.Hosting.Dapr/CommunityToolkit.Aspire.Hosting.Dapr.csproj new file mode 100644 index 00000000..07c92f43 --- /dev/null +++ b/src/CommunityToolkit.Aspire.Hosting.Dapr/CommunityToolkit.Aspire.Hosting.Dapr.csproj @@ -0,0 +1,15 @@ + + + + aspire integration hosting dapr + Dapr support for .NET Aspire. + + + + + + + + + + diff --git a/src/CommunityToolkit.Aspire.Hosting.Dapr/DaprComponentOptions.cs b/src/CommunityToolkit.Aspire.Hosting.Dapr/DaprComponentOptions.cs new file mode 100644 index 00000000..27d3de2d --- /dev/null +++ b/src/CommunityToolkit.Aspire.Hosting.Dapr/DaprComponentOptions.cs @@ -0,0 +1,18 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +namespace CommunityToolkit.Aspire.Hosting.Dapr; + +/// +/// Options for configuring a Dapr component. +/// +public sealed record DaprComponentOptions +{ + /// + /// Gets or sets the path to the component configuration file. + /// + /// + /// If specified, the folder containing the configuration file will be added to all associated Dapr sidecars' resources paths. + /// + public string? LocalPath { get; init; } +} diff --git a/src/CommunityToolkit.Aspire.Hosting.Dapr/DaprComponentReferenceAnnotation.cs b/src/CommunityToolkit.Aspire.Hosting.Dapr/DaprComponentReferenceAnnotation.cs new file mode 100644 index 00000000..fad4bbf3 --- /dev/null +++ b/src/CommunityToolkit.Aspire.Hosting.Dapr/DaprComponentReferenceAnnotation.cs @@ -0,0 +1,14 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using Aspire.Hosting.ApplicationModel; + +namespace CommunityToolkit.Aspire.Hosting.Dapr; + +/// +/// Indicates that a Dapr component should be used with the sidecar for the associated resource. +/// +/// The Dapr component to use. +public sealed record DaprComponentReferenceAnnotation(IDaprComponentResource Component) : IResourceAnnotation +{ +} diff --git a/src/CommunityToolkit.Aspire.Hosting.Dapr/DaprComponentResource.cs b/src/CommunityToolkit.Aspire.Hosting.Dapr/DaprComponentResource.cs new file mode 100644 index 00000000..6a05e1eb --- /dev/null +++ b/src/CommunityToolkit.Aspire.Hosting.Dapr/DaprComponentResource.cs @@ -0,0 +1,28 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using Aspire.Hosting.ApplicationModel; + +namespace CommunityToolkit.Aspire.Hosting.Dapr; + +/// +/// Represents a Dapr component resource. +/// +public sealed class DaprComponentResource : Resource, IDaprComponentResource +{ + /// + /// Initializes a new instance of . + /// + /// The resource name. + /// The Dapr component type. This may be a generic "state" or "pubsub" if Aspire should choose an appropriate type when running or deploying. + public DaprComponentResource(string name, string type) : base(name) + { + this.Type = type; + } + + /// + public string Type { get; } + + /// + public DaprComponentOptions? Options { get; init; } +} diff --git a/src/CommunityToolkit.Aspire.Hosting.Dapr/DaprConstants.cs b/src/CommunityToolkit.Aspire.Hosting.Dapr/DaprConstants.cs new file mode 100644 index 00000000..8a720d0b --- /dev/null +++ b/src/CommunityToolkit.Aspire.Hosting.Dapr/DaprConstants.cs @@ -0,0 +1,14 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +namespace CommunityToolkit.Aspire.Hosting.Dapr; + +internal static class DaprConstants +{ + public static class BuildingBlocks + { + public const string PubSub = "pubsub"; + + public const string StateStore = "state"; + } +} diff --git a/src/CommunityToolkit.Aspire.Hosting.Dapr/DaprDistributedApplicationLifecycleHook.cs b/src/CommunityToolkit.Aspire.Hosting.Dapr/DaprDistributedApplicationLifecycleHook.cs new file mode 100644 index 00000000..d27cb849 --- /dev/null +++ b/src/CommunityToolkit.Aspire.Hosting.Dapr/DaprDistributedApplicationLifecycleHook.cs @@ -0,0 +1,498 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using Aspire.Hosting; +using Aspire.Hosting.ApplicationModel; +using Aspire.Hosting.Lifecycle; +using Aspire.Hosting.Utils; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.Hosting; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Options; +using System.Diagnostics.CodeAnalysis; +using System.Globalization; +using System.Net.Sockets; +using static CommunityToolkit.Aspire.Hosting.Dapr.CommandLineArgs; + +namespace CommunityToolkit.Aspire.Hosting.Dapr; + +internal sealed class DaprDistributedApplicationLifecycleHook : IDistributedApplicationLifecycleHook, IDisposable +{ + private readonly IConfiguration _configuration; + private readonly IHostEnvironment _environment; + private readonly ILogger _logger; + private readonly DaprOptions _options; + + private string? _onDemandResourcesRootPath; + + public DaprDistributedApplicationLifecycleHook(IConfiguration configuration, IHostEnvironment environment, ILogger logger, IOptions options) + { + _configuration = configuration; + _environment = environment; + _logger = logger; + _options = options.Value; + } + + public async Task BeforeStartAsync(DistributedApplicationModel appModel, CancellationToken cancellationToken = default) + { + string appHostDirectory = _configuration["AppHost:Directory"] ?? throw new InvalidOperationException("Unable to obtain the application host directory."); + + var onDemandResourcesPaths = await StartOnDemandDaprComponentsAsync(appModel, cancellationToken).ConfigureAwait(false); + + var sideCars = new List(); + + var fileName = this._options.DaprPath + ?? GetDefaultDaprPath() + ?? throw new DistributedApplicationException("Unable to locate the Dapr CLI."); + + foreach (var resource in appModel.Resources) + { + if (!resource.TryGetLastAnnotation(out var daprAnnotation)) + { + continue; + } + + var daprSidecar = daprAnnotation.Sidecar; + + var sidecarOptionsAnnotation = daprSidecar.Annotations.OfType().LastOrDefault(); + + var sidecarOptions = sidecarOptionsAnnotation?.Options; + + [return: NotNullIfNotNull(nameof(path))] + string? NormalizePath(string? path) + { + if (path is null) + { + return null; + } + + return Path.GetFullPath(Path.Combine(appHostDirectory, path)); + } + + var aggregateResourcesPaths = sidecarOptions?.ResourcesPaths.Select(path => NormalizePath(path)).ToHashSet() ?? new HashSet(); + + var componentReferenceAnnotations = resource.Annotations.OfType(); + + var waitAnnotationsToCopyToDaprCli = new List(); + + foreach (var componentReferenceAnnotation in componentReferenceAnnotations) + { + // Whilst we are passing over each component annotations collect the list of annotations to copy to the Dapr CLI. + if (componentReferenceAnnotation.Component.TryGetAnnotationsOfType(out var componentWaitAnnotations)) + { + waitAnnotationsToCopyToDaprCli.AddRange(componentWaitAnnotations); + } + + if (componentReferenceAnnotation.Component.Options?.LocalPath is not null) + { + var localPathDirectory = Path.GetDirectoryName(NormalizePath(componentReferenceAnnotation.Component.Options.LocalPath)); + + if (localPathDirectory is not null) + { + aggregateResourcesPaths.Add(localPathDirectory); + } + } + else if (onDemandResourcesPaths.TryGetValue(componentReferenceAnnotation.Component.Name, out var onDemandResourcesPath)) + { + string onDemandResourcesPathDirectory = Path.GetDirectoryName(onDemandResourcesPath)!; + + if (onDemandResourcesPathDirectory is not null) + { + aggregateResourcesPaths.Add(onDemandResourcesPathDirectory); + } + } + } + + // It is possible that we have duplicate wate annotations so we just dedupe them here. + var distinctWaitAnnotationsToCopyToDaprCli = waitAnnotationsToCopyToDaprCli.DistinctBy(w => (w.Resource, w.WaitType)); + + var daprAppPortArg = (int? port) => ModelNamedArg("--app-port", port); + var daprGrpcPortArg = (object port) => ModelNamedObjectArg("--dapr-grpc-port", port); + var daprHttpPortArg = (object port) => ModelNamedObjectArg("--dapr-http-port", port); + var daprMetricsPortArg = (object port) => ModelNamedObjectArg("--metrics-port", port); + var daprProfilePortArg = (object port) => ModelNamedObjectArg("--profile-port", port); + var daprAppChannelAddressArg = (string? address) => ModelNamedArg("--app-channel-address", address); + var daprAppProtocol = (string? protocol) => ModelNamedArg("--app-protocol", protocol); + + var appId = sidecarOptions?.AppId ?? resource.Name; + + var daprCommandLine = + CommandLineBuilder + .Create( + fileName, + Command("run"), + daprAppPortArg(sidecarOptions?.AppPort), + ModelNamedArg("--app-channel-address", sidecarOptions?.AppChannelAddress), + ModelNamedArg("--app-health-check-path", sidecarOptions?.AppHealthCheckPath), + ModelNamedArg("--app-health-probe-interval", sidecarOptions?.AppHealthProbeInterval), + ModelNamedArg("--app-health-probe-timeout", sidecarOptions?.AppHealthProbeTimeout), + ModelNamedArg("--app-health-threshold", sidecarOptions?.AppHealthThreshold), + ModelNamedArg("--app-id", appId), + ModelNamedArg("--app-max-concurrency", sidecarOptions?.AppMaxConcurrency), + ModelNamedArg("--app-protocol", sidecarOptions?.AppProtocol), + ModelNamedArg("--config", NormalizePath(sidecarOptions?.Config)), + ModelNamedArg("--dapr-http-max-request-size", sidecarOptions?.DaprHttpMaxRequestSize), + ModelNamedArg("--dapr-http-read-buffer-size", sidecarOptions?.DaprHttpReadBufferSize), + ModelNamedArg("--dapr-internal-grpc-port", sidecarOptions?.DaprInternalGrpcPort), + ModelNamedArg("--dapr-listen-addresses", sidecarOptions?.DaprListenAddresses), + Flag("--enable-api-logging", sidecarOptions?.EnableApiLogging), + Flag("--enable-app-health-check", sidecarOptions?.EnableAppHealthCheck), + Flag("--enable-profiling", sidecarOptions?.EnableProfiling), + ModelNamedArg("--log-level", sidecarOptions?.LogLevel), + ModelNamedArg("--placement-host-address", sidecarOptions?.PlacementHostAddress), + ModelNamedArg("--resources-path", aggregateResourcesPaths), + ModelNamedArg("--run-file", NormalizePath(sidecarOptions?.RunFile)), + ModelNamedArg("--runtime-path", NormalizePath(sidecarOptions?.RuntimePath)), + ModelNamedArg("--scheduler-host-address", sidecarOptions?.SchedulerHostAddress), + ModelNamedArg("--unix-domain-socket", sidecarOptions?.UnixDomainSocket), + PostOptionsArgs(Args(sidecarOptions?.Command))); + + var daprCliResourceName = $"{daprSidecar.Name}-cli"; + var daprCli = new ExecutableResource(daprCliResourceName, fileName, appHostDirectory); + + // Add all the unique wait annotations to the CLI. + daprCli.Annotations.AddRange(distinctWaitAnnotationsToCopyToDaprCli); + + resource.Annotations.Add( + new EnvironmentCallbackAnnotation( + context => + { + if (context.ExecutionContext.IsPublishMode) + { + return; + } + + var http = daprCli.GetEndpoint("http"); + var grpc = daprCli.GetEndpoint("grpc"); + + context.EnvironmentVariables.TryAdd("DAPR_HTTP_PORT", http.Port.ToString(CultureInfo.InvariantCulture)); + context.EnvironmentVariables.TryAdd("DAPR_GRPC_PORT", grpc.Port.ToString(CultureInfo.InvariantCulture)); + + context.EnvironmentVariables.TryAdd("DAPR_GRPC_ENDPOINT", grpc); + context.EnvironmentVariables.TryAdd("DAPR_HTTP_ENDPOINT", http); + })); + + daprCli.Annotations.Add(new EndpointAnnotation(ProtocolType.Tcp, uriScheme: "http", name: "grpc", port: sidecarOptions?.DaprGrpcPort)); + daprCli.Annotations.Add(new EndpointAnnotation(ProtocolType.Tcp, uriScheme: "http", name: "http", port: sidecarOptions?.DaprHttpPort)); + daprCli.Annotations.Add(new EndpointAnnotation(ProtocolType.Tcp, uriScheme: "http", name: "metrics", port: sidecarOptions?.MetricsPort)); + if (sidecarOptions?.EnableProfiling == true) + { + daprCli.Annotations.Add(new EndpointAnnotation(ProtocolType.Tcp, name: "profile", port: sidecarOptions?.ProfilePort, uriScheme: "http")); + } + + // NOTE: Telemetry is enabled by default. + if (this._options.EnableTelemetry != false) + { + OtlpConfigurationExtensions.AddOtlpEnvironment(daprCli, _configuration, _environment); + } + + daprCli.Annotations.Add( + new CommandLineArgsCallbackAnnotation( + updatedArgs => + { + updatedArgs.AddRange(daprCommandLine.Arguments); + var endPoint = GetEndpointReference(sidecarOptions, resource); + + if (sidecarOptions?.AppPort is null && endPoint is { appEndpoint.IsAllocated: true }) + { + updatedArgs.AddRange(daprAppPortArg(endPoint.Value.appEndpoint.Port)()); + } + + var grpc = daprCli.GetEndpoint("grpc"); + var http = daprCli.GetEndpoint("http"); + var metrics = daprCli.GetEndpoint("metrics"); + + updatedArgs.AddRange(daprGrpcPortArg(grpc.Property(EndpointProperty.TargetPort))()); + updatedArgs.AddRange(daprHttpPortArg(http.Property(EndpointProperty.TargetPort))()); + updatedArgs.AddRange(daprMetricsPortArg(metrics.Property(EndpointProperty.TargetPort))()); + + if (sidecarOptions?.EnableProfiling == true) + { + var profiling = daprCli.GetEndpoint("profiling"); + + updatedArgs.AddRange(daprProfilePortArg(profiling.Property(EndpointProperty.TargetPort))()); + } + + if (sidecarOptions?.AppChannelAddress is null && endPoint is { appEndpoint.IsAllocated: true }) + { + updatedArgs.AddRange(daprAppChannelAddressArg(endPoint.Value.appEndpoint.Host)()); + } + if (sidecarOptions?.AppProtocol is null && endPoint is { appEndpoint.IsAllocated: true }) + { + updatedArgs.AddRange(daprAppProtocol(endPoint.Value.protocol)()); + } + })); + + // Apply environment variables to the CLI... + daprCli.Annotations.AddRange(daprSidecar.Annotations.OfType()); + + // The CLI is an artifact of a local run, so it should not be published... + daprCli.Annotations.Add(ManifestPublishingCallbackAnnotation.Ignore); + + daprSidecar.Annotations.Add( + new ManifestPublishingCallbackAnnotation( + context => + { + context.Writer.WriteString("type", "dapr.v0"); + context.Writer.WriteStartObject("dapr"); + + context.Writer.WriteString("application", resource.Name); + context.Writer.TryWriteString("appChannelAddress", sidecarOptions?.AppChannelAddress); + context.Writer.TryWriteString("appHealthCheckPath", sidecarOptions?.AppHealthCheckPath); + context.Writer.TryWriteNumber("appHealthProbeInterval", sidecarOptions?.AppHealthProbeInterval); + context.Writer.TryWriteNumber("appHealthProbeTimeout", sidecarOptions?.AppHealthProbeTimeout); + context.Writer.TryWriteNumber("appHealthThreshold", sidecarOptions?.AppHealthThreshold); + context.Writer.TryWriteString("appId", appId); + context.Writer.TryWriteNumber("appMaxConcurrency", sidecarOptions?.AppMaxConcurrency); + context.Writer.TryWriteNumber("appPort", sidecarOptions?.AppPort); + context.Writer.TryWriteString("appProtocol", sidecarOptions?.AppProtocol); + context.Writer.TryWriteStringArray("command", sidecarOptions?.Command); + context.Writer.TryWriteStringArray("components", componentReferenceAnnotations.Select(componentReferenceAnnotation => componentReferenceAnnotation.Component.Name)); + context.Writer.TryWriteString("config", context.GetManifestRelativePath(sidecarOptions?.Config)); + context.Writer.TryWriteNumber("daprGrpcPort", sidecarOptions?.DaprGrpcPort); + context.Writer.TryWriteNumber("daprHttpMaxRequestSize", sidecarOptions?.DaprHttpMaxRequestSize); + context.Writer.TryWriteNumber("daprHttpPort", sidecarOptions?.DaprHttpPort); + context.Writer.TryWriteNumber("daprHttpReadBufferSize", sidecarOptions?.DaprHttpReadBufferSize); + context.Writer.TryWriteNumber("daprInternalGrpcPort", sidecarOptions?.DaprInternalGrpcPort); + context.Writer.TryWriteString("daprListenAddresses", sidecarOptions?.DaprListenAddresses); + context.Writer.TryWriteBoolean("enableApiLogging", sidecarOptions?.EnableApiLogging); + context.Writer.TryWriteBoolean("enableAppHealthCheck", sidecarOptions?.EnableAppHealthCheck); + context.Writer.TryWriteString("logLevel", sidecarOptions?.LogLevel); + context.Writer.TryWriteNumber("metricsPort", sidecarOptions?.MetricsPort); + context.Writer.TryWriteString("placementHostAddress", sidecarOptions?.PlacementHostAddress); + context.Writer.TryWriteNumber("profilePort", sidecarOptions?.ProfilePort); + context.Writer.TryWriteStringArray("resourcesPath", sidecarOptions?.ResourcesPaths.Select(path => context.GetManifestRelativePath(path))); + context.Writer.TryWriteString("runFile", context.GetManifestRelativePath(sidecarOptions?.RunFile)); + context.Writer.TryWriteString("runtimePath", context.GetManifestRelativePath(sidecarOptions?.RuntimePath)); + context.Writer.TryWriteString("schedulerHostAddress", sidecarOptions?.SchedulerHostAddress); + context.Writer.TryWriteString("unixDomainSocket", sidecarOptions?.UnixDomainSocket); + + context.Writer.WriteEndObject(); + })); + + sideCars.Add(daprCli); + } + + appModel.Resources.AddRange(sideCars); + } + + // This method resolves the application's endpoint and the protocol that the dapr side car will use. + // It depends on DaprSidecarOptions.AppProtocol and DaprSidecarOptions.AppEndpoint. + // - If both are null default to 'http' for both. + // - If AppProtocol is not null try to get an endpoint with the name of the protocol. + // - if AppEndpoint is not null try to use the scheme as the protocol. + // - if both are not null just use both options. + static (EndpointReference appEndpoint, string protocol)? GetEndpointReference(DaprSidecarOptions? sidecarOptions, IResource resource) + { + if (resource is IResourceWithEndpoints resourceWithEndpoints) + { + return (sidecarOptions?.AppProtocol, sidecarOptions?.AppEndpoint) switch + { + (null, null) => (resourceWithEndpoints.GetEndpoint("http"), "http"), + (null, string appEndpoint) => (resourceWithEndpoints.GetEndpoint(appEndpoint), resourceWithEndpoints.GetEndpoint(appEndpoint).Scheme), + (string appProtocol, null) => (resourceWithEndpoints.GetEndpoint(appProtocol), appProtocol), + (string appProtocol, string appEndpoint) => (resourceWithEndpoints.GetEndpoint(appEndpoint), appProtocol) + }; + } + return null; + } + + /// + /// Return the first verified dapr path + /// + static string? GetDefaultDaprPath() + { + foreach (var path in GetAvailablePaths()) + { + if (File.Exists(path)) + { + return path; + } + } + + return default; + + // Return all the possible paths for dapr + static IEnumerable GetAvailablePaths() + { + if (OperatingSystem.IsWindows()) + { + var pathRoot = Path.GetPathRoot(Environment.GetFolderPath(Environment.SpecialFolder.Windows)) ?? "C:"; + + // Installed windows paths: + yield return Path.Combine(pathRoot, "dapr", "dapr.exe"); + + yield break; + } + + // Add $HOME/dapr path: + var homePath = Environment.GetFolderPath(Environment.SpecialFolder.UserProfile); + yield return Path.Combine(homePath, "dapr", "dapr"); + + // Linux & MacOS path: + yield return "/usr/local/bin/dapr"; + + // Arch Linux path: + yield return "/usr/bin/dapr"; + + // MacOS Homebrew path: + if (OperatingSystem.IsMacOS() && Environment.GetEnvironmentVariable("HOMEBREW_PREFIX") is string homebrewPrefix) + { + yield return Path.Combine(homebrewPrefix, "bin", "dapr"); + } + } + } + + public void Dispose() + { + if (_onDemandResourcesRootPath is not null) + { + _logger.LogInformation("Stopping Dapr-related resources..."); + + try + { + Directory.Delete(_onDemandResourcesRootPath, recursive: true); + } + catch (Exception ex) + { + _logger.LogWarning(ex, "Failed to delete temporary Dapr resources directory: {OnDemandResourcesRootPath}", _onDemandResourcesRootPath); + } + } + } + + private async Task> StartOnDemandDaprComponentsAsync(DistributedApplicationModel appModel, CancellationToken cancellationToken) + { + var onDemandComponents = + appModel + .Resources + .OfType() + .Where(component => component.Options?.LocalPath is null) + .ToList(); + + var onDemandResourcesPaths = new Dictionary(); + + if (onDemandComponents.Any()) + { + _logger.LogInformation("Starting Dapr-related resources..."); + + _onDemandResourcesRootPath = Directory.CreateTempSubdirectory("aspire-dapr.").FullName; + + foreach (var component in onDemandComponents) + { + Func> contentWriter = + async content => + { + string componentDirectory = Path.Combine(_onDemandResourcesRootPath, component.Name); + + Directory.CreateDirectory(componentDirectory); + + string componentPath = Path.Combine(componentDirectory, $"{component.Name}.yaml"); + + await File.WriteAllTextAsync(componentPath, content, cancellationToken).ConfigureAwait(false); + + return componentPath; + }; + + string componentPath = await (component.Type switch + { + DaprConstants.BuildingBlocks.PubSub => GetPubSubAsync(component, contentWriter, cancellationToken), + DaprConstants.BuildingBlocks.StateStore => GetStateStoreAsync(component, contentWriter, cancellationToken), + _ => throw new InvalidOperationException($"Unsupported Dapr component type '{component.Type}'.") + }).ConfigureAwait(false); + + onDemandResourcesPaths.Add(component.Name, componentPath); + } + } + + return onDemandResourcesPaths; + } + + private async Task GetPubSubAsync(DaprComponentResource component, Func> contentWriter, CancellationToken cancellationToken) + { + string userDirectory = Environment.GetFolderPath(Environment.SpecialFolder.UserProfile); + string daprDefaultComponentsDirectory = Path.Combine(userDirectory, ".dapr", "components"); + string daprDefaultStateStorePath = Path.Combine(daprDefaultComponentsDirectory, "pubsub.yaml"); + + if (File.Exists(daprDefaultStateStorePath)) + { + _logger.LogInformation("Using default Dapr pub-sub for component '{ComponentName}'.", component.Name); + + string defaultContent = await File.ReadAllTextAsync(daprDefaultStateStorePath, cancellationToken).ConfigureAwait(false); + string newContent = defaultContent.Replace("name: pubsub", $"name: {component.Name}"); + + return await contentWriter(newContent).ConfigureAwait(false); + } + else + { + _logger.LogInformation("Using in-memory Dapr pub-sub for component '{ComponentName}'.", component.Name); + + return await contentWriter(GetInMemoryPubSubContent(component)).ConfigureAwait(false); + } + } + + private async Task GetStateStoreAsync(DaprComponentResource component, Func> contentWriter, CancellationToken cancellationToken) + { + string userDirectory = Environment.GetFolderPath(Environment.SpecialFolder.UserProfile); + string daprDefaultComponentsDirectory = Path.Combine(userDirectory, ".dapr", "components"); + string daprDefaultStateStorePath = Path.Combine(daprDefaultComponentsDirectory, "statestore.yaml"); + + if (File.Exists(daprDefaultStateStorePath)) + { + _logger.LogInformation("Using default Dapr state store for component '{ComponentName}'.", component.Name); + + string defaultContent = await File.ReadAllTextAsync(daprDefaultStateStorePath, cancellationToken).ConfigureAwait(false); + string newContent = defaultContent.Replace("name: statestore", $"name: {component.Name}"); + + return await contentWriter(newContent).ConfigureAwait(false); + } + else + { + _logger.LogInformation("Using in-memory Dapr state store for component '{ComponentName}'.", component.Name); + + return await contentWriter(GetInMemoryStateStoreContent(component)).ConfigureAwait(false); + } + } + + private static string GetInMemoryPubSubContent(DaprComponentResource component) + { + // NOTE: This component can only be used within a single Dapr application. + + return + $""" + apiVersion: dapr.io/v1alpha1 + kind: Component + metadata: + name: {component.Name} + spec: + type: pubsub.in-memory + version: v1 + metadata: [] + """; + } + + private static string GetInMemoryStateStoreContent(DaprComponentResource component) + { + return + $""" + apiVersion: dapr.io/v1alpha1 + kind: Component + metadata: + name: {component.Name} + spec: + type: state.in-memory + version: v1 + metadata: [] + """; + } +} + +internal static class IListExtensions +{ + public static void AddRange(this IList list, IEnumerable collection) + { + foreach (var item in collection) + { + list.Add(item); + } + } +} diff --git a/src/CommunityToolkit.Aspire.Hosting.Dapr/DaprOptions.cs b/src/CommunityToolkit.Aspire.Hosting.Dapr/DaprOptions.cs new file mode 100644 index 00000000..1e89a2a9 --- /dev/null +++ b/src/CommunityToolkit.Aspire.Hosting.Dapr/DaprOptions.cs @@ -0,0 +1,23 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +namespace CommunityToolkit.Aspire.Hosting.Dapr; + +/// +/// Options for configuring Dapr. +/// +public sealed record DaprOptions +{ + /// + /// Gets or sets the path to the Dapr CLI. + /// + public string? DaprPath { get; set; } + + /// + /// Gets or sets whether Dapr sidecars export telemetry to the Aspire dashboard. + /// + /// + /// Telemetry is enabled by default. + /// + public bool? EnableTelemetry { get; set; } +} diff --git a/src/CommunityToolkit.Aspire.Hosting.Dapr/DaprSidecarAnnotation.cs b/src/CommunityToolkit.Aspire.Hosting.Dapr/DaprSidecarAnnotation.cs new file mode 100644 index 00000000..109aaa52 --- /dev/null +++ b/src/CommunityToolkit.Aspire.Hosting.Dapr/DaprSidecarAnnotation.cs @@ -0,0 +1,13 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using Aspire.Hosting.ApplicationModel; + +namespace CommunityToolkit.Aspire.Hosting.Dapr; + +/// +/// Indicates that a Dapr sidecar should be started for the associated resource. +/// +public sealed record DaprSidecarAnnotation(IDaprSidecarResource Sidecar) : IResourceAnnotation +{ +} diff --git a/src/CommunityToolkit.Aspire.Hosting.Dapr/DaprSidecarOptions.cs b/src/CommunityToolkit.Aspire.Hosting.Dapr/DaprSidecarOptions.cs new file mode 100644 index 00000000..7c5eecb3 --- /dev/null +++ b/src/CommunityToolkit.Aspire.Hosting.Dapr/DaprSidecarOptions.cs @@ -0,0 +1,176 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Collections.Immutable; + +namespace CommunityToolkit.Aspire.Hosting.Dapr; + +/// +/// Options for configuring a Dapr sidecar. +/// +public sealed record DaprSidecarOptions +{ + /// + /// Gets or sets the network address at which the application listens. + /// + public string? AppChannelAddress { get; init; } + + /// + /// Gets or sets the path used for health checks (HTTP only). + /// + public string? AppHealthCheckPath { get; init; } + + /// + /// Gets or sets the interval, in seconds, to probe for the health of the application. + /// + public int? AppHealthProbeInterval { get; init; } + + /// + /// Gets or sets the timeout, in milliseconds, for application health probes. + /// + public int? AppHealthProbeTimeout { get; init; } + + /// + /// Gets or sets the number of consecutive failures for the application to be considered unhealthy. + /// + public int? AppHealthThreshold { get; init; } + + /// + /// Gets or sets the ID for the application, used for service discovery. + /// + public string? AppId { get; init; } + + /// + /// Gets or sets the concurrency level of the application (unlimited if omitted). + /// + public int? AppMaxConcurrency { get; init; } + + /// + /// Gets or sets the port on which the application is listening. + /// + public int? AppPort { get; init; } + + /// + /// Gets or sets the protocol (i.e. grpc, grpcs, http, https, h2c) the Dapr sidecar uses to talk to the application. + /// + public string? AppProtocol { get; init; } + + /// + /// Gets or sets the endpoint of the application the sidecar is connected to. + /// + public string? AppEndpoint { get; init; } + + /// + /// Gets or sets the command run by the Dapr CLI as part of starting the sidecar. + /// + public IImmutableList Command { get; init; } = ImmutableList.Empty; + + /// + /// Gets or sets the path to the Dapr sidecar configuration file. + /// + public string? Config { get; init; } + + /// + /// Gets or sets the gRPC port on which the Dapr sidecar should listen. + /// + public int? DaprGrpcPort { get; init; } + + /// + /// Gets or sets the maximum size, in MB, of a Dapr request body. + /// + public int? DaprHttpMaxRequestSize { get; init; } + + /// + /// Gets or sets the HTTP port on which the Dapr sidecard should listen. + /// + public int? DaprHttpPort { get; init; } + + /// + /// Gets or sets the maximum size, in KB, of the HTTP header read buffer. + /// + public int? DaprHttpReadBufferSize { get; init; } + + /// + /// Gets or sets the gRPC port on which the Dapr sidecar should listen for sidecar-to-sidecar calls. + /// + public int? DaprInternalGrpcPort { get; init; } + + /// + /// Gets or sets a comma (,) delimited list of IP addresses at which the Dapr sidecar will listen. + /// + public string? DaprListenAddresses { get; init; } + + /// + /// Gets or sets whether the Dapr sidecar logs API calls at INFO verbosity. + /// + public bool? EnableApiLogging { get; init; } + + /// + /// Gets or sets whether health checks are performed for the application. + /// + public bool? EnableAppHealthCheck { get; init; } + + /// + /// Gets or sets whether to perform pprof profiling via the application HTTP endpoint. + /// + public bool? EnableProfiling { get; init; } + + /// + /// Gets or sets the Dapr sidecar log verbosity (i.e. debug, info, warn, error, fatal, or panic). + /// + /// + /// The default log verbosity is "info". + /// + public string? LogLevel { get; init; } + + /// + /// Gets or sets the port on which the Dapr sidecar reports metrics. + /// + public int? MetricsPort { get; init; } + + /// + /// Gets or sets the address of the placement service. + /// + /// + /// The format is either "hostname" for the default port or "hostname:port" for a custom port. + /// The default is "localhost". + /// + public string? PlacementHostAddress { get; init; } + + /// + /// Gets or sets the port on which the Dapr sidecar reports profiling data. + /// + public int? ProfilePort { get; init; } + + /// + /// Gets or sets the paths of Dapr sidecar resources (i.e. resources). + /// + public IImmutableSet ResourcesPaths { get; init; } = ImmutableHashSet.Empty; + + /// + /// Gets or sets the path to the Dapr run file to run. + /// + public string? RunFile { get; init; } + + /// + /// Gets or sets the directory of the Dapr runtime (i.e. daprd). + /// + public string? RuntimePath { get; init; } + + /// + /// Gets or sets the address of the scheduler service. + /// + /// + /// The format is either "hostname" for the default port or "hostname:port" for a custom port. + /// The default is "localhost". + /// + public string? SchedulerHostAddress { get; init; } + + /// + /// Gets or sets the path to a Unix Domain Socket (UDS) directory. + /// + /// + /// If specified, the Dapr sidecar will use Unix Domain Sockets for API calls. + /// + public string? UnixDomainSocket { get; init; } +} diff --git a/src/CommunityToolkit.Aspire.Hosting.Dapr/DaprSidecarOptionsAnnotation.cs b/src/CommunityToolkit.Aspire.Hosting.Dapr/DaprSidecarOptionsAnnotation.cs new file mode 100644 index 00000000..6dacd623 --- /dev/null +++ b/src/CommunityToolkit.Aspire.Hosting.Dapr/DaprSidecarOptionsAnnotation.cs @@ -0,0 +1,13 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using Aspire.Hosting.ApplicationModel; + +namespace CommunityToolkit.Aspire.Hosting.Dapr; + +/// +/// Indicates the options used to configure a Dapr sidecar. +/// +public sealed record DaprSidecarOptionsAnnotation(DaprSidecarOptions Options) : IResourceAnnotation +{ +} diff --git a/src/CommunityToolkit.Aspire.Hosting.Dapr/DaprSidecarResource.cs b/src/CommunityToolkit.Aspire.Hosting.Dapr/DaprSidecarResource.cs new file mode 100644 index 00000000..660fd171 --- /dev/null +++ b/src/CommunityToolkit.Aspire.Hosting.Dapr/DaprSidecarResource.cs @@ -0,0 +1,11 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using Aspire.Hosting.ApplicationModel; + +namespace CommunityToolkit.Aspire.Hosting.Dapr; + +/// +/// Represents a Dapr sidecar resource. +/// +internal sealed class DaprSidecarResource(string name) : Resource(name), IDaprSidecarResource { } diff --git a/src/CommunityToolkit.Aspire.Hosting.Dapr/IDaprComponentResource.cs b/src/CommunityToolkit.Aspire.Hosting.Dapr/IDaprComponentResource.cs new file mode 100644 index 00000000..c66cddc7 --- /dev/null +++ b/src/CommunityToolkit.Aspire.Hosting.Dapr/IDaprComponentResource.cs @@ -0,0 +1,25 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using Aspire.Hosting.ApplicationModel; + +namespace CommunityToolkit.Aspire.Hosting.Dapr; + +/// +/// Represents a Dapr component resource. +/// +public interface IDaprComponentResource : IResource, IResourceWithWaitSupport +{ + /// + /// Gets the type of the Dapr component. + /// + /// + /// This may be a generic "state" or "pubsub" if Aspire should choose an appropriate type when running or deploying. + /// + string Type { get; } + + /// + /// Gets options used to configure the component, if any. + /// + DaprComponentOptions? Options { get; } +} diff --git a/src/CommunityToolkit.Aspire.Hosting.Dapr/IDaprSidecarResource.cs b/src/CommunityToolkit.Aspire.Hosting.Dapr/IDaprSidecarResource.cs new file mode 100644 index 00000000..8c084236 --- /dev/null +++ b/src/CommunityToolkit.Aspire.Hosting.Dapr/IDaprSidecarResource.cs @@ -0,0 +1,13 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using Aspire.Hosting.ApplicationModel; + +namespace CommunityToolkit.Aspire.Hosting.Dapr; + +/// +/// Represents a Dapr sidecar resource. +/// +public interface IDaprSidecarResource : IResource +{ +} diff --git a/src/CommunityToolkit.Aspire.Hosting.Dapr/IDistributedApplicationBuilderExtensions.cs b/src/CommunityToolkit.Aspire.Hosting.Dapr/IDistributedApplicationBuilderExtensions.cs new file mode 100644 index 00000000..843b0624 --- /dev/null +++ b/src/CommunityToolkit.Aspire.Hosting.Dapr/IDistributedApplicationBuilderExtensions.cs @@ -0,0 +1,94 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using Aspire.Hosting.ApplicationModel; +using CommunityToolkit.Aspire.Hosting.Dapr; +using Aspire.Hosting.Lifecycle; +using Aspire.Hosting.Publishing; +using Aspire.Hosting.Utils; +using Microsoft.Extensions.DependencyInjection; + +namespace Aspire.Hosting; + +/// +/// Extensions to related to Dapr. +/// +public static class IDistributedApplicationBuilderExtensions +{ + /// + /// Adds Dapr support to Aspire, including the ability to add Dapr sidecar to application resource. + /// + /// The distributed application builder instance. + /// Callback to configure dapr options. + /// The distributed application builder instance. + public static IDistributedApplicationBuilder AddDapr(this IDistributedApplicationBuilder builder, Action? configure = null) + { + if (configure is not null) + { + builder.Services.Configure(configure); + } + builder.Services.TryAddLifecycleHook(); + + return builder; + } + + /// + /// Adds a Dapr component to the application model. + /// + /// The distributed application builder instance. + /// The name of the component. + /// The type of the component. This can be a generic "state" or "pubsub" string, to have Aspire choose an appropriate type when running or deploying. + /// Options for configuring the component, if any. + /// A reference to the . + public static IResourceBuilder AddDaprComponent(this IDistributedApplicationBuilder builder, [ResourceName] string name, string type, DaprComponentOptions? options = null) + { + var resource = new DaprComponentResource(name, type) { Options = options }; + return builder + .AddResource(resource) + .WithInitialState(new() + { + Properties = [], + ResourceType = "DaprComponent", + State = KnownResourceStates.Hidden + }) + .WithAnnotation(new ManifestPublishingCallbackAnnotation(context => WriteDaprComponentResourceToManifest(context, resource))); + } + + /// + /// Adds a "generic" Dapr pub-sub component to the application model. Aspire will configure an appropriate type when running or deploying. + /// + /// The distributed application builder instance. + /// The name of the component. + /// Options for configuring the component, if any. + /// A reference to the . + public static IResourceBuilder AddDaprPubSub(this IDistributedApplicationBuilder builder, [ResourceName] string name, DaprComponentOptions? options = null) + { + return builder.AddDaprComponent(name, DaprConstants.BuildingBlocks.PubSub, options); + } + + /// + /// Adds a Dapr state store component to the application model. Aspire will configure an appropriate type when running or deploying. + /// + /// The distributed application builder instance. + /// The name of the component. + /// Options for configuring the component, if any. + /// A reference to the . + public static IResourceBuilder AddDaprStateStore(this IDistributedApplicationBuilder builder, [ResourceName] string name, DaprComponentOptions? options = null) + { + return builder.AddDaprComponent(name, DaprConstants.BuildingBlocks.StateStore, options); + } + + private static void WriteDaprComponentResourceToManifest(ManifestPublishingContext context, DaprComponentResource resource) + { + context.Writer.WriteString("type", "dapr.component.v0"); + context.Writer.WriteStartObject("daprComponent"); + + if (resource.Options?.LocalPath is { } localPath) + { + context.Writer.TryWriteString("localPath", context.GetManifestRelativePath(localPath)); + } + context.Writer.WriteString("type", resource.Type); + + context.Writer.WriteEndObject(); + } +} diff --git a/src/CommunityToolkit.Aspire.Hosting.Dapr/IDistributedApplicationComponentBuilderExtensions.cs b/src/CommunityToolkit.Aspire.Hosting.Dapr/IDistributedApplicationComponentBuilderExtensions.cs new file mode 100644 index 00000000..bd4929d2 --- /dev/null +++ b/src/CommunityToolkit.Aspire.Hosting.Dapr/IDistributedApplicationComponentBuilderExtensions.cs @@ -0,0 +1,91 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using Aspire.Hosting.ApplicationModel; +using CommunityToolkit.Aspire.Hosting.Dapr; + +namespace Aspire.Hosting; + +/// +/// Extensions to related to Dapr. +/// +public static class IDistributedApplicationResourceBuilderExtensions +{ + /// + /// Ensures that a Dapr sidecar is started for the resource. + /// + /// The type of the resource. + /// The resource builder instance. + /// The ID for the application, used for service discovery. + /// The resource builder instance. + public static IResourceBuilder WithDaprSidecar(this IResourceBuilder builder, string appId) where T : IResource + { + return builder.WithDaprSidecar(new DaprSidecarOptions { AppId = appId }); + } + + /// + /// Ensures that a Dapr sidecar is started for the resource. + /// + /// The type of the resource. + /// The resource builder instance. + /// Options for configuring the Dapr sidecar, if any. + /// The resource builder instance. + public static IResourceBuilder WithDaprSidecar(this IResourceBuilder builder, DaprSidecarOptions? options = null) where T : IResource + { + return builder.WithDaprSidecar( + sidecarBuilder => + { + if (options is not null) + { + sidecarBuilder.WithOptions(options); + } + }); + } + + /// + /// Ensures that a Dapr sidecar is started for the resource. + /// + /// The type of the resource. + /// The resource builder instance. + /// A callback that can be use to configure the Dapr sidecar. + /// The resource builder instance. + public static IResourceBuilder WithDaprSidecar(this IResourceBuilder builder, Action> configureSidecar) where T : IResource + { + // Add Dapr is idempotent, so we can call it multiple times. + builder.ApplicationBuilder.AddDapr(); + + var sidecarBuilder = builder.ApplicationBuilder.AddResource(new DaprSidecarResource($"{builder.Resource.Name}-dapr")) + .WithInitialState(new() + { + Properties = [], + ResourceType = "DaprSidecar", + State = KnownResourceStates.Hidden + }); + + configureSidecar(sidecarBuilder); + + return builder.WithAnnotation(new DaprSidecarAnnotation(sidecarBuilder.Resource)); + } + + /// + /// Configures a Dapr sidecar with the specified options. + /// + /// The Dapr sidecar resource builder instance. + /// Options for configuring the Dapr sidecar. + /// The Dapr sidecar resource builder instance. + public static IResourceBuilder WithOptions(this IResourceBuilder builder, DaprSidecarOptions options) + { + return builder.WithAnnotation(new DaprSidecarOptionsAnnotation(options)); + } + + /// + /// Associates a Dapr component with the Dapr sidecar started for the resource. + /// + /// The type of the resource. + /// The resource builder instance. + /// The Dapr component to use with the sidecar. + public static IResourceBuilder WithReference(this IResourceBuilder builder, IResourceBuilder component) where TDestination : IResource + { + return builder.WithAnnotation(new DaprComponentReferenceAnnotation(component.Resource)); + } +} diff --git a/src/CommunityToolkit.Aspire.Hosting.Dapr/PublicAPI.Shipped.txt b/src/CommunityToolkit.Aspire.Hosting.Dapr/PublicAPI.Shipped.txt new file mode 100644 index 00000000..1dc52379 --- /dev/null +++ b/src/CommunityToolkit.Aspire.Hosting.Dapr/PublicAPI.Shipped.txt @@ -0,0 +1,100 @@ +#nullable enable +CommunityToolkit.Aspire.Hosting.Dapr.DaprComponentOptions +CommunityToolkit.Aspire.Hosting.Dapr.DaprComponentOptions.LocalPath.get -> string? +CommunityToolkit.Aspire.Hosting.Dapr.DaprComponentOptions.LocalPath.init -> void +CommunityToolkit.Aspire.Hosting.Dapr.DaprComponentReferenceAnnotation +CommunityToolkit.Aspire.Hosting.Dapr.DaprComponentReferenceAnnotation.Component.get -> CommunityToolkit.Aspire.Hosting.Dapr.IDaprComponentResource! +CommunityToolkit.Aspire.Hosting.Dapr.DaprComponentReferenceAnnotation.Component.init -> void +CommunityToolkit.Aspire.Hosting.Dapr.DaprComponentReferenceAnnotation.DaprComponentReferenceAnnotation(CommunityToolkit.Aspire.Hosting.Dapr.IDaprComponentResource! Component) -> void +CommunityToolkit.Aspire.Hosting.Dapr.DaprComponentResource +CommunityToolkit.Aspire.Hosting.Dapr.DaprComponentResource.DaprComponentResource(string! name, string! type) -> void +CommunityToolkit.Aspire.Hosting.Dapr.DaprComponentResource.Options.get -> CommunityToolkit.Aspire.Hosting.Dapr.DaprComponentOptions? +CommunityToolkit.Aspire.Hosting.Dapr.DaprComponentResource.Options.init -> void +CommunityToolkit.Aspire.Hosting.Dapr.DaprComponentResource.Type.get -> string! +CommunityToolkit.Aspire.Hosting.Dapr.DaprOptions +CommunityToolkit.Aspire.Hosting.Dapr.DaprOptions.DaprPath.get -> string? +CommunityToolkit.Aspire.Hosting.Dapr.DaprOptions.DaprPath.set -> void +CommunityToolkit.Aspire.Hosting.Dapr.DaprOptions.EnableTelemetry.get -> bool? +CommunityToolkit.Aspire.Hosting.Dapr.DaprOptions.EnableTelemetry.set -> void +CommunityToolkit.Aspire.Hosting.Dapr.DaprSidecarAnnotation +CommunityToolkit.Aspire.Hosting.Dapr.DaprSidecarAnnotation.DaprSidecarAnnotation(CommunityToolkit.Aspire.Hosting.Dapr.IDaprSidecarResource! Sidecar) -> void +CommunityToolkit.Aspire.Hosting.Dapr.DaprSidecarAnnotation.Sidecar.get -> CommunityToolkit.Aspire.Hosting.Dapr.IDaprSidecarResource! +CommunityToolkit.Aspire.Hosting.Dapr.DaprSidecarAnnotation.Sidecar.init -> void +CommunityToolkit.Aspire.Hosting.Dapr.DaprSidecarOptions +CommunityToolkit.Aspire.Hosting.Dapr.DaprSidecarOptions.AppChannelAddress.get -> string? +CommunityToolkit.Aspire.Hosting.Dapr.DaprSidecarOptions.AppChannelAddress.init -> void +CommunityToolkit.Aspire.Hosting.Dapr.DaprSidecarOptions.AppEndpoint.get -> string? +CommunityToolkit.Aspire.Hosting.Dapr.DaprSidecarOptions.AppEndpoint.init -> void +CommunityToolkit.Aspire.Hosting.Dapr.DaprSidecarOptions.AppHealthCheckPath.get -> string? +CommunityToolkit.Aspire.Hosting.Dapr.DaprSidecarOptions.AppHealthCheckPath.init -> void +CommunityToolkit.Aspire.Hosting.Dapr.DaprSidecarOptions.AppHealthProbeInterval.get -> int? +CommunityToolkit.Aspire.Hosting.Dapr.DaprSidecarOptions.AppHealthProbeInterval.init -> void +CommunityToolkit.Aspire.Hosting.Dapr.DaprSidecarOptions.AppHealthProbeTimeout.get -> int? +CommunityToolkit.Aspire.Hosting.Dapr.DaprSidecarOptions.AppHealthProbeTimeout.init -> void +CommunityToolkit.Aspire.Hosting.Dapr.DaprSidecarOptions.AppHealthThreshold.get -> int? +CommunityToolkit.Aspire.Hosting.Dapr.DaprSidecarOptions.AppHealthThreshold.init -> void +CommunityToolkit.Aspire.Hosting.Dapr.DaprSidecarOptions.AppId.get -> string? +CommunityToolkit.Aspire.Hosting.Dapr.DaprSidecarOptions.AppId.init -> void +CommunityToolkit.Aspire.Hosting.Dapr.DaprSidecarOptions.AppMaxConcurrency.get -> int? +CommunityToolkit.Aspire.Hosting.Dapr.DaprSidecarOptions.AppMaxConcurrency.init -> void +CommunityToolkit.Aspire.Hosting.Dapr.DaprSidecarOptions.AppPort.get -> int? +CommunityToolkit.Aspire.Hosting.Dapr.DaprSidecarOptions.AppPort.init -> void +CommunityToolkit.Aspire.Hosting.Dapr.DaprSidecarOptions.AppProtocol.get -> string? +CommunityToolkit.Aspire.Hosting.Dapr.DaprSidecarOptions.AppProtocol.init -> void +CommunityToolkit.Aspire.Hosting.Dapr.DaprSidecarOptions.Command.get -> System.Collections.Immutable.IImmutableList! +CommunityToolkit.Aspire.Hosting.Dapr.DaprSidecarOptions.Command.init -> void +CommunityToolkit.Aspire.Hosting.Dapr.DaprSidecarOptions.Config.get -> string? +CommunityToolkit.Aspire.Hosting.Dapr.DaprSidecarOptions.Config.init -> void +CommunityToolkit.Aspire.Hosting.Dapr.DaprSidecarOptions.DaprGrpcPort.get -> int? +CommunityToolkit.Aspire.Hosting.Dapr.DaprSidecarOptions.DaprGrpcPort.init -> void +CommunityToolkit.Aspire.Hosting.Dapr.DaprSidecarOptions.DaprHttpMaxRequestSize.get -> int? +CommunityToolkit.Aspire.Hosting.Dapr.DaprSidecarOptions.DaprHttpMaxRequestSize.init -> void +CommunityToolkit.Aspire.Hosting.Dapr.DaprSidecarOptions.DaprHttpPort.get -> int? +CommunityToolkit.Aspire.Hosting.Dapr.DaprSidecarOptions.DaprHttpPort.init -> void +CommunityToolkit.Aspire.Hosting.Dapr.DaprSidecarOptions.DaprHttpReadBufferSize.get -> int? +CommunityToolkit.Aspire.Hosting.Dapr.DaprSidecarOptions.DaprHttpReadBufferSize.init -> void +CommunityToolkit.Aspire.Hosting.Dapr.DaprSidecarOptions.DaprInternalGrpcPort.get -> int? +CommunityToolkit.Aspire.Hosting.Dapr.DaprSidecarOptions.DaprInternalGrpcPort.init -> void +CommunityToolkit.Aspire.Hosting.Dapr.DaprSidecarOptions.DaprListenAddresses.get -> string? +CommunityToolkit.Aspire.Hosting.Dapr.DaprSidecarOptions.DaprListenAddresses.init -> void +CommunityToolkit.Aspire.Hosting.Dapr.DaprSidecarOptions.EnableApiLogging.get -> bool? +CommunityToolkit.Aspire.Hosting.Dapr.DaprSidecarOptions.EnableApiLogging.init -> void +CommunityToolkit.Aspire.Hosting.Dapr.DaprSidecarOptions.EnableAppHealthCheck.get -> bool? +CommunityToolkit.Aspire.Hosting.Dapr.DaprSidecarOptions.EnableAppHealthCheck.init -> void +CommunityToolkit.Aspire.Hosting.Dapr.DaprSidecarOptions.EnableProfiling.get -> bool? +CommunityToolkit.Aspire.Hosting.Dapr.DaprSidecarOptions.EnableProfiling.init -> void +CommunityToolkit.Aspire.Hosting.Dapr.DaprSidecarOptions.LogLevel.get -> string? +CommunityToolkit.Aspire.Hosting.Dapr.DaprSidecarOptions.LogLevel.init -> void +CommunityToolkit.Aspire.Hosting.Dapr.DaprSidecarOptions.MetricsPort.get -> int? +CommunityToolkit.Aspire.Hosting.Dapr.DaprSidecarOptions.MetricsPort.init -> void +CommunityToolkit.Aspire.Hosting.Dapr.DaprSidecarOptions.PlacementHostAddress.get -> string? +CommunityToolkit.Aspire.Hosting.Dapr.DaprSidecarOptions.PlacementHostAddress.init -> void +CommunityToolkit.Aspire.Hosting.Dapr.DaprSidecarOptions.ProfilePort.get -> int? +CommunityToolkit.Aspire.Hosting.Dapr.DaprSidecarOptions.ProfilePort.init -> void +CommunityToolkit.Aspire.Hosting.Dapr.DaprSidecarOptions.ResourcesPaths.get -> System.Collections.Immutable.IImmutableSet! +CommunityToolkit.Aspire.Hosting.Dapr.DaprSidecarOptions.ResourcesPaths.init -> void +CommunityToolkit.Aspire.Hosting.Dapr.DaprSidecarOptions.RunFile.get -> string? +CommunityToolkit.Aspire.Hosting.Dapr.DaprSidecarOptions.RunFile.init -> void +CommunityToolkit.Aspire.Hosting.Dapr.DaprSidecarOptions.RuntimePath.get -> string? +CommunityToolkit.Aspire.Hosting.Dapr.DaprSidecarOptions.RuntimePath.init -> void +CommunityToolkit.Aspire.Hosting.Dapr.DaprSidecarOptions.UnixDomainSocket.get -> string? +CommunityToolkit.Aspire.Hosting.Dapr.DaprSidecarOptions.UnixDomainSocket.init -> void +CommunityToolkit.Aspire.Hosting.Dapr.DaprSidecarOptionsAnnotation +CommunityToolkit.Aspire.Hosting.Dapr.DaprSidecarOptionsAnnotation.DaprSidecarOptionsAnnotation(CommunityToolkit.Aspire.Hosting.Dapr.DaprSidecarOptions! Options) -> void +CommunityToolkit.Aspire.Hosting.Dapr.DaprSidecarOptionsAnnotation.Options.get -> CommunityToolkit.Aspire.Hosting.Dapr.DaprSidecarOptions! +CommunityToolkit.Aspire.Hosting.Dapr.DaprSidecarOptionsAnnotation.Options.init -> void +CommunityToolkit.Aspire.Hosting.Dapr.IDaprComponentResource +CommunityToolkit.Aspire.Hosting.Dapr.IDaprComponentResource.Options.get -> CommunityToolkit.Aspire.Hosting.Dapr.DaprComponentOptions? +CommunityToolkit.Aspire.Hosting.Dapr.IDaprComponentResource.Type.get -> string! +CommunityToolkit.Aspire.Hosting.Dapr.IDaprSidecarResource +Aspire.Hosting.IDistributedApplicationBuilderExtensions +Aspire.Hosting.IDistributedApplicationResourceBuilderExtensions +static Aspire.Hosting.IDistributedApplicationBuilderExtensions.AddDapr(this Aspire.Hosting.IDistributedApplicationBuilder! builder, System.Action? configure = null) -> Aspire.Hosting.IDistributedApplicationBuilder! +static Aspire.Hosting.IDistributedApplicationBuilderExtensions.AddDaprComponent(this Aspire.Hosting.IDistributedApplicationBuilder! builder, string! name, string! type, CommunityToolkit.Aspire.Hosting.Dapr.DaprComponentOptions? options = null) -> Aspire.Hosting.ApplicationModel.IResourceBuilder! +static Aspire.Hosting.IDistributedApplicationBuilderExtensions.AddDaprPubSub(this Aspire.Hosting.IDistributedApplicationBuilder! builder, string! name, CommunityToolkit.Aspire.Hosting.Dapr.DaprComponentOptions? options = null) -> Aspire.Hosting.ApplicationModel.IResourceBuilder! +static Aspire.Hosting.IDistributedApplicationBuilderExtensions.AddDaprStateStore(this Aspire.Hosting.IDistributedApplicationBuilder! builder, string! name, CommunityToolkit.Aspire.Hosting.Dapr.DaprComponentOptions? options = null) -> Aspire.Hosting.ApplicationModel.IResourceBuilder! +static Aspire.Hosting.IDistributedApplicationResourceBuilderExtensions.WithDaprSidecar(this Aspire.Hosting.ApplicationModel.IResourceBuilder! builder, CommunityToolkit.Aspire.Hosting.Dapr.DaprSidecarOptions? options = null) -> Aspire.Hosting.ApplicationModel.IResourceBuilder! +static Aspire.Hosting.IDistributedApplicationResourceBuilderExtensions.WithDaprSidecar(this Aspire.Hosting.ApplicationModel.IResourceBuilder! builder, string! appId) -> Aspire.Hosting.ApplicationModel.IResourceBuilder! +static Aspire.Hosting.IDistributedApplicationResourceBuilderExtensions.WithDaprSidecar(this Aspire.Hosting.ApplicationModel.IResourceBuilder! builder, System.Action!>! configureSidecar) -> Aspire.Hosting.ApplicationModel.IResourceBuilder! +static Aspire.Hosting.IDistributedApplicationResourceBuilderExtensions.WithOptions(this Aspire.Hosting.ApplicationModel.IResourceBuilder! builder, CommunityToolkit.Aspire.Hosting.Dapr.DaprSidecarOptions! options) -> Aspire.Hosting.ApplicationModel.IResourceBuilder! +static Aspire.Hosting.IDistributedApplicationResourceBuilderExtensions.WithReference(this Aspire.Hosting.ApplicationModel.IResourceBuilder! builder, Aspire.Hosting.ApplicationModel.IResourceBuilder! component) -> Aspire.Hosting.ApplicationModel.IResourceBuilder! diff --git a/src/CommunityToolkit.Aspire.Hosting.Dapr/PublicAPI.Unshipped.txt b/src/CommunityToolkit.Aspire.Hosting.Dapr/PublicAPI.Unshipped.txt new file mode 100644 index 00000000..c88170cd --- /dev/null +++ b/src/CommunityToolkit.Aspire.Hosting.Dapr/PublicAPI.Unshipped.txt @@ -0,0 +1,3 @@ +#nullable enable +CommunityToolkit.Aspire.Hosting.Dapr.DaprSidecarOptions.SchedulerHostAddress.get -> string? +CommunityToolkit.Aspire.Hosting.Dapr.DaprSidecarOptions.SchedulerHostAddress.init -> void diff --git a/src/CommunityToolkit.Aspire.Hosting.Dapr/README.md b/src/CommunityToolkit.Aspire.Hosting.Dapr/README.md new file mode 100644 index 00000000..f9f1cb18 --- /dev/null +++ b/src/CommunityToolkit.Aspire.Hosting.Dapr/README.md @@ -0,0 +1,39 @@ +# CommunityToolkit.Aspire.Hosting.Dapr library + +Provides extension methods and resource definitions for a .NET Aspire AppHost to configure Dapr resources. + +## Getting started + +### Install the package + +In your AppHost project, install the .NET Aspire Dapr Hosting library with [NuGet](https://www.nuget.org): + +```dotnetcli +dotnet add package CommunityToolkit.Aspire.Hosting.Dapr +``` + +## Usage example + +Then, in the _Program.cs_ file of `AppHost`, add Dapr resources and consume the connection using the following methods: + +```csharp +var builder = DistributedApplication.CreateBuilder(args); + +var stateStore = builder.AddDaprStateStore("statestore"); +var pubSub = builder.AddDaprPubSub("pubsub"); + +builder.AddProject("myapp") + .WithDaprSidecar() + .WithReference(stateStore) + .WithReference(pubSub); + +builder.Build().Run(); +``` + +## Additional documentation + +https://dapr.io/ + +## Feedback & contributing + +https://github.com/dotnet/aspire diff --git a/src/Shared/Utf8JsonWriterExtensions.cs b/src/Shared/Utf8JsonWriterExtensions.cs new file mode 100644 index 00000000..1b7a782c --- /dev/null +++ b/src/Shared/Utf8JsonWriterExtensions.cs @@ -0,0 +1,100 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Text.Json; + +namespace Aspire.Hosting.Utils; + +/// +/// Extensions to the type. +/// +internal static class Utf8JsonWriterExtensions +{ + /// + /// Writes a string array to the JSON writer, if the array is not null or empty. + /// + /// The JSON writer. + /// The property name for the array. + /// The array values to write. + /// True if an array was written, otherwise false. + public static bool TryWriteStringArray(this Utf8JsonWriter writer, string name, IEnumerable? values) + { + if (values is not null) + { + var valuesList = values.ToList(); + + if (valuesList.Any()) + { + writer.WriteStartArray(name); + + foreach (var value in valuesList) + { + writer.WriteStringValue(value); + } + + writer.WriteEndArray(); + + return true; + } + } + + return false; + } + + /// + /// Writes a boolean value to the JSON writer, if the value is not null. + /// + /// The JSON writer. + /// The property name for the boolean. + /// The boolean value to write. + /// True if the value was written, otherwise, false. + public static bool TryWriteBoolean(this Utf8JsonWriter writer, string name, bool? value) + { + if (value.HasValue) + { + writer.WriteBoolean(name, value.Value); + + return true; + } + + return false; + } + + /// + /// Writes a number to the JSON writer, if the value is not null. + /// + /// The JSON writer. + /// The property name for the number. + /// The number value to write. + /// True if the value was written, otherwise, false. + public static bool TryWriteNumber(this Utf8JsonWriter writer, string name, int? value) + { + if (value.HasValue) + { + writer.WriteNumber(name, value.Value); + + return true; + } + + return false; + } + + /// + /// Writes a string value to the JSON writer, if the value is not null. + /// + /// The JSON writer. + /// The property name for the string. + /// The string value to write. + /// True if the value was written, otherwise, false. + public static bool TryWriteString(this Utf8JsonWriter writer, string name, string? value) + { + if (value is not null) + { + writer.WriteString(name, value); + + return true; + } + + return false; + } +} diff --git a/tests/CommunityToolkit.Aspire.Hosting.Dapr.Tests/CommunityToolkit.Aspire.Hosting.Dapr.Tests.csproj b/tests/CommunityToolkit.Aspire.Hosting.Dapr.Tests/CommunityToolkit.Aspire.Hosting.Dapr.Tests.csproj new file mode 100644 index 00000000..fb6f124a --- /dev/null +++ b/tests/CommunityToolkit.Aspire.Hosting.Dapr.Tests/CommunityToolkit.Aspire.Hosting.Dapr.Tests.csproj @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/tests/CommunityToolkit.Aspire.Hosting.Dapr.Tests/DaprTests.cs b/tests/CommunityToolkit.Aspire.Hosting.Dapr.Tests/DaprTests.cs new file mode 100644 index 00000000..a75b5a7e --- /dev/null +++ b/tests/CommunityToolkit.Aspire.Hosting.Dapr.Tests/DaprTests.cs @@ -0,0 +1,172 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Runtime.CompilerServices; +using Aspire.Hosting; +using Aspire.Hosting.Tests.Utils; +using Aspire.Hosting.Utils; + +namespace CommunityToolkit.Aspire.Hosting.Dapr.Tests; + +public class DaprTests +{ + [Fact] + public async Task WithDaprSideCarAddsAnnotationAndSidecarResource() + { + using var builder = TestDistributedApplicationBuilder.Create(); + + builder.AddDapr(o => + { + // Fake path to avoid throwing + o.DaprPath = "dapr"; + }); + + builder.AddContainer("name", "image") + .WithEndpoint("http", e => + { + e.Port = 8000; + e.AllocatedEndpoint = new(e, "localhost", 80); + }) + .WithDaprSidecar(); + + using var app = builder.Build(); + + await ExecuteBeforeStartHooksAsync(app, default); + + var model = app.Services.GetRequiredService(); + + Assert.Equal(3, model.Resources.Count); + var container = Assert.Single(model.Resources.OfType()); + var sidecarResource = Assert.Single(model.Resources.OfType()); + var sideCarCli = Assert.Single(model.Resources.OfType()); + + Assert.True(sideCarCli.TryGetEndpoints(out var endpoints)); + + var ports = new Dictionary + { + ["http"] = 3500, + ["grpc"] = 50001, + ["metrics"] = 9090 + }; + + foreach (var e in endpoints) + { + e.AllocatedEndpoint = new(e, "localhost", ports[e.Name], targetPortExpression: $$$"""{{- portForServing "{{{e.Name}}}" -}}"""); + } + + var config = await container.GetEnvironmentVariableValuesAsync(DistributedApplicationOperation.Run); + var sidecarArgs = await ArgumentEvaluator.GetArgumentListAsync(sideCarCli); + + Assert.Equal("3500", config["DAPR_HTTP_PORT"]); + Assert.Equal("50001", config["DAPR_GRPC_PORT"]); + + Assert.Equal("http://localhost:3500", config["DAPR_HTTP_ENDPOINT"]); + Assert.Equal("http://localhost:50001", config["DAPR_GRPC_ENDPOINT"]); + + var expectedArgs = new[] + { + "run", + "--app-id", + "name", + "--app-port", + "80", + "--dapr-grpc-port", + "{{- portForServing \"grpc\" -}}", + "--dapr-http-port", + "{{- portForServing \"http\" -}}", + "--metrics-port", + "{{- portForServing \"metrics\" -}}", + "--app-channel-address", + "localhost", + "--app-protocol", + "http" + }; + + Assert.Equal(expectedArgs, sidecarArgs); + Assert.NotNull(container.Annotations.OfType()); + } + + [Theory] + [InlineData("https", "https", 555, "https", "localhost", 555)] + [InlineData(null, null, null, "http", "localhost", 8000)] + [InlineData("https", null, null, "https", "localhost", 8001)] + [InlineData(null, "https", null, "https", "localhost", 8001)] + [InlineData(null, null, 555, "http", "localhost", 555)] + [InlineData("https", "http", null, "https", "localhost", 8000)] + public async Task WithDaprSideCarAddsAnnotationBasedOnTheSidecarAppOptions(string? schema, string? endPoint, int? port, string expectedSchema, string expectedChannelAddress, int expectedPort) + { + using var builder = TestDistributedApplicationBuilder.Create(); + + builder.AddDapr(o => + { + // Fake path to avoid throwing + o.DaprPath = "dapr"; + }); + + var containerResource = builder.AddContainer("name", "image") + .WithEndpoint("http", e => + { + e.Port = 8000; + e.UriScheme = "http"; + e.AllocatedEndpoint = new(e, "localhost", 8000); + }) + .WithEndpoint("https", e => + { + e.Port = 8001; + e.UriScheme = "https"; + e.AllocatedEndpoint = new(e, "localhost", 8001); + }); + if (schema is null && endPoint is null && port is null) + { + containerResource.WithDaprSidecar(); + } + else + { + containerResource.WithDaprSidecar(new DaprSidecarOptions() + { + AppProtocol = schema, + AppEndpoint = endPoint, + AppPort = port + }); + } + using var app = builder.Build(); + await ExecuteBeforeStartHooksAsync(app, default); + + var model = app.Services.GetRequiredService(); + + Assert.Equal(3, model.Resources.Count); + var container = Assert.Single(model.Resources.OfType()); + var sidecarResource = Assert.Single(model.Resources.OfType()); + var sideCarCli = Assert.Single(model.Resources.OfType()); + + Assert.True(sideCarCli.TryGetEndpoints(out var endpoints)); + + var ports = new Dictionary + { + ["http"] = 3500, + ["grpc"] = 50001, + ["metrics"] = 9090 + }; + + foreach (var e in endpoints) + { + e.AllocatedEndpoint = new(e, "localhost", ports[e.Name], targetPortExpression: $$$"""{{- portForServing "{{{e.Name}}}" -}}"""); + } + + var config = await container.GetEnvironmentVariableValuesAsync(DistributedApplicationOperation.Run); + var sidecarArgs = await ArgumentEvaluator.GetArgumentListAsync(sideCarCli); + + Assert.Equal("http://localhost:3500", config["DAPR_HTTP_ENDPOINT"]); + Assert.Equal("http://localhost:50001", config["DAPR_GRPC_ENDPOINT"]); + + // because the order of the parameters is changing, we are just checking if the important ones here. + var commandline = string.Join(" ", sidecarArgs); + Assert.Contains($"--app-port {expectedPort}", commandline); + Assert.Contains($"--app-channel-address {expectedChannelAddress}", commandline); + Assert.Contains($"--app-protocol {expectedSchema}", commandline); + Assert.NotNull(container.Annotations.OfType()); + } + + [UnsafeAccessor(UnsafeAccessorKind.Method, Name = "ExecuteBeforeStartHooksAsync")] + private static extern Task ExecuteBeforeStartHooksAsync(DistributedApplication app, CancellationToken cancellationToken); +} From 3c8d42ff18a10ff67b96cb92164992e775086922 Mon Sep 17 00:00:00 2001 From: Brett Smith Date: Tue, 21 Jan 2025 10:57:15 +1300 Subject: [PATCH 02/10] Dapr azure hosting ext (#371) * Create Dap Azure extensions project Create Dapr Azure Redis project Create Example AppHost + ApiService * Work in progress Create Dapr resource for provisioning * Remove specific resource as not generating properly Use AddAzureInfrastructure * Updated to use secret refs * Fix - Remove code used for testing Add AzureDaprComponentResource Start of unit tests * Unit tests + Fixes based on unit tests * remove bicep file * Tests for AzureRedis * Add Readme and perform small cleanup tasks * Update src/CommunityToolkit.Aspire.Hosting.Dapr.AzureExtensions/ApplicationModel/AzureDaprComponentResource.cs Co-authored-by: Aaron Powell * Apply suggestions from code review Co-authored-by: Aaron Powell * Revert unintentional change to Java.AppHost * Update tests/CommunityToolkit.Aspire.Hosting.Dapr.AzureRedis.Tests/ResourceCreationTests.cs Co-authored-by: Aaron Powell * Updated azure redis documentation also added missing xmldocs for api * correct unit test approach * remove unnecessary comment * null checking * More null checking * Update Azure redis readme * Update AzureExtension readme * move extensions to shared files remove Dapr Azure extensions project make dapr azure extensions internal include extensions in dapr redis package include extensions in dapr extensions tests make dapr redis internals visible to dapr redis tests * change redisCache to infra rename source to redisBuilder --------- Co-authored-by: Aaron Powell --- .devcontainer/devcontainer.json | 9 +- CommunityToolkit.Aspire.sln | 45 +++ Directory.Packages.props | 3 + ....Hosting.Dapr.AzureRedis.ApiService.csproj | 8 + ...re.Hosting.Dapr.AzureRedis.ApiService.http | 6 + .../Program.cs | 33 ++ .../Properties/launchSettings.json | 23 ++ .../appsettings.json | 9 + ...ire.Hosting.Dapr.AzureRedis.AppHost.csproj | 22 ++ .../Program.cs | 16 + .../Properties/launchSettings.json | 29 ++ .../appsettings.json | 9 + .../AzureRedisCacheDaprHostingExtensions.cs | 154 ++++++++ ...lkit.Aspire.Hosting.Dapr.AzureRedis.csproj | 23 ++ .../PublicAPI.Shipped.txt | 1 + .../PublicAPI.Unshipped.txt | 3 + .../README.md | 31 ++ .../AzureDaprComponentResource.cs | 13 + .../AzureDaprHostingExtensions.cs | 129 +++++++ src/Shared/DaprAzureExtensions/README.md | 24 ++ ....Hosting.Dapr.AzureExtensions.Tests.csproj | 22 ++ .../ResourceCreationTests.cs | 302 ++++++++++++++++ ...spire.Hosting.Dapr.AzureRedis.Tests.csproj | 12 + .../ResourceCreationTests.cs | 339 ++++++++++++++++++ 24 files changed, 1260 insertions(+), 5 deletions(-) create mode 100644 examples/dapr-ext/CommunityToolkit.Aspire.Hosting.Dapr.AzureRedis.ApiService/CommunityToolkit.Aspire.Hosting.Dapr.AzureRedis.ApiService.csproj create mode 100644 examples/dapr-ext/CommunityToolkit.Aspire.Hosting.Dapr.AzureRedis.ApiService/CommunityToolkit.Aspire.Hosting.Dapr.AzureRedis.ApiService.http create mode 100644 examples/dapr-ext/CommunityToolkit.Aspire.Hosting.Dapr.AzureRedis.ApiService/Program.cs create mode 100644 examples/dapr-ext/CommunityToolkit.Aspire.Hosting.Dapr.AzureRedis.ApiService/Properties/launchSettings.json create mode 100644 examples/dapr-ext/CommunityToolkit.Aspire.Hosting.Dapr.AzureRedis.ApiService/appsettings.json create mode 100644 examples/dapr-ext/CommunityToolkit.Aspire.Hosting.Dapr.AzureRedis.AppHost/CommunityToolkit.Aspire.Hosting.Dapr.AzureRedis.AppHost.csproj create mode 100644 examples/dapr-ext/CommunityToolkit.Aspire.Hosting.Dapr.AzureRedis.AppHost/Program.cs create mode 100644 examples/dapr-ext/CommunityToolkit.Aspire.Hosting.Dapr.AzureRedis.AppHost/Properties/launchSettings.json create mode 100644 examples/dapr-ext/CommunityToolkit.Aspire.Hosting.Dapr.AzureRedis.AppHost/appsettings.json create mode 100644 src/CommunityToolkit.Aspire.Hosting.Dapr.AzureRedis/AzureRedisCacheDaprHostingExtensions.cs create mode 100644 src/CommunityToolkit.Aspire.Hosting.Dapr.AzureRedis/CommunityToolkit.Aspire.Hosting.Dapr.AzureRedis.csproj create mode 100644 src/CommunityToolkit.Aspire.Hosting.Dapr.AzureRedis/PublicAPI.Shipped.txt create mode 100644 src/CommunityToolkit.Aspire.Hosting.Dapr.AzureRedis/PublicAPI.Unshipped.txt create mode 100644 src/CommunityToolkit.Aspire.Hosting.Dapr.AzureRedis/README.md create mode 100644 src/Shared/DaprAzureExtensions/AzureDaprComponentResource.cs create mode 100644 src/Shared/DaprAzureExtensions/AzureDaprHostingExtensions.cs create mode 100644 src/Shared/DaprAzureExtensions/README.md create mode 100644 tests/CommunityToolkit.Aspire.Hosting.Dapr.AzureExtensions.Tests/CommunityToolkit.Aspire.Hosting.Dapr.AzureExtensions.Tests.csproj create mode 100644 tests/CommunityToolkit.Aspire.Hosting.Dapr.AzureExtensions.Tests/ResourceCreationTests.cs create mode 100644 tests/CommunityToolkit.Aspire.Hosting.Dapr.AzureRedis.Tests/CommunityToolkit.Aspire.Hosting.Dapr.AzureRedis.Tests.csproj create mode 100644 tests/CommunityToolkit.Aspire.Hosting.Dapr.AzureRedis.Tests/ResourceCreationTests.cs diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json index ec8e6fa3..66798455 100644 --- a/.devcontainer/devcontainer.json +++ b/.devcontainer/devcontainer.json @@ -5,9 +5,7 @@ "ghcr.io/azure/azure-dev/azd:latest": {}, "ghcr.io/devcontainers/features/dotnet:latest": { "version": "8.0", - "additionalVersions": [ - "9.0" - ] + "additionalVersions": ["9.0"] }, "ghcr.io/devcontainers/features/github-cli:latest": {}, "ghcr.io/devcontainers/features/java:1": { @@ -20,7 +18,8 @@ "ghcr.io/devcontainers-community/features/deno": {}, "ghcr.io/devcontainers/features/go:latest": {}, "ghcr.io/devcontainers/features/rust:latest": {}, - "ghcr.io/devcontainers/features/python:1": {} + "ghcr.io/devcontainers/features/python:1": {}, + "ghcr.io/dapr/cli/dapr-cli:0": {} }, "customizations": { "vscode": { @@ -51,4 +50,4 @@ "onAutoForward": "notify" } } -} \ No newline at end of file +} diff --git a/CommunityToolkit.Aspire.sln b/CommunityToolkit.Aspire.sln index d4494f34..39fe5585 100644 --- a/CommunityToolkit.Aspire.sln +++ b/CommunityToolkit.Aspire.sln @@ -189,6 +189,20 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "CommunityToolkit.Aspire.Mas EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "CommunityToolkit.Aspire.Hosting.ActiveMQ", "src\CommunityToolkit.Aspire.Hosting.ActiveMQ\CommunityToolkit.Aspire.Hosting.ActiveMQ.csproj", "{0761C6CF-28E8-FC0F-6AF3-213E4B312DD0}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "CommunityToolkit.Aspire.Hosting.Dapr.AzureExtensions", "src\CommunityToolkit.Aspire.Hosting.Dapr.AzureExtensions\CommunityToolkit.Aspire.Hosting.Dapr.AzureExtensions.csproj", "{CCA702F6-9399-49DE-85DE-326251BDBEB5}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "dapr-ext", "dapr-ext", "{914A2506-9587-4DFF-ADC0-1D97798ABC8F}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "CommunityToolkit.Aspire.Hosting.Dapr.AzureRedis.AppHost", "examples\dapr-ext\CommunityToolkit.Aspire.Hosting.Dapr.AzureRedis.AppHost\CommunityToolkit.Aspire.Hosting.Dapr.AzureRedis.AppHost.csproj", "{022998DB-98FE-45EB-A145-DB0B1C12EEE5}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "CommunityToolkit.Aspire.Hosting.Dapr.AzureRedis", "src\CommunityToolkit.Aspire.Hosting.Dapr.AzureRedis\CommunityToolkit.Aspire.Hosting.Dapr.AzureRedis.csproj", "{8575F535-5E8A-49AB-BC2E-2A0417FB636B}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "CommunityToolkit.Aspire.Hosting.Dapr.AzureRedis.ApiService", "examples\dapr-ext\CommunityToolkit.Aspire.Hosting.Dapr.AzureRedis.ApiService\CommunityToolkit.Aspire.Hosting.Dapr.AzureRedis.ApiService.csproj", "{995804A3-7D89-4E0A-952E-A5A4161734F5}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "CommunityToolkit.Aspire.Hosting.Dapr.AzureExtensions.Tests", "tests\CommunityToolkit.Aspire.Hosting.Dapr.AzureExtensions.Tests\CommunityToolkit.Aspire.Hosting.Dapr.AzureExtensions.Tests.csproj", "{EC41302D-7E37-4703-A053-BF4097FF6B26}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "CommunityToolkit.Aspire.Hosting.Dapr.AzureRedis.Tests", "tests\CommunityToolkit.Aspire.Hosting.Dapr.AzureRedis.Tests\CommunityToolkit.Aspire.Hosting.Dapr.AzureRedis.Tests.csproj", "{ADF36205-629B-4822-99F3-88544F6B79CA}" +EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "CommunityToolkit.Aspire.Hosting.Ngrok", "src\CommunityToolkit.Aspire.Hosting.Ngrok\CommunityToolkit.Aspire.Hosting.Ngrok.csproj", "{84DCC422-2F8D-4309-A324-07E2C8C2EE8E}" EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "ngrok", "ngrok", "{7431DE02-23CA-4024-B22D-FCF008AFE3CB}" @@ -527,6 +541,30 @@ Global {0761C6CF-28E8-FC0F-6AF3-213E4B312DD0}.Debug|Any CPU.Build.0 = Debug|Any CPU {0761C6CF-28E8-FC0F-6AF3-213E4B312DD0}.Release|Any CPU.ActiveCfg = Release|Any CPU {0761C6CF-28E8-FC0F-6AF3-213E4B312DD0}.Release|Any CPU.Build.0 = Release|Any CPU + {CCA702F6-9399-49DE-85DE-326251BDBEB5}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {CCA702F6-9399-49DE-85DE-326251BDBEB5}.Debug|Any CPU.Build.0 = Debug|Any CPU + {CCA702F6-9399-49DE-85DE-326251BDBEB5}.Release|Any CPU.ActiveCfg = Release|Any CPU + {CCA702F6-9399-49DE-85DE-326251BDBEB5}.Release|Any CPU.Build.0 = Release|Any CPU + {022998DB-98FE-45EB-A145-DB0B1C12EEE5}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {022998DB-98FE-45EB-A145-DB0B1C12EEE5}.Debug|Any CPU.Build.0 = Debug|Any CPU + {022998DB-98FE-45EB-A145-DB0B1C12EEE5}.Release|Any CPU.ActiveCfg = Release|Any CPU + {022998DB-98FE-45EB-A145-DB0B1C12EEE5}.Release|Any CPU.Build.0 = Release|Any CPU + {8575F535-5E8A-49AB-BC2E-2A0417FB636B}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {8575F535-5E8A-49AB-BC2E-2A0417FB636B}.Debug|Any CPU.Build.0 = Debug|Any CPU + {8575F535-5E8A-49AB-BC2E-2A0417FB636B}.Release|Any CPU.ActiveCfg = Release|Any CPU + {8575F535-5E8A-49AB-BC2E-2A0417FB636B}.Release|Any CPU.Build.0 = Release|Any CPU + {995804A3-7D89-4E0A-952E-A5A4161734F5}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {995804A3-7D89-4E0A-952E-A5A4161734F5}.Debug|Any CPU.Build.0 = Debug|Any CPU + {995804A3-7D89-4E0A-952E-A5A4161734F5}.Release|Any CPU.ActiveCfg = Release|Any CPU + {995804A3-7D89-4E0A-952E-A5A4161734F5}.Release|Any CPU.Build.0 = Release|Any CPU + {EC41302D-7E37-4703-A053-BF4097FF6B26}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {EC41302D-7E37-4703-A053-BF4097FF6B26}.Debug|Any CPU.Build.0 = Debug|Any CPU + {EC41302D-7E37-4703-A053-BF4097FF6B26}.Release|Any CPU.ActiveCfg = Release|Any CPU + {EC41302D-7E37-4703-A053-BF4097FF6B26}.Release|Any CPU.Build.0 = Release|Any CPU + {ADF36205-629B-4822-99F3-88544F6B79CA}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {ADF36205-629B-4822-99F3-88544F6B79CA}.Debug|Any CPU.Build.0 = Debug|Any CPU + {ADF36205-629B-4822-99F3-88544F6B79CA}.Release|Any CPU.ActiveCfg = Release|Any CPU + {ADF36205-629B-4822-99F3-88544F6B79CA}.Release|Any CPU.Build.0 = Release|Any CPU {84DCC422-2F8D-4309-A324-07E2C8C2EE8E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {84DCC422-2F8D-4309-A324-07E2C8C2EE8E}.Debug|Any CPU.Build.0 = Debug|Any CPU {84DCC422-2F8D-4309-A324-07E2C8C2EE8E}.Release|Any CPU.ActiveCfg = Release|Any CPU @@ -686,6 +724,13 @@ Global {1200FB2E-F476-4151-BDFD-1DAEE3E99FF5} = {899F0713-7FC6-4750-BAFC-AC650B35B453} {DE596B1A-B923-4D19-89B6-A361FA4EB5BF} = {899F0713-7FC6-4750-BAFC-AC650B35B453} {0761C6CF-28E8-FC0F-6AF3-213E4B312DD0} = {414151D4-7009-4E78-A5C6-D99EBD1E67D1} + {CCA702F6-9399-49DE-85DE-326251BDBEB5} = {414151D4-7009-4E78-A5C6-D99EBD1E67D1} + {914A2506-9587-4DFF-ADC0-1D97798ABC8F} = {8519CC01-1370-47C8-AD94-B0F326B1563F} + {022998DB-98FE-45EB-A145-DB0B1C12EEE5} = {914A2506-9587-4DFF-ADC0-1D97798ABC8F} + {8575F535-5E8A-49AB-BC2E-2A0417FB636B} = {414151D4-7009-4E78-A5C6-D99EBD1E67D1} + {995804A3-7D89-4E0A-952E-A5A4161734F5} = {914A2506-9587-4DFF-ADC0-1D97798ABC8F} + {EC41302D-7E37-4703-A053-BF4097FF6B26} = {899F0713-7FC6-4750-BAFC-AC650B35B453} + {ADF36205-629B-4822-99F3-88544F6B79CA} = {899F0713-7FC6-4750-BAFC-AC650B35B453} {84DCC422-2F8D-4309-A324-07E2C8C2EE8E} = {414151D4-7009-4E78-A5C6-D99EBD1E67D1} {7431DE02-23CA-4024-B22D-FCF008AFE3CB} = {8519CC01-1370-47C8-AD94-B0F326B1563F} {FF2CE5E5-41C7-4BED-92EA-9F9FD8A7A444} = {7431DE02-23CA-4024-B22D-FCF008AFE3CB} diff --git a/Directory.Packages.props b/Directory.Packages.props index 2a5f5d36..d95e36f2 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -6,6 +6,8 @@ + + @@ -47,6 +49,7 @@ + diff --git a/examples/dapr-ext/CommunityToolkit.Aspire.Hosting.Dapr.AzureRedis.ApiService/CommunityToolkit.Aspire.Hosting.Dapr.AzureRedis.ApiService.csproj b/examples/dapr-ext/CommunityToolkit.Aspire.Hosting.Dapr.AzureRedis.ApiService/CommunityToolkit.Aspire.Hosting.Dapr.AzureRedis.ApiService.csproj new file mode 100644 index 00000000..0cd5293a --- /dev/null +++ b/examples/dapr-ext/CommunityToolkit.Aspire.Hosting.Dapr.AzureRedis.ApiService/CommunityToolkit.Aspire.Hosting.Dapr.AzureRedis.ApiService.csproj @@ -0,0 +1,8 @@ + + + + enable + enable + + + diff --git a/examples/dapr-ext/CommunityToolkit.Aspire.Hosting.Dapr.AzureRedis.ApiService/CommunityToolkit.Aspire.Hosting.Dapr.AzureRedis.ApiService.http b/examples/dapr-ext/CommunityToolkit.Aspire.Hosting.Dapr.AzureRedis.ApiService/CommunityToolkit.Aspire.Hosting.Dapr.AzureRedis.ApiService.http new file mode 100644 index 00000000..b9b2acde --- /dev/null +++ b/examples/dapr-ext/CommunityToolkit.Aspire.Hosting.Dapr.AzureRedis.ApiService/CommunityToolkit.Aspire.Hosting.Dapr.AzureRedis.ApiService.http @@ -0,0 +1,6 @@ +@CommunityToolkit.Aspire.Hosting.Dapr.AzureRedis.ExampleAPI_HostAddress = http://localhost:5156 + +GET {{CommunityToolkit.Aspire.Hosting.Dapr.AzureRedis.ExampleAPI_HostAddress}}/weatherforecast/ +Accept: application/json + +### diff --git a/examples/dapr-ext/CommunityToolkit.Aspire.Hosting.Dapr.AzureRedis.ApiService/Program.cs b/examples/dapr-ext/CommunityToolkit.Aspire.Hosting.Dapr.AzureRedis.ApiService/Program.cs new file mode 100644 index 00000000..a2869e22 --- /dev/null +++ b/examples/dapr-ext/CommunityToolkit.Aspire.Hosting.Dapr.AzureRedis.ApiService/Program.cs @@ -0,0 +1,33 @@ +var builder = WebApplication.CreateBuilder(args); + +// Add services to the container. + +var app = builder.Build(); + +app.UseHttpsRedirection(); + +var summaries = new[] +{ + "Freezing", "Bracing", "Chilly", "Cool", "Mild", "Warm", "Balmy", "Hot", "Sweltering", "Scorching" +}; + +app.MapGet("/weatherforecast", () => +{ + var forecast = Enumerable.Range(1, 5).Select(index => + new WeatherForecast + ( + DateOnly.FromDateTime(DateTime.Now.AddDays(index)), + Random.Shared.Next(-20, 55), + summaries[Random.Shared.Next(summaries.Length)] + )) + .ToArray(); + return forecast; +}) +.WithName("GetWeatherForecast"); + +app.Run(); + +record WeatherForecast(DateOnly Date, int TemperatureC, string? Summary) +{ + public int TemperatureF => 32 + (int)(TemperatureC / 0.5556); +} diff --git a/examples/dapr-ext/CommunityToolkit.Aspire.Hosting.Dapr.AzureRedis.ApiService/Properties/launchSettings.json b/examples/dapr-ext/CommunityToolkit.Aspire.Hosting.Dapr.AzureRedis.ApiService/Properties/launchSettings.json new file mode 100644 index 00000000..c4d657c8 --- /dev/null +++ b/examples/dapr-ext/CommunityToolkit.Aspire.Hosting.Dapr.AzureRedis.ApiService/Properties/launchSettings.json @@ -0,0 +1,23 @@ +{ + "$schema": "https://json.schemastore.org/launchsettings.json", + "profiles": { + "http": { + "commandName": "Project", + "dotnetRunMessages": true, + "launchBrowser": false, + "applicationUrl": "http://localhost:5156", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + }, + "https": { + "commandName": "Project", + "dotnetRunMessages": true, + "launchBrowser": false, + "applicationUrl": "https://localhost:7275;http://localhost:5156", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + } + } +} diff --git a/examples/dapr-ext/CommunityToolkit.Aspire.Hosting.Dapr.AzureRedis.ApiService/appsettings.json b/examples/dapr-ext/CommunityToolkit.Aspire.Hosting.Dapr.AzureRedis.ApiService/appsettings.json new file mode 100644 index 00000000..10f68b8c --- /dev/null +++ b/examples/dapr-ext/CommunityToolkit.Aspire.Hosting.Dapr.AzureRedis.ApiService/appsettings.json @@ -0,0 +1,9 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.AspNetCore": "Warning" + } + }, + "AllowedHosts": "*" +} diff --git a/examples/dapr-ext/CommunityToolkit.Aspire.Hosting.Dapr.AzureRedis.AppHost/CommunityToolkit.Aspire.Hosting.Dapr.AzureRedis.AppHost.csproj b/examples/dapr-ext/CommunityToolkit.Aspire.Hosting.Dapr.AzureRedis.AppHost/CommunityToolkit.Aspire.Hosting.Dapr.AzureRedis.AppHost.csproj new file mode 100644 index 00000000..536aab6b --- /dev/null +++ b/examples/dapr-ext/CommunityToolkit.Aspire.Hosting.Dapr.AzureRedis.AppHost/CommunityToolkit.Aspire.Hosting.Dapr.AzureRedis.AppHost.csproj @@ -0,0 +1,22 @@ + + + + + + Exe + enable + enable + true + 068ca29f-dd8a-4898-9ba1-5839fd6a13db + + + + + + + + + + + + diff --git a/examples/dapr-ext/CommunityToolkit.Aspire.Hosting.Dapr.AzureRedis.AppHost/Program.cs b/examples/dapr-ext/CommunityToolkit.Aspire.Hosting.Dapr.AzureRedis.AppHost/Program.cs new file mode 100644 index 00000000..d1a9201c --- /dev/null +++ b/examples/dapr-ext/CommunityToolkit.Aspire.Hosting.Dapr.AzureRedis.AppHost/Program.cs @@ -0,0 +1,16 @@ +var builder = DistributedApplication.CreateBuilder(args); + + +var redisState = builder.AddAzureRedis("redisState").RunAsContainer(); + +// This currently only effects publishing +// local development still uses dapr redis state container +var daprState = builder.AddDaprStateStore("daprState") + .WithReference(redisState); + +// API does not provide any functional example of Dapr - it simply demonstrates referencing the dapr state +var api = builder.AddProject("example-api") + .WithReference(daprState) + .WithDaprSidecar(); + +builder.Build().Run(); diff --git a/examples/dapr-ext/CommunityToolkit.Aspire.Hosting.Dapr.AzureRedis.AppHost/Properties/launchSettings.json b/examples/dapr-ext/CommunityToolkit.Aspire.Hosting.Dapr.AzureRedis.AppHost/Properties/launchSettings.json new file mode 100644 index 00000000..ec77a5bd --- /dev/null +++ b/examples/dapr-ext/CommunityToolkit.Aspire.Hosting.Dapr.AzureRedis.AppHost/Properties/launchSettings.json @@ -0,0 +1,29 @@ +{ + "$schema": "https://json.schemastore.org/launchsettings.json", + "profiles": { + "https": { + "commandName": "Project", + "dotnetRunMessages": true, + "launchBrowser": true, + "applicationUrl": "https://localhost:17028;http://localhost:15086", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development", + "DOTNET_ENVIRONMENT": "Development", + "DOTNET_DASHBOARD_OTLP_ENDPOINT_URL": "https://localhost:21092", + "DOTNET_RESOURCE_SERVICE_ENDPOINT_URL": "https://localhost:22240" + } + }, + "http": { + "commandName": "Project", + "dotnetRunMessages": true, + "launchBrowser": true, + "applicationUrl": "http://localhost:15086", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development", + "DOTNET_ENVIRONMENT": "Development", + "DOTNET_DASHBOARD_OTLP_ENDPOINT_URL": "http://localhost:19004", + "DOTNET_RESOURCE_SERVICE_ENDPOINT_URL": "http://localhost:20299" + } + } + } +} diff --git a/examples/dapr-ext/CommunityToolkit.Aspire.Hosting.Dapr.AzureRedis.AppHost/appsettings.json b/examples/dapr-ext/CommunityToolkit.Aspire.Hosting.Dapr.AzureRedis.AppHost/appsettings.json new file mode 100644 index 00000000..31c092aa --- /dev/null +++ b/examples/dapr-ext/CommunityToolkit.Aspire.Hosting.Dapr.AzureRedis.AppHost/appsettings.json @@ -0,0 +1,9 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.AspNetCore": "Warning", + "Aspire.Hosting.Dcp": "Warning" + } + } +} diff --git a/src/CommunityToolkit.Aspire.Hosting.Dapr.AzureRedis/AzureRedisCacheDaprHostingExtensions.cs b/src/CommunityToolkit.Aspire.Hosting.Dapr.AzureRedis/AzureRedisCacheDaprHostingExtensions.cs new file mode 100644 index 00000000..8d82ae18 --- /dev/null +++ b/src/CommunityToolkit.Aspire.Hosting.Dapr.AzureRedis/AzureRedisCacheDaprHostingExtensions.cs @@ -0,0 +1,154 @@ +using Aspire.Hosting.ApplicationModel; +using Aspire.Hosting.Azure; +using Aspire.Hosting.Dapr; +using Azure.Provisioning; +using Azure.Provisioning.AppContainers; +using Azure.Provisioning.Expressions; +using Azure.Provisioning.KeyVault; +using AzureRedisResource = Azure.Provisioning.Redis.RedisResource; + +namespace Aspire.Hosting; + +/// +/// Provides extension methods for configuring Dapr components with Azure Redis. +/// +public static class AzureRedisCacheDaprHostingExtensions +{ + private const string redisDaprState = nameof(redisDaprState); + /// + /// Configures a Dapr component resource to use an Azure Redis cache resource. + /// + /// The Dapr component resource builder. + /// The Azure Redis cache resource builder. + /// The updated Dapr component resource builder. + public static IResourceBuilder WithReference( + this IResourceBuilder builder, + IResourceBuilder source) => + builder.ApplicationBuilder.ExecutionContext.IsRunMode ? builder : builder.Resource.Type switch + { + "state" => builder.ConfigureRedisStateComponent(source), + _ => throw new InvalidOperationException($"Unsupported Dapr component type: {builder.Resource.Type}"), + }; + + /// + /// Configures the Redis state component for the Dapr component resource. + /// + /// The Dapr component resource builder. + /// The Azure Redis cache resource builder. + /// The updated Dapr component resource builder. + private static IResourceBuilder ConfigureRedisStateComponent( + this IResourceBuilder builder, + IResourceBuilder redisBuilder) + { + ArgumentNullException.ThrowIfNull(builder, nameof(builder)); + ArgumentNullException.ThrowIfNull(redisBuilder, nameof(redisBuilder)); + + var daprComponent = AzureDaprHostingExtensions.CreateDaprComponent(redisDaprState, "state.redis", "v1.0"); + + var redisHost = new ProvisioningParameter("redisHost", typeof(string)); + var principalIdParameter = new ProvisioningParameter(AzureBicepResource.KnownParameters.PrincipalId, typeof(string)); + + var configureInfrastructure = AzureDaprHostingExtensions.GetInfrastructureConfigurationAction(daprComponent, [redisHost]); + + var daprResourceBuilder = builder.AddAzureDaprResource(redisDaprState, configureInfrastructure); + + + redisBuilder.ConfigureInfrastructure(infra => + { + var redisCacheResource = infra.GetProvisionableResources().OfType().Single(); + + // Make necessary changes to the redis resource + bool useEntraID = redisCacheResource.RedisConfiguration.IsAadEnabled.Value == "true"; + bool enableTLS = redisCacheResource.EnableNonSslPort.Value == false; + + BicepValue port = enableTLS ? redisCacheResource.SslPort : redisCacheResource.Port; + + infra.Add(new ProvisioningOutput("daprConnectionString", typeof(string)) + { + Value = BicepFunction.Interpolate($"{redisCacheResource.HostName}:{port}") + }); + + daprResourceBuilder.WithParameter("redisHost", redisBuilder.GetOutput("daprConnectionString")); + + daprComponent.Metadata = [ + new ContainerAppDaprMetadata { Name = "redisHost", Value = redisHost }, + new ContainerAppDaprMetadata { Name = "enableTLS", Value = enableTLS? "true":"false"}, + new ContainerAppDaprMetadata { Name = "actorStateStore", Value = "true" } + ]; + + + if (useEntraID) + { + daprComponent.Metadata.Add(new ContainerAppDaprMetadata + { + Name = "useEntraID", + Value = "true" + }); + daprComponent.Metadata.Add(new ContainerAppDaprMetadata + { + Name = "azureClientId", + Value = principalIdParameter + }); + } + else + { + infra.ConfigureSecretAccess(daprComponent, redisCacheResource); + daprResourceBuilder.WithParameter("redisPasswordSecretUri", redisBuilder.GetOutput("redisPasswordSecretUri")); + } + }); + + // return the original builder to allow chaining + return builder; + } + + /// + /// Configures secrets access for the Azure Redis Cache and sets up the necessary Dapr component secrets. + /// + /// The Azure Redis Cache resource infrastructure. + /// The Dapr component for the container app managed environment. + /// The Azure Redis resource containing the keys. + private static void ConfigureSecretAccess(this AzureResourceInfrastructure redisCache, + ContainerAppManagedEnvironmentDaprComponent daprComponent, + AzureRedisResource redisCacheResource) + { + ArgumentNullException.ThrowIfNull(redisCache, nameof(redisCache)); + ArgumentNullException.ThrowIfNull(daprComponent, nameof(daprComponent)); + ArgumentNullException.ThrowIfNull(redisCacheResource, nameof(redisCacheResource)); + + var redisPasswordSecret = new ProvisioningParameter("redisPasswordSecretUri", typeof(Uri)); + + var keyVault = redisCache.GetProvisionableResources() + .OfType() + .FirstOrDefault() ?? redisCache.ConfigureKeyVaultSecrets(); + + var redisPassword = new KeyVaultSecret("daprRedisPassword") + { + Parent = keyVault, + Name = "daprRedisPassword", + Properties = new SecretProperties + { + Value = redisCacheResource.GetKeys().PrimaryKey + } + }; + + redisCache.Add(redisPassword); + + redisCache.Add(new ProvisioningOutput("redisPasswordSecretUri", typeof(Uri)) + { + Value = redisPassword.Properties.SecretUri + }); + + daprComponent.Metadata.Add(new ContainerAppDaprMetadata + { + Name = "redisPassword", + SecretRef = "redisPassword" + }); + + daprComponent.Secrets = [ + new ContainerAppWritableSecret { + Name = "redisPassword", + KeyVaultUri = redisPasswordSecret + } + ]; + } +} diff --git a/src/CommunityToolkit.Aspire.Hosting.Dapr.AzureRedis/CommunityToolkit.Aspire.Hosting.Dapr.AzureRedis.csproj b/src/CommunityToolkit.Aspire.Hosting.Dapr.AzureRedis/CommunityToolkit.Aspire.Hosting.Dapr.AzureRedis.csproj new file mode 100644 index 00000000..231436f2 --- /dev/null +++ b/src/CommunityToolkit.Aspire.Hosting.Dapr.AzureRedis/CommunityToolkit.Aspire.Hosting.Dapr.AzureRedis.csproj @@ -0,0 +1,23 @@ + + + + Aspire.Hosting + + + + + + + + + + + + + + + + + + + diff --git a/src/CommunityToolkit.Aspire.Hosting.Dapr.AzureRedis/PublicAPI.Shipped.txt b/src/CommunityToolkit.Aspire.Hosting.Dapr.AzureRedis/PublicAPI.Shipped.txt new file mode 100644 index 00000000..815c9200 --- /dev/null +++ b/src/CommunityToolkit.Aspire.Hosting.Dapr.AzureRedis/PublicAPI.Shipped.txt @@ -0,0 +1 @@ +#nullable enable \ No newline at end of file diff --git a/src/CommunityToolkit.Aspire.Hosting.Dapr.AzureRedis/PublicAPI.Unshipped.txt b/src/CommunityToolkit.Aspire.Hosting.Dapr.AzureRedis/PublicAPI.Unshipped.txt new file mode 100644 index 00000000..bb92bde3 --- /dev/null +++ b/src/CommunityToolkit.Aspire.Hosting.Dapr.AzureRedis/PublicAPI.Unshipped.txt @@ -0,0 +1,3 @@ +#nullable enable +Aspire.Hosting.AzureRedisCacheDaprHostingExtensions +static Aspire.Hosting.AzureRedisCacheDaprHostingExtensions.WithReference(this Aspire.Hosting.ApplicationModel.IResourceBuilder! builder, Aspire.Hosting.ApplicationModel.IResourceBuilder! source) -> Aspire.Hosting.ApplicationModel.IResourceBuilder! \ No newline at end of file diff --git a/src/CommunityToolkit.Aspire.Hosting.Dapr.AzureRedis/README.md b/src/CommunityToolkit.Aspire.Hosting.Dapr.AzureRedis/README.md new file mode 100644 index 00000000..9c78eff3 --- /dev/null +++ b/src/CommunityToolkit.Aspire.Hosting.Dapr.AzureRedis/README.md @@ -0,0 +1,31 @@ +# CommunityToolkit.Aspire.Hosting.Dapr.AzureRedis library + +This package provides [.NET Aspire](https://learn.microsoft.com/en-us/dotnet/aspire/get-started/aspire-overview) integration for Azure Redis as a Dapr component. It allows you to configure dapr state to use Azure Redis as part of your .NET Aspire AppHost projects. + +## Usage +To use this package, install it into your .NET Aspire AppHost project: + +```bash +dotnet add package CommunityToolkit.Aspire.Hosting.Dapr.AzureRedis +``` + +```csharp +var builder = DistributedApplication.CreateBuilder(args); + +var redisState = builder.AddAzureRedis("redisState") + .RunAsContainer(); // for local development + +var daprState = builder.AddDaprStateStore("daprState") + .WithReference(redisState); //instructs aspire to use azure redis when publishing + +var api = builder.AddProject("example-api") + .WithReference(daprState) + .WithDaprSidecar(); + +builder.Build().Run(); + +``` + +## Notes + +The current version of the integration currently focuses on publishing and does not make any changes to how dapr components are handled in local development \ No newline at end of file diff --git a/src/Shared/DaprAzureExtensions/AzureDaprComponentResource.cs b/src/Shared/DaprAzureExtensions/AzureDaprComponentResource.cs new file mode 100644 index 00000000..cdbe92c5 --- /dev/null +++ b/src/Shared/DaprAzureExtensions/AzureDaprComponentResource.cs @@ -0,0 +1,13 @@ +using Aspire.Hosting.Azure; + +namespace Aspire.Hosting.ApplicationModel; + +/// +/// Represents an Azure Dapr component resource. +/// +/// +/// Initializes a new instance of the class. +/// +/// The Bicep identifier. +/// The action to configure the Azure resource infrastructure. +internal class AzureDaprComponentResource(string bicepIdentifier, Action configureInfrastructure) : AzureProvisioningResource(bicepIdentifier, configureInfrastructure); \ No newline at end of file diff --git a/src/Shared/DaprAzureExtensions/AzureDaprHostingExtensions.cs b/src/Shared/DaprAzureExtensions/AzureDaprHostingExtensions.cs new file mode 100644 index 00000000..aeb901d7 --- /dev/null +++ b/src/Shared/DaprAzureExtensions/AzureDaprHostingExtensions.cs @@ -0,0 +1,129 @@ +using Aspire.Hosting.ApplicationModel; +using Aspire.Hosting.Azure; +using Aspire.Hosting.Dapr; +using Azure.Provisioning.AppContainers; +using Azure.Provisioning.Expressions; +using Azure.Provisioning; +using Azure.Provisioning.KeyVault; + +namespace Aspire.Hosting; + +/// +/// Provides extension methods for configuring Dapr components in an Azure hosting environment. +/// +internal static class AzureDaprHostingExtensions +{ + /// + /// Adds an Azure Dapr resource to the resource builder. + /// + /// The resource builder. + /// The name of the Dapr resource. + /// The action to configure the Azure resource infrastructure. + /// The updated resource builder. + public static IResourceBuilder AddAzureDaprResource( + this IResourceBuilder builder, + [ResourceName] string name, + Action configureInfrastructure) + { + ArgumentNullException.ThrowIfNull(builder, nameof(builder)); + ArgumentException.ThrowIfNullOrEmpty(name, nameof(name)); + ArgumentNullException.ThrowIfNull(configureInfrastructure, nameof(configureInfrastructure)); + + builder.ExcludeFromManifest(); + + var azureDaprComponentResource = new AzureDaprComponentResource(name, configureInfrastructure); + + return builder.ApplicationBuilder + .AddResource(azureDaprComponentResource) + .WithManifestPublishingCallback(azureDaprComponentResource.WriteToManifest); + } + + /// + /// Configures the infrastructure for a Dapr component in a container app managed environment. + /// + /// The Dapr component to configure. + /// The parameters to provide to the component + /// An action to configure the Azure resource infrastructure. + public static Action GetInfrastructureConfigurationAction( + ContainerAppManagedEnvironmentDaprComponent daprComponent, + IEnumerable? parameters = null) => + (AzureResourceInfrastructure infrastructure) => + { + ArgumentNullException.ThrowIfNull(daprComponent, nameof(daprComponent)); + ArgumentNullException.ThrowIfNull(infrastructure, nameof(infrastructure)); + + ProvisioningVariable resourceToken = new("resourceToken", typeof(string)) + { + Value = BicepFunction.GetUniqueString(BicepFunction.GetResourceGroup().Id) + }; + + infrastructure.Add(resourceToken); + + var containerAppEnvironment = ContainerAppManagedEnvironment.FromExisting("containerAppEnvironment"); + containerAppEnvironment.Name = BicepFunction.Interpolate($"cae-{resourceToken}"); + + infrastructure.Add(containerAppEnvironment); + daprComponent.Parent = containerAppEnvironment; + + if (!daprComponent.ProvisionableProperties.TryGetValue("Name", out IBicepValue? name) || name.IsEmpty) + { + daprComponent.Name = BicepFunction.Take(BicepFunction.Interpolate($"{daprComponent.BicepIdentifier}{resourceToken}"), 24); + } + + infrastructure.Add(daprComponent); + + foreach (var parameter in parameters ?? []) + { + infrastructure.Add(parameter); + } + }; + + + /// + /// Configures Key Vault secrets for the Azure resource infrastructure. + /// + /// The Azure resource infrastructure. + /// The Key Vault secrets to configure. + /// The configured Key Vault service. + public static KeyVaultService ConfigureKeyVaultSecrets( + this AzureResourceInfrastructure infrastructure, IEnumerable? keyVaultSecrets = null) + { + ArgumentNullException.ThrowIfNull(infrastructure, nameof(infrastructure)); + + var kvNameParam = new ProvisioningParameter("keyVaultName", typeof(string)); + infrastructure.Add(kvNameParam); + + var keyVault = KeyVaultService.FromExisting("keyVault"); + keyVault.Name = kvNameParam; + infrastructure.Add(keyVault); + + foreach (var secret in keyVaultSecrets ?? []) + { + secret.Parent = keyVault; + infrastructure.Add(secret); + } + return keyVault; + } + /// + /// Creates a new Dapr component for a container app managed environment. + /// + /// The name of the resource. + /// The type of the Dapr component. + /// The version of the Dapr component. + /// A new instance of . + public static ContainerAppManagedEnvironmentDaprComponent CreateDaprComponent( + string bicepIdentifier, + string componentType, + string version) + { + ArgumentException.ThrowIfNullOrEmpty(bicepIdentifier, nameof(bicepIdentifier)); + ArgumentException.ThrowIfNullOrEmpty(componentType, nameof(componentType)); + ArgumentException.ThrowIfNullOrEmpty(version, nameof(version)); + + return new(bicepIdentifier) + { + ComponentType = componentType, + Version = version + }; + } +} diff --git a/src/Shared/DaprAzureExtensions/README.md b/src/Shared/DaprAzureExtensions/README.md new file mode 100644 index 00000000..0757d6c6 --- /dev/null +++ b/src/Shared/DaprAzureExtensions/README.md @@ -0,0 +1,24 @@ +# CommunityToolkit.Aspire.Hosting.Dapr.AzureExtensions library + +This package provides extension methods and resource definitions that support the development of packages that integrate **Dapr** with **Azure** resources as part of an Aspire Hosting application. + +## Functionality + +1. **`AzureDaprComponentResource`** + A resource that defines 'extends' AzureProvisioningResource. This resource currently contains no additional functionality but ensures API consistency as well as resource identification when extending infrastructure configuration + +2. **`AddAzureDaprResource`** + An extension method that configures an `AzureDaprComponentResource` and integrates it into the Aspire Hosting resource builder pipeline. + +3. **`GetInfrastructureConfigurationAction`** + Provides a reusable action that sets up a Container App Managed Environment for hosting Dapr components. It also handles naming and parameter configuration using Bicep functions. + +4. **`ConfigureKeyVaultSecrets`** + An extension method that configures Key Vault secrets and attaches them to an existing infrastructure setup, allowing your Dapr component (and other Azure services) to securely access secrets. + +5. **`CreateDaprComponent`** + A factory-like method to quickly instantiate a Dapr component with the specified type and version. + +## Notes + + This designed to be consumed by more focused packages (such as `CommunityToolkit.Aspire.Hosting.Dapr.AzureRedis`), which build on top of these shared infrastructure definitions. \ No newline at end of file diff --git a/tests/CommunityToolkit.Aspire.Hosting.Dapr.AzureExtensions.Tests/CommunityToolkit.Aspire.Hosting.Dapr.AzureExtensions.Tests.csproj b/tests/CommunityToolkit.Aspire.Hosting.Dapr.AzureExtensions.Tests/CommunityToolkit.Aspire.Hosting.Dapr.AzureExtensions.Tests.csproj new file mode 100644 index 00000000..8d8e495f --- /dev/null +++ b/tests/CommunityToolkit.Aspire.Hosting.Dapr.AzureExtensions.Tests/CommunityToolkit.Aspire.Hosting.Dapr.AzureExtensions.Tests.csproj @@ -0,0 +1,22 @@ + + + + false + true + + + + + + + + + + + + + + + + + diff --git a/tests/CommunityToolkit.Aspire.Hosting.Dapr.AzureExtensions.Tests/ResourceCreationTests.cs b/tests/CommunityToolkit.Aspire.Hosting.Dapr.AzureExtensions.Tests/ResourceCreationTests.cs new file mode 100644 index 00000000..dd509222 --- /dev/null +++ b/tests/CommunityToolkit.Aspire.Hosting.Dapr.AzureExtensions.Tests/ResourceCreationTests.cs @@ -0,0 +1,302 @@ +using Aspire.Hosting; +using Aspire.Hosting.Utils; +using Azure.Provisioning; +using Azure.Provisioning.KeyVault; + +namespace CommunityToolkit.Aspire.Hosting.Dapr.AzureExtensions.Tests; + +public class ResourceCreationTests +{ + [Fact] + public void AddAzureDaprResource_AddsToAppBuilder() + { + var builder = DistributedApplication.CreateBuilder(); + + var daprStateBuilder = builder.AddDaprStateStore("daprState") + .AddAzureDaprResource("AzureDaprResource", _ => + { + // no-op + }); + + using var app = builder.Build(); + + var appModel = app.Services.GetRequiredService(); + + var resource = Assert.Single(appModel.Resources.OfType()); + + } + + [Fact] + public void CreateDaprComponent_ReturnsPopulatedComponent() + { + var daprResource = AzureDaprHostingExtensions.CreateDaprComponent("daprComponent", "state.redis", "v1"); + + Assert.NotNull(daprResource); + Assert.Equal("daprComponent", daprResource.BicepIdentifier); + Assert.Equal("state.redis", daprResource.ComponentType.Value); + Assert.Equal("v1", daprResource.Version.Value); + } + + [Fact] + public void GetInfrastructureConfigurationAction_ComponentNameCanBeOverwritten() + { + using var builder = TestDistributedApplicationBuilder.Create(); + + var redisHost = new ProvisioningParameter("daprConnectionString", typeof(string)); + var daprResource = AzureDaprHostingExtensions.CreateDaprComponent("daprComponent", "state.redis", "v1"); + var configureInfrastructure = AzureDaprHostingExtensions.GetInfrastructureConfigurationAction(daprResource, [redisHost]); + + daprResource.Name = "myDaprComponent"; + + var azureDaprResourceBuilder = builder.AddDaprStateStore("daprState") + .AddAzureDaprResource("AzureDaprResource", configureInfrastructure); + + using var app = builder.Build(); + + var appModel = app.Services.GetRequiredService(); + + var resource = Assert.Single(appModel.Resources.OfType()); + + string bicepTemplate = resource.GetBicepTemplateString(); + + string expectedBicep = $$""" + @description('The location for the resource(s) to be deployed.') + param location string = resourceGroup().location + + param daprConnectionString string + + var resourceToken = uniqueString(resourceGroup().id) + + resource containerAppEnvironment 'Microsoft.App/managedEnvironments@2024-03-01' existing = { + name: 'cae-${resourceToken}' + } + + resource daprComponent 'Microsoft.App/managedEnvironments/daprComponents@2024-03-01' = { + name: 'myDaprComponent' + properties: { + componentType: 'state.redis' + version: 'v1' + } + parent: containerAppEnvironment + } + """.ReplaceLineEndings("\n"); + + Assert.Equal(expectedBicep, bicepTemplate); + } + + [Fact] + public void GetInfrastructureConfigurationAction_AddsContainerAppEnv_AndDaprComponent_AndParametersAsync() + { + using var builder = TestDistributedApplicationBuilder.Create(); + + var redisHost = new ProvisioningParameter("daprConnectionString", typeof(string)); + var daprResource = AzureDaprHostingExtensions.CreateDaprComponent("daprComponent", "state.redis", "v1"); + var configureInfrastructure = AzureDaprHostingExtensions.GetInfrastructureConfigurationAction(daprResource, [redisHost]); + + var azureDaprResourceBuilder = builder.AddDaprStateStore("daprState") + .AddAzureDaprResource("AzureDaprResource", configureInfrastructure); + + using var app = builder.Build(); + + var appModel = app.Services.GetRequiredService(); + + var resource = Assert.Single(appModel.Resources.OfType()); + + string bicepTemplate = resource.GetBicepTemplateString(); + + string expectedBicep = $$""" + @description('The location for the resource(s) to be deployed.') + param location string = resourceGroup().location + + param daprConnectionString string + + var resourceToken = uniqueString(resourceGroup().id) + + resource containerAppEnvironment 'Microsoft.App/managedEnvironments@2024-03-01' existing = { + name: 'cae-${resourceToken}' + } + + resource daprComponent 'Microsoft.App/managedEnvironments/daprComponents@2024-03-01' = { + name: take('daprComponent${resourceToken}', 24) + properties: { + componentType: 'state.redis' + version: 'v1' + } + parent: containerAppEnvironment + } + """.ReplaceLineEndings("\n"); + + Assert.Equal(expectedBicep, bicepTemplate); + } + + [Fact] + public void GetInfrastructureConfigurationAction_HandlesNullParameters() + { + using var builder = TestDistributedApplicationBuilder.Create(); + + var daprResource = AzureDaprHostingExtensions.CreateDaprComponent("daprComponent", "state.redis", "v1"); + var configureInfrastructure = AzureDaprHostingExtensions.GetInfrastructureConfigurationAction(daprResource); + + var azureDaprResourceBuilder = builder.AddDaprStateStore("daprState") + .AddAzureDaprResource("AzureDaprResource", configureInfrastructure); + + using var app = builder.Build(); + + var appModel = app.Services.GetRequiredService(); + + var resource = Assert.Single(appModel.Resources.OfType()); + + string bicepTemplate = resource.GetBicepTemplateString(); + + string expectedBicep = $$""" + @description('The location for the resource(s) to be deployed.') + param location string = resourceGroup().location + + var resourceToken = uniqueString(resourceGroup().id) + + resource containerAppEnvironment 'Microsoft.App/managedEnvironments@2024-03-01' existing = { + name: 'cae-${resourceToken}' + } + + resource daprComponent 'Microsoft.App/managedEnvironments/daprComponents@2024-03-01' = { + name: take('daprComponent${resourceToken}', 24) + properties: { + componentType: 'state.redis' + version: 'v1' + } + parent: containerAppEnvironment + } + """.ReplaceLineEndings("\n"); + + Assert.Equal(expectedBicep, bicepTemplate); + } + + [Fact] + public void ConfigureKeyVaultSecrets_AddsKeyVaultNameParameterAndService_AndSecrets() + { + using var builder = TestDistributedApplicationBuilder.Create(); + + var redisHost = new ProvisioningParameter("daprConnectionString", typeof(string)); + var daprResource = AzureDaprHostingExtensions.CreateDaprComponent("daprComponent", "state.redis", "v1"); + var configureInfrastructure = AzureDaprHostingExtensions.GetInfrastructureConfigurationAction(daprResource, [redisHost]); + + var azureDaprResourceBuilder = builder.AddDaprStateStore("daprState") + .AddAzureDaprResource("AzureDaprResource", (infra) => + { + configureInfrastructure(infra); + infra.ConfigureKeyVaultSecrets([ + new KeyVaultSecret("mysecret") + { + Name = "mysecret", + Properties = new SecretProperties + { + Value = "secretValue" + } + } + ]); + }); + + using var app = builder.Build(); + + var appModel = app.Services.GetRequiredService(); + + var resource = Assert.Single(appModel.Resources.OfType()); + + string bicepTemplate = resource.GetBicepTemplateString(); + + string expectedBicep = $$""" + @description('The location for the resource(s) to be deployed.') + param location string = resourceGroup().location + + param daprConnectionString string + + param keyVaultName string + + var resourceToken = uniqueString(resourceGroup().id) + + resource containerAppEnvironment 'Microsoft.App/managedEnvironments@2024-03-01' existing = { + name: 'cae-${resourceToken}' + } + + resource daprComponent 'Microsoft.App/managedEnvironments/daprComponents@2024-03-01' = { + name: take('daprComponent${resourceToken}', 24) + properties: { + componentType: 'state.redis' + version: 'v1' + } + parent: containerAppEnvironment + } + + resource keyVault 'Microsoft.KeyVault/vaults@2023-07-01' existing = { + name: keyVaultName + } + + resource mysecret 'Microsoft.KeyVault/vaults/secrets@2023-07-01' = { + name: 'mysecret' + properties: { + value: 'secretValue' + } + parent: keyVault + } + """.ReplaceLineEndings("\n"); + + Assert.Equal(expectedBicep, bicepTemplate); + } + + [Fact] + public void ConfigureKeyVaultSecrets_HandlesNullSecrets() + { + using var builder = TestDistributedApplicationBuilder.Create(); + + var redisHost = new ProvisioningParameter("daprConnectionString", typeof(string)); + var daprResource = AzureDaprHostingExtensions.CreateDaprComponent("daprComponent", "state.redis", "v1"); + var configureInfrastructure = AzureDaprHostingExtensions.GetInfrastructureConfigurationAction(daprResource, [redisHost]); + + var azureDaprResourceBuilder = builder.AddDaprStateStore("daprState") + .AddAzureDaprResource("AzureDaprResource", (infra) => + { + configureInfrastructure(infra); + infra.ConfigureKeyVaultSecrets(); + }); + + using var app = builder.Build(); + + var appModel = app.Services.GetRequiredService(); + + var resource = Assert.Single(appModel.Resources.OfType()); + + string bicepTemplate = resource.GetBicepTemplateString(); + + string expectedBicep = $$""" + @description('The location for the resource(s) to be deployed.') + param location string = resourceGroup().location + + param daprConnectionString string + + param keyVaultName string + + var resourceToken = uniqueString(resourceGroup().id) + + resource containerAppEnvironment 'Microsoft.App/managedEnvironments@2024-03-01' existing = { + name: 'cae-${resourceToken}' + } + + resource daprComponent 'Microsoft.App/managedEnvironments/daprComponents@2024-03-01' = { + name: take('daprComponent${resourceToken}', 24) + properties: { + componentType: 'state.redis' + version: 'v1' + } + parent: containerAppEnvironment + } + + resource keyVault 'Microsoft.KeyVault/vaults@2023-07-01' existing = { + name: keyVaultName + } + """.ReplaceLineEndings("\n"); + + Assert.Equal(expectedBicep, bicepTemplate); + } + +} + diff --git a/tests/CommunityToolkit.Aspire.Hosting.Dapr.AzureRedis.Tests/CommunityToolkit.Aspire.Hosting.Dapr.AzureRedis.Tests.csproj b/tests/CommunityToolkit.Aspire.Hosting.Dapr.AzureRedis.Tests/CommunityToolkit.Aspire.Hosting.Dapr.AzureRedis.Tests.csproj new file mode 100644 index 00000000..e75b1dae --- /dev/null +++ b/tests/CommunityToolkit.Aspire.Hosting.Dapr.AzureRedis.Tests/CommunityToolkit.Aspire.Hosting.Dapr.AzureRedis.Tests.csproj @@ -0,0 +1,12 @@ + + + + false + true + + + + + + + diff --git a/tests/CommunityToolkit.Aspire.Hosting.Dapr.AzureRedis.Tests/ResourceCreationTests.cs b/tests/CommunityToolkit.Aspire.Hosting.Dapr.AzureRedis.Tests/ResourceCreationTests.cs new file mode 100644 index 00000000..882cacec --- /dev/null +++ b/tests/CommunityToolkit.Aspire.Hosting.Dapr.AzureRedis.Tests/ResourceCreationTests.cs @@ -0,0 +1,339 @@ +using Aspire.Hosting; +using Aspire.Hosting.Utils; +using Aspire.Hosting.Azure; + +using AzureRedisResource = Azure.Provisioning.Redis.RedisResource; + +namespace CommunityToolkit.Aspire.Hosting.Dapr.AzureRedis.Tests; + +public class ResourceCreationTests +{ + [Fact] + public void WithReference_WhenAADDisabled_UsesPasswordSecret() + { + using var builder = TestDistributedApplicationBuilder.Create(DistributedApplicationOperation.Publish); + + var redisState = builder.AddAzureRedis("redisState") + .WithAccessKeyAuthentication() + .RunAsContainer(); + + var daprState = builder.AddDaprStateStore("daprState") + .WithReference(redisState); + + using var app = builder.Build(); + + var appModel = app.Services.GetRequiredService(); + + var redisCache = Assert.Single(appModel.Resources.OfType()); + + string redisBicep = redisCache.GetBicepTemplateString(); + + string expectedRedisBicep = $$""" + @description('The location for the resource(s) to be deployed.') + param location string = resourceGroup().location + + param keyVaultName string + + resource redisState 'Microsoft.Cache/redis@2024-03-01' = { + name: take('redisState-${uniqueString(resourceGroup().id)}', 63) + location: location + properties: { + sku: { + name: 'Basic' + family: 'C' + capacity: 1 + } + enableNonSslPort: false + minimumTlsVersion: '1.2' + } + tags: { + 'aspire-resource-name': 'redisState' + } + } + + resource keyVault 'Microsoft.KeyVault/vaults@2023-07-01' existing = { + name: keyVaultName + } + + resource connectionString 'Microsoft.KeyVault/vaults/secrets@2023-07-01' = { + name: 'connectionString' + properties: { + value: '${redisState.properties.hostName},ssl=true,password=${redisState.listKeys().primaryKey}' + } + parent: keyVault + } + + resource daprRedisPassword 'Microsoft.KeyVault/vaults/secrets@2023-07-01' = { + name: 'daprRedisPassword' + properties: { + value: redisState.listKeys().primaryKey + } + parent: keyVault + } + + output daprConnectionString string = '${redisState.properties.hostName}:${redisState.properties.sslPort}' + + output redisPasswordSecretUri string = daprRedisPassword.properties.secretUri + """.ReplaceLineEndings("\n"); + + Assert.Equal(expectedRedisBicep, redisBicep); + + var daprResource = Assert.Single(appModel.Resources.OfType()); + + string daprBicep = daprResource.GetBicepTemplateString(); + + string expectedDaprBicep = $$""" + @description('The location for the resource(s) to be deployed.') + param location string = resourceGroup().location + + param redisHost string + + param redisPasswordSecretUri string + + var resourceToken = uniqueString(resourceGroup().id) + + resource containerAppEnvironment 'Microsoft.App/managedEnvironments@2024-03-01' existing = { + name: 'cae-${resourceToken}' + } + + resource redisDaprState 'Microsoft.App/managedEnvironments/daprComponents@2024-03-01' = { + name: take('redisDaprState${resourceToken}', 24) + properties: { + componentType: 'state.redis' + metadata: [ + { + name: 'redisHost' + value: redisHost + } + { + name: 'enableTLS' + value: 'true' + } + { + name: 'actorStateStore' + value: 'true' + } + { + name: 'redisPassword' + secretRef: 'redisPassword' + } + ] + secrets: [ + { + name: 'redisPassword' + keyVaultUrl: redisPasswordSecretUri + } + ] + version: 'v1.0' + } + parent: containerAppEnvironment + } + """.ReplaceLineEndings("\n"); + + Assert.Equal(expectedDaprBicep, daprBicep); + } + + [Fact] + public void WithReference_WhenAADEnabled_SkipsPasswordSecret() + { + using var builder = TestDistributedApplicationBuilder.Create(DistributedApplicationOperation.Publish); + + var redisState = builder.AddAzureRedis("redisState").RunAsContainer(); + + var daprState = builder.AddDaprStateStore("daprState") + .WithReference(redisState); + + using var app = builder.Build(); + + var appModel = app.Services.GetRequiredService(); + + var redisCache = Assert.Single(appModel.Resources.OfType()); + + string redisBicep = redisCache.GetBicepTemplateString(); + + string expectedRedisBicep = $$""" + @description('The location for the resource(s) to be deployed.') + param location string = resourceGroup().location + + param principalId string + + param principalName string + + resource redisState 'Microsoft.Cache/redis@2024-03-01' = { + name: take('redisState-${uniqueString(resourceGroup().id)}', 63) + location: location + properties: { + sku: { + name: 'Basic' + family: 'C' + capacity: 1 + } + enableNonSslPort: false + disableAccessKeyAuthentication: true + minimumTlsVersion: '1.2' + redisConfiguration: { + 'aad-enabled': 'true' + } + } + tags: { + 'aspire-resource-name': 'redisState' + } + } + + resource redisState_contributor 'Microsoft.Cache/redis/accessPolicyAssignments@2024-03-01' = { + name: take('redisstatecontributor${uniqueString(resourceGroup().id)}', 24) + properties: { + accessPolicyName: 'Data Contributor' + objectId: principalId + objectIdAlias: principalName + } + parent: redisState + } + + output connectionString string = '${redisState.properties.hostName},ssl=true' + + output daprConnectionString string = '${redisState.properties.hostName}:${redisState.properties.sslPort}' + """.ReplaceLineEndings("\n"); + + Assert.Equal(expectedRedisBicep, redisBicep); + + var daprResource = Assert.Single(appModel.Resources.OfType()); + + string daprBicep = daprResource.GetBicepTemplateString(); + + string expectedDaprBicep = $$""" + @description('The location for the resource(s) to be deployed.') + param location string = resourceGroup().location + + param redisHost string + + var resourceToken = uniqueString(resourceGroup().id) + + resource containerAppEnvironment 'Microsoft.App/managedEnvironments@2024-03-01' existing = { + name: 'cae-${resourceToken}' + } + + resource redisDaprState 'Microsoft.App/managedEnvironments/daprComponents@2024-03-01' = { + name: take('redisDaprState${resourceToken}', 24) + properties: { + componentType: 'state.redis' + metadata: [ + { + name: 'redisHost' + value: redisHost + } + { + name: 'enableTLS' + value: 'true' + } + { + name: 'actorStateStore' + value: 'true' + } + { + name: 'useEntraID' + value: 'true' + } + { + name: 'azureClientId' + value: principalId + } + ] + version: 'v1.0' + } + parent: containerAppEnvironment + } + """.ReplaceLineEndings("\n"); + + Assert.Equal(expectedDaprBicep, daprBicep); + + } + + [Fact] + public void WithReference_WhenTLSDisabled_UsesNonSslPort() + { + using var builder = TestDistributedApplicationBuilder.Create(DistributedApplicationOperation.Publish); + + var redisState = builder.AddAzureRedis("redisState") + .ConfigureInfrastructure(infr => + { + var redis = infr.GetProvisionableResources().OfType().Single(); + redis.EnableNonSslPort = true; + }) + .RunAsContainer(); + + var daprState = builder.AddDaprStateStore("daprState") + .WithReference(redisState); + + using var app = builder.Build(); + + var appModel = app.Services.GetRequiredService(); + + var redisCache = Assert.Single(appModel.Resources.OfType()); + + string redisBicep = redisCache.GetBicepTemplateString(); + + + string expectedRedisBicep = $$""" + @description('The location for the resource(s) to be deployed.') + param location string = resourceGroup().location + + param principalId string + + param principalName string + + resource redisState 'Microsoft.Cache/redis@2024-03-01' = { + name: take('redisState-${uniqueString(resourceGroup().id)}', 63) + location: location + properties: { + sku: { + name: 'Basic' + family: 'C' + capacity: 1 + } + enableNonSslPort: true + disableAccessKeyAuthentication: true + minimumTlsVersion: '1.2' + redisConfiguration: { + 'aad-enabled': 'true' + } + } + tags: { + 'aspire-resource-name': 'redisState' + } + } + + resource redisState_contributor 'Microsoft.Cache/redis/accessPolicyAssignments@2024-03-01' = { + name: take('redisstatecontributor${uniqueString(resourceGroup().id)}', 24) + properties: { + accessPolicyName: 'Data Contributor' + objectId: principalId + objectIdAlias: principalName + } + parent: redisState + } + + output connectionString string = '${redisState.properties.hostName},ssl=true' + + output daprConnectionString string = '${redisState.properties.hostName}:${redisState.properties.port}' + """.ReplaceLineEndings("\n"); + + + Assert.Equal(expectedRedisBicep, redisBicep); + } + + [Fact] + public void WithReference_WhenNonStateType_ThrowsException() + { + using var builder = TestDistributedApplicationBuilder.Create(DistributedApplicationOperation.Publish); + + var redisState = builder.AddAzureRedis("redisState").RunAsContainer(); + var ex = Assert.Throws(() => + { + var daprPubSub = builder.AddDaprPubSub("daprState") + .WithReference(redisState); + }); + + Assert.Contains("Unsupported Dapr component type: pubsub", ex.Message); + } +} From 2d0a00e59c0c838f251da5ab8af61090f8aaf651 Mon Sep 17 00:00:00 2001 From: Aaron Powell Date: Mon, 20 Jan 2025 23:04:36 +0000 Subject: [PATCH 03/10] Fixing broken sln --- CommunityToolkit.Aspire.sln | 17 ----------------- 1 file changed, 17 deletions(-) diff --git a/CommunityToolkit.Aspire.sln b/CommunityToolkit.Aspire.sln index 39fe5585..03c0f9ee 100644 --- a/CommunityToolkit.Aspire.sln +++ b/CommunityToolkit.Aspire.sln @@ -189,8 +189,6 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "CommunityToolkit.Aspire.Mas EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "CommunityToolkit.Aspire.Hosting.ActiveMQ", "src\CommunityToolkit.Aspire.Hosting.ActiveMQ\CommunityToolkit.Aspire.Hosting.ActiveMQ.csproj", "{0761C6CF-28E8-FC0F-6AF3-213E4B312DD0}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "CommunityToolkit.Aspire.Hosting.Dapr.AzureExtensions", "src\CommunityToolkit.Aspire.Hosting.Dapr.AzureExtensions\CommunityToolkit.Aspire.Hosting.Dapr.AzureExtensions.csproj", "{CCA702F6-9399-49DE-85DE-326251BDBEB5}" -EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "dapr-ext", "dapr-ext", "{914A2506-9587-4DFF-ADC0-1D97798ABC8F}" EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "CommunityToolkit.Aspire.Hosting.Dapr.AzureRedis.AppHost", "examples\dapr-ext\CommunityToolkit.Aspire.Hosting.Dapr.AzureRedis.AppHost\CommunityToolkit.Aspire.Hosting.Dapr.AzureRedis.AppHost.csproj", "{022998DB-98FE-45EB-A145-DB0B1C12EEE5}" @@ -541,10 +539,6 @@ Global {0761C6CF-28E8-FC0F-6AF3-213E4B312DD0}.Debug|Any CPU.Build.0 = Debug|Any CPU {0761C6CF-28E8-FC0F-6AF3-213E4B312DD0}.Release|Any CPU.ActiveCfg = Release|Any CPU {0761C6CF-28E8-FC0F-6AF3-213E4B312DD0}.Release|Any CPU.Build.0 = Release|Any CPU - {CCA702F6-9399-49DE-85DE-326251BDBEB5}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {CCA702F6-9399-49DE-85DE-326251BDBEB5}.Debug|Any CPU.Build.0 = Debug|Any CPU - {CCA702F6-9399-49DE-85DE-326251BDBEB5}.Release|Any CPU.ActiveCfg = Release|Any CPU - {CCA702F6-9399-49DE-85DE-326251BDBEB5}.Release|Any CPU.Build.0 = Release|Any CPU {022998DB-98FE-45EB-A145-DB0B1C12EEE5}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {022998DB-98FE-45EB-A145-DB0B1C12EEE5}.Debug|Any CPU.Build.0 = Debug|Any CPU {022998DB-98FE-45EB-A145-DB0B1C12EEE5}.Release|Any CPU.ActiveCfg = Release|Any CPU @@ -724,7 +718,6 @@ Global {1200FB2E-F476-4151-BDFD-1DAEE3E99FF5} = {899F0713-7FC6-4750-BAFC-AC650B35B453} {DE596B1A-B923-4D19-89B6-A361FA4EB5BF} = {899F0713-7FC6-4750-BAFC-AC650B35B453} {0761C6CF-28E8-FC0F-6AF3-213E4B312DD0} = {414151D4-7009-4E78-A5C6-D99EBD1E67D1} - {CCA702F6-9399-49DE-85DE-326251BDBEB5} = {414151D4-7009-4E78-A5C6-D99EBD1E67D1} {914A2506-9587-4DFF-ADC0-1D97798ABC8F} = {8519CC01-1370-47C8-AD94-B0F326B1563F} {022998DB-98FE-45EB-A145-DB0B1C12EEE5} = {914A2506-9587-4DFF-ADC0-1D97798ABC8F} {8575F535-5E8A-49AB-BC2E-2A0417FB636B} = {414151D4-7009-4E78-A5C6-D99EBD1E67D1} @@ -747,16 +740,6 @@ Global {0E6EBCFB-DEF5-496C-95AF-00884826CFC8} = {899F0713-7FC6-4750-BAFC-AC650B35B453} {861FE61C-90EE-49B0-BCC8-8417C293CC21} = {899F0713-7FC6-4750-BAFC-AC650B35B453} {52846E18-99D1-4040-AF5F-17FC69198BCE} = {899F0713-7FC6-4750-BAFC-AC650B35B453} - {BEA41234-DFF9-49AE-AD6C-42A9D54202E7} = {414151D4-7009-4E78-A5C6-D99EBD1E67D1} - {6782F1C1-5146-549F-82A8-60C82F1C7F16} = {8519CC01-1370-47C8-AD94-B0F326B1563F} - {97E455C1-C914-4C51-87A9-2C213CE2ED5B} = {6782F1C1-5146-549F-82A8-60C82F1C7F16} - {5DF8F833-F6F8-4C9C-ABEC-80EC0C734A88} = {414151D4-7009-4E78-A5C6-D99EBD1E67D1} - {E48F6DDD-D62D-4723-810D-0F178C35E8B8} = {6782F1C1-5146-549F-82A8-60C82F1C7F16} - {DD7042A1-8E44-40A8-B338-DC2F7B755702} = {6782F1C1-5146-549F-82A8-60C82F1C7F16} - {E54E9DCA-1420-4306-83B6-D45D6EC49DBF} = {414151D4-7009-4E78-A5C6-D99EBD1E67D1} - {0E6EBCFB-DEF5-496C-95AF-00884826CFC8} = {899F0713-7FC6-4750-BAFC-AC650B35B453} - {861FE61C-90EE-49B0-BCC8-8417C293CC21} = {899F0713-7FC6-4750-BAFC-AC650B35B453} - {52846E18-99D1-4040-AF5F-17FC69198BCE} = {899F0713-7FC6-4750-BAFC-AC650B35B453} {2165F65B-83F2-4269-8781-86AB6ACF043D} = {414151D4-7009-4E78-A5C6-D99EBD1E67D1} {B2384D1A-DD13-4D03-B8FE-B194DEF71A0C} = {899F0713-7FC6-4750-BAFC-AC650B35B453} EndGlobalSection From 94f8d7fbc22c2e475fb2faac85dfcbf30732bfff Mon Sep 17 00:00:00 2001 From: Aaron Powell Date: Wed, 22 Jan 2025 11:20:37 +1100 Subject: [PATCH 04/10] Dapr example and tests (#394) * Migrating sample from dotnet/aspire repo * Adding dapr tests * Adding dapr setup to CI workflow * Following the setup-dapr instructions * Debugging CI * Falling back to looking at PATH * Fixing line endings replacement * Added PATH lookup to Windows * Adding some diagnostic info * Changing log level * init was only running on linux, which I think is wrong * Turning up logging * Bypass logging * Interpolated strings * Adding resource logger service * Java app build extension (#348) * Adding the ability to do a maven build in the app host This uses an event to run the mvnw build (or it can be customised via options), so it is only used when the app host is running the resource Fixes #339 * Adding a 'build with maven' command Allows you to rebuild the java app without having to restart the whole app host. * Expanding test coverage * Some more tests * Windows exe needs a file extension * Rolling back some changes * Renaming step * Tidying up the tests * Requiring docker for dapr tests --- .devcontainer/post-create.sh | 3 + .github/workflows/dotnet-ci.yml | 11 + CommunityToolkit.Aspire.sln | 38 +++ Directory.Packages.props | 2 + ...Toolkit.Aspire.Hosting.Dapr.AppHost.csproj | 25 ++ .../Program.cs | 26 +++ .../Properties/launchSettings.json | 29 +++ .../appsettings.json | 9 + .../pubsub.yaml | 17 ++ ...oolkit.Aspire.Hosting.Dapr.ServiceA.csproj | 8 + .../Program.cs | 53 +++++ .../Properties/launchSettings.json | 23 ++ .../appsettings.json | 9 + ...oolkit.Aspire.Hosting.Dapr.ServiceB.csproj | 8 + .../Program.cs | 47 ++++ .../Properties/launchSettings.json | 23 ++ .../appsettings.json | 9 + ...oolkit.Aspire.Hosting.Dapr.ServiceC.csproj | 13 ++ .../Program.cs | 44 ++++ .../Properties/launchSettings.json | 23 ++ .../appsettings.json | 9 + ...Aspire.Hosting.Dapr.ServiceDefaults.csproj | 21 ++ .../Extensions.cs | 119 ++++++++++ ...Toolkit.Aspire.Hosting.Java.AppHost.csproj | 5 - .../Program.cs | 1 + ...ommunityToolkit.Aspire.Hosting.Dapr.csproj | 4 + ...DaprDistributedApplicationLifecycleHook.cs | 33 ++- ...ommunityToolkit.Aspire.Hosting.Java.csproj | 3 + .../JavaAppHostingExtension.Container.cs | 56 +++++ .../JavaAppHostingExtension.Executable.cs | 218 ++++++++++++++++++ .../JavaAppHostingExtension.cs | 106 --------- .../MavenBuildAnnotation.cs | 8 + .../MavenOptions.cs | 20 ++ .../PublicAPI.Unshipped.txt | 10 +- .../ResourceCreationTests.cs | 12 +- .../ResourceCreationTests.cs | 10 +- .../AddDaprPubSubTests.cs | 79 +++++++ .../AddDaprStateStoreTests.cs | 79 +++++++ .../AppHostTests.cs | 21 ++ ...tyToolkit.Aspire.Hosting.Dapr.Tests.csproj | 1 + .../WithDaprSidecarTests.cs | 63 +++++ .../ExecutableResourceCreationTests.cs | 179 ++++++++++++++ 42 files changed, 1349 insertions(+), 128 deletions(-) create mode 100644 examples/dapr/CommunityToolkit.Aspire.Hosting.Dapr.AppHost/CommunityToolkit.Aspire.Hosting.Dapr.AppHost.csproj create mode 100644 examples/dapr/CommunityToolkit.Aspire.Hosting.Dapr.AppHost/Program.cs create mode 100644 examples/dapr/CommunityToolkit.Aspire.Hosting.Dapr.AppHost/Properties/launchSettings.json create mode 100644 examples/dapr/CommunityToolkit.Aspire.Hosting.Dapr.AppHost/appsettings.json create mode 100644 examples/dapr/CommunityToolkit.Aspire.Hosting.Dapr.AppHost/pubsub.yaml create mode 100644 examples/dapr/CommunityToolkit.Aspire.Hosting.Dapr.ServiceA/CommunityToolkit.Aspire.Hosting.Dapr.ServiceA.csproj create mode 100644 examples/dapr/CommunityToolkit.Aspire.Hosting.Dapr.ServiceA/Program.cs create mode 100644 examples/dapr/CommunityToolkit.Aspire.Hosting.Dapr.ServiceA/Properties/launchSettings.json create mode 100644 examples/dapr/CommunityToolkit.Aspire.Hosting.Dapr.ServiceA/appsettings.json create mode 100644 examples/dapr/CommunityToolkit.Aspire.Hosting.Dapr.ServiceB/CommunityToolkit.Aspire.Hosting.Dapr.ServiceB.csproj create mode 100644 examples/dapr/CommunityToolkit.Aspire.Hosting.Dapr.ServiceB/Program.cs create mode 100644 examples/dapr/CommunityToolkit.Aspire.Hosting.Dapr.ServiceB/Properties/launchSettings.json create mode 100644 examples/dapr/CommunityToolkit.Aspire.Hosting.Dapr.ServiceB/appsettings.json create mode 100644 examples/dapr/CommunityToolkit.Aspire.Hosting.Dapr.ServiceC/CommunityToolkit.Aspire.Hosting.Dapr.ServiceC.csproj create mode 100644 examples/dapr/CommunityToolkit.Aspire.Hosting.Dapr.ServiceC/Program.cs create mode 100644 examples/dapr/CommunityToolkit.Aspire.Hosting.Dapr.ServiceC/Properties/launchSettings.json create mode 100644 examples/dapr/CommunityToolkit.Aspire.Hosting.Dapr.ServiceC/appsettings.json create mode 100644 examples/dapr/CommunityToolkit.Aspire.Hosting.Dapr.ServiceDefaults/CommunityToolkit.Aspire.Hosting.Dapr.ServiceDefaults.csproj create mode 100644 examples/dapr/CommunityToolkit.Aspire.Hosting.Dapr.ServiceDefaults/Extensions.cs create mode 100644 src/CommunityToolkit.Aspire.Hosting.Java/JavaAppHostingExtension.Container.cs create mode 100644 src/CommunityToolkit.Aspire.Hosting.Java/JavaAppHostingExtension.Executable.cs delete mode 100644 src/CommunityToolkit.Aspire.Hosting.Java/JavaAppHostingExtension.cs create mode 100644 src/CommunityToolkit.Aspire.Hosting.Java/MavenBuildAnnotation.cs create mode 100644 src/CommunityToolkit.Aspire.Hosting.Java/MavenOptions.cs create mode 100644 tests/CommunityToolkit.Aspire.Hosting.Dapr.Tests/AddDaprPubSubTests.cs create mode 100644 tests/CommunityToolkit.Aspire.Hosting.Dapr.Tests/AddDaprStateStoreTests.cs create mode 100644 tests/CommunityToolkit.Aspire.Hosting.Dapr.Tests/AppHostTests.cs create mode 100644 tests/CommunityToolkit.Aspire.Hosting.Dapr.Tests/WithDaprSidecarTests.cs diff --git a/.devcontainer/post-create.sh b/.devcontainer/post-create.sh index dd29cecb..2cd00cb8 100755 --- a/.devcontainer/post-create.sh +++ b/.devcontainer/post-create.sh @@ -25,6 +25,9 @@ curl -fsSL https://bun.sh/install | bash echo Installing uvicorn pip install uvicorn +echo Setting up dapr +dapr init + echo Installing uv pip install uv diff --git a/.github/workflows/dotnet-ci.yml b/.github/workflows/dotnet-ci.yml index 8a756d2d..64a21e07 100644 --- a/.github/workflows/dotnet-ci.yml +++ b/.github/workflows/dotnet-ci.yml @@ -25,6 +25,7 @@ jobs: runs-on: "${{ matrix.os }}" env: DOTNET_CONFIGURATION: Release + DAPR_VERSION: "1.14.1" steps: - uses: actions/checkout@v4 @@ -76,6 +77,11 @@ jobs: with: bun-version: latest + - uses: dapr/setup-dapr@v2 + name: Setup Dapr + with: + version: ${{ env.DAPR_VERSION }} + - uses: actions/cache@v4 name: Cache NuGet packages with: @@ -120,6 +126,11 @@ jobs: cd examples/swa/CommunityToolkit.Aspire.StaticWebApps.WebApp npm ci + - name: Init Dapr + run: | + dapr init --runtime-version=${{ env.DAPR_VERSION }} + dapr --version + - name: Restore dependencies run: dotnet restore - name: Build diff --git a/CommunityToolkit.Aspire.sln b/CommunityToolkit.Aspire.sln index 03c0f9ee..014aa37c 100644 --- a/CommunityToolkit.Aspire.sln +++ b/CommunityToolkit.Aspire.sln @@ -237,6 +237,18 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "CommunityToolkit.Aspire.Hos EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "CommunityToolkit.Aspire.Hosting.Dapr.Tests", "tests\CommunityToolkit.Aspire.Hosting.Dapr.Tests\CommunityToolkit.Aspire.Hosting.Dapr.Tests.csproj", "{B2384D1A-DD13-4D03-B8FE-B194DEF71A0C}" EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "dapr", "dapr", "{E3C2B4B7-B3B0-4E7F-A975-A6C7FD926792}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "CommunityToolkit.Aspire.Hosting.Dapr.AppHost", "examples\dapr\CommunityToolkit.Aspire.Hosting.Dapr.AppHost\CommunityToolkit.Aspire.Hosting.Dapr.AppHost.csproj", "{B81CEEE6-991E-418C-96D3-F831540C6DE1}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "CommunityToolkit.Aspire.Hosting.Dapr.ServiceA", "examples\dapr\CommunityToolkit.Aspire.Hosting.Dapr.ServiceA\CommunityToolkit.Aspire.Hosting.Dapr.ServiceA.csproj", "{B9BEA97B-D722-4390-A34D-228AE7947E7C}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "CommunityToolkit.Aspire.Hosting.Dapr.ServiceB", "examples\dapr\CommunityToolkit.Aspire.Hosting.Dapr.ServiceB\CommunityToolkit.Aspire.Hosting.Dapr.ServiceB.csproj", "{D2DDEA96-4A7E-496B-AFBE-69A133156C5F}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "CommunityToolkit.Aspire.Hosting.Dapr.ServiceC", "examples\dapr\CommunityToolkit.Aspire.Hosting.Dapr.ServiceC\CommunityToolkit.Aspire.Hosting.Dapr.ServiceC.csproj", "{5ADBE907-7E0B-4AD7-9073-C032C4183914}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "CommunityToolkit.Aspire.Hosting.Dapr.ServiceDefaults", "examples\dapr\CommunityToolkit.Aspire.Hosting.Dapr.ServiceDefaults\CommunityToolkit.Aspire.Hosting.Dapr.ServiceDefaults.csproj", "{99441705-4BFA-499F-9897-371238665E38}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -623,6 +635,26 @@ Global {B2384D1A-DD13-4D03-B8FE-B194DEF71A0C}.Debug|Any CPU.Build.0 = Debug|Any CPU {B2384D1A-DD13-4D03-B8FE-B194DEF71A0C}.Release|Any CPU.ActiveCfg = Release|Any CPU {B2384D1A-DD13-4D03-B8FE-B194DEF71A0C}.Release|Any CPU.Build.0 = Release|Any CPU + {B81CEEE6-991E-418C-96D3-F831540C6DE1}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {B81CEEE6-991E-418C-96D3-F831540C6DE1}.Debug|Any CPU.Build.0 = Debug|Any CPU + {B81CEEE6-991E-418C-96D3-F831540C6DE1}.Release|Any CPU.ActiveCfg = Release|Any CPU + {B81CEEE6-991E-418C-96D3-F831540C6DE1}.Release|Any CPU.Build.0 = Release|Any CPU + {B9BEA97B-D722-4390-A34D-228AE7947E7C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {B9BEA97B-D722-4390-A34D-228AE7947E7C}.Debug|Any CPU.Build.0 = Debug|Any CPU + {B9BEA97B-D722-4390-A34D-228AE7947E7C}.Release|Any CPU.ActiveCfg = Release|Any CPU + {B9BEA97B-D722-4390-A34D-228AE7947E7C}.Release|Any CPU.Build.0 = Release|Any CPU + {D2DDEA96-4A7E-496B-AFBE-69A133156C5F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {D2DDEA96-4A7E-496B-AFBE-69A133156C5F}.Debug|Any CPU.Build.0 = Debug|Any CPU + {D2DDEA96-4A7E-496B-AFBE-69A133156C5F}.Release|Any CPU.ActiveCfg = Release|Any CPU + {D2DDEA96-4A7E-496B-AFBE-69A133156C5F}.Release|Any CPU.Build.0 = Release|Any CPU + {5ADBE907-7E0B-4AD7-9073-C032C4183914}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {5ADBE907-7E0B-4AD7-9073-C032C4183914}.Debug|Any CPU.Build.0 = Debug|Any CPU + {5ADBE907-7E0B-4AD7-9073-C032C4183914}.Release|Any CPU.ActiveCfg = Release|Any CPU + {5ADBE907-7E0B-4AD7-9073-C032C4183914}.Release|Any CPU.Build.0 = Release|Any CPU + {99441705-4BFA-499F-9897-371238665E38}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {99441705-4BFA-499F-9897-371238665E38}.Debug|Any CPU.Build.0 = Debug|Any CPU + {99441705-4BFA-499F-9897-371238665E38}.Release|Any CPU.ActiveCfg = Release|Any CPU + {99441705-4BFA-499F-9897-371238665E38}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -742,6 +774,12 @@ Global {52846E18-99D1-4040-AF5F-17FC69198BCE} = {899F0713-7FC6-4750-BAFC-AC650B35B453} {2165F65B-83F2-4269-8781-86AB6ACF043D} = {414151D4-7009-4E78-A5C6-D99EBD1E67D1} {B2384D1A-DD13-4D03-B8FE-B194DEF71A0C} = {899F0713-7FC6-4750-BAFC-AC650B35B453} + {E3C2B4B7-B3B0-4E7F-A975-A6C7FD926792} = {8519CC01-1370-47C8-AD94-B0F326B1563F} + {B81CEEE6-991E-418C-96D3-F831540C6DE1} = {E3C2B4B7-B3B0-4E7F-A975-A6C7FD926792} + {B9BEA97B-D722-4390-A34D-228AE7947E7C} = {E3C2B4B7-B3B0-4E7F-A975-A6C7FD926792} + {D2DDEA96-4A7E-496B-AFBE-69A133156C5F} = {E3C2B4B7-B3B0-4E7F-A975-A6C7FD926792} + {5ADBE907-7E0B-4AD7-9073-C032C4183914} = {E3C2B4B7-B3B0-4E7F-A975-A6C7FD926792} + {99441705-4BFA-499F-9897-371238665E38} = {E3C2B4B7-B3B0-4E7F-A975-A6C7FD926792} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {08B1D4B8-D2C5-4A64-BB8B-E1A2B29525F0} diff --git a/Directory.Packages.props b/Directory.Packages.props index d95e36f2..41508922 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -16,6 +16,8 @@ + + diff --git a/examples/dapr/CommunityToolkit.Aspire.Hosting.Dapr.AppHost/CommunityToolkit.Aspire.Hosting.Dapr.AppHost.csproj b/examples/dapr/CommunityToolkit.Aspire.Hosting.Dapr.AppHost/CommunityToolkit.Aspire.Hosting.Dapr.AppHost.csproj new file mode 100644 index 00000000..774be4f7 --- /dev/null +++ b/examples/dapr/CommunityToolkit.Aspire.Hosting.Dapr.AppHost/CommunityToolkit.Aspire.Hosting.Dapr.AppHost.csproj @@ -0,0 +1,25 @@ + + + + + + Exe + enable + enable + true + e7f9178b-87a6-4047-b90a-a1fa9d8137b9 + + + + + + + + + + + + + + + diff --git a/examples/dapr/CommunityToolkit.Aspire.Hosting.Dapr.AppHost/Program.cs b/examples/dapr/CommunityToolkit.Aspire.Hosting.Dapr.AppHost/Program.cs new file mode 100644 index 00000000..c2d653d4 --- /dev/null +++ b/examples/dapr/CommunityToolkit.Aspire.Hosting.Dapr.AppHost/Program.cs @@ -0,0 +1,26 @@ +var builder = DistributedApplication.CreateBuilder(args); + +var rmq = builder.AddRabbitMQ("rabbitMQ") + .WithManagementPlugin() + .WithEndpoint("tcp", e => e.Port = 5672) + .WithEndpoint("management", e => e.Port = 15672); + +var stateStore = builder.AddDaprStateStore("statestore"); +var pubSub = builder.AddDaprPubSub("pubsub") + .WaitFor(rmq); + +builder.AddProject("servicea") + .WithDaprSidecar() + .WithReference(stateStore) + .WithReference(pubSub); + +builder.AddProject("serviceb") + .WithDaprSidecar() + .WithReference(pubSub); + +// console app with no appPort (sender only) +builder.AddProject("servicec") + .WithReference(stateStore) + .WithDaprSidecar(); + +builder.Build().Run(); diff --git a/examples/dapr/CommunityToolkit.Aspire.Hosting.Dapr.AppHost/Properties/launchSettings.json b/examples/dapr/CommunityToolkit.Aspire.Hosting.Dapr.AppHost/Properties/launchSettings.json new file mode 100644 index 00000000..25ba222f --- /dev/null +++ b/examples/dapr/CommunityToolkit.Aspire.Hosting.Dapr.AppHost/Properties/launchSettings.json @@ -0,0 +1,29 @@ +{ + "$schema": "https://json.schemastore.org/launchsettings.json", + "profiles": { + "https": { + "commandName": "Project", + "dotnetRunMessages": true, + "launchBrowser": true, + "applicationUrl": "https://localhost:17135;http://localhost:15260", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development", + "DOTNET_ENVIRONMENT": "Development", + "DOTNET_DASHBOARD_OTLP_ENDPOINT_URL": "https://localhost:21008", + "DOTNET_RESOURCE_SERVICE_ENDPOINT_URL": "https://localhost:22043" + } + }, + "http": { + "commandName": "Project", + "dotnetRunMessages": true, + "launchBrowser": true, + "applicationUrl": "http://localhost:15260", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development", + "DOTNET_ENVIRONMENT": "Development", + "DOTNET_DASHBOARD_OTLP_ENDPOINT_URL": "http://localhost:19002", + "DOTNET_RESOURCE_SERVICE_ENDPOINT_URL": "http://localhost:20002" + } + } + } +} diff --git a/examples/dapr/CommunityToolkit.Aspire.Hosting.Dapr.AppHost/appsettings.json b/examples/dapr/CommunityToolkit.Aspire.Hosting.Dapr.AppHost/appsettings.json new file mode 100644 index 00000000..31c092aa --- /dev/null +++ b/examples/dapr/CommunityToolkit.Aspire.Hosting.Dapr.AppHost/appsettings.json @@ -0,0 +1,9 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.AspNetCore": "Warning", + "Aspire.Hosting.Dcp": "Warning" + } + } +} diff --git a/examples/dapr/CommunityToolkit.Aspire.Hosting.Dapr.AppHost/pubsub.yaml b/examples/dapr/CommunityToolkit.Aspire.Hosting.Dapr.AppHost/pubsub.yaml new file mode 100644 index 00000000..419df9f0 --- /dev/null +++ b/examples/dapr/CommunityToolkit.Aspire.Hosting.Dapr.AppHost/pubsub.yaml @@ -0,0 +1,17 @@ +apiVersion: dapr.io/v1alpha1 +kind: Component +metadata: + name: pubsub + namespace: default +spec: + type: pubsub.rabbitmq + version: v1 + metadata: + - name: protocol + value: amqp + - name: hostname + value: localhost + - name: username + value: guest + - name: password + value: guest diff --git a/examples/dapr/CommunityToolkit.Aspire.Hosting.Dapr.ServiceA/CommunityToolkit.Aspire.Hosting.Dapr.ServiceA.csproj b/examples/dapr/CommunityToolkit.Aspire.Hosting.Dapr.ServiceA/CommunityToolkit.Aspire.Hosting.Dapr.ServiceA.csproj new file mode 100644 index 00000000..dbd28bd2 --- /dev/null +++ b/examples/dapr/CommunityToolkit.Aspire.Hosting.Dapr.ServiceA/CommunityToolkit.Aspire.Hosting.Dapr.ServiceA.csproj @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/examples/dapr/CommunityToolkit.Aspire.Hosting.Dapr.ServiceA/Program.cs b/examples/dapr/CommunityToolkit.Aspire.Hosting.Dapr.ServiceA/Program.cs new file mode 100644 index 00000000..65c8cce2 --- /dev/null +++ b/examples/dapr/CommunityToolkit.Aspire.Hosting.Dapr.ServiceA/Program.cs @@ -0,0 +1,53 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using Dapr; +using Dapr.Client; + +var builder = WebApplication.CreateBuilder(args); + +builder.AddServiceDefaults(); + +// Add services to the container. +builder.Services.AddDaprClient(); + +var app = builder.Build(); + +// Configure the HTTP request pipeline. +app.UseCloudEvents(); +app.MapSubscribeHandler(); + +app.MapGet("/weatherforecast", async (DaprClient client) => +{ + var cachedForecasts = await client.GetStateAsync("statestore", "cache"); + + if (cachedForecasts is not null && cachedForecasts.CachedAt > DateTimeOffset.UtcNow.AddMinutes(-1)) + { + return cachedForecasts.Forecasts; + } + + var forecasts = await client.InvokeMethodAsync(HttpMethod.Get, "serviceb", "weatherforecast"); + + await client.SaveStateAsync("statestore", "cache", new CachedWeatherForecast(forecasts, DateTimeOffset.UtcNow)); + + return forecasts; +}) +.WithName("GetWeatherForecast"); + +app.MapPost("/subscriptions/weather", [Topic("pubsub", "weather")] (ILogger logger, WeatherForecastMessage message) => +{ + logger.LogInformation("Weather forecast message received: {Message}", message.Message); +}); + +app.MapDefaultEndpoints(); + +app.Run(); + +internal sealed record WeatherForecastMessage(string Message); + +internal sealed record CachedWeatherForecast(WeatherForecast[] Forecasts, DateTimeOffset CachedAt); + +internal sealed record WeatherForecast(DateOnly Date, int TemperatureC, string? Summary) +{ + public int TemperatureF => 32 + (int)(TemperatureC / 0.5556); +} diff --git a/examples/dapr/CommunityToolkit.Aspire.Hosting.Dapr.ServiceA/Properties/launchSettings.json b/examples/dapr/CommunityToolkit.Aspire.Hosting.Dapr.ServiceA/Properties/launchSettings.json new file mode 100644 index 00000000..42faa78b --- /dev/null +++ b/examples/dapr/CommunityToolkit.Aspire.Hosting.Dapr.ServiceA/Properties/launchSettings.json @@ -0,0 +1,23 @@ +{ + "$schema": "https://json.schemastore.org/launchsettings.json", + "profiles": { + "http": { + "commandName": "Project", + "dotnetRunMessages": true, + "launchBrowser": true, + "applicationUrl": "http://localhost:5291", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + }, + "https": { + "commandName": "Project", + "dotnetRunMessages": true, + "launchBrowser": true, + "applicationUrl": "https://localhost:7219;http://localhost:5291", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + } + } +} diff --git a/examples/dapr/CommunityToolkit.Aspire.Hosting.Dapr.ServiceA/appsettings.json b/examples/dapr/CommunityToolkit.Aspire.Hosting.Dapr.ServiceA/appsettings.json new file mode 100644 index 00000000..10f68b8c --- /dev/null +++ b/examples/dapr/CommunityToolkit.Aspire.Hosting.Dapr.ServiceA/appsettings.json @@ -0,0 +1,9 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.AspNetCore": "Warning" + } + }, + "AllowedHosts": "*" +} diff --git a/examples/dapr/CommunityToolkit.Aspire.Hosting.Dapr.ServiceB/CommunityToolkit.Aspire.Hosting.Dapr.ServiceB.csproj b/examples/dapr/CommunityToolkit.Aspire.Hosting.Dapr.ServiceB/CommunityToolkit.Aspire.Hosting.Dapr.ServiceB.csproj new file mode 100644 index 00000000..dbd28bd2 --- /dev/null +++ b/examples/dapr/CommunityToolkit.Aspire.Hosting.Dapr.ServiceB/CommunityToolkit.Aspire.Hosting.Dapr.ServiceB.csproj @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/examples/dapr/CommunityToolkit.Aspire.Hosting.Dapr.ServiceB/Program.cs b/examples/dapr/CommunityToolkit.Aspire.Hosting.Dapr.ServiceB/Program.cs new file mode 100644 index 00000000..06f3f399 --- /dev/null +++ b/examples/dapr/CommunityToolkit.Aspire.Hosting.Dapr.ServiceB/Program.cs @@ -0,0 +1,47 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using Dapr.Client; + +var builder = WebApplication.CreateBuilder(args); + +builder.AddServiceDefaults(); + +// Add services to the container. +builder.Services.AddDaprClient(); + +var app = builder.Build(); + +// Configure the HTTP request pipeline. + +var summaries = new[] +{ + "Freezing", "Bracing", "Chilly", "Cool", "Mild", "Warm", "Balmy", "Hot", "Sweltering", "Scorching" +}; + +app.MapGet("/weatherforecast", async (DaprClient client) => +{ + await client.PublishEventAsync("pubsub", "weather", new WeatherForecastMessage("Weather forecast requested!")); + + var forecast = Enumerable.Range(1, 5).Select(index => + new WeatherForecast + ( + DateOnly.FromDateTime(DateTime.Now.AddDays(index)), + Random.Shared.Next(-20, 55), + summaries[Random.Shared.Next(summaries.Length)] + )) + .ToArray(); + return forecast; +}) +.WithName("GetWeatherForecast"); + +app.MapDefaultEndpoints(); + +app.Run(); + +internal sealed record WeatherForecastMessage(string Message); + +internal sealed record WeatherForecast(DateOnly Date, int TemperatureC, string? Summary) +{ + public int TemperatureF => 32 + (int)(TemperatureC / 0.5556); +} diff --git a/examples/dapr/CommunityToolkit.Aspire.Hosting.Dapr.ServiceB/Properties/launchSettings.json b/examples/dapr/CommunityToolkit.Aspire.Hosting.Dapr.ServiceB/Properties/launchSettings.json new file mode 100644 index 00000000..7498dc10 --- /dev/null +++ b/examples/dapr/CommunityToolkit.Aspire.Hosting.Dapr.ServiceB/Properties/launchSettings.json @@ -0,0 +1,23 @@ +{ + "$schema": "https://json.schemastore.org/launchsettings.json", + "profiles": { + "http": { + "commandName": "Project", + "dotnetRunMessages": true, + "launchBrowser": true, + "applicationUrl": "http://localhost:5086", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + }, + "https": { + "commandName": "Project", + "dotnetRunMessages": true, + "launchBrowser": true, + "applicationUrl": "https://localhost:7134;http://localhost:5086", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + } + } +} diff --git a/examples/dapr/CommunityToolkit.Aspire.Hosting.Dapr.ServiceB/appsettings.json b/examples/dapr/CommunityToolkit.Aspire.Hosting.Dapr.ServiceB/appsettings.json new file mode 100644 index 00000000..10f68b8c --- /dev/null +++ b/examples/dapr/CommunityToolkit.Aspire.Hosting.Dapr.ServiceB/appsettings.json @@ -0,0 +1,9 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.AspNetCore": "Warning" + } + }, + "AllowedHosts": "*" +} diff --git a/examples/dapr/CommunityToolkit.Aspire.Hosting.Dapr.ServiceC/CommunityToolkit.Aspire.Hosting.Dapr.ServiceC.csproj b/examples/dapr/CommunityToolkit.Aspire.Hosting.Dapr.ServiceC/CommunityToolkit.Aspire.Hosting.Dapr.ServiceC.csproj new file mode 100644 index 00000000..08d51f5b --- /dev/null +++ b/examples/dapr/CommunityToolkit.Aspire.Hosting.Dapr.ServiceC/CommunityToolkit.Aspire.Hosting.Dapr.ServiceC.csproj @@ -0,0 +1,13 @@ + + + Exe + e7f9178b-87a6-4047-b90a-a1fa9d8137b9 + + + + + + + + + diff --git a/examples/dapr/CommunityToolkit.Aspire.Hosting.Dapr.ServiceC/Program.cs b/examples/dapr/CommunityToolkit.Aspire.Hosting.Dapr.ServiceC/Program.cs new file mode 100644 index 00000000..bf691b37 --- /dev/null +++ b/examples/dapr/CommunityToolkit.Aspire.Hosting.Dapr.ServiceC/Program.cs @@ -0,0 +1,44 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using Dapr.Client; +using Test; + +var builder = Host.CreateApplicationBuilder(args); + +// Removing as it gets very chatty. But leaving it here for reference. +// builder.AddServiceDefaults(); + +var dapr = new DaprClientBuilder() + .Build(); + +builder.Services.AddSingleton(dapr); + +builder.Services.AddHostedService(); + +var app = builder.Build(); + +await app.RunAsync(); + +Console.WriteLine("Goodbye, World!"); + +namespace Test +{ + public sealed class Worker(ILogger logger, DaprClient dapr) : BackgroundService + { + protected override async Task ExecuteAsync(CancellationToken stoppingToken) + { + await dapr.WaitForSidecarAsync(stoppingToken); + + while (!stoppingToken.IsCancellationRequested) + { + var state = await dapr.GetStateAsync( + "statestore", "cache", cancellationToken: stoppingToken); + + logger.LogInformation("State: {0}", state ?? ""); + + await Task.Delay(1000, stoppingToken); + } + } + } +} diff --git a/examples/dapr/CommunityToolkit.Aspire.Hosting.Dapr.ServiceC/Properties/launchSettings.json b/examples/dapr/CommunityToolkit.Aspire.Hosting.Dapr.ServiceC/Properties/launchSettings.json new file mode 100644 index 00000000..c4484fb2 --- /dev/null +++ b/examples/dapr/CommunityToolkit.Aspire.Hosting.Dapr.ServiceC/Properties/launchSettings.json @@ -0,0 +1,23 @@ +{ + "$schema": "https://json.schemastore.org/launchsettings.json", + "profiles": { + "http": { + "commandName": "Project", + "dotnetRunMessages": true, + "launchBrowser": true, + "applicationUrl": "http://localhost:5121", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + }, + "https": { + "commandName": "Project", + "dotnetRunMessages": true, + "launchBrowser": true, + "applicationUrl": "https://localhost:7283;http://localhost:5121", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + } + } +} diff --git a/examples/dapr/CommunityToolkit.Aspire.Hosting.Dapr.ServiceC/appsettings.json b/examples/dapr/CommunityToolkit.Aspire.Hosting.Dapr.ServiceC/appsettings.json new file mode 100644 index 00000000..10f68b8c --- /dev/null +++ b/examples/dapr/CommunityToolkit.Aspire.Hosting.Dapr.ServiceC/appsettings.json @@ -0,0 +1,9 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.AspNetCore": "Warning" + } + }, + "AllowedHosts": "*" +} diff --git a/examples/dapr/CommunityToolkit.Aspire.Hosting.Dapr.ServiceDefaults/CommunityToolkit.Aspire.Hosting.Dapr.ServiceDefaults.csproj b/examples/dapr/CommunityToolkit.Aspire.Hosting.Dapr.ServiceDefaults/CommunityToolkit.Aspire.Hosting.Dapr.ServiceDefaults.csproj new file mode 100644 index 00000000..caa6344d --- /dev/null +++ b/examples/dapr/CommunityToolkit.Aspire.Hosting.Dapr.ServiceDefaults/CommunityToolkit.Aspire.Hosting.Dapr.ServiceDefaults.csproj @@ -0,0 +1,21 @@ + + + + enable + enable + true + + + + + + + + + + + + + + + diff --git a/examples/dapr/CommunityToolkit.Aspire.Hosting.Dapr.ServiceDefaults/Extensions.cs b/examples/dapr/CommunityToolkit.Aspire.Hosting.Dapr.ServiceDefaults/Extensions.cs new file mode 100644 index 00000000..13151bf4 --- /dev/null +++ b/examples/dapr/CommunityToolkit.Aspire.Hosting.Dapr.ServiceDefaults/Extensions.cs @@ -0,0 +1,119 @@ +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Diagnostics.HealthChecks; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Diagnostics.HealthChecks; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.ServiceDiscovery; +using OpenTelemetry; +using OpenTelemetry.Metrics; +using OpenTelemetry.Trace; + +namespace Microsoft.Extensions.Hosting; + +// Adds common .NET Aspire services: service discovery, resilience, health checks, and OpenTelemetry. +// This project should be referenced by each service project in your solution. +// To learn more about using this project, see https://aka.ms/dotnet/aspire/service-defaults +public static class Extensions +{ + public static TBuilder AddServiceDefaults(this TBuilder builder) where TBuilder : IHostApplicationBuilder + { + builder.ConfigureOpenTelemetry(); + + builder.AddDefaultHealthChecks(); + + builder.Services.AddServiceDiscovery(); + + builder.Services.ConfigureHttpClientDefaults(http => + { + // Turn on resilience by default + http.AddStandardResilienceHandler(); + + // Turn on service discovery by default + http.AddServiceDiscovery(); + }); + + // Uncomment the following to restrict the allowed schemes for service discovery. + // builder.Services.Configure(options => + // { + // options.AllowedSchemes = ["https"]; + // }); + + return builder; + } + + public static TBuilder ConfigureOpenTelemetry(this TBuilder builder) where TBuilder : IHostApplicationBuilder + { + builder.Logging.AddOpenTelemetry(logging => + { + logging.IncludeFormattedMessage = true; + logging.IncludeScopes = true; + }); + + builder.Services.AddOpenTelemetry() + .WithMetrics(metrics => + { + metrics.AddAspNetCoreInstrumentation() + .AddHttpClientInstrumentation() + .AddRuntimeInstrumentation(); + }) + .WithTracing(tracing => + { + tracing.AddSource(builder.Environment.ApplicationName) + .AddAspNetCoreInstrumentation() + // Uncomment the following line to enable gRPC instrumentation (requires the OpenTelemetry.Instrumentation.GrpcNetClient package) + //.AddGrpcClientInstrumentation() + .AddHttpClientInstrumentation(); + }); + + builder.AddOpenTelemetryExporters(); + + return builder; + } + + private static TBuilder AddOpenTelemetryExporters(this TBuilder builder) where TBuilder : IHostApplicationBuilder + { + var useOtlpExporter = !string.IsNullOrWhiteSpace(builder.Configuration["OTEL_EXPORTER_OTLP_ENDPOINT"]); + + if (useOtlpExporter) + { + builder.Services.AddOpenTelemetry().UseOtlpExporter(); + } + + // Uncomment the following lines to enable the Azure Monitor exporter (requires the Azure.Monitor.OpenTelemetry.AspNetCore package) + //if (!string.IsNullOrEmpty(builder.Configuration["APPLICATIONINSIGHTS_CONNECTION_STRING"])) + //{ + // builder.Services.AddOpenTelemetry() + // .UseAzureMonitor(); + //} + + return builder; + } + + public static TBuilder AddDefaultHealthChecks(this TBuilder builder) where TBuilder : IHostApplicationBuilder + { + builder.Services.AddHealthChecks() + // Add a default liveness check to ensure app is responsive + .AddCheck("self", () => HealthCheckResult.Healthy(), ["live"]); + + return builder; + } + + public static WebApplication MapDefaultEndpoints(this WebApplication app) + { + // Adding health checks endpoints to applications in non-development environments has security implications. + // See https://aka.ms/dotnet/aspire/healthchecks for details before enabling these endpoints in non-development environments. + if (app.Environment.IsDevelopment()) + { + // All health checks must pass for app to be considered ready to accept traffic after starting + app.MapHealthChecks("/health"); + + // Only health checks tagged with the "live" tag must pass for app to be considered alive + app.MapHealthChecks("/alive", new HealthCheckOptions + { + Predicate = r => r.Tags.Contains("live") + }); + } + + return app; + } +} diff --git a/examples/java/CommunityToolkit.Aspire.Hosting.Java.AppHost/CommunityToolkit.Aspire.Hosting.Java.AppHost.csproj b/examples/java/CommunityToolkit.Aspire.Hosting.Java.AppHost/CommunityToolkit.Aspire.Hosting.Java.AppHost.csproj index ae901202..236c1840 100644 --- a/examples/java/CommunityToolkit.Aspire.Hosting.Java.AppHost/CommunityToolkit.Aspire.Hosting.Java.AppHost.csproj +++ b/examples/java/CommunityToolkit.Aspire.Hosting.Java.AppHost/CommunityToolkit.Aspire.Hosting.Java.AppHost.csproj @@ -20,9 +20,4 @@ - - - - - diff --git a/examples/java/CommunityToolkit.Aspire.Hosting.Java.AppHost/Program.cs b/examples/java/CommunityToolkit.Aspire.Hosting.Java.AppHost/Program.cs index d5033d5c..78c1ff05 100644 --- a/examples/java/CommunityToolkit.Aspire.Hosting.Java.AppHost/Program.cs +++ b/examples/java/CommunityToolkit.Aspire.Hosting.Java.AppHost/Program.cs @@ -16,6 +16,7 @@ Port = 8085, OtelAgentPath = "../../../agents", }) + .WithMavenBuild() .PublishAsDockerFile( [ new DockerBuildArg("JAR_NAME", "spring-maven-0.0.1-SNAPSHOT.jar"), diff --git a/src/CommunityToolkit.Aspire.Hosting.Dapr/CommunityToolkit.Aspire.Hosting.Dapr.csproj b/src/CommunityToolkit.Aspire.Hosting.Dapr/CommunityToolkit.Aspire.Hosting.Dapr.csproj index 07c92f43..79cf85e8 100644 --- a/src/CommunityToolkit.Aspire.Hosting.Dapr/CommunityToolkit.Aspire.Hosting.Dapr.csproj +++ b/src/CommunityToolkit.Aspire.Hosting.Dapr/CommunityToolkit.Aspire.Hosting.Dapr.csproj @@ -12,4 +12,8 @@ + + + + diff --git a/src/CommunityToolkit.Aspire.Hosting.Dapr/DaprDistributedApplicationLifecycleHook.cs b/src/CommunityToolkit.Aspire.Hosting.Dapr/DaprDistributedApplicationLifecycleHook.cs index d27cb849..4b15d395 100644 --- a/src/CommunityToolkit.Aspire.Hosting.Dapr/DaprDistributedApplicationLifecycleHook.cs +++ b/src/CommunityToolkit.Aspire.Hosting.Dapr/DaprDistributedApplicationLifecycleHook.cs @@ -21,15 +21,17 @@ internal sealed class DaprDistributedApplicationLifecycleHook : IDistributedAppl private readonly IConfiguration _configuration; private readonly IHostEnvironment _environment; private readonly ILogger _logger; + private readonly ResourceLoggerService _resourceLoggerService; private readonly DaprOptions _options; private string? _onDemandResourcesRootPath; - public DaprDistributedApplicationLifecycleHook(IConfiguration configuration, IHostEnvironment environment, ILogger logger, IOptions options) + public DaprDistributedApplicationLifecycleHook(IConfiguration configuration, IHostEnvironment environment, ILogger logger, IOptions options, ResourceLoggerService resourceLoggerService) { _configuration = configuration; _environment = environment; _logger = logger; + _resourceLoggerService = resourceLoggerService; _options = options.Value; } @@ -41,10 +43,6 @@ public async Task BeforeStartAsync(DistributedApplicationModel appModel, Cancell var sideCars = new List(); - var fileName = this._options.DaprPath - ?? GetDefaultDaprPath() - ?? throw new DistributedApplicationException("Unable to locate the Dapr CLI."); - foreach (var resource in appModel.Resources) { if (!resource.TryGetLastAnnotation(out var daprAnnotation)) @@ -52,6 +50,11 @@ public async Task BeforeStartAsync(DistributedApplicationModel appModel, Cancell continue; } + var fileName = this._options.DaprPath + ?? GetDefaultDaprPath() + ?? throw new DistributedApplicationException("Unable to locate the Dapr CLI."); + + var daprSidecar = daprAnnotation.Sidecar; var sidecarOptionsAnnotation = daprSidecar.Annotations.OfType().LastOrDefault(); @@ -322,6 +325,16 @@ static IEnumerable GetAvailablePaths() // Installed windows paths: yield return Path.Combine(pathRoot, "dapr", "dapr.exe"); + // Add all the paths that are reachable via the `PATH` environment variable: + var possibleWindowsDaprPaths = Environment.GetEnvironmentVariable("PATH")? + .Split(Path.PathSeparator) + .Select(path => Path.Combine(path, "dapr.exe")) + .Where(File.Exists) ?? []; + foreach (var path in possibleWindowsDaprPaths) + { + yield return path; + } + yield break; } @@ -340,6 +353,16 @@ static IEnumerable GetAvailablePaths() { yield return Path.Combine(homebrewPrefix, "bin", "dapr"); } + + // Add all the paths that are reachable via the `PATH` environment variable: + var possibleDaprPaths = Environment.GetEnvironmentVariable("PATH")? + .Split(Path.PathSeparator) + .Select(path => Path.Combine(path, "dapr")) + .Where(File.Exists) ?? []; + foreach (var path in possibleDaprPaths) + { + yield return path; + } } } diff --git a/src/CommunityToolkit.Aspire.Hosting.Java/CommunityToolkit.Aspire.Hosting.Java.csproj b/src/CommunityToolkit.Aspire.Hosting.Java/CommunityToolkit.Aspire.Hosting.Java.csproj index 3dce2f4e..42b2c016 100644 --- a/src/CommunityToolkit.Aspire.Hosting.Java/CommunityToolkit.Aspire.Hosting.Java.csproj +++ b/src/CommunityToolkit.Aspire.Hosting.Java/CommunityToolkit.Aspire.Hosting.Java.csproj @@ -12,4 +12,7 @@ + + + diff --git a/src/CommunityToolkit.Aspire.Hosting.Java/JavaAppHostingExtension.Container.cs b/src/CommunityToolkit.Aspire.Hosting.Java/JavaAppHostingExtension.Container.cs new file mode 100644 index 00000000..51a69e21 --- /dev/null +++ b/src/CommunityToolkit.Aspire.Hosting.Java/JavaAppHostingExtension.Container.cs @@ -0,0 +1,56 @@ +using System.Globalization; +using Aspire.Hosting.ApplicationModel; +using CommunityToolkit.Aspire.Utils; + +namespace Aspire.Hosting; + +/// +/// Provides extension methods for adding Java applications to an . +/// +public static partial class JavaAppHostingExtension +{ + /// + /// Adds a Java application to the application model. Executes the containerized Java app. + /// + /// The to add the resource to. + /// The name of the resource. + /// The to configure the Java application." + /// A reference to the . + public static IResourceBuilder AddJavaApp(this IDistributedApplicationBuilder builder, [ResourceName] string name, JavaAppContainerResourceOptions options) + { + ArgumentNullException.ThrowIfNull(builder, nameof(builder)); + ArgumentNullException.ThrowIfNull(options, nameof(options)); + ArgumentException.ThrowIfNullOrWhiteSpace(name, nameof(name)); + ArgumentException.ThrowIfNullOrWhiteSpace(options.ContainerImageName, nameof(options.ContainerImageName)); + + var resource = new JavaAppContainerResource(name); + + var rb = builder.AddResource(resource) + .WithAnnotation(new ContainerImageAnnotation { Image = options.ContainerImageName, Tag = options.ContainerImageTag, Registry = options.ContainerRegistry }) + .WithHttpEndpoint(port: options.Port, targetPort: options.TargetPort, name: JavaAppContainerResource.HttpEndpointName) + .WithJavaDefaults(options); + + if (options.Args is { Length: > 0 }) + { + rb.WithArgs(options.Args); + } + + return rb; + } + + /// + /// Adds a Spring application to the application model. Executes the containerized Spring app. + /// + /// The to add the resource to. + /// The name of the resource. + /// The to configure the Java application." + /// A reference to the . + public static IResourceBuilder AddSpringApp(this IDistributedApplicationBuilder builder, [ResourceName] string name, JavaAppContainerResourceOptions options) => + builder.AddJavaApp(name, options); + + private static IResourceBuilder WithJavaDefaults( + this IResourceBuilder builder, + JavaAppContainerResourceOptions options) => + builder.WithOtlpExporter() + .WithEnvironment("JAVA_TOOL_OPTIONS", $"-javaagent:{options.OtelAgentPath?.TrimEnd('/')}/opentelemetry-javaagent.jar"); +} diff --git a/src/CommunityToolkit.Aspire.Hosting.Java/JavaAppHostingExtension.Executable.cs b/src/CommunityToolkit.Aspire.Hosting.Java/JavaAppHostingExtension.Executable.cs new file mode 100644 index 00000000..c0e97d31 --- /dev/null +++ b/src/CommunityToolkit.Aspire.Hosting.Java/JavaAppHostingExtension.Executable.cs @@ -0,0 +1,218 @@ +using System.Diagnostics; +using System.Globalization; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; +using Aspire.Hosting.ApplicationModel; +using CommunityToolkit.Aspire.Utils; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Logging; + +namespace Aspire.Hosting; + +/// +/// Provides extension methods for adding Java applications to an . +/// +public static partial class JavaAppHostingExtension +{ + /// + /// Adds a Java application to the application model. Executes the executable Java app. + /// + /// The to add the resource to. + /// The name of the resource. + /// The working directory to use for the command. If null, the working directory of the current process is used. + /// The to configure the Java application." + /// A reference to the . + public static IResourceBuilder AddJavaApp(this IDistributedApplicationBuilder builder, [ResourceName] string name, string workingDirectory, JavaAppExecutableResourceOptions options) + { + ArgumentNullException.ThrowIfNull(builder, nameof(builder)); + ArgumentNullException.ThrowIfNull(options, nameof(options)); + ArgumentException.ThrowIfNullOrWhiteSpace(name, nameof(name)); + ArgumentException.ThrowIfNullOrWhiteSpace(workingDirectory, nameof(workingDirectory)); + +#pragma warning disable CS8601 // Possible null reference assignment. + string[] allArgs = options.Args is { Length: > 0 } + ? ["-jar", options.ApplicationName, .. options.Args] + : ["-jar", options.ApplicationName]; +#pragma warning restore CS8601 // Possible null reference assignment. + + workingDirectory = PathNormalizer.NormalizePathForCurrentPlatform(Path.Combine(builder.AppHostDirectory, workingDirectory)); + var resource = new JavaAppExecutableResource(name, "java", workingDirectory); + + return builder.AddResource(resource) + .WithJavaDefaults(options) + .WithHttpEndpoint(port: options.Port, name: JavaAppContainerResource.HttpEndpointName, isProxied: false) + .WithArgs(allArgs); + } + + /// + /// Adds a Spring application to the application model. Executes the executable Spring app. + /// + /// The to add the resource to. + /// The name of the resource. + /// The working directory to use for the command. If null, the working directory of the current process is used. + /// The to configure the Java application." + /// A reference to the . + public static IResourceBuilder AddSpringApp(this IDistributedApplicationBuilder builder, [ResourceName] string name, string workingDirectory, JavaAppExecutableResourceOptions options) => + builder.AddJavaApp(name, workingDirectory, options); + + /// + /// Adds a Maven build step to the application model. + /// + /// The to add the Maven build step to. + /// The to configure the Maven build step. + /// A reference to the . + /// + /// This method adds a Maven build step to the application model. The Maven build step is executed before the Java application is started. + /// + /// The Maven build step is added as an executable resource named "maven" with the command "mvnw --quiet clean package". + /// + /// The Maven build step is excluded from the manifest file. + /// + public static IResourceBuilder WithMavenBuild( + this IResourceBuilder builder, + MavenOptions? mavenOptions = null) + { + mavenOptions ??= new MavenOptions(); + + if (mavenOptions.WorkingDirectory is null) + { + mavenOptions.WorkingDirectory = builder.Resource.WorkingDirectory; + } + + var annotation = new MavenBuildAnnotation(mavenOptions); + + if (builder.Resource.TryGetLastAnnotation(out _)) + { + // Replace the existing annotation, but don't continue on and subscribe to the event again. + builder.WithAnnotation(annotation, ResourceAnnotationMutationBehavior.Replace); + return builder; + } + + builder.WithAnnotation(annotation); + + builder.ApplicationBuilder.Eventing.Subscribe(builder.Resource, async (e, ct) => + { + if (e.Resource is not JavaAppExecutableResource javaAppResource) + { + return; + } + + await BuildWithMaven(javaAppResource, e.Services, ct).ConfigureAwait(false); + }); + + builder.WithCommand( + "build-with-maven", + "Build with Maven", + async (context) => + await BuildWithMaven(builder.Resource, context.ServiceProvider, context.CancellationToken, false).ConfigureAwait(false) ? + new ExecuteCommandResult { Success = true } : + new ExecuteCommandResult { Success = false, ErrorMessage = "Failed to build with Maven" }, + (context) => context.ResourceSnapshot.State switch + { + { Text: "Stopped" } or + { Text: "Exited" } or + { Text: "Finished" } or + { Text: "FailedToStart" } => ResourceCommandState.Enabled, + _ => ResourceCommandState.Disabled + }, + iconName: "build" + ); + + return builder; + + static async Task BuildWithMaven(JavaAppExecutableResource javaAppResource, IServiceProvider services, CancellationToken ct, bool useNotificationService = true) + { + if (!javaAppResource.TryGetLastAnnotation(out var mavenOptionsAnnotation)) + { + return false; + } + + var mavenOptions = mavenOptionsAnnotation.MavenOptions; + var logger = services.GetRequiredService().GetLogger(javaAppResource); + var notificationService = services.GetRequiredService(); + + if (useNotificationService) + { + await notificationService.PublishUpdateAsync(javaAppResource, state => state with + { + State = new("Building Maven project", KnownResourceStates.Starting) + }).ConfigureAwait(false); + } + + logger.LogInformation("Building Maven project"); + + var isWindows = RuntimeInformation.IsOSPlatform(OSPlatform.Windows); + + var mvnw = new Process + { + StartInfo = new ProcessStartInfo + { + FileName = isWindows ? "cmd" : "sh", + Arguments = isWindows ? $"/c {mavenOptions.Command} {string.Join(" ", mavenOptions.Args)}" : $"./{mavenOptions.Command} {string.Join(" ", mavenOptions.Args)}", + WorkingDirectory = mavenOptions.WorkingDirectory, + RedirectStandardOutput = true, + RedirectStandardError = true, + CreateNoWindow = true, + UseShellExecute = false, + } + }; + + mvnw.OutputDataReceived += async (sender, args) => + { + if (!string.IsNullOrWhiteSpace(args.Data)) + { + if (useNotificationService) + { + await notificationService.PublishUpdateAsync(javaAppResource, state => state with + { + State = new(args.Data, KnownResourceStates.Starting) + }).ConfigureAwait(false); + } + + logger.LogInformation("{Data}", args.Data); + } + }; + + mvnw.ErrorDataReceived += async (sender, args) => + { + if (!string.IsNullOrWhiteSpace(args.Data)) + { + if (useNotificationService) + { + await notificationService.PublishUpdateAsync(javaAppResource, state => state with + { + State = new(args.Data, KnownResourceStates.FailedToStart) + }).ConfigureAwait(false); + } + + logger.LogError("{Data}", args.Data); + } + }; + + mvnw.Start(); + mvnw.BeginOutputReadLine(); + mvnw.BeginErrorReadLine(); + + await mvnw.WaitForExitAsync(ct).ConfigureAwait(false); + + if (mvnw.ExitCode != 0) + { + // always use notification service to push out errors in the maven build + await notificationService.PublishUpdateAsync(javaAppResource, state => state with + { + State = new($"mvnw exited with {mvnw.ExitCode}", KnownResourceStates.FailedToStart) + }).ConfigureAwait(false); + + return false; + } + return true; + } + } + + private static IResourceBuilder WithJavaDefaults( + this IResourceBuilder builder, + JavaAppExecutableResourceOptions options) => + builder.WithOtlpExporter() + .WithEnvironment("JAVA_TOOL_OPTIONS", $"-javaagent:{options.OtelAgentPath?.TrimEnd('/')}/opentelemetry-javaagent.jar") + .WithEnvironment("SERVER_PORT", options.Port.ToString(CultureInfo.InvariantCulture)); +} diff --git a/src/CommunityToolkit.Aspire.Hosting.Java/JavaAppHostingExtension.cs b/src/CommunityToolkit.Aspire.Hosting.Java/JavaAppHostingExtension.cs deleted file mode 100644 index 850687bf..00000000 --- a/src/CommunityToolkit.Aspire.Hosting.Java/JavaAppHostingExtension.cs +++ /dev/null @@ -1,106 +0,0 @@ -using System.Globalization; -using Aspire.Hosting.ApplicationModel; -using CommunityToolkit.Aspire.Utils; - -namespace Aspire.Hosting; - -/// -/// Provides extension methods for adding Java applications to an . -/// -public static class JavaAppHostingExtension -{ - /// - /// Adds a Java application to the application model. Executes the containerized Java app. - /// - /// The to add the resource to. - /// The name of the resource. - /// The to configure the Java application." - /// A reference to the . - public static IResourceBuilder AddJavaApp(this IDistributedApplicationBuilder builder, [ResourceName] string name, JavaAppContainerResourceOptions options) - { - ArgumentNullException.ThrowIfNull(builder, nameof(builder)); - ArgumentNullException.ThrowIfNull(options, nameof(options)); - ArgumentException.ThrowIfNullOrWhiteSpace(name, nameof(name)); - ArgumentException.ThrowIfNullOrWhiteSpace(options.ContainerImageName, nameof(options.ContainerImageName)); - - var resource = new JavaAppContainerResource(name); - - var rb = builder.AddResource(resource) - .WithAnnotation(new ContainerImageAnnotation { Image = options.ContainerImageName, Tag = options.ContainerImageTag, Registry = options.ContainerRegistry }) - .WithHttpEndpoint(port: options.Port, targetPort: options.TargetPort, name: JavaAppContainerResource.HttpEndpointName) - .WithJavaDefaults(options); - - if (options.Args is { Length: > 0 }) - { - rb.WithArgs(options.Args); - } - - return rb; - } - - /// - /// Adds a Spring application to the application model. Executes the containerized Spring app. - /// - /// The to add the resource to. - /// The name of the resource. - /// The to configure the Java application." - /// A reference to the . - public static IResourceBuilder AddSpringApp(this IDistributedApplicationBuilder builder, [ResourceName] string name, JavaAppContainerResourceOptions options) => - builder.AddJavaApp(name, options); - - /// - /// Adds a Java application to the application model. Executes the executable Java app. - /// - /// The to add the resource to. - /// The name of the resource. - /// The working directory to use for the command. If null, the working directory of the current process is used. - /// The to configure the Java application." - /// A reference to the . - public static IResourceBuilder AddJavaApp(this IDistributedApplicationBuilder builder, [ResourceName] string name, string workingDirectory, JavaAppExecutableResourceOptions options) - { - ArgumentNullException.ThrowIfNull(builder, nameof(builder)); - ArgumentNullException.ThrowIfNull(options, nameof(options)); - ArgumentException.ThrowIfNullOrWhiteSpace(name, nameof(name)); - ArgumentException.ThrowIfNullOrWhiteSpace(workingDirectory, nameof(workingDirectory)); - -#pragma warning disable CS8601 // Possible null reference assignment. - string[] allArgs = options.Args is { Length: > 0 } - ? ["-jar", options.ApplicationName, .. options.Args] - : ["-jar", options.ApplicationName]; -#pragma warning restore CS8601 // Possible null reference assignment. - - workingDirectory = PathNormalizer.NormalizePathForCurrentPlatform(Path.Combine(builder.AppHostDirectory, workingDirectory)); - var resource = new JavaAppExecutableResource(name, "java", workingDirectory); - - return builder.AddResource(resource) - .WithJavaDefaults(options) - .WithHttpEndpoint(port: options.Port, name: JavaAppContainerResource.HttpEndpointName, isProxied: false) - .WithArgs(allArgs); - } - - /// - /// Adds a Spring application to the application model. Executes the executable Spring app. - /// - /// The to add the resource to. - /// The name of the resource. - /// The working directory to use for the command. If null, the working directory of the current process is used. - /// The to configure the Java application." - /// A reference to the . - public static IResourceBuilder AddSpringApp(this IDistributedApplicationBuilder builder, [ResourceName] string name, string workingDirectory, JavaAppExecutableResourceOptions options) => - builder.AddJavaApp(name, workingDirectory, options); - - private static IResourceBuilder WithJavaDefaults( - this IResourceBuilder builder, - JavaAppContainerResourceOptions options) => - builder.WithOtlpExporter() - .WithEnvironment("JAVA_TOOL_OPTIONS", $"-javaagent:{options.OtelAgentPath?.TrimEnd('/')}/opentelemetry-javaagent.jar") - ; - - private static IResourceBuilder WithJavaDefaults( - this IResourceBuilder builder, - JavaAppExecutableResourceOptions options) => - builder.WithOtlpExporter() - .WithEnvironment("JAVA_TOOL_OPTIONS", $"-javaagent:{options.OtelAgentPath?.TrimEnd('/')}/opentelemetry-javaagent.jar") - .WithEnvironment("SERVER_PORT", options.Port.ToString(CultureInfo.InvariantCulture)) - ; -} diff --git a/src/CommunityToolkit.Aspire.Hosting.Java/MavenBuildAnnotation.cs b/src/CommunityToolkit.Aspire.Hosting.Java/MavenBuildAnnotation.cs new file mode 100644 index 00000000..8b678b61 --- /dev/null +++ b/src/CommunityToolkit.Aspire.Hosting.Java/MavenBuildAnnotation.cs @@ -0,0 +1,8 @@ +using Aspire.Hosting.ApplicationModel; + +namespace Aspire.Hosting; + +internal class MavenBuildAnnotation(MavenOptions mavenOptions) : IResourceAnnotation +{ + public MavenOptions MavenOptions { get; } = mavenOptions; +} \ No newline at end of file diff --git a/src/CommunityToolkit.Aspire.Hosting.Java/MavenOptions.cs b/src/CommunityToolkit.Aspire.Hosting.Java/MavenOptions.cs new file mode 100644 index 00000000..c45cb7f5 --- /dev/null +++ b/src/CommunityToolkit.Aspire.Hosting.Java/MavenOptions.cs @@ -0,0 +1,20 @@ +namespace Aspire.Hosting; + +/// +/// Represents the options for configuring a Maven build step. +/// +public sealed class MavenOptions +{ + /// + /// Gets or sets the working directory to use for the command. If null, the working directory of the current process is used. + /// + public string? WorkingDirectory { get; set; } + /// + /// Gets or sets the command to execute. Default is "mvnw". + /// + public string Command { get; set; } = "mvnw"; + /// + /// Gets or sets the arguments to pass to the command. Default is "--quiet clean package". + /// + public string[] Args { get; set; } = ["--quiet", "clean", "package"]; +} \ No newline at end of file diff --git a/src/CommunityToolkit.Aspire.Hosting.Java/PublicAPI.Unshipped.txt b/src/CommunityToolkit.Aspire.Hosting.Java/PublicAPI.Unshipped.txt index 074c6ad1..9995eac8 100644 --- a/src/CommunityToolkit.Aspire.Hosting.Java/PublicAPI.Unshipped.txt +++ b/src/CommunityToolkit.Aspire.Hosting.Java/PublicAPI.Unshipped.txt @@ -1,2 +1,10 @@ #nullable enable - +Aspire.Hosting.MavenOptions.WorkingDirectory.get -> string? +static Aspire.Hosting.JavaAppHostingExtension.WithMavenBuild(this Aspire.Hosting.ApplicationModel.IResourceBuilder! builder, Aspire.Hosting.MavenOptions? mavenOptions = null) -> Aspire.Hosting.ApplicationModel.IResourceBuilder! +Aspire.Hosting.MavenOptions +Aspire.Hosting.MavenOptions.MavenOptions() -> void +Aspire.Hosting.MavenOptions.WorkingDirectory.set -> void +Aspire.Hosting.MavenOptions.Command.get -> string! +Aspire.Hosting.MavenOptions.Command.set -> void +Aspire.Hosting.MavenOptions.Args.get -> string![]! +Aspire.Hosting.MavenOptions.Args.set -> void diff --git a/tests/CommunityToolkit.Aspire.Hosting.Dapr.AzureExtensions.Tests/ResourceCreationTests.cs b/tests/CommunityToolkit.Aspire.Hosting.Dapr.AzureExtensions.Tests/ResourceCreationTests.cs index dd509222..b7e3594a 100644 --- a/tests/CommunityToolkit.Aspire.Hosting.Dapr.AzureExtensions.Tests/ResourceCreationTests.cs +++ b/tests/CommunityToolkit.Aspire.Hosting.Dapr.AzureExtensions.Tests/ResourceCreationTests.cs @@ -47,7 +47,7 @@ public void GetInfrastructureConfigurationAction_ComponentNameCanBeOverwritten() var configureInfrastructure = AzureDaprHostingExtensions.GetInfrastructureConfigurationAction(daprResource, [redisHost]); daprResource.Name = "myDaprComponent"; - + var azureDaprResourceBuilder = builder.AddDaprStateStore("daprState") .AddAzureDaprResource("AzureDaprResource", configureInfrastructure); @@ -79,7 +79,7 @@ param daprConnectionString string } parent: containerAppEnvironment } - """.ReplaceLineEndings("\n"); + """; Assert.Equal(expectedBicep, bicepTemplate); } @@ -124,7 +124,7 @@ param daprConnectionString string } parent: containerAppEnvironment } - """.ReplaceLineEndings("\n"); + """; Assert.Equal(expectedBicep, bicepTemplate); } @@ -166,7 +166,7 @@ public void GetInfrastructureConfigurationAction_HandlesNullParameters() } parent: containerAppEnvironment } - """.ReplaceLineEndings("\n"); + """; Assert.Equal(expectedBicep, bicepTemplate); } @@ -238,7 +238,7 @@ param keyVaultName string } parent: keyVault } - """.ReplaceLineEndings("\n"); + """; Assert.Equal(expectedBicep, bicepTemplate); } @@ -293,7 +293,7 @@ param keyVaultName string resource keyVault 'Microsoft.KeyVault/vaults@2023-07-01' existing = { name: keyVaultName } - """.ReplaceLineEndings("\n"); + """; Assert.Equal(expectedBicep, bicepTemplate); } diff --git a/tests/CommunityToolkit.Aspire.Hosting.Dapr.AzureRedis.Tests/ResourceCreationTests.cs b/tests/CommunityToolkit.Aspire.Hosting.Dapr.AzureRedis.Tests/ResourceCreationTests.cs index 882cacec..38b2d5a5 100644 --- a/tests/CommunityToolkit.Aspire.Hosting.Dapr.AzureRedis.Tests/ResourceCreationTests.cs +++ b/tests/CommunityToolkit.Aspire.Hosting.Dapr.AzureRedis.Tests/ResourceCreationTests.cs @@ -74,7 +74,7 @@ param keyVaultName string output daprConnectionString string = '${redisState.properties.hostName}:${redisState.properties.sslPort}' output redisPasswordSecretUri string = daprRedisPassword.properties.secretUri - """.ReplaceLineEndings("\n"); + """; Assert.Equal(expectedRedisBicep, redisBicep); @@ -128,7 +128,7 @@ param redisPasswordSecretUri string } parent: containerAppEnvironment } - """.ReplaceLineEndings("\n"); + """; Assert.Equal(expectedDaprBicep, daprBicep); } @@ -193,7 +193,7 @@ param principalName string output connectionString string = '${redisState.properties.hostName},ssl=true' output daprConnectionString string = '${redisState.properties.hostName}:${redisState.properties.sslPort}' - """.ReplaceLineEndings("\n"); + """; Assert.Equal(expectedRedisBicep, redisBicep); @@ -243,7 +243,7 @@ param redisHost string } parent: containerAppEnvironment } - """.ReplaceLineEndings("\n"); + """; Assert.Equal(expectedDaprBicep, daprBicep); @@ -316,7 +316,7 @@ param principalName string output connectionString string = '${redisState.properties.hostName},ssl=true' output daprConnectionString string = '${redisState.properties.hostName}:${redisState.properties.port}' - """.ReplaceLineEndings("\n"); + """; Assert.Equal(expectedRedisBicep, redisBicep); diff --git a/tests/CommunityToolkit.Aspire.Hosting.Dapr.Tests/AddDaprPubSubTests.cs b/tests/CommunityToolkit.Aspire.Hosting.Dapr.Tests/AddDaprPubSubTests.cs new file mode 100644 index 00000000..f2b16b0e --- /dev/null +++ b/tests/CommunityToolkit.Aspire.Hosting.Dapr.Tests/AddDaprPubSubTests.cs @@ -0,0 +1,79 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using Aspire.Hosting; + +namespace CommunityToolkit.Aspire.Hosting.Dapr.Tests; + +public class AddDaprPubSubTests +{ + [Fact] + public void DistributedApplicationBuilderCannotBeNull() + { + Assert.Throws(() => DistributedApplication.CreateBuilder().AddDaprPubSub(null!)); + } + + [Fact] + public void ResourceNameCannotBeOmitted() + { + string name = ""; + Assert.Throws(() => DistributedApplication.CreateBuilder().AddDaprPubSub(name)); + + name = " "; + Assert.Throws(() => DistributedApplication.CreateBuilder().AddDaprPubSub(name)); + + name = null!; + Assert.Throws(() => DistributedApplication.CreateBuilder().AddDaprPubSub(name)); + } + + [Fact] + public void OptionsConfiguredOnDaprComponent() + { + var builder = DistributedApplication.CreateBuilder(); + var name = "pubsub"; + var type = DaprConstants.BuildingBlocks.PubSub; + var options = new DaprComponentOptions { LocalPath = "path" }; + + builder.AddDaprPubSub(name, options); + + var resource = builder.Resources.Single(); + var daprResource = Assert.IsType(resource); + Assert.Equal(name, resource.Name); + Assert.Equal(type, daprResource.Type); + Assert.Equal(options, daprResource.Options); + } + + [Fact] + public void ResourceConfiguredWithHiddenIntialState() + { + var builder = DistributedApplication.CreateBuilder(); + var name = "pubsub"; + + builder.AddDaprPubSub(name); + + var resource = builder.Resources.Single(); + var daprResource = Assert.IsType(resource); + + Assert.True(daprResource.TryGetAnnotationsOfType(out var annotations)); + var annotation = Assert.Single(annotations); + + Assert.Equal(KnownResourceStates.Hidden, annotation.InitialSnapshot.State?.Text); + } + + [Fact] + public void ResourceIncludedInManifest() + { + var builder = DistributedApplication.CreateBuilder(); + var name = "pubsub"; + + builder.AddDaprPubSub(name); + + var resource = builder.Resources.Single(); + var daprResource = Assert.IsType(resource); + + Assert.True(daprResource.TryGetAnnotationsOfType(out var annotations)); + var annotation = Assert.Single(annotations); + + Assert.NotNull(annotation.Callback); + } +} diff --git a/tests/CommunityToolkit.Aspire.Hosting.Dapr.Tests/AddDaprStateStoreTests.cs b/tests/CommunityToolkit.Aspire.Hosting.Dapr.Tests/AddDaprStateStoreTests.cs new file mode 100644 index 00000000..a3df00f5 --- /dev/null +++ b/tests/CommunityToolkit.Aspire.Hosting.Dapr.Tests/AddDaprStateStoreTests.cs @@ -0,0 +1,79 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using Aspire.Hosting; + +namespace CommunityToolkit.Aspire.Hosting.Dapr.Tests; + +public class AddDaprStateStoreTests +{ + [Fact] + public void DistributedApplicationBuilderCannotBeNull() + { + Assert.Throws(() => DistributedApplication.CreateBuilder().AddDaprStateStore(null!)); + } + + [Fact] + public void ResourceNameCannotBeOmitted() + { + string name = ""; + Assert.Throws(() => DistributedApplication.CreateBuilder().AddDaprStateStore(name)); + + name = " "; + Assert.Throws(() => DistributedApplication.CreateBuilder().AddDaprStateStore(name)); + + name = null!; + Assert.Throws(() => DistributedApplication.CreateBuilder().AddDaprStateStore(name)); + } + + [Fact] + public void OptionsConfiguredOnDaprComponent() + { + var builder = DistributedApplication.CreateBuilder(); + var name = "statestore"; + var type = DaprConstants.BuildingBlocks.StateStore; + var options = new DaprComponentOptions { LocalPath = "path" }; + + builder.AddDaprStateStore(name, options); + + var resource = builder.Resources.Single(); + var daprResource = Assert.IsType(resource); + Assert.Equal(name, resource.Name); + Assert.Equal(type, daprResource.Type); + Assert.Equal(options, daprResource.Options); + } + + [Fact] + public void ResourceConfiguredWithHiddenIntialState() + { + var builder = DistributedApplication.CreateBuilder(); + var name = "statestore"; + + builder.AddDaprStateStore(name); + + var resource = builder.Resources.Single(); + var daprResource = Assert.IsType(resource); + + Assert.True(daprResource.TryGetAnnotationsOfType(out var annotations)); + var annotation = Assert.Single(annotations); + + Assert.Equal(KnownResourceStates.Hidden, annotation.InitialSnapshot.State?.Text); + } + + [Fact] + public void ResourceIncludedInManifest() + { + var builder = DistributedApplication.CreateBuilder(); + var name = "statestore"; + + builder.AddDaprStateStore(name); + + var resource = builder.Resources.Single(); + var daprResource = Assert.IsType(resource); + + Assert.True(daprResource.TryGetAnnotationsOfType(out var annotations)); + var annotation = Assert.Single(annotations); + + Assert.NotNull(annotation.Callback); + } +} \ No newline at end of file diff --git a/tests/CommunityToolkit.Aspire.Hosting.Dapr.Tests/AppHostTests.cs b/tests/CommunityToolkit.Aspire.Hosting.Dapr.Tests/AppHostTests.cs new file mode 100644 index 00000000..454600ba --- /dev/null +++ b/tests/CommunityToolkit.Aspire.Hosting.Dapr.Tests/AppHostTests.cs @@ -0,0 +1,21 @@ +using Aspire.Components.Common.Tests; +using CommunityToolkit.Aspire.Testing; + +namespace CommunityToolkit.Aspire.Hosting.Dapr.Tests; + +[RequiresDocker] +public class AppHostTests(AspireIntegrationTestFixture fixture) : + IClassFixture> +{ + [Fact] + public async Task ResourceStartsAndRespondsOk() + { + var resourceName = "servicea"; + await fixture.ResourceNotificationService.WaitForResourceHealthyAsync(resourceName).WaitAsync(TimeSpan.FromMinutes(5)); + var httpClient = fixture.CreateHttpClient(resourceName); + + var response = await httpClient.GetAsync("/weatherforecast"); + + Assert.Equal(HttpStatusCode.OK, response.StatusCode); + } +} diff --git a/tests/CommunityToolkit.Aspire.Hosting.Dapr.Tests/CommunityToolkit.Aspire.Hosting.Dapr.Tests.csproj b/tests/CommunityToolkit.Aspire.Hosting.Dapr.Tests/CommunityToolkit.Aspire.Hosting.Dapr.Tests.csproj index fb6f124a..ad08149e 100644 --- a/tests/CommunityToolkit.Aspire.Hosting.Dapr.Tests/CommunityToolkit.Aspire.Hosting.Dapr.Tests.csproj +++ b/tests/CommunityToolkit.Aspire.Hosting.Dapr.Tests/CommunityToolkit.Aspire.Hosting.Dapr.Tests.csproj @@ -3,6 +3,7 @@ + diff --git a/tests/CommunityToolkit.Aspire.Hosting.Dapr.Tests/WithDaprSidecarTests.cs b/tests/CommunityToolkit.Aspire.Hosting.Dapr.Tests/WithDaprSidecarTests.cs new file mode 100644 index 00000000..cbe7df0b --- /dev/null +++ b/tests/CommunityToolkit.Aspire.Hosting.Dapr.Tests/WithDaprSidecarTests.cs @@ -0,0 +1,63 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using Aspire.Hosting; + +namespace CommunityToolkit.Aspire.Hosting.Dapr.Tests; + +public class WithDaprSidecarTests +{ + [Fact] + public void ParentResourceConfiguredWithSidecarAnnotation() + { + var builder = DistributedApplication.CreateBuilder(); + + var rb = builder.AddProject("test") + .WithDaprSidecar(); + + Assert.Single(rb.Resource.Annotations.OfType()); + } + + [Fact] + public void ResourceAddedWithHiddenInitialState() + { + var builder = DistributedApplication.CreateBuilder(); + + var rb = builder.AddProject("test") + .WithDaprSidecar(); + + var resource = Assert.Single(builder.Resources.OfType()); + Assert.True(resource.TryGetAnnotationsOfType(out var annotations)); + var annotation = Assert.Single(annotations); + + Assert.Equal(KnownResourceStates.Hidden, annotation.InitialSnapshot.State?.Text); + } + + [Fact] + public void OptionsCanBeConfiguredOnSidecar() + { + var builder = DistributedApplication.CreateBuilder(); + + builder.AddProject("test") + .WithDaprSidecar(new DaprSidecarOptions { AppId = "appId" }); + + var resource = Assert.Single(builder.Resources.OfType()); + var annotation = Assert.Single(resource.Annotations.OfType()); + + Assert.Equal("appId", annotation.Options.AppId); + } + + [Fact] + public void OptionsCanBeConfiguredUsingCallback() + { + var builder = DistributedApplication.CreateBuilder(); + + builder.AddProject("test") + .WithDaprSidecar(b => b.WithOptions(new DaprSidecarOptions { AppId = "appId" })); + + var resource = Assert.Single(builder.Resources.OfType()); + var annotation = Assert.Single(resource.Annotations.OfType()); + + Assert.Equal("appId", annotation.Options.AppId); + } +} diff --git a/tests/CommunityToolkit.Aspire.Hosting.Java.Tests/ExecutableResourceCreationTests.cs b/tests/CommunityToolkit.Aspire.Hosting.Java.Tests/ExecutableResourceCreationTests.cs index 4f0068ab..a8f51769 100644 --- a/tests/CommunityToolkit.Aspire.Hosting.Java.Tests/ExecutableResourceCreationTests.cs +++ b/tests/CommunityToolkit.Aspire.Hosting.Java.Tests/ExecutableResourceCreationTests.cs @@ -1,4 +1,5 @@ using Aspire.Hosting; +using Aspire.Hosting.Eventing; namespace CommunityToolkit.Aspire.Hosting.Java.Tests; @@ -104,4 +105,182 @@ public async Task AddJavaAppContainerDetailsSetOnResource() Assert.Contains("-jar", context.Args); Assert.Contains(options.ApplicationName, context.Args); } + + [Fact] + public void AddingMavenOptions() + { + var builder = DistributedApplication.CreateBuilder(); + + var options = new JavaAppExecutableResourceOptions + { + ApplicationName = "test.jar", + Args = ["--test"], + OtelAgentPath = "otel-agent", + Port = 8080 + }; + + builder.AddJavaApp("java", Environment.CurrentDirectory, options) + .WithMavenBuild(); + + using var app = builder.Build(); + + var appModel = app.Services.GetRequiredService(); + var resource = Assert.Single(appModel.Resources.OfType()); + var annotation = Assert.Single(resource.Annotations.OfType()); + + Assert.NotNull(annotation.MavenOptions); + Assert.Equal("mvnw", annotation.MavenOptions.Command); + Assert.Equal("--quiet clean package", string.Join(' ', annotation.MavenOptions.Args)); + Assert.Equal(Environment.CurrentDirectory, annotation.MavenOptions.WorkingDirectory); + } + + [Fact] + public void AddingMavenOptionsWithOverrides() + { + var builder = DistributedApplication.CreateBuilder(); + + var options = new JavaAppExecutableResourceOptions + { + ApplicationName = "test.jar", + Args = ["--test"], + OtelAgentPath = "otel-agent", + Port = 8080 + }; + + builder.AddJavaApp("java", Environment.CurrentDirectory, options) + .WithMavenBuild(new() + { + Args = ["clean", "package"], + }); + + using var app = builder.Build(); + + var appModel = app.Services.GetRequiredService(); + var resource = Assert.Single(appModel.Resources.OfType()); + var annotation = Assert.Single(resource.Annotations.OfType()); + + Assert.NotNull(annotation.MavenOptions); + Assert.Equal("mvnw", annotation.MavenOptions.Command); + Assert.Equal("clean package", string.Join(' ', annotation.MavenOptions.Args)); + Assert.Equal(Environment.CurrentDirectory, annotation.MavenOptions.WorkingDirectory); + } + + [Fact] + public void ChainingAddMavenBuildOverridesPreviousOptions() + { + var builder = DistributedApplication.CreateBuilder(); + + var options = new JavaAppExecutableResourceOptions + { + ApplicationName = "test.jar", + Args = ["--test"], + OtelAgentPath = "otel-agent", + Port = 8080 + }; + + builder.AddJavaApp("java", Environment.CurrentDirectory, options) + .WithMavenBuild(new() + { + Args = ["clean", "package"], + }) + .WithMavenBuild(); + + using var app = builder.Build(); + + var appModel = app.Services.GetRequiredService(); + var resource = Assert.Single(appModel.Resources.OfType()); + var annotation = Assert.Single(resource.Annotations.OfType()); + + Assert.NotNull(annotation.MavenOptions); + Assert.Equal("mvnw", annotation.MavenOptions.Command); + Assert.Equal("--quiet clean package", string.Join(' ', annotation.MavenOptions.Args)); + Assert.Equal(Environment.CurrentDirectory, annotation.MavenOptions.WorkingDirectory); + } + + [Fact] + public void AddingMavenBuildRegistersRebuildCommand() + { + var builder = DistributedApplication.CreateBuilder(); + + var options = new JavaAppExecutableResourceOptions + { + ApplicationName = "test.jar", + Args = ["--test"], + OtelAgentPath = "otel-agent", + Port = 8080 + }; + + builder.AddJavaApp("java", Environment.CurrentDirectory, options) + .WithMavenBuild(); + + using var app = builder.Build(); + + var appModel = app.Services.GetRequiredService(); + var resource = Assert.Single(appModel.Resources.OfType()); + Assert.Single(resource.Annotations.OfType(), a => a.Name == "build-with-maven"); + } + + [Fact] + public void MultipleAddingMavenBuildRegistersSingleRebuildCommand() + { + var builder = DistributedApplication.CreateBuilder(); + + var options = new JavaAppExecutableResourceOptions + { + ApplicationName = "test.jar", + Args = ["--test"], + OtelAgentPath = "otel-agent", + Port = 8080 + }; + + builder.AddJavaApp("java", Environment.CurrentDirectory, options) + .WithMavenBuild(); + + using var app = builder.Build(); + + var appModel = app.Services.GetRequiredService(); + var resource = Assert.Single(appModel.Resources.OfType()); + Assert.Single(resource.Annotations.OfType(), a => a.Name == "build-with-maven"); + } + + [Theory] + [InlineData("Stopped", ResourceCommandState.Enabled)] + [InlineData("Finished", ResourceCommandState.Enabled)] + [InlineData("Exited", ResourceCommandState.Enabled)] + [InlineData("FailedToStart", ResourceCommandState.Enabled)] + [InlineData("Starting", ResourceCommandState.Disabled)] + [InlineData("Running", ResourceCommandState.Disabled)] + public void MavenBuildCommandAvailability(string text, ResourceCommandState expectedCommandState) + { + var builder = DistributedApplication.CreateBuilder(); + + var options = new JavaAppExecutableResourceOptions + { + ApplicationName = "test.jar", + Args = ["--test"], + OtelAgentPath = "otel-agent", + Port = 8080 + }; + + builder.AddJavaApp("java", Environment.CurrentDirectory, options) + .WithMavenBuild(); + + using var app = builder.Build(); + + var appModel = app.Services.GetRequiredService(); + var resource = Assert.Single(appModel.Resources.OfType()); + var annoitation = Assert.Single(resource.Annotations.OfType(), a => a.Name == "build-with-maven"); + + var updateState = annoitation.UpdateState(new UpdateCommandStateContext() + { + ResourceSnapshot = new CustomResourceSnapshot() + { + State = new ResourceStateSnapshot(text, null), + ResourceType = "JavaAppExecutableResource", + Properties = [] + }, + ServiceProvider = app.Services + }); + Assert.Equal(expectedCommandState, updateState); + } } From bd43cda149fc99b0d5b865bd1ef6d6699458fcb7 Mon Sep 17 00:00:00 2001 From: Aaron Powell Date: Thu, 23 Jan 2025 04:14:06 +0000 Subject: [PATCH 05/10] Adding codeowners --- CODEOWNERS | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/CODEOWNERS b/CODEOWNERS index 8777088f..fceaced2 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -63,3 +63,8 @@ /src/CommunityToolkit.Aspire.Hosting.Ngrok/ @esskar /tests/CommunityToolkit.Aspire.Hosting.Ngrok.Tests/ @esskar +# CommunityToolkit.Aspire.Dapr.* + +/examples/dapr**/ @FullStackChef @WhitWaldo @Paule96 +/src/CommunityToolkit.Aspire.Hosting.Dapr**/ @FullStackChef @WhitWaldo @Paule96 +/tests/CommunityToolkit.Aspire.Hosting.Dapr**.Tests/ @FullStackChef @WhitWaldo @Paule96 From a6ba572b0a15db527e30ae5a0e4151daf1c1ee22 Mon Sep 17 00:00:00 2001 From: Aaron Powell Date: Thu, 23 Jan 2025 04:52:34 +0000 Subject: [PATCH 06/10] Moving dapr extensions to use our dapr package --- .../AzureRedisCacheDaprHostingExtensions.cs | 2 +- .../CommunityToolkit.Aspire.Hosting.Dapr.AzureRedis.csproj | 7 +++++-- .../PublicAPI.Unshipped.txt | 2 +- .../DaprAzureExtensions/AzureDaprHostingExtensions.cs | 4 ++-- 4 files changed, 9 insertions(+), 6 deletions(-) diff --git a/src/CommunityToolkit.Aspire.Hosting.Dapr.AzureRedis/AzureRedisCacheDaprHostingExtensions.cs b/src/CommunityToolkit.Aspire.Hosting.Dapr.AzureRedis/AzureRedisCacheDaprHostingExtensions.cs index 8d82ae18..bdb5c68c 100644 --- a/src/CommunityToolkit.Aspire.Hosting.Dapr.AzureRedis/AzureRedisCacheDaprHostingExtensions.cs +++ b/src/CommunityToolkit.Aspire.Hosting.Dapr.AzureRedis/AzureRedisCacheDaprHostingExtensions.cs @@ -1,10 +1,10 @@ using Aspire.Hosting.ApplicationModel; using Aspire.Hosting.Azure; -using Aspire.Hosting.Dapr; using Azure.Provisioning; using Azure.Provisioning.AppContainers; using Azure.Provisioning.Expressions; using Azure.Provisioning.KeyVault; +using CommunityToolkit.Aspire.Hosting.Dapr; using AzureRedisResource = Azure.Provisioning.Redis.RedisResource; namespace Aspire.Hosting; diff --git a/src/CommunityToolkit.Aspire.Hosting.Dapr.AzureRedis/CommunityToolkit.Aspire.Hosting.Dapr.AzureRedis.csproj b/src/CommunityToolkit.Aspire.Hosting.Dapr.AzureRedis/CommunityToolkit.Aspire.Hosting.Dapr.AzureRedis.csproj index 231436f2..ffc29710 100644 --- a/src/CommunityToolkit.Aspire.Hosting.Dapr.AzureRedis/CommunityToolkit.Aspire.Hosting.Dapr.AzureRedis.csproj +++ b/src/CommunityToolkit.Aspire.Hosting.Dapr.AzureRedis/CommunityToolkit.Aspire.Hosting.Dapr.AzureRedis.csproj @@ -5,7 +5,6 @@ - @@ -17,7 +16,11 @@ - + + + + + diff --git a/src/CommunityToolkit.Aspire.Hosting.Dapr.AzureRedis/PublicAPI.Unshipped.txt b/src/CommunityToolkit.Aspire.Hosting.Dapr.AzureRedis/PublicAPI.Unshipped.txt index bb92bde3..15126ce3 100644 --- a/src/CommunityToolkit.Aspire.Hosting.Dapr.AzureRedis/PublicAPI.Unshipped.txt +++ b/src/CommunityToolkit.Aspire.Hosting.Dapr.AzureRedis/PublicAPI.Unshipped.txt @@ -1,3 +1,3 @@ #nullable enable Aspire.Hosting.AzureRedisCacheDaprHostingExtensions -static Aspire.Hosting.AzureRedisCacheDaprHostingExtensions.WithReference(this Aspire.Hosting.ApplicationModel.IResourceBuilder! builder, Aspire.Hosting.ApplicationModel.IResourceBuilder! source) -> Aspire.Hosting.ApplicationModel.IResourceBuilder! \ No newline at end of file +static Aspire.Hosting.AzureRedisCacheDaprHostingExtensions.WithReference(this Aspire.Hosting.ApplicationModel.IResourceBuilder! builder, Aspire.Hosting.ApplicationModel.IResourceBuilder! source) -> Aspire.Hosting.ApplicationModel.IResourceBuilder! \ No newline at end of file diff --git a/src/Shared/DaprAzureExtensions/AzureDaprHostingExtensions.cs b/src/Shared/DaprAzureExtensions/AzureDaprHostingExtensions.cs index aeb901d7..f3a5f37b 100644 --- a/src/Shared/DaprAzureExtensions/AzureDaprHostingExtensions.cs +++ b/src/Shared/DaprAzureExtensions/AzureDaprHostingExtensions.cs @@ -1,10 +1,10 @@ using Aspire.Hosting.ApplicationModel; using Aspire.Hosting.Azure; -using Aspire.Hosting.Dapr; using Azure.Provisioning.AppContainers; using Azure.Provisioning.Expressions; using Azure.Provisioning; using Azure.Provisioning.KeyVault; +using CommunityToolkit.Aspire.Hosting.Dapr; namespace Aspire.Hosting; @@ -119,7 +119,7 @@ public static ContainerAppManagedEnvironmentDaprComponent CreateDaprComponent( ArgumentException.ThrowIfNullOrEmpty(bicepIdentifier, nameof(bicepIdentifier)); ArgumentException.ThrowIfNullOrEmpty(componentType, nameof(componentType)); ArgumentException.ThrowIfNullOrEmpty(version, nameof(version)); - + return new(bicepIdentifier) { ComponentType = componentType, From c209664a918e095a9d272a56106ccba970b529ae Mon Sep 17 00:00:00 2001 From: Aaron Powell Date: Thu, 23 Jan 2025 05:00:37 +0000 Subject: [PATCH 07/10] Forgot to update the tests project --- ...nityToolkit.Aspire.Hosting.Dapr.AzureExtensions.Tests.csproj | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/CommunityToolkit.Aspire.Hosting.Dapr.AzureExtensions.Tests/CommunityToolkit.Aspire.Hosting.Dapr.AzureExtensions.Tests.csproj b/tests/CommunityToolkit.Aspire.Hosting.Dapr.AzureExtensions.Tests/CommunityToolkit.Aspire.Hosting.Dapr.AzureExtensions.Tests.csproj index 8d8e495f..3577ab5e 100644 --- a/tests/CommunityToolkit.Aspire.Hosting.Dapr.AzureExtensions.Tests/CommunityToolkit.Aspire.Hosting.Dapr.AzureExtensions.Tests.csproj +++ b/tests/CommunityToolkit.Aspire.Hosting.Dapr.AzureExtensions.Tests/CommunityToolkit.Aspire.Hosting.Dapr.AzureExtensions.Tests.csproj @@ -6,12 +6,12 @@ - + From 48ffbdcbd9cc4c08919fea98b665cb9d021a577d Mon Sep 17 00:00:00 2001 From: Aaron Powell Date: Thu, 23 Jan 2025 05:27:41 +0000 Subject: [PATCH 08/10] Adding some assembly filters --- .github/workflows/code-coverage.yml | 2 +- ...tyToolkit.Aspire.Hosting.Dapr.AzureExtensions.Tests.csproj | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/code-coverage.yml b/.github/workflows/code-coverage.yml index 10e399b1..918fb30a 100644 --- a/.github/workflows/code-coverage.yml +++ b/.github/workflows/code-coverage.yml @@ -25,7 +25,7 @@ jobs: tag: "${{ github.run_number }}_${{ github.run_id }}" customSettings: "" toolpath: "reportgeneratortool" - assemblyfilters: "-*.AppHost;-CommunityToolkit.Aspire.Hosting.Azure.DataApiBuilder.BlazorApp;-*.ServiceDefaults;-CommunityToolkit.Aspire.Testing;-Aspire.Hosting;-Aspire.Hosting.NodeJs;-Aspire.Hosting.SqlServer;-Aspire.Hosting.Python" + assemblyfilters: "-*.AppHost;-CommunityToolkit.Aspire.Hosting.Azure.DataApiBuilder.BlazorApp;-*.ServiceDefaults;-CommunityToolkit.Aspire.Testing;-Aspire.Hosting;-Aspire.Hosting.NodeJs;-Aspire.Hosting.SqlServer;-Aspire.Hosting.Python;-Aspire.Hosting.RabbitMQ;-Aspire.Hosting.Redis" - name: Upload combined coverage XML uses: actions/upload-artifact@v4 diff --git a/tests/CommunityToolkit.Aspire.Hosting.Dapr.AzureExtensions.Tests/CommunityToolkit.Aspire.Hosting.Dapr.AzureExtensions.Tests.csproj b/tests/CommunityToolkit.Aspire.Hosting.Dapr.AzureExtensions.Tests/CommunityToolkit.Aspire.Hosting.Dapr.AzureExtensions.Tests.csproj index 3577ab5e..2fe1f4fa 100644 --- a/tests/CommunityToolkit.Aspire.Hosting.Dapr.AzureExtensions.Tests/CommunityToolkit.Aspire.Hosting.Dapr.AzureExtensions.Tests.csproj +++ b/tests/CommunityToolkit.Aspire.Hosting.Dapr.AzureExtensions.Tests/CommunityToolkit.Aspire.Hosting.Dapr.AzureExtensions.Tests.csproj @@ -5,10 +5,10 @@ true - + - + From 3469c3aa42fb1275e7f28f085e81faad93fbf338 Mon Sep 17 00:00:00 2001 From: Aaron Powell Date: Fri, 31 Jan 2025 03:02:24 +0000 Subject: [PATCH 09/10] Adding readme updates --- README.md | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/README.md b/README.md index 413c1cfe..4463446e 100644 --- a/README.md +++ b/README.md @@ -34,6 +34,8 @@ This repository contains the source code for the .NET Aspire Community Toolkit, | - **Learn More**: [`Hosting.Sqlite`][sqlite-integration-docs]
- Stable 📦: [![CommunityToolkit.Aspire.Hosting.Sqlite][sqlite-shields]][sqlite-hosting-nuget]
- Preview 📦: [![CommunityToolkit.Aspire.Hosting.Sqlite][sqlite-shields-preview]][sqlite-hosting-nuget-preview] | An Aspire hosting integration to setup a SQLite database with optional SQLite Web as a dev UI. | | - **Learn More**: [`Microsoft.Data.Sqlite`][sqlite-integration-docs]
- Stable 📦: [![CommunityToolkit.Aspire.Microsoft.Data.Sqlite][sqlite-shields]][sqlite-nuget]
- Preview 📦: [![CommunityToolkit.Aspire.Microsoft.Data.Sqlite][sqlite-shields-preview]][sqlite-nuget-preview] | An Aspire client integration for the Microsoft.Data.Sqlite NuGet package. | | - **Learn More**: [`Microsoft.EntityFrameworkCore.Sqlite`][sqlite-ef-integration-docs]
- Stable 📦: [![CommunityToolkit.Aspire.Microsoft.EntityFrameworkCore.Sqlite][sqlite-ef-shields]][sqlite-ef-nuget]
- Preview 📦: [![CommunityToolkit.Aspire.Microsoft.EntityFrameworkCore.Sqlite][sqlite-ef-shields-preview]][sqlite-ef-nuget-preview] | An Aspire client integration for the Microsoft.EntityFrameworkCore.Sqlite NuGet package. | +| - **Learn More**: [`Hosting.Dapr`][dapr-integration-docs]
- Stable 📦: [![CommunityToolkit.Aspire.Hosting.Dapr][dapr-shields]][dapr-nuget]
- Preview 📦: [![CommunityToolkit.Aspire.Hosting.Dapr][dapr-shields-preview]][dapr-nuget-preview] | An Aspire hosting integration for Dapr. | +| - **Learn More**: [`Hosting.Dapr.AzureRedis`][dapr-azureredis-integration-docs]
- Stable 📦: [![CommunityToolkit.Aspire.Hosting.Dapr.AzureRedis][dapr-azureredis-shields]][dapr-azureredis-nuget]
- Preview 📦: [![CommunityToolkit.Aspire.Hosting.Dapr.]AzureRedis[dapr-azureredis-shields-preview]][dapr-azureredis-nuget-preview] | An extension for the Dapr hosting integration for using Dapr with Azure Redis cache. | ## 🙌 Getting Started @@ -170,3 +172,13 @@ This project is supported by the [.NET Foundation](https://dotnetfoundation.org) [sqlite-ef-nuget]: https://nuget.org/packages/CommunityToolkit.Aspire.Microsoft.EntityFrameworkCore.Sqlite/ [sqlite-ef-shields-preview]: https://img.shields.io/nuget/vpre/CommunityToolkit.Aspire.Microsoft.EntityFrameworkCore.Sqlite?label=nuget%20(preview) [sqlite-ef-nuget-preview]: https://nuget.org/packages/CommunityToolkit.Aspire.Microsoft.EntityFrameworkCore.Sqlite/absoluteLatest +[dapr-integration-docs]: https://learn.microsoft.com/dotnet/aspire/frameworks/dapr +[dapr-shields]: https://img.shields.io/nuget/v/CommunityToolkit.Aspire.Hosting.Dapr +[dapr-nuget]: https://nuget.org/packages/CommunityToolkit.Aspire.Hosting.Dapr/ +[dapr-shields-preview]: https://img.shields.io/nuget/vpre/CommunityToolkit.Aspire.Hosting.Dapr?label=nuget%20(preview) +[dapr-nuget-preview]: https://nuget.org/packages/CommunityToolkit.Aspire.Hosting.Dapr/absoluteLatest +[dapr-azureredis-integration-docs]: https://learn.microsoft.com/dotnet/aspire/frameworks/dapr +[dapr-azureredis-shields]: https://img.shields.io/nuget/v/CommunityToolkit.Aspire.Hosting.Dapr.AzureRedis +[dapr-azureredis-nuget]: https://nuget.org/packages/CommunityToolkit.Aspire.Hosting.Dapr.AzureRedis/ +[dapr-azureredis-shields-preview]: https://img.shields.io/nuget/vpre/CommunityToolkit.Aspire.Hosting.Dapr.AzureRedis?label=nuget%20(preview) +[dapr-azureredis-nuget-preview]: https://nuget.org/packages/CommunityToolkit.Aspire.Hosting.Dapr.AzureRedis/absoluteLatest From 865641df98911a377b8011c108a0e6ae9a74a4d5 Mon Sep 17 00:00:00 2001 From: Aaron Powell Date: Fri, 31 Jan 2025 03:03:24 +0000 Subject: [PATCH 10/10] Slight break in the markdown --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 4463446e..7e08344b 100644 --- a/README.md +++ b/README.md @@ -35,7 +35,7 @@ This repository contains the source code for the .NET Aspire Community Toolkit, | - **Learn More**: [`Microsoft.Data.Sqlite`][sqlite-integration-docs]
- Stable 📦: [![CommunityToolkit.Aspire.Microsoft.Data.Sqlite][sqlite-shields]][sqlite-nuget]
- Preview 📦: [![CommunityToolkit.Aspire.Microsoft.Data.Sqlite][sqlite-shields-preview]][sqlite-nuget-preview] | An Aspire client integration for the Microsoft.Data.Sqlite NuGet package. | | - **Learn More**: [`Microsoft.EntityFrameworkCore.Sqlite`][sqlite-ef-integration-docs]
- Stable 📦: [![CommunityToolkit.Aspire.Microsoft.EntityFrameworkCore.Sqlite][sqlite-ef-shields]][sqlite-ef-nuget]
- Preview 📦: [![CommunityToolkit.Aspire.Microsoft.EntityFrameworkCore.Sqlite][sqlite-ef-shields-preview]][sqlite-ef-nuget-preview] | An Aspire client integration for the Microsoft.EntityFrameworkCore.Sqlite NuGet package. | | - **Learn More**: [`Hosting.Dapr`][dapr-integration-docs]
- Stable 📦: [![CommunityToolkit.Aspire.Hosting.Dapr][dapr-shields]][dapr-nuget]
- Preview 📦: [![CommunityToolkit.Aspire.Hosting.Dapr][dapr-shields-preview]][dapr-nuget-preview] | An Aspire hosting integration for Dapr. | -| - **Learn More**: [`Hosting.Dapr.AzureRedis`][dapr-azureredis-integration-docs]
- Stable 📦: [![CommunityToolkit.Aspire.Hosting.Dapr.AzureRedis][dapr-azureredis-shields]][dapr-azureredis-nuget]
- Preview 📦: [![CommunityToolkit.Aspire.Hosting.Dapr.]AzureRedis[dapr-azureredis-shields-preview]][dapr-azureredis-nuget-preview] | An extension for the Dapr hosting integration for using Dapr with Azure Redis cache. | +| - **Learn More**: [`Hosting.Dapr.AzureRedis`][dapr-azureredis-integration-docs]
- Stable 📦: [![CommunityToolkit.Aspire.Hosting.Dapr.AzureRedis][dapr-azureredis-shields]][dapr-azureredis-nuget]
- Preview 📦: [![CommunityToolkit.Aspire.Hosting.Dapr.AzureRedis][dapr-azureredis-shields-preview]][dapr-azureredis-nuget-preview] | An extension for the Dapr hosting integration for using Dapr with Azure Redis cache. | ## 🙌 Getting Started