File: RequestDelegateGenerator\RequestDelegateCreationTests.AsParameters.cs
Web Access
Project: src\src\Http\Http.Extensions\test\Microsoft.AspNetCore.Http.Extensions.Tests.csproj (Microsoft.AspNetCore.Http.Extensions.Tests)
// 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.Security.Claims;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Primitives;
 
namespace Microsoft.AspNetCore.Http.Generators.Tests;
 
public partial class RequestDelegateCreationTests
{
    [Fact]
    public async Task RequestDelegatePopulatesFromRouteParameterBased_FromParameterList()
    {
        const int originalRouteParam = 42;
 
        var source = """
static void TestAction([AsParameters] ParameterListFromRoute args)
{
    args.HttpContext.Items["input"] = args.Value;
}
app.MapGet("/{Value}", TestAction);
""";
        var (_, compilation) = await RunGeneratorAsync(source);
        var endpoint = GetEndpointFromCompilation(compilation);
 
        var httpContext = CreateHttpContext();
        httpContext.Request.RouteValues["value"] = originalRouteParam.ToString(NumberFormatInfo.InvariantInfo);
 
        await endpoint.RequestDelegate(httpContext);
 
        Assert.Equal(originalRouteParam, httpContext.Items["input"]);
    }
 
    [Fact]
    public async Task RequestDelegatePopulatesFromQueryParameter_FromParameterList()
    {
        // QueryCollection is case sensitve, since we now getting
        // the parameter name from the Property/Record constructor
        // we should match the case here
        const string paramName = "Value";
        const int originalQueryParam = 42;
        const string customParamName = "customQuery";
        const int originalCustomQueryParam = 43;
        const string anotherCustomParamName = "anotherCustomQuery";
        const int originalAnotherCustomQueryParam = 44;
 
        var source = """
static void TestAction([AsParameters] ParameterListFromQuery args)
{
    args.HttpContext.Items.Add("input", args.Value);
    args.HttpContext.Items.Add("customInput", args.CustomValue);
    args.HttpContext.Items.Add("anotherCustomInput", args.AnotherCustomValue);
}
app.MapGet("/", TestAction);
""";
        var (_, compilation) = await RunGeneratorAsync(source);
        var endpoint = GetEndpointFromCompilation(compilation);
 
        var query = new QueryCollection(new Dictionary<string, StringValues>()
        {
            [paramName] = originalQueryParam.ToString(NumberFormatInfo.InvariantInfo),
            [customParamName] = originalCustomQueryParam.ToString(NumberFormatInfo.InvariantInfo),
            [anotherCustomParamName] = originalAnotherCustomQueryParam.ToString(NumberFormatInfo.InvariantInfo)
        });
 
        var httpContext = CreateHttpContext();
        httpContext.Request.Query = query;
 
        await endpoint.RequestDelegate(httpContext);
 
        Assert.Equal(originalQueryParam, httpContext.Items["input"]);
        Assert.Equal(originalCustomQueryParam, httpContext.Items["customInput"]);
        Assert.Equal(originalAnotherCustomQueryParam, httpContext.Items["anotherCustomInput"]);
    }
 
    [Theory]
    [InlineData("ParameterListFromHeader")]
    [InlineData("ParameterListFromHeaderWithProperties")]
    public async Task RequestDelegatePopulatesFromHeaderParameter_FromParameterList(string type)
    {
        const string customHeaderName = "X-Custom-Header";
        const int originalHeaderParam = 42;
 
        var source = $$"""
static void TestAction([AsParameters] {{type}} args)
{
    args.HttpContext.Items.Add("input", args.Value);
}
app.MapGet("/", TestAction);
""";
        var (_, compilation) = await RunGeneratorAsync(source);
        var endpoint = GetEndpointFromCompilation(compilation);
 
        var httpContext = CreateHttpContext();
        httpContext.Request.Headers[customHeaderName] = originalHeaderParam.ToString(NumberFormatInfo.InvariantInfo);
 
        await endpoint.RequestDelegate(httpContext);
 
        Assert.Equal(originalHeaderParam, httpContext.Items["input"]);
    }
 
