-
Notifications
You must be signed in to change notification settings - Fork 881
Implement richer extensibility mechanisms for routes and clusters in … #2682
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
Conversation
@microsoft-github-policy-service agree |
Any news? |
@samsp-msft did you get a chance to look at this one? |
Hi @MihaZupan |
Sorry, been busy with another project. I trust Miha's judgement on this issue. |
@dotnet-policy-service agree |
@MihaZupan Hi |
I'm not entirely happy with this implementation. I'd like to take some time to redo this PR – I'd really value your input |
Sorry about the delay here. |
This is my refactored code. I’m quite happy with the current structure, but I’d love to hear any suggestions or feedback you might have. Thank you so much for taking the time to review it! /// <summary>
/// Factory delegate for creating route extensions.
/// </summary>
/// <typeparam name="T">The type of extension to create.</typeparam>
/// <param name="section">The configuration section for this extension.</param>
/// <param name="routeConfig">The route configuration being extended.</param>
/// <param name="existingExtension">The existing extension instance if available (during updates).</param>
/// <returns>The created or updated extension instance.</returns>
public delegate T RouteExtensionFactory<T>(IConfigurationSection section, RouteConfig routeConfig, T existingExtension);
/// <summary>
/// Interface for providers that can create route extensions from configuration.
/// </summary>
public interface IRouteExtensionProvider
{
/// <summary>
/// Gets the configuration section name this provider handles.
/// </summary>
string SectionName { get; }
/// <summary>
/// Creates an extension instance from the given configuration section.
/// </summary>
/// <param name="section">The configuration section.</param>
/// <param name="routeConfig">The route configuration.</param>
/// <param name="extensions">Existing extensions dictionary.</param>
/// <returns>True if an extension was created, false otherwise.</returns>
bool CreateExtension(IConfigurationSection section, RouteConfig routeConfig, IDictionary<Type, object> extensions);
}
/// <summary>
/// A provider that creates route extensions of a specific type.
/// </summary>
/// <typeparam name="T">The type of extension to create.</typeparam>
internal sealed class RouteExtensionProvider<T> : IRouteExtensionProvider
{
private readonly RouteExtensionFactory<T> _factory;
public RouteExtensionProvider(string sectionName, RouteExtensionFactory<T> factory)
{
SectionName = sectionName ?? throw new ArgumentNullException(nameof(sectionName));
_factory = factory ?? throw new ArgumentNullException(nameof(factory));
}
/// <inheritdoc/>
public string SectionName { get; }
/// <inheritdoc/>
public bool CreateExtension(IConfigurationSection section, RouteConfig routeConfig, IDictionary<Type, object> extensions)
{
if (section is null || routeConfig is null || extensions is null)
{
return false;
}
var extensionSection = section.GetSection(SectionName);
if (!extensionSection.Exists())
{
return false;
}
T existingExtension = default;
extensions.TryGetValue(typeof(T), out var existingObject);
if (existingObject is T existing)
{
existingExtension = existing;
}
var extension = _factory(extensionSection, routeConfig, existingExtension);
if (extension != null)
{
extensions[typeof(T)] = extension;
return true;
}
return false;
}
}
public static class ReverseProxyBuilderExtensions
{
/// <summary>
/// Adds a route extension factory that will be used to create extension instances from configuration.
/// </summary>
/// <typeparam name="T">The type of extension to create.</typeparam>
/// <param name="builder">The <see cref="IReverseProxyBuilder"/>.</param>
/// <param name="sectionName">The configuration section name for this extension.</param>
/// <param name="factory">The factory function to create the extension.</param>
/// <returns>The <see cref="IReverseProxyBuilder"/>.</returns>
public static IReverseProxyBuilder AddRouteExtension<T>(
this IReverseProxyBuilder builder,
string sectionName,
RouteExtensionFactory<T> factory)
{
if (builder == null)
{
throw new ArgumentNullException(nameof(builder));
}
if (string.IsNullOrEmpty(sectionName))
{
throw new ArgumentException("Section name cannot be null or empty", nameof(sectionName));
}
if (factory == null)
{
throw new ArgumentNullException(nameof(factory));
}
builder.Services.AddSingleton<IRouteExtensionProvider>(
new RouteExtensionProvider<T>(sectionName, factory));
return builder;
}
}
internal class ConfigurationConfigProvider : IProxyConfigProvider
{
private readonly IEnumerable<IRouteExtensionProvider> _routeExtensionProviders;
// ... other members ...
private void ProcessRouteExtensions(IConfigurationSection section, RouteConfig route)
{
var extensions = new Dictionary<Type, object>();
var extensionsSection = section.GetSection("Extensions");
if (extensionsSection.Exists())
{
// Apply all registered route extension providers
foreach (var provider in _routeExtensionProviders)
{
provider.CreateExtension(extensionsSection, route, extensions);
}
}
route.Extensions = extensions;
}
// ... other methods ...
} |
First of all thank you for looking at improving things here and appologies that it's taking us this long to get to it. Is this functionality that you're looking to use yourself / within your company? Or are you mainly interested at contributing and just happened to see this issue? At this time we are hesitant to design a feature like this without at least a few clear real-world use cases, and indication that it would meaningfully improve things for more users. As for your proposal #2682 (comment), a couple questions come to mind:
|
Thank you very much for your reply. Regarding my motivation for submitting this PR: while using During my research, I discovered relevant proposals in the issue tracker: This inspired me to dedicate time to implementing the feature. I'm truly happy to contribute this to the community.
Regarding this question, I saw
Initially, I wanted the new ClusterConfig()
{
ClusterId = "cluster1",
Extensions = new Dictionary<Type, IConfigExtension>
{
{ typeof(UserModel), new UserModel { UserName = "admin" } }
}
}; But I realized this wasn't extensible-friendly. Later I redesigned it to use
My initial idea was simply to initialize objects as described in the documentation. Thanks for pointing out that multiple deserializers for the same type could indeed interfere with each other. Here I should probably apply the last-one-wins principle, similar to services.AddReverseProxy()
.LoadFromConfig(Configuration.GetSection("ReverseProxy"))
.AddRouteExtension("A-B", (section, _, _) => new ABState(section));
Yes, the current implementation is very friendly to
Yes. Even if multiple instances of the same type are injected, it will still be treated as one type.
Sorry, I haven't considered this scenario yet. Could you show me a simple example?
Absolutely. We could provide a
Through our discussion above, I've also realized that implementing this feature is quite difficult and we currently have some unclear points. Also, it's almost certain that users would not think to use this feature. Going back to my original need, perhaps users really just want the If you agree, I'd like to spend some time implementing this Please forgive me for rushing through the PR submission. I really appreciate your review. |
I think the actual implementation should be rather trivial, e.g. internal static class ClusterConfigExtensions
{
public static T GetMetadata<T>(this ClusterState cluster, string name)
{
ArgumentNullException.ThrowIfNull(cluster);
var metadata = cluster.Model.Config.Metadata;
if (metadata is null || !metadata.TryGetValue(name, out var metadataString))
{
throw new KeyNotFoundException($"Metadata '{name}' not found in cluster '{cluster.ClusterId}'.");
}
return JsonSerializer.Deserialize<T>(metadataString)
?? throw new InvalidOperationException($"Metadata '{name}' in cluster '{cluster.ClusterId}' is not of type '{typeof(T).Name}'.");
}
} It's more of a question if that's the right thing to expose that'd be useful for more users. |
Based on our previous discussion, I propose to implement the |
This PR is based on the Route Extensibility Design Document and primarily addresses the extensibility issues mentioned in the related issue: #1709.
I have carefully reviewed and studied the above document and related links, and I believe this functionality is highly beneficial for the extensibility of YARP, especially in supporting custom route and cluster configurations. After extensive preparation and development, I have successfully completed the implementation of this PR.
If you are already working on this feature or have a better implementation plan, I would be more than happy to close this PR and look forward to contributing ideas or providing assistance for the final implementation.
Thank you for your time and review, and I look forward to your feedback!