diff --git a/src/Middleware/Middleware.slnf b/src/Middleware/Middleware.slnf index abfa304b9ed1..4801e81c46e4 100644 --- a/src/Middleware/Middleware.slnf +++ b/src/Middleware/Middleware.slnf @@ -125,6 +125,7 @@ "src\\Servers\\Kestrel\\Kestrel\\src\\Microsoft.AspNetCore.Server.Kestrel.csproj", "src\\Servers\\Kestrel\\Transport.Quic\\src\\Microsoft.AspNetCore.Server.Kestrel.Transport.Quic.csproj", "src\\Servers\\Kestrel\\Transport.Sockets\\src\\Microsoft.AspNetCore.Server.Kestrel.Transport.Sockets.csproj", + "src\\Testing\\src\\Microsoft.AspNetCore.Testing.csproj", "src\\WebEncoders\\src\\Microsoft.Extensions.WebEncoders.csproj" ] } diff --git a/src/Middleware/RateLimiting/test/RateLimiterEndpointConventionBuilderExtensionsTests.cs b/src/Middleware/RateLimiting/test/RateLimiterEndpointConventionBuilderExtensionsTests.cs index 4caeecf88297..58f7f6fe83af 100644 --- a/src/Middleware/RateLimiting/test/RateLimiterEndpointConventionBuilderExtensionsTests.cs +++ b/src/Middleware/RateLimiting/test/RateLimiterEndpointConventionBuilderExtensionsTests.cs @@ -73,22 +73,4 @@ public void DisableRateLimiting_MetadataAdded() var metadata = endpoint.Metadata.GetMetadata(); Assert.NotNull(metadata); } - - private class TestEndpointBuilder : EndpointBuilder - { - public override Endpoint Build() - { - return new Endpoint(RequestDelegate, new EndpointMetadataCollection(Metadata), DisplayName); - } - } - - private class TestEndpointConventionBuilder : IEndpointConventionBuilder - { - public IList> Conventions { get; } = new List>(); - - public void Add(Action convention) - { - Conventions.Add(convention); - } - } } diff --git a/src/Middleware/RateLimiting/test/RateLimitingMiddlewareTests.cs b/src/Middleware/RateLimiting/test/RateLimitingMiddlewareTests.cs index 751a8352b8d1..24c6ccd93990 100644 --- a/src/Middleware/RateLimiting/test/RateLimitingMiddlewareTests.cs +++ b/src/Middleware/RateLimiting/test/RateLimitingMiddlewareTests.cs @@ -2,6 +2,7 @@ // The .NET Foundation licenses this file to you under the MIT license. using System.Threading.RateLimiting; +using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Testing; using Microsoft.Extensions.Logging; @@ -46,9 +47,11 @@ public void Ctor_ThrowsExceptionsWhenNullArgs() [Fact] public async Task RequestsCallNextIfAccepted() { + // Arrange var flag = false; var options = CreateOptionsAccessor(); options.Value.GlobalLimiter = new TestPartitionedRateLimiter(new TestRateLimiter(true)); + var middleware = new RateLimitingMiddleware(c => { flag = true; @@ -58,13 +61,17 @@ public async Task RequestsCallNextIfAccepted() options, Mock.Of()); + // Act await middleware.Invoke(new DefaultHttpContext()); + + // Assert Assert.True(flag); } [Fact] public async Task RequestRejected_CallsOnRejectedAndGives503() { + // Arrange var onRejectedInvoked = false; var options = CreateOptionsAccessor(); options.Value.GlobalLimiter = new TestPartitionedRateLimiter(new TestRateLimiter(false)); @@ -74,16 +81,14 @@ public async Task RequestRejected_CallsOnRejectedAndGives503() return ValueTask.CompletedTask; }; - var middleware = new RateLimitingMiddleware(c => - { - return Task.CompletedTask; - }, - new NullLoggerFactory().CreateLogger(), - options, - Mock.Of()); + var middleware = CreateTestRateLimitingMiddleware(options); var context = new DefaultHttpContext(); + + // Act await middleware.Invoke(context).DefaultTimeout(); + + // Assert Assert.True(onRejectedInvoked); Assert.Equal(StatusCodes.Status503ServiceUnavailable, context.Response.StatusCode); } @@ -91,6 +96,7 @@ public async Task RequestRejected_CallsOnRejectedAndGives503() [Fact] public async Task RequestRejected_WinsOverDefaultStatusCode() { + // Arrange var onRejectedInvoked = false; var options = CreateOptionsAccessor(); options.Value.GlobalLimiter = new TestPartitionedRateLimiter(new TestRateLimiter(false)); @@ -101,16 +107,13 @@ public async Task RequestRejected_WinsOverDefaultStatusCode() return ValueTask.CompletedTask; }; - var middleware = new RateLimitingMiddleware(c => - { - return Task.CompletedTask; - }, - new NullLoggerFactory().CreateLogger(), - options, - Mock.Of()); - + var middleware = CreateTestRateLimitingMiddleware(options); var context = new DefaultHttpContext(); + + // Act await middleware.Invoke(context).DefaultTimeout(); + + // Assert Assert.True(onRejectedInvoked); Assert.Equal(StatusCodes.Status429TooManyRequests, context.Response.StatusCode); } @@ -118,6 +121,7 @@ public async Task RequestRejected_WinsOverDefaultStatusCode() [Fact] public async Task RequestAborted_DoesNotThrowTaskCanceledException() { + // Arrange var sink = new TestSink( TestSink.EnableWithTypeName, TestSink.EnableWithTypeName); @@ -126,17 +130,15 @@ public async Task RequestAborted_DoesNotThrowTaskCanceledException() var options = CreateOptionsAccessor(); options.Value.GlobalLimiter = new TestPartitionedRateLimiter(new TestRateLimiter(false)); - var middleware = new RateLimitingMiddleware(c => - { - return Task.CompletedTask; - }, - loggerFactory.CreateLogger(), - options, - Mock.Of()); + var middleware = CreateTestRateLimitingMiddleware(options, logger: loggerFactory.CreateLogger()); var context = new DefaultHttpContext(); - context.RequestAborted = new CancellationToken(true); + context.RequestAborted = new CancellationToken(true); + + // Act await middleware.Invoke(context); + + // Assert Assert.Equal(StatusCodes.Status200OK, context.Response.StatusCode); var logMessages = sink.Writes.ToList(); @@ -150,25 +152,23 @@ public async Task RequestAborted_DoesNotThrowTaskCanceledException() [Fact] public async Task EndpointLimiterRequested_NoPolicy_Throws() { + // Arrange var options = CreateOptionsAccessor(); var name = "myEndpoint"; - var middleware = new RateLimitingMiddleware(c => - { - return Task.CompletedTask; - }, - new NullLoggerFactory().CreateLogger(), - options, - Mock.Of()); - + var middleware = CreateTestRateLimitingMiddleware(options); var context = new DefaultHttpContext(); - context.SetEndpoint(new Endpoint(c => Task.CompletedTask, new EndpointMetadataCollection(new EnableRateLimitingAttribute(name)), "Test endpoint")); + var endpoint = CreateEndpointWithRateLimitPolicy(name); + context.SetEndpoint(endpoint); + + // Act & Assert await Assert.ThrowsAsync(() => middleware.Invoke(context)).DefaultTimeout(); } [Fact] public async Task EndpointLimiter_Rejects() { + // Arrange var onRejectedInvoked = false; var options = CreateOptionsAccessor(); var name = "myEndpoint"; @@ -186,17 +186,16 @@ public async Task EndpointLimiter_Rejects() return ValueTask.CompletedTask; }; - var middleware = new RateLimitingMiddleware(c => - { - return Task.CompletedTask; - }, - new NullLoggerFactory().CreateLogger(), - options, - Mock.Of()); + var middleware = CreateTestRateLimitingMiddleware(options); var context = new DefaultHttpContext(); - context.SetEndpoint(new Endpoint(c => Task.CompletedTask, new EndpointMetadataCollection(new EnableRateLimitingAttribute(name)), "Test endpoint")); + var endpoint = CreateEndpointWithRateLimitPolicy(name); + context.SetEndpoint(endpoint); + + // Act await middleware.Invoke(context).DefaultTimeout(); + + // Assert Assert.True(onRejectedInvoked); Assert.Equal(StatusCodes.Status429TooManyRequests, context.Response.StatusCode); } @@ -204,6 +203,7 @@ public async Task EndpointLimiter_Rejects() [Fact] public async Task EndpointLimiterConvenienceMethod_Rejects() { + // Arrange var onRejectedInvoked = false; var options = CreateOptionsAccessor(); var name = "myEndpoint"; @@ -222,16 +222,13 @@ public async Task EndpointLimiterConvenienceMethod_Rejects() return ValueTask.CompletedTask; }; - var middleware = new RateLimitingMiddleware(c => - { - return Task.CompletedTask; - }, - new NullLoggerFactory().CreateLogger(), - options, - Mock.Of()); + var middleware = CreateTestRateLimitingMiddleware(options); var context = new DefaultHttpContext(); - context.SetEndpoint(new Endpoint(c => Task.CompletedTask, new EndpointMetadataCollection(new EnableRateLimitingAttribute(name)), "Test endpoint")); + var endpoint = CreateEndpointWithRateLimitPolicy(name); + context.SetEndpoint(endpoint); + + // Act & Assert await middleware.Invoke(context).DefaultTimeout(); Assert.False(onRejectedInvoked); await middleware.Invoke(context).DefaultTimeout(); @@ -242,6 +239,7 @@ public async Task EndpointLimiterConvenienceMethod_Rejects() [Fact] public async Task EndpointLimiterRejects_EndpointOnRejectedFires() { + // Arrange var globalOnRejectedInvoked = false; var options = CreateOptionsAccessor(); var name = "myEndpoint"; @@ -255,25 +253,24 @@ public async Task EndpointLimiterRejects_EndpointOnRejectedFires() return ValueTask.CompletedTask; }; - var middleware = new RateLimitingMiddleware(c => - { - return Task.CompletedTask; - }, - new NullLoggerFactory().CreateLogger(), - options, - Mock.Of()); + var middleware = CreateTestRateLimitingMiddleware(options); var context = new DefaultHttpContext(); - context.SetEndpoint(new Endpoint(c => Task.CompletedTask, new EndpointMetadataCollection(new EnableRateLimitingAttribute(name)), "Test endpoint")); + var endpoint = CreateEndpointWithRateLimitPolicy(name); + context.SetEndpoint(endpoint); + + // Act await middleware.Invoke(context).DefaultTimeout(); - Assert.False(globalOnRejectedInvoked); + // Assert + Assert.False(globalOnRejectedInvoked); Assert.Equal(StatusCodes.Status404NotFound, context.Response.StatusCode); } [Fact] public async Task GlobalAndEndpoint_GlobalRejects_GlobalWins() { + // Arrange var globalOnRejectedInvoked = false; var options = CreateOptionsAccessor(); var name = "myEndpoint"; @@ -288,25 +285,24 @@ public async Task GlobalAndEndpoint_GlobalRejects_GlobalWins() return ValueTask.CompletedTask; }; - var middleware = new RateLimitingMiddleware(c => - { - return Task.CompletedTask; - }, - new NullLoggerFactory().CreateLogger(), - options, - Mock.Of()); + var middleware = CreateTestRateLimitingMiddleware(options); var context = new DefaultHttpContext(); - context.SetEndpoint(new Endpoint(c => Task.CompletedTask, new EndpointMetadataCollection(new EnableRateLimitingAttribute(name)), "Test endpoint")); + var endpoint = CreateEndpointWithRateLimitPolicy(name); + context.SetEndpoint(endpoint); + + // Act await middleware.Invoke(context).DefaultTimeout(); - Assert.True(globalOnRejectedInvoked); + // Assert + Assert.True(globalOnRejectedInvoked); Assert.Equal(StatusCodes.Status429TooManyRequests, context.Response.StatusCode); } [Fact] public async Task GlobalAndEndpoint_EndpointRejects_EndpointWins() { + // Arrange var globalOnRejectedInvoked = false; var options = CreateOptionsAccessor(); var name = "myEndpoint"; @@ -321,25 +317,24 @@ public async Task GlobalAndEndpoint_EndpointRejects_EndpointWins() return ValueTask.CompletedTask; }; - var middleware = new RateLimitingMiddleware(c => - { - return Task.CompletedTask; - }, - new NullLoggerFactory().CreateLogger(), - options, - Mock.Of()); + var middleware = CreateTestRateLimitingMiddleware(options); var context = new DefaultHttpContext(); - context.SetEndpoint(new Endpoint(c => Task.CompletedTask, new EndpointMetadataCollection(new EnableRateLimitingAttribute(name)), "Test endpoint")); + var endpoint = CreateEndpointWithRateLimitPolicy(name); + context.SetEndpoint(endpoint); + + // Act await middleware.Invoke(context).DefaultTimeout(); - Assert.False(globalOnRejectedInvoked); + // Assert + Assert.False(globalOnRejectedInvoked); Assert.Equal(StatusCodes.Status404NotFound, context.Response.StatusCode); } [Fact] public async Task GlobalAndEndpoint_BothReject_GlobalWins() { + // Arrange var globalOnRejectedInvoked = false; var options = CreateOptionsAccessor(); var name = "myEndpoint"; @@ -354,25 +349,24 @@ public async Task GlobalAndEndpoint_BothReject_GlobalWins() return ValueTask.CompletedTask; }; - var middleware = new RateLimitingMiddleware(c => - { - return Task.CompletedTask; - }, - new NullLoggerFactory().CreateLogger(), - options, - Mock.Of()); + var middleware = CreateTestRateLimitingMiddleware(options); var context = new DefaultHttpContext(); - context.SetEndpoint(new Endpoint(c => Task.CompletedTask, new EndpointMetadataCollection(new EnableRateLimitingAttribute(name)), "Test endpoint")); + var endpoint = CreateEndpointWithRateLimitPolicy(name); + context.SetEndpoint(endpoint); + + // Act await middleware.Invoke(context).DefaultTimeout(); - Assert.True(globalOnRejectedInvoked); + // Assert + Assert.True(globalOnRejectedInvoked); Assert.Equal(StatusCodes.Status429TooManyRequests, context.Response.StatusCode); } [Fact] public async Task EndpointLimiterRejects_EndpointOnRejectedFires_WithIRateLimiterPolicy() { + // Arrange var globalOnRejectedInvoked = false; var options = CreateOptionsAccessor(); var name = "myEndpoint"; @@ -398,25 +392,24 @@ public async Task EndpointLimiterRejects_EndpointOnRejectedFires_WithIRateLimite .Setup(sp => sp.GetService(typeof(bool))) .Returns(false); - var middleware = new RateLimitingMiddleware(c => - { - return Task.CompletedTask; - }, - new NullLoggerFactory().CreateLogger(), - options, - mockServiceProvider.Object); + var middleware = CreateTestRateLimitingMiddleware(options, serviceProvider: mockServiceProvider.Object); var context = new DefaultHttpContext(); - context.SetEndpoint(new Endpoint(c => Task.CompletedTask, new EndpointMetadataCollection(new EnableRateLimitingAttribute(name)), "Test endpoint")); + var endpoint = CreateEndpointWithRateLimitPolicy(name); + context.SetEndpoint(endpoint); + + // Act await middleware.Invoke(context).DefaultTimeout(); - Assert.False(globalOnRejectedInvoked); + // Assert + Assert.False(globalOnRejectedInvoked); Assert.Equal(StatusCodes.Status404NotFound, context.Response.StatusCode); } [Fact] public async Task EndpointLimiter_DuplicatePartitionKey_NoCollision() { + // Arrange var globalOnRejectedInvoked = false; var options = CreateOptionsAccessor(); var endpointName1 = "myEndpoint1"; @@ -433,19 +426,15 @@ public async Task EndpointLimiter_DuplicatePartitionKey_NoCollision() return ValueTask.CompletedTask; }; - var middleware = new RateLimitingMiddleware(c => - { - return Task.CompletedTask; - }, - new NullLoggerFactory().CreateLogger(), - options, - Mock.Of()); + var middleware = CreateTestRateLimitingMiddleware(options); var context = new DefaultHttpContext(); - var endpoint1 = new Endpoint(c => Task.CompletedTask, new EndpointMetadataCollection(new EnableRateLimitingAttribute(endpointName1)), "Test endpoint 1"); - var endpoint2 = new Endpoint(c => Task.CompletedTask, new EndpointMetadataCollection(new EnableRateLimitingAttribute(endpointName2)), "Test endpoint 2"); + var endpoint1 = CreateEndpointWithRateLimitPolicy(endpointName1); + var endpoint2 = CreateEndpointWithRateLimitPolicy(endpointName2); context.SetEndpoint(endpoint1); + + // Act & Assert await middleware.Invoke(context).DefaultTimeout(); Assert.False(globalOnRejectedInvoked); // This should hit endpointName1 @@ -461,6 +450,7 @@ public async Task EndpointLimiter_DuplicatePartitionKey_NoCollision() [Fact] public async Task EndpointLimiter_DuplicatePartitionKey_Lambda_NoCollision() { + // Arrange var globalOnRejectedInvoked = false; var options = CreateOptionsAccessor(); var endpointName1 = "myEndpoint1"; @@ -488,18 +478,13 @@ public async Task EndpointLimiter_DuplicatePartitionKey_Lambda_NoCollision() return ValueTask.CompletedTask; }; - var middleware = new RateLimitingMiddleware(c => - { - return Task.CompletedTask; - }, - new NullLoggerFactory().CreateLogger(), - options, - Mock.Of()); + var middleware = CreateTestRateLimitingMiddleware(options); var context = new DefaultHttpContext(); var endpoint1 = new Endpoint(c => Task.CompletedTask, new EndpointMetadataCollection(new EnableRateLimitingAttribute(endpointName1)), "Test endpoint 1"); var endpoint2 = new Endpoint(c => Task.CompletedTask, new EndpointMetadataCollection(new EnableRateLimitingAttribute(endpointName2)), "Test endpoint 2"); + // Act & Assert context.SetEndpoint(endpoint1); await middleware.Invoke(context).DefaultTimeout(); Assert.True(globalOnRejectedInvoked); @@ -516,6 +501,7 @@ public async Task EndpointLimiter_DuplicatePartitionKey_Lambda_NoCollision() [Fact] public async Task DisableRateLimitingAttribute_SkipsGlobalAndEndpoint() { + // Arrange var globalOnRejectedInvoked = false; var options = CreateOptionsAccessor(); var name = "myEndpoint"; @@ -530,14 +516,9 @@ public async Task DisableRateLimitingAttribute_SkipsGlobalAndEndpoint() return ValueTask.CompletedTask; }; - var middleware = new RateLimitingMiddleware(c => - { - return Task.CompletedTask; - }, - new NullLoggerFactory().CreateLogger(), - options, - Mock.Of()); + var middleware = CreateTestRateLimitingMiddleware(options); + // Act & Assert var context = new DefaultHttpContext(); // DisableRateLimitingAttribute last context.SetEndpoint(new Endpoint(c => Task.CompletedTask, new EndpointMetadataCollection(new EnableRateLimitingAttribute(name), new DisableRateLimitingAttribute()), "Test endpoint")); @@ -549,15 +530,16 @@ public async Task DisableRateLimitingAttribute_SkipsGlobalAndEndpoint() // DisableRateLimitingAttribute first context = new DefaultHttpContext(); context.SetEndpoint(new Endpoint(c => Task.CompletedTask, new EndpointMetadataCollection(new DisableRateLimitingAttribute(), new EnableRateLimitingAttribute(name)), "Test endpoint")); + await middleware.Invoke(context).DefaultTimeout(); Assert.False(globalOnRejectedInvoked); - Assert.Equal(StatusCodes.Status200OK, context.Response.StatusCode); } [Fact] public async Task PolicyDirectlyOnEndpoint_GetsUsed() { + // Arrange var globalOnRejectedInvoked = false; var options = CreateOptionsAccessor(); // Policy will disallow @@ -570,25 +552,24 @@ public async Task PolicyDirectlyOnEndpoint_GetsUsed() return ValueTask.CompletedTask; }; - var middleware = new RateLimitingMiddleware(c => - { - return Task.CompletedTask; - }, - new NullLoggerFactory().CreateLogger(), - options, - Mock.Of()); + var middleware = CreateTestRateLimitingMiddleware(options); var context = new DefaultHttpContext(); - context.SetEndpoint(new Endpoint(c => Task.CompletedTask, new EndpointMetadataCollection(new EnableRateLimitingAttribute(defaultRateLimiterPolicy)), "Test endpoint")); + var endpoint = CreateEndpointWithRateLimitPolicy(policy); + context.SetEndpoint(endpoint); + + // Act await middleware.Invoke(context).DefaultTimeout(); - Assert.False(globalOnRejectedInvoked); + // Assert + Assert.False(globalOnRejectedInvoked); Assert.Equal(StatusCodes.Status404NotFound, context.Response.StatusCode); } [Fact] public async Task MultipleEndpointPolicies_LastOneWins() { + // Arrange var globalOnRejectedInvoked = false; var options = CreateOptionsAccessor(); // Policy will disallow @@ -605,22 +586,58 @@ public async Task MultipleEndpointPolicies_LastOneWins() return ValueTask.CompletedTask; }; - var middleware = new RateLimitingMiddleware(c => - { - return Task.CompletedTask; - }, - new NullLoggerFactory().CreateLogger(), - options, - Mock.Of()); + var endpoint = new TestEndpointBuilder(); + + var testConventionBuilder = new TestEndpointConventionBuilder() + .RequireRateLimiting(defaultRateLimiterPolicy) + .RequireRateLimiting(name) + .ApplyToEndpoint(endpoint); + + var middleware = CreateTestRateLimitingMiddleware(options); var context = new DefaultHttpContext(); - context.SetEndpoint(new Endpoint(c => Task.CompletedTask, new EndpointMetadataCollection(new EnableRateLimitingAttribute(defaultRateLimiterPolicy), new EnableRateLimitingAttribute(name)), "Test endpoint")); + context.SetEndpoint(endpoint.Build()); + + // Act await middleware.Invoke(context).DefaultTimeout(); - Assert.False(globalOnRejectedInvoked); + // Assert + Assert.False(globalOnRejectedInvoked); Assert.Equal(StatusCodes.Status403Forbidden, context.Response.StatusCode); } - private IOptions CreateOptionsAccessor() => Options.Create(new RateLimiterOptions()); + private Endpoint CreateEndpointWithRateLimitPolicy(IRateLimiterPolicy policy) + { + var endpointBuilder = new TestEndpointBuilder(); + + var testConventionBuilder = new TestEndpointConventionBuilder() + .RequireRateLimiting(policy) + .ApplyToEndpoint(endpointBuilder); + + return endpointBuilder.Build(); + } + + private Endpoint CreateEndpointWithRateLimitPolicy(string policy) + { + var testConventionBuilder = new TestEndpointConventionBuilder(); + testConventionBuilder.RequireRateLimiting(policy); + var addEnableRateLimitingAttribute = Assert.Single(testConventionBuilder.Conventions); + + var endpointModel = new TestEndpointBuilder(); + addEnableRateLimitingAttribute(endpointModel); + + return endpointModel.Build(); + } + + private RateLimitingMiddleware CreateTestRateLimitingMiddleware(IOptions options, ILogger logger = null, IServiceProvider serviceProvider = null) => + new RateLimitingMiddleware(c => + { + return Task.CompletedTask; + }, + logger ?? new NullLoggerFactory().CreateLogger(), + options, + serviceProvider ?? Mock.Of()); + + private IOptions CreateOptionsAccessor() => Options.Create(new RateLimiterOptions()); } diff --git a/src/Middleware/RateLimiting/test/TestEndpointBuilder.cs b/src/Middleware/RateLimiting/test/TestEndpointBuilder.cs new file mode 100644 index 000000000000..9e477f4e636e --- /dev/null +++ b/src/Middleware/RateLimiting/test/TestEndpointBuilder.cs @@ -0,0 +1,40 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Http; + +namespace Microsoft.AspNetCore.RateLimiting; + +internal class TestEndpointBuilder : EndpointBuilder +{ + public override Endpoint Build() + { + return new Endpoint(RequestDelegate, new EndpointMetadataCollection(Metadata), DisplayName); + } +} + +internal class TestEndpointConventionBuilder : IEndpointConventionBuilder +{ + public IList> Conventions { get; } = new List>(); + + public void Add(Action convention) + { + Conventions.Add(convention); + } + + public TestEndpointConventionBuilder ApplyToEndpoint(EndpointBuilder endpoint) + { + foreach (var convention in Conventions) + { + convention(endpoint); + } + + return this; + } +}