    public static object[][] FromParameterListActions
    {
        get
        {
            var TestParameterListRecordStruct = """
void TestAction([AsParameters] ParameterListRecordStruct args)
{
    args.HttpContext.Items.Add("input", args.Value);
}
""";
 
            var TestParameterListRecordClass = """
void TestAction([AsParameters] ParameterListRecordClass args)
{
    args.HttpContext.Items.Add("input", args.Value);
}
""";
 
            var TestParameterListRecordWithoutPositionalParameters = """
void TestAction([AsParameters] ParameterListRecordWithoutPositionalParameters args)
{
    args.HttpContext!.Items.Add("input", args.Value);
}
""";
 
            var TestParameterListStruct = """
void TestAction([AsParameters] ParameterListStruct args)
{
    args.HttpContext.Items.Add("input", args.Value);
}
""";
 
            var TestParameterListMutableStruct = """
void TestAction([AsParameters] ParameterListMutableStruct args)
{
    args.HttpContext.Items.Add("input", args.Value);
}
""";
 
            var TestParameterListStructWithParameterizedContructor = """
void TestAction([AsParameters] ParameterListStructWithParameterizedContructor args)
{
    args.HttpContext.Items.Add("input", args.Value);
}
""";
 
            var TestParameterListStructWithMultipleParameterizedContructor = """
void TestAction([AsParameters] ParameterListStructWithMultipleParameterizedContructor args)
{
    args.HttpContext.Items.Add("input", args.Value);
}
""";
 
            var TestParameterListClass = """
void TestAction([AsParameters] ParameterListClass args)
{
    args.HttpContext!.Items.Add("input", args.Value);
}
""";
 
            var TestParameterListClassWithParameterizedContructor = """
void TestAction([AsParameters] ParameterListClassWithParameterizedContructor args)
{
    args.HttpContext.Items.Add("input", args.Value);
}
""";
 
            return new[]
            {
                new object[] { TestParameterListRecordStruct },
                new object[] { TestParameterListRecordClass },
                new object[] { TestParameterListRecordWithoutPositionalParameters },
                new object[] { TestParameterListStruct },
                new object[] { TestParameterListMutableStruct },
                new object[] { TestParameterListStructWithParameterizedContructor },
                new object[] { TestParameterListStructWithMultipleParameterizedContructor },
                new object[] { TestParameterListClass },
                new object[] { TestParameterListClassWithParameterizedContructor },
            };
        }
    }
 
    [Theory]
    [MemberData(nameof(FromParameterListActions))]
    public async Task RequestDelegatePopulatesFromParameterList(string innerSource)
    {
        var source = $$"""
{{innerSource}}
app.MapGet("/{value}", TestAction);
""";
        const string paramName = "value";
        const int originalRouteParam = 42;
 
        var (_, compilation) = await RunGeneratorAsync(source);
        var endpoint = GetEndpointFromCompilation(compilation);
 
        var httpContext = CreateHttpContext();
        httpContext.Request.RouteValues[paramName] = originalRouteParam.ToString(NumberFormatInfo.InvariantInfo);
 
        await endpoint.RequestDelegate(httpContext);
 
        Assert.Equal(originalRouteParam, httpContext.Items["input"]);
    }
 
    [Fact]
    public async Task RequestDelegatePopulatesFromParameterListUsesDefaultValue()
    {
        const int expectedValue = 42;
        var source = """
void TestAction([AsParameters] ParameterListWitDefaultValue args)
{
    args.HttpContext.Items.Add("input", args.Value);
}
app.MapGet("/{value}", TestAction);
""";
        var httpContext = CreateHttpContext();
 
        var (_, compilation) = await RunGeneratorAsync(source);
        var endpoint = GetEndpointFromCompilation(compilation);
 
        await endpoint.RequestDelegate(httpContext);
 
        Assert.Equal(expectedValue, httpContext.Items["input"]);
    }
 
