-
Notifications
You must be signed in to change notification settings - Fork 720
Add ExternalServiceResource for modeling external services with service discovery support #9965
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
Changes from 4 commits
Commits
Show all changes
35 commits
Select commit
Hold shift + click to select a range
00cf76c
Initial plan for issue
Copilot a02a4a1
Implement basic ExternalServiceResource with service discovery support
Copilot 9d4b64d
Complete ExternalServiceResource implementation with working endpoint…
Copilot ca43b32
Update implementation to follow DamianEdwards' proposal - WIP endpoin…
Copilot b6d971b
Fix port determination for external service endpoints to consider scheme
Copilot b28113b
Address code review feedback - add health checks, fix endpoint naming…
Copilot 8696040
Refactor ExternalServiceResource to follow DamianEdwards feedback - u…
Copilot 0f3918e
WIP
DamianEdwards 38822e4
Merge branch 'main' into copilot/fix-2311
DamianEdwards fdd7e8c
ExternalServiceResource working now
DamianEdwards 8c3edca
Add support for Yarp and ExternalServiceResource
DamianEdwards 4e98018
Fix things
DamianEdwards facb128
Yarp fixes
DamianEdwards bd0f2fc
Use playground ServiceDefaults
DamianEdwards 7a1ce28
Address code review feedback - use "http" endpoint name and remove un…
Copilot cc6f735
More changes
DamianEdwards a107a76
Merge branch 'copilot/fix-2311' of https://github.com/dotnet/aspire i…
DamianEdwards b08db43
Use the parameter to set env var values directly
DamianEdwards d5c44de
Don't read parameter value in publish mode
DamianEdwards f187960
Throw at publish time if URLs come from parameters
DamianEdwards 490717d
Make publishing URL from parameter a warning & dashboard updates for …
DamianEdwards 6e1268f
Fix uninstrumented peers name resolution for hypens & show non-endpoi…
DamianEdwards 83fe7a5
Merge branch 'main' into copilot/fix-2311
DamianEdwards baaaeb8
Post merge fix
DamianEdwards b52d120
Make Yarp usable with service discovery
DamianEdwards 2275f4e
Update Aspire.slnx
DamianEdwards 35f6b8d
PR review feedback
DamianEdwards 7580749
Update ApplicationKeyTests.cs
DamianEdwards 973652e
Update StringComparers.cs
DamianEdwards e579d05
Rewrite ExternalServiceTests with comprehensive test coverage for cur…
Copilot 56848cb
Reimplement ExternalServiceResource to follow @DamianEdwards' proposal
Copilot 71175bd
Remove Bootstrap vendor files and use CDN instead to reduce diff size
Copilot 6392b16
Revert changes from commit 56848cb and update tests appropriately
Copilot 1813278
Fix build
DamianEdwards 207cf72
Fix tests
DamianEdwards File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,72 @@ | ||
| // 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 Aspire.Hosting; | ||
|
|
||
| /// <summary> | ||
| /// A special allocated endpoint for external services that provides the URL expression as the URI. | ||
| /// </summary> | ||
| internal class ExternalServiceAllocatedEndpoint : AllocatedEndpoint | ||
| { | ||
| private readonly ReferenceExpression _urlExpression; | ||
| private readonly string _literalUrl; | ||
|
|
||
| public ExternalServiceAllocatedEndpoint(EndpointAnnotation endpointAnnotation, ReferenceExpression urlExpression) | ||
| : base(endpointAnnotation, GetHostFromUrl(urlExpression), GetPortFromUrl(urlExpression)) | ||
| { | ||
| _urlExpression = urlExpression; | ||
| _literalUrl = IsLiteralUrl(urlExpression, out var url) ? url : ""; | ||
| } | ||
|
|
||
| /// <summary> | ||
| /// Gets the URL expression that represents this external service endpoint. | ||
| /// </summary> | ||
| public ReferenceExpression UrlExpression => _urlExpression; | ||
|
|
||
| private static string GetHostFromUrl(ReferenceExpression urlExpression) | ||
| { | ||
| // For literal URLs, extract the host; for expressions, use a placeholder | ||
| if (IsLiteralUrl(urlExpression, out var literalUrl) && Uri.TryCreate(literalUrl, UriKind.Absolute, out var uri)) | ||
| { | ||
| return uri.Host; | ||
| } | ||
| return "external.service"; | ||
| } | ||
|
|
||
| private static int GetPortFromUrl(ReferenceExpression urlExpression) | ||
| { | ||
| // For literal URLs, extract the port; for expressions, use default port | ||
| if (IsLiteralUrl(urlExpression, out var literalUrl) && Uri.TryCreate(literalUrl, UriKind.Absolute, out var uri)) | ||
| { | ||
| return uri.Port; | ||
| } | ||
| return 80; | ||
| } | ||
|
|
||
| private static bool IsLiteralUrl(ReferenceExpression expression, out string url) | ||
| { | ||
| if (expression.ValueProviders.Count == 0) | ||
| { | ||
| url = expression.Format; | ||
| return true; | ||
| } | ||
|
|
||
| url = string.Empty; | ||
| return false; | ||
| } | ||
|
|
||
| /// <summary> | ||
| /// Returns the original URL for literal URLs, or the expression for parameterized URLs. | ||
| /// </summary> | ||
| /// <returns>The URL or expression value.</returns> | ||
| public override string ToString() | ||
| { | ||
| if (!string.IsNullOrEmpty(_literalUrl)) | ||
| { | ||
| return _literalUrl; | ||
| } | ||
| return _urlExpression.ValueExpression; | ||
| } | ||
| } | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,154 @@ | ||
| // Licensed to the .NET Foundation under one or more agreements. | ||
| // The .NET Foundation licenses this file to you under the MIT license. | ||
|
|
||
| using System.Net.Sockets; | ||
| using Aspire.Hosting.ApplicationModel; | ||
|
|
||
| namespace Aspire.Hosting; | ||
|
|
||
| /// <summary> | ||
| /// Provides extension methods for adding external services to an application. | ||
| /// </summary> | ||
| public static class ExternalServiceBuilderExtensions | ||
| { | ||
| /// <summary> | ||
| /// Adds an external service resource to the distributed application with the specified URL. | ||
| /// </summary> | ||
| /// <param name="builder">The distributed application builder.</param> | ||
| /// <param name="name">The name of the resource.</param> | ||
| /// <param name="url">The URL of the external service.</param> | ||
| /// <returns>An <see cref="IResourceBuilder{ExternalServiceResource}"/> instance.</returns> | ||
| /// <exception cref="ArgumentException">Thrown when the URL is not a valid, absolute URI.</exception> | ||
| public static IResourceBuilder<ExternalServiceResource> AddExternalService(this IDistributedApplicationBuilder builder, [ResourceName] string name, string url) | ||
| { | ||
| ArgumentNullException.ThrowIfNull(builder); | ||
| ArgumentNullException.ThrowIfNull(name); | ||
| ArgumentNullException.ThrowIfNull(url); | ||
|
|
||
| if (!Uri.IsWellFormedUriString(url, UriKind.Absolute)) | ||
| { | ||
| throw new ArgumentException($"The URL '{url}' is not a valid absolute URI.", nameof(url)); | ||
| } | ||
|
|
||
| var uri = new Uri(url); | ||
| return builder.AddExternalService(name, uri); | ||
| } | ||
|
|
||
| /// <summary> | ||
| /// Adds an external service resource to the distributed application with the specified URI. | ||
| /// </summary> | ||
| /// <param name="builder">The distributed application builder.</param> | ||
| /// <param name="name">The name of the resource.</param> | ||
| /// <param name="uri">The URI of the external service.</param> | ||
| /// <returns>An <see cref="IResourceBuilder{ExternalServiceResource}"/> instance.</returns> | ||
| /// <exception cref="ArgumentException">Thrown when the URI is not absolute.</exception> | ||
| public static IResourceBuilder<ExternalServiceResource> AddExternalService(this IDistributedApplicationBuilder builder, [ResourceName] string name, Uri uri) | ||
| { | ||
| ArgumentNullException.ThrowIfNull(builder); | ||
| ArgumentNullException.ThrowIfNull(name); | ||
| ArgumentNullException.ThrowIfNull(uri); | ||
|
|
||
| if (!uri.IsAbsoluteUri) | ||
| { | ||
| throw new ArgumentException("The URI for the external service must be absolute.", nameof(uri)); | ||
| } | ||
|
|
||
| var rb = new ReferenceExpressionBuilder(); | ||
| rb.AppendLiteral(uri.ToString()); | ||
| var urlExpression = rb.Build(); | ||
| return builder.AddExternalService(name, urlExpression); | ||
| } | ||
|
|
||
| /// <summary> | ||
| /// Adds an external service resource to the distributed application with the specified URL expression. | ||
| /// </summary> | ||
| /// <param name="builder">The distributed application builder.</param> | ||
| /// <param name="name">The name of the resource.</param> | ||
| /// <param name="urlExpression">The URL expression for the external service.</param> | ||
| /// <returns>An <see cref="IResourceBuilder{ExternalServiceResource}"/> instance.</returns> | ||
| public static IResourceBuilder<ExternalServiceResource> AddExternalService(this IDistributedApplicationBuilder builder, [ResourceName] string name, ReferenceExpression urlExpression) | ||
| { | ||
| ArgumentNullException.ThrowIfNull(builder); | ||
| ArgumentNullException.ThrowIfNull(name); | ||
| ArgumentNullException.ThrowIfNull(urlExpression); | ||
|
|
||
| var resource = new ExternalServiceResource(name, urlExpression); | ||
|
|
||
| return builder.AddResource(resource) | ||
DamianEdwards marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
| .WithReferenceRelationship(urlExpression) | ||
| .WithInitialState(new CustomResourceSnapshot | ||
| { | ||
| ResourceType = "ExternalService", | ||
| Properties = [], | ||
| State = "Running" | ||
| }) | ||
| .WithExternalServiceEndpoints(); | ||
| } | ||
|
|
||
| /// <summary> | ||
| /// Adds an external service resource to the distributed application with a parameterized URL. | ||
| /// </summary> | ||
| /// <param name="builder">The distributed application builder.</param> | ||
| /// <param name="name">The name of the resource.</param> | ||
| /// <returns>An <see cref="IResourceBuilder{IResourceWithServiceDiscovery}"/> instance.</returns> | ||
| public static IResourceBuilder<IResourceWithServiceDiscovery> AddExternalService(this IDistributedApplicationBuilder builder, [ResourceName] string name) | ||
| { | ||
| ArgumentNullException.ThrowIfNull(builder); | ||
| ArgumentNullException.ThrowIfNull(name); | ||
|
|
||
| return builder.AddParameter( | ||
| new ExternalServiceParameterResource( | ||
| name, | ||
| parameterDefault => GetParameterValue(builder.Configuration, name, parameterDefault))); | ||
| } | ||
|
|
||
| private static string GetParameterValue(Microsoft.Extensions.Configuration.ConfigurationManager configuration, string name, ParameterDefault? parameterDefault) | ||
| { | ||
| var configurationKey = $"Parameters:{name}"; | ||
| return configuration[configurationKey] | ||
| ?? parameterDefault?.GetDefaultValue() | ||
| ?? throw new DistributedApplicationException($"External service parameter resource could not be used because parameter '{name}' is missing."); | ||
| } | ||
|
|
||
| /// <summary> | ||
| /// Configures the external service to provide endpoints for service discovery and endpoint references. | ||
| /// </summary> | ||
| private static IResourceBuilder<ExternalServiceResource> WithExternalServiceEndpoints(this IResourceBuilder<ExternalServiceResource> builder) | ||
| { | ||
| var urlExpression = builder.Resource.UrlExpression; | ||
|
|
||
| // Determine the scheme from the URL if it's a literal URL | ||
| var scheme = "http"; // default | ||
| if (IsLiteralUrl(urlExpression, out var literalUrl) && Uri.TryCreate(literalUrl, UriKind.Absolute, out var uri)) | ||
| { | ||
| scheme = uri.Scheme; | ||
| } | ||
|
|
||
| // Add a default endpoint annotation that represents the external service | ||
| // This allows GetEndpoint() to work and enables endpoint references | ||
| var endpointAnnotation = new EndpointAnnotation(ProtocolType.Tcp, scheme, name: "default"); | ||
DamianEdwards marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
|
|
||
| // Create a special allocated endpoint that will resolve the URL expression at runtime | ||
| endpointAnnotation.AllocatedEndpoint = new ExternalServiceAllocatedEndpoint(endpointAnnotation, urlExpression); | ||
|
|
||
| builder.WithAnnotation(endpointAnnotation); | ||
|
|
||
| return builder; | ||
| } | ||
|
|
||
| /// <summary> | ||
| /// Checks if a ReferenceExpression represents a literal URL string. | ||
| /// </summary> | ||
| private static bool IsLiteralUrl(ReferenceExpression expression, out string url) | ||
| { | ||
| // If the expression has no value providers, it's a literal string | ||
| if (expression.ValueProviders.Count == 0) | ||
| { | ||
| url = expression.Format; | ||
| return true; | ||
| } | ||
|
|
||
| url = string.Empty; | ||
| return false; | ||
| } | ||
| } | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,20 @@ | ||
| // 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 Aspire.Hosting; | ||
|
|
||
| internal sealed class ExternalServiceParameterResource : ParameterResource, IResourceWithServiceDiscovery | ||
| { | ||
| public ExternalServiceParameterResource(string name, Func<ParameterDefault?, string> callback) : base(name, callback, secret: false) | ||
| { | ||
| // Add endpoint annotation for service discovery | ||
| var endpointAnnotation = new EndpointAnnotation(System.Net.Sockets.ProtocolType.Tcp, "http", name: "default"); | ||
| endpointAnnotation.AllocatedEndpoint = new ExternalServiceAllocatedEndpoint(endpointAnnotation, UrlExpression); | ||
| Annotations.Add(endpointAnnotation); | ||
| } | ||
|
|
||
| public ReferenceExpression UrlExpression => | ||
| ReferenceExpression.Create($"{this}"); | ||
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,19 @@ | ||
| // 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 Aspire.Hosting; | ||
|
|
||
| /// <summary> | ||
| /// Represents an external service resource with service discovery capabilities. | ||
| /// </summary> | ||
| /// <param name="name">The name of the resource.</param> | ||
| /// <param name="urlExpression">The URL expression for the external service.</param> | ||
| public sealed class ExternalServiceResource(string name, ReferenceExpression urlExpression) : Resource(name), IResourceWithServiceDiscovery, IResourceWithoutLifetime | ||
| { | ||
| /// <summary> | ||
| /// Gets the URL expression for the external service. | ||
| /// </summary> | ||
| public ReferenceExpression UrlExpression => urlExpression; | ||
| } |
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.