Skip to content

Commit

Permalink
Fix handling of default values in RDG (dotnet#51193)
Browse files Browse the repository at this point in the history
* Fix handling of default values in RDG

* Address feedback

* Cover more test scenarios

* Add tests for culture + decimal

* Add UseCulture test attribute

* Fix formatting
  • Loading branch information
captainsafia authored Oct 15, 2023
1 parent 746ec24 commit 6e1cebd
Show file tree
Hide file tree
Showing 3 changed files with 199 additions and 1 deletion.
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
using System.Text.Json;
using Microsoft.AspNetCore.Http.Features;
using Microsoft.AspNetCore.Http.RequestDelegateGenerator.StaticRouteHandlerModel;
using Microsoft.AspNetCore.Testing;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Primitives;

Expand Down Expand Up @@ -148,4 +149,138 @@ static void TestAction([AsParameters] ParametersListWithHttpContext args)
Assert.Same(httpContext.Request, httpContext.Items["request"]);
Assert.Same(httpContext.Response, httpContext.Items["response"]);
}

public static object[][] DefaultValues
{
get
{
return new[]
{
new object[] { "string?", "default", default(string), true },
new object[] { "string", "\"test\"", "test", true },
new object[] { "string", "\"a\" + \"b\"", "ab", true },
new object[] { "DateOnly?", "default", default(DateOnly?), false },
new object[] { "bool", "default", default(bool), true },
new object[] { "bool", "false", false, true },
new object[] { "bool", "true", true, true},
new object[] { "System.Threading.CancellationToken", "default", default(CancellationToken), false },
new object[] { "Todo?", "default", default(Todo), false },
new object[] { "char", "\'a\'", 'a', true },
new object[] { "int", "default", 0, true },
new object[] { "int", "1234", 1234, true },
new object[] { "int", "1234 * 4", 1234 * 4, true },
new object[] { "double", "1.0", 1.0, true },
new object[] { "double", "double.NaN", double.NaN, true },
new object[] { "double", "double.PositiveInfinity", double.PositiveInfinity, true },
new object[] { "double", "double.NegativeInfinity", double.NegativeInfinity, true },
new object[] { "double", "double.E", double.E, true },
new object[] { "double", "double.Epsilon", double.Epsilon, true },
new object[] { "double", "double.NegativeZero", double.NegativeZero, true },
new object[] { "double", "double.MaxValue", double.MaxValue, true },
new object[] { "double", "double.MinValue", double.MinValue, true },
new object[] { "double", "double.Pi", double.Pi, true },
new object[] { "double", "double.Tau", double.Tau, true },
new object[] { "float", "float.NaN", float.NaN, true },
new object[] { "float", "float.PositiveInfinity", float.PositiveInfinity, true },
new object[] { "float", "float.NegativeInfinity", float.NegativeInfinity, true },
new object[] { "float", "float.E", float.E, true },
new object[] { "float", "float.Epsilon", float.Epsilon, true },
new object[] { "float", "float.NegativeZero", float.NegativeZero, true },
new object[] { "float", "float.MaxValue", float.MaxValue, true },
new object[] { "float", "float.MinValue", float.MinValue, true },
new object[] { "float", "float.Pi", float.Pi, true },
new object[] { "float", "float.Tau", float.Tau, true },
new object[] {"decimal", "decimal.MaxValue", decimal.MaxValue, true },
new object[] {"decimal", "decimal.MinusOne", decimal.MinusOne, true },
new object[] {"decimal", "decimal.MinValue", decimal.MinValue, true },
new object[] {"decimal", "decimal.One", decimal.One, true },
new object[] {"decimal", "decimal.Zero", decimal.Zero, true },
new object[] {"long", "long.MaxValue", long.MaxValue, true },
new object[] {"long", "long.MinValue", long.MinValue, true },
new object[] {"short", "short.MaxValue", short.MaxValue, true },
new object[] {"short", "short.MinValue", short.MinValue, true },
new object[] {"ulong", "ulong.MaxValue", ulong.MaxValue, true },
new object[] {"ulong", "ulong.MinValue", ulong.MinValue, true },
new object[] {"ushort", "ushort.MaxValue", ushort.MaxValue, true },
new object[] {"ushort", "ushort.MinValue", ushort.MinValue, true },
};
}
}