    [Fact]
    public async Task VerifyAsParametersBaseline()
    {
        var (_, compilation) = await RunGeneratorAsync("""
void parameterListWithDefaultValue([AsParameters] ParameterListWitDefaultValue args)
{
    args.HttpContext.Items.Add("input", args.Value);
}
void parameterListRecordStruct([AsParameters] ParameterListRecordStruct args)
{
    args.HttpContext.Items.Add("input", args.Value);
}
static void parametersListWithHttpContext([AsParameters] ParametersListWithHttpContext args)
{
    args.HttpContext.Items.Add("input", args.HttpContext);
    args.HttpContext.Items.Add("user", args.User);
    args.HttpContext.Items.Add("request", args.Request);
    args.HttpContext.Items.Add("response", args.Response);
}
static void parametersListWithMetadataType([AsParameters] ParametersListWithMetadataType args)
{
    args.HttpContext.Items.Add("value", args.Value);
}
app.MapGet("/parameterListWithDefaultValue/{value}", parameterListWithDefaultValue);
app.MapPost("/parameterListRecordStruct/{value}", parameterListRecordStruct);
app.MapPut("/parametersListWithHttpContext", parametersListWithHttpContext);
app.MapPatch("/parametersListWithImplicitFromBody", ([AsParameters] ParametersListWithImplicitFromBody args) => args.Todo.Name ?? string.Empty);
app.MapGet("/parametersListWithMetadataType", parametersListWithMetadataType);
app.MapPost("/parameterRecordStructWithJsonBodyOrService", ([AsParameters] ParameterRecordStructWithJsonBodyOrService args) => args.Todo.Name ?? string.Empty);
""");
 
        await VerifyAgainstBaselineUsingFile(compilation);
    }
 
    [Fact]
    public async Task RequestDelegatePopulatesFromParameterListAndSkipReadOnlyProperties()
    {
        const int routeParamValue = 42;
        var expectedInput = new ParameterListWithReadOnlyProperties() { Value = routeParamValue };
 
        var source = """
void TestAction(HttpContext context, [AsParameters] ParameterListWithReadOnlyProperties args)
{
    context.Items.Add("input", args);
}
app.MapGet("/{value}", TestAction);
""";
        var (_, compilation) = await RunGeneratorAsync(source);
        var endpoint = GetEndpointFromCompilation(compilation);
 
        var httpContext = CreateHttpContext();
 
        httpContext.Request.RouteValues[nameof(ParameterListWithReadOnlyProperties.Value)] = routeParamValue.ToString(NumberFormatInfo.InvariantInfo);
        httpContext.Request.RouteValues[nameof(ParameterListWithReadOnlyProperties.ConstantValue)] = routeParamValue.ToString(NumberFormatInfo.InvariantInfo);
        httpContext.Request.RouteValues[nameof(ParameterListWithReadOnlyProperties.ReadOnlyValue)] = routeParamValue.ToString(NumberFormatInfo.InvariantInfo);
        httpContext.Request.RouteValues[nameof(ParameterListWithReadOnlyProperties.PrivateSetValue)] = routeParamValue.ToString(NumberFormatInfo.InvariantInfo);
 
        await endpoint.RequestDelegate(httpContext);
 
        var input = Assert.IsType<ParameterListWithReadOnlyProperties>(httpContext.Items["input"]);
        Assert.Equal(expectedInput.Value, input.Value);
        Assert.Equal(expectedInput.ConstantValue, input.ConstantValue);
        Assert.Equal(expectedInput.ReadOnlyValue, input.ReadOnlyValue);
        Assert.Equal(expectedInput.PrivateSetValue, input.PrivateSetValue);
    }
 
