|
| 1 | +// Licensed to the .NET Foundation under one or more agreements. |
| 2 | +// The .NET Foundation licenses this file to you under the MIT license. |
| 3 | + |
| 4 | +using Microsoft.AspNetCore.Builder; |
| 5 | +using Microsoft.AspNetCore.Http; |
| 6 | +using Microsoft.AspNetCore.Routing.Patterns; |
| 7 | +using Microsoft.Extensions.Primitives; |
| 8 | + |
| 9 | +namespace Microsoft.AspNetCore.Routing; |
| 10 | + |
| 11 | +/// <summary> |
| 12 | +/// A builder for defining groups of endpoints with a common prefix that implements both the <see cref="IEndpointRouteBuilder"/> |
| 13 | +/// and <see cref="IEndpointConventionBuilder"/> interfaces. This can be used to add endpoints with the given <see cref="GroupPrefix"/>, |
| 14 | +/// and to customize those endpoints using conventions. |
| 15 | +/// </summary> |
| 16 | +public sealed class GroupRouteBuilder : IEndpointRouteBuilder, IEndpointConventionBuilder |
| 17 | +{ |
| 18 | + private readonly IEndpointRouteBuilder _outerEndpointRouteBuilder; |
| 19 | + private readonly RoutePattern _pattern; |
| 20 | + |
| 21 | + private readonly List<EndpointDataSource> _dataSources = new(); |
| 22 | + private readonly List<Action<EndpointBuilder>> _conventions = new(); |
| 23 | + |
| 24 | + internal GroupRouteBuilder(IEndpointRouteBuilder outerEndpointRouteBuilder, RoutePattern pattern) |
| 25 | + { |
| 26 | + _outerEndpointRouteBuilder = outerEndpointRouteBuilder; |
| 27 | + _pattern = pattern; |
| 28 | + |
| 29 | + if (outerEndpointRouteBuilder is GroupRouteBuilder outerGroup) |
| 30 | + { |
| 31 | + GroupPrefix = RoutePatternFactory.Combine(outerGroup.GroupPrefix, pattern); |
| 32 | + } |
| 33 | + else |
| 34 | + { |
| 35 | + GroupPrefix = pattern; |
| 36 | + } |
| 37 | + |
| 38 | + _outerEndpointRouteBuilder.DataSources.Add(new GroupDataSource(this)); |
| 39 | + } |
| 40 | + |
| 41 | + /// <summary> |
| 42 | + /// The <see cref="RoutePattern"/> prefixing all endpoints defined using this <see cref="GroupRouteBuilder"/>. |
| 43 | + /// This accounts for nested groups and gives the full group prefix, not just the prefix supplied to the last call to |
| 44 | + /// <see cref="EndpointRouteBuilderExtensions.MapGroup(IEndpointRouteBuilder, RoutePattern)"/>. |
| 45 | + /// </summary> |
| 46 | + public RoutePattern GroupPrefix { get; } |
| 47 | + |
| 48 | + IServiceProvider IEndpointRouteBuilder.ServiceProvider => _outerEndpointRouteBuilder.ServiceProvider; |
| 49 | + IApplicationBuilder IEndpointRouteBuilder.CreateApplicationBuilder() => _outerEndpointRouteBuilder.CreateApplicationBuilder(); |
| 50 | + ICollection<EndpointDataSource> IEndpointRouteBuilder.DataSources => _dataSources; |
| 51 | + void IEndpointConventionBuilder.Add(Action<EndpointBuilder> convention) => _conventions.Add(convention); |
| 52 | + |
| 53 | + private bool IsRoot => ReferenceEquals(GroupPrefix, _pattern); |
| 54 | + |
| 55 | + private sealed class GroupDataSource : EndpointDataSource |
| 56 | + { |
| 57 | + private readonly GroupRouteBuilder _groupRouteBuilder; |
| 58 | + |
| 59 | + public GroupDataSource(GroupRouteBuilder groupRouteBuilder) |
| 60 | + { |
| 61 | + _groupRouteBuilder = groupRouteBuilder; |
| 62 | + } |
| 63 | + |
| 64 | + public override IReadOnlyList<Endpoint> Endpoints |
| 65 | + { |
| 66 | + get |
| 67 | + { |
| 68 | + var list = new List<Endpoint>(); |
| 69 | + |
| 70 | + foreach (var dataSource in _groupRouteBuilder._dataSources) |
| 71 | + { |
| 72 | + foreach (var endpoint in dataSource.Endpoints) |
| 73 | + { |
| 74 | + // Endpoint does not provide a RoutePattern but RouteEndpoint does. So it's impossible to apply a prefix for custom Endpoints. |
| 75 | + // Supporting arbitrary Endpoints just to add group metadata would require changing the Endpoint type breaking any real scenario. |
| 76 | + if (endpoint is not RouteEndpoint routeEndpoint) |
| 77 | + { |
| 78 | + throw new NotSupportedException(Resources.FormatMapGroup_CustomEndpointUnsupported(endpoint.GetType())); |
| 79 | + } |
| 80 | + |
| 81 | + // Make the full route pattern visible to IEndpointConventionBuilder extension methods called on the group. |
| 82 | + // This includes patterns from any parent groups. |
| 83 | + var fullRoutePattern = RoutePatternFactory.Combine(_groupRouteBuilder.GroupPrefix, routeEndpoint.RoutePattern); |
| 84 | + |
| 85 | + // RequestDelegate can never be null on a RouteEndpoint. The nullability carries over from Endpoint. |
| 86 | + var routeEndpointBuilder = new RouteEndpointBuilder(routeEndpoint.RequestDelegate!, fullRoutePattern, routeEndpoint.Order) |
| 87 | + { |
| 88 | + DisplayName = routeEndpoint.DisplayName, |
| 89 | + ServiceProvider = _groupRouteBuilder._outerEndpointRouteBuilder.ServiceProvider, |
| 90 | + }; |
| 91 | + |
| 92 | + // Apply group conventions to each endpoint in the group at a lower precedent than metadata already on the endpoint. |
| 93 | + foreach (var convention in _groupRouteBuilder._conventions) |
| 94 | + { |
| 95 | + convention(routeEndpointBuilder); |
| 96 | + } |
| 97 | + |
| 98 | + // If we supported mutating the route pattern via a group convention, RouteEndpointBuilder.RoutePattern would have |
| 99 | + // to be the partialRoutePattern (below) instead of the fullRoutePattern (above) since that's all we can control. We cannot |
| 100 | + // change a parent prefix. In order to allow to conventions to read the fullRoutePattern, we do not support mutation. |
| 101 | + if (!ReferenceEquals(fullRoutePattern, routeEndpointBuilder.RoutePattern)) |
| 102 | + { |
| 103 | + throw new NotSupportedException(Resources.FormatMapGroup_ChangingRoutePatternUnsupported( |
| 104 | + fullRoutePattern.RawText, routeEndpointBuilder.RoutePattern.RawText)); |
| 105 | + } |
| 106 | + |
| 107 | + // Any metadata already on the RouteEndpoint must have been applied directly to the endpoint or to a nested group. |
| 108 | + // This makes the metadata more specific than what's being applied to this group. So add it after this group's conventions. |
| 109 | + // |
| 110 | + // REVIEW: This means group conventions don't get visibility into endpoint-specific metadata nor the ability to override it. |
| 111 | + // We should consider allowing group-aware conventions the ability to read and mutate this metadata in future releases. |
| 112 | + foreach (var metadata in routeEndpoint.Metadata) |
| 113 | + { |
| 114 | + routeEndpointBuilder.Metadata.Add(metadata); |
| 115 | + } |
| 116 | + |
| 117 | + // Use _pattern instead of GroupPrefix when we're calculating an intermediate RouteEndpoint. |
| 118 | + var partialRoutePattern = _groupRouteBuilder.IsRoot |
| 119 | + ? fullRoutePattern : RoutePatternFactory.Combine(_groupRouteBuilder._pattern, routeEndpoint.RoutePattern); |
| 120 | + |
| 121 | + // The RequestDelegate, Order and DisplayName can all be overridden by non-group-aware conventions. Unlike with metadata, |
| 122 | + // if a convention is applied to a group that changes any of these, I would expect these to be overridden as there's no |
| 123 | + // reasonable way to merge these properties. |
| 124 | + list.Add(new RouteEndpoint( |
| 125 | + // Again, RequestDelegate can never be null given a RouteEndpoint. |
| 126 | + routeEndpointBuilder.RequestDelegate!, |
| 127 | + partialRoutePattern, |
| 128 | + routeEndpointBuilder.Order, |
| 129 | + new(routeEndpointBuilder.Metadata), |
| 130 | + routeEndpointBuilder.DisplayName)); |
| 131 | + } |
| 132 | + } |
| 133 | + |
| 134 | + return list; |
| 135 | + } |
| 136 | + } |
| 137 | + |
| 138 | + public override IChangeToken GetChangeToken() => new CompositeEndpointDataSource(_groupRouteBuilder._dataSources).GetChangeToken(); |
| 139 | + } |
| 140 | +} |
0 commit comments