[Theory]
[MemberData(nameof(DefaultValues))]
public async Task RequestDelegatePopulatesParametersWithDefaultValues(string type, string defaultValue, object expectedValue, bool declareConst)
{
var source = string.Empty;
if (declareConst)
{
source = $$"""
const {{type}} defaultConst = {{defaultValue}};
static void TestAction(
HttpContext context,
{{type}} parameterWithDefault = {{defaultValue}},
{{type}} parameterWithConst = defaultConst)
{
context.Items.Add("parameterWithDefault", parameterWithDefault);
context.Items.Add("parameterWithConst", parameterWithConst);
}
app.MapPost("/", TestAction);
""";
}
else
{
source = $$"""
static void TestAction(
HttpContext context,
{{type}} parameterWithDefault = {{defaultValue}})
{
context.Items.Add("parameterWithDefault", parameterWithDefault);
}
app.MapPost("/", TestAction);
""";
}

var (_, compilation) = await RunGeneratorAsync(source);
var endpoint = GetEndpointFromCompilation(compilation);

var httpContext = CreateHttpContext();
httpContext.User = new ClaimsPrincipal();

await endpoint.RequestDelegate(httpContext);

Assert.Equal(expectedValue, httpContext.Items["parameterWithDefault"]);
if (declareConst)
{
Assert.Equal(expectedValue, httpContext.Items["parameterWithConst"]);
}
}

[Fact]
[UseCulture("fr-FR")]
public async Task RequestDelegatePopulatesDecimalWithDefaultValuesAndCultureSet()
{
var source = $$"""
const decimal defaultConst = 3.15m;
static void TestAction(
HttpContext context,
decimal parameterWithDefault = 2.15m,
decimal parameterWithConst = defaultConst)
{
context.Items.Add("parameterWithDefault", parameterWithDefault);
context.Items.Add("parameterWithConst", parameterWithConst);
}
app.MapPost("/", TestAction);
""";

var (_, compilation) = await RunGeneratorAsync(source);
var endpoint = GetEndpointFromCompilation(compilation);

var httpContext = CreateHttpContext();
httpContext.User = new ClaimsPrincipal();

await endpoint.RequestDelegate(httpContext);

Assert.Equal(2.15m, httpContext.Items["parameterWithDefault"]);
Assert.Equal(3.15m, httpContext.Items["parameterWithConst"]);
}
}
23 changes: 22 additions & 1 deletion src/Shared/RoslynUtils/SymbolExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
using System.Collections.Immutable;
using System.Diagnostics;
using System.Diagnostics.CodeAnalysis;
using System.Globalization;
using System.Linq;
using System.Reflection.PortableExecutable;
using Microsoft.CodeAnalysis;
Expand Down Expand Up @@ -176,7 +177,27 @@ public static string GetDefaultValueString(this IParameterSymbol parameterSymbol
{
return !parameterSymbol.HasExplicitDefaultValue
? "null"
: SymbolDisplay.FormatLiteral((parameterSymbol.ExplicitDefaultValue ?? "null").ToString(), parameterSymbol.ExplicitDefaultValue is string);
: InnerGetDefaultValueString(parameterSymbol.ExplicitDefaultValue);
}

private static string InnerGetDefaultValueString(object? defaultValue)
{
return defaultValue switch
{
string s => SymbolDisplay.FormatLiteral(s, true),
char c => SymbolDisplay.FormatLiteral(c, true),
bool b => b ? "true" : "false",
null => "default",
float f when f is float.NegativeInfinity => "float.NegativeInfinity",
float f when f is float.PositiveInfinity => "float.PositiveInfinity",
float f when f is float.NaN => "float.NaN",
float f => $"{SymbolDisplay.FormatPrimitive(f, false, false)}F",
double d when d is double.NegativeInfinity => "double.NegativeInfinity",
double d when d is double.PositiveInfinity => "double.PositiveInfinity",
double d when d is double.NaN => "double.NaN",
decimal d => $"{SymbolDisplay.FormatPrimitive(d, false, false)}M",
_ => SymbolDisplay.FormatPrimitive(defaultValue, false, false),
};
}

public static bool TryGetNamedArgumentValue<T>(this AttributeData attribute, string argumentName, out T? argumentValue)
Expand Down
42 changes: 42 additions & 0 deletions src/Testing/src/UseCultureAttribute.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
// 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.Globalization;
using System.Reflection;
using Xunit.Sdk;

namespace Microsoft.AspNetCore.Testing;

[AttributeUsage(AttributeTargets.Class | AttributeTargets.Method)]
public sealed class UseCultureAttribute : BeforeAfterTestAttribute
{
private CultureInfo _originalCulture;
private CultureInfo _originalUiCulture;
public UseCultureAttribute(string culture)
: this(culture, culture)
{
}

public UseCultureAttribute(string culture, string uiCulture)
{
Culture = new CultureInfo(culture);
UiCulture = new CultureInfo(uiCulture);
}

public CultureInfo Culture { get; }
public CultureInfo UiCulture { get; }

public override void Before(MethodInfo methodUnderTest)
{
_originalCulture = CultureInfo.CurrentCulture;
_originalUiCulture = CultureInfo.CurrentUICulture;
CultureInfo.CurrentCulture = Culture;
CultureInfo.CurrentUICulture = UiCulture;
}

public override void After(MethodInfo methodUnderTest)
{
CultureInfo.CurrentCulture = _originalCulture;
CultureInfo.CurrentUICulture = _originalUiCulture;
}
}

0 comments on commit 6e1cebd

Please sign in to comment.