    [Fact]
    public async Task RequestDelegatePopulatesFromMultipleParameterLists()
    {
        const int foo = 1;
        const int bar = 2;
 
        var source = """
void TestAction(HttpContext context, [AsParameters] SampleParameterList args, [AsParameters] AdditionalSampleParameterList args2)
{
    context.Items.Add("foo", args.Foo);
    context.Items.Add("bar", args2.Bar);
}
app.MapGet("/{foo}/{bar}", TestAction);
""";
 
        var (_, compilation) = await RunGeneratorAsync(source);
        var endpoint = GetEndpointFromCompilation(compilation);
 
        var httpContext = CreateHttpContext();
        httpContext.Request.RouteValues[nameof(SampleParameterList.Foo)] = foo.ToString(NumberFormatInfo.InvariantInfo);
        httpContext.Request.RouteValues[nameof(AdditionalSampleParameterList.Bar)] = bar.ToString(NumberFormatInfo.InvariantInfo);
 
        await endpoint.RequestDelegate(httpContext);
 
        Assert.Equal(foo, httpContext.Items["foo"]);
        Assert.Equal(bar, httpContext.Items["bar"]);
    }
 
    [Fact]
    public async Task RequestDelegatePopulatesFromBindAsyncParameterList()
    {
        const string uriValue = "https://example.org/";
 
        var source = """
void TestAction([AsParameters] ParametersListWithBindAsyncType args)
{
    args.HttpContext.Items.Add("value", args.Value);
    args.HttpContext.Items.Add("anotherValue", args.MyBindAsyncParam);
}
app.MapGet("/", TestAction);
""";
 
        var (_, compilation) = await RunGeneratorAsync(source);
        var endpoint = GetEndpointFromCompilation(compilation);
 
        var httpContext = CreateHttpContext();
        httpContext.Request.Headers.Referer = uriValue;
 
        await endpoint.RequestDelegate(httpContext);
 
        var value = Assert.IsType<InheritBindAsync>(httpContext.Items["value"]);
        var anotherValue = Assert.IsType<MyBindAsyncRecord>(httpContext.Items["anotherValue"]);
 
        Assert.Equal(uriValue, value.Uri?.ToString());
        Assert.Equal(uriValue, anotherValue.Uri?.ToString());
    }
 
    [Fact]
    public async Task RequestDelegatePopulatesFromMetadataProviderParameterList()
    {
        var source = """
void TestAction([AsParameters] ParametersListWithMetadataType args)
{
    args.HttpContext.Items.Add("value", args.Value);
}
app.MapPost("/", TestAction);
""";
 
        var (_, compilation) = await RunGeneratorAsync(source);
        var endpoint = GetEndpointFromCompilation(compilation);
 
        Assert.Contains(endpoint.Metadata, m => m is ParameterNameMetadata { Name: "Value" });
    }
 
    [Fact]
    public async Task RequestDelegateFactory_AsParameters_SupportsRequiredMember()
    {
        // Arrange
        var source = """
app.MapGet("/{requiredRouteParam}", TestAction);
static void TestAction([AsParameters] ParameterListRequiredStringFromDifferentSources args) { }
""";
 
        var (_, compilation) = await RunGeneratorAsync(source);
        var endpoint = GetEndpointFromCompilation(compilation);
 
        var httpContext = CreateHttpContext();
 
        // Act
        await endpoint.RequestDelegate(httpContext);
 
        // Assert that the required modifier on members that
        // are not nullable treats them as required.
        Assert.Equal(400, httpContext.Response.StatusCode);
 
        var logs = TestSink.Writes.ToArray();
 
        Assert.Equal(3, logs.Length);
 
        Assert.Equal(new EventId(4, "RequiredParameterNotProvided"), logs[0].EventId);
        Assert.Equal(LogLevel.Debug, logs[0].LogLevel);
        Assert.Equal(@"Required parameter ""string RequiredRouteParam"" was not provided from route.", logs[0].Message);
 
        Assert.Equal(new EventId(4, "RequiredParameterNotProvided"), logs[1].EventId);
        Assert.Equal(LogLevel.Debug, logs[1].LogLevel);
        Assert.Equal(@"Required parameter ""string RequiredQueryParam"" was not provided from query string.", logs[1].Message);
 
        Assert.Equal(new EventId(4, "RequiredParameterNotProvided"), logs[2].EventId);
        Assert.Equal(LogLevel.Debug, logs[2].LogLevel);
        Assert.Equal(@"Required parameter ""string RequiredHeaderParam"" was not provided from header.", logs[2].Message);
    }
}