Skip to content

[automated] Merge branch 'release/6.0-rc1' => 'release/6.0' #35556

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

2 changes: 1 addition & 1 deletion eng/scripts/CodeCheck.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -213,7 +213,7 @@ try {
}
}
# Check for changes in Unshipped in servicing branches
if ($targetBranch -like 'release*' -and $targetBranch -notlike '*preview*' -and $file -like '*PublicAPI.Unshipped.txt') {
if ($targetBranch -like 'release*' -and $targetBranch -notlike '*preview*' -and $targetBranch -notlike '*rc*' -and $file -like '*PublicAPI.Unshipped.txt') {
$changedAPIBaselines.Add($file)
}
}
Expand Down
15 changes: 12 additions & 3 deletions src/Components/Components.slnf
Original file line number Diff line number Diff line change
Expand Up @@ -46,23 +46,27 @@
"src\\Components\\test\\E2ETestMigration\\Microsoft.AspNetCore.Components.Migration.E2ETests.csproj",
"src\\Components\\test\\E2ETest\\Microsoft.AspNetCore.Components.E2ETests.csproj",
"src\\Components\\test\\testassets\\BasicTestApp\\BasicTestApp.csproj",
"src\\Components\\test\\testassets\\ComponentsApp.App\\ComponentsApp.App.csproj",
"src\\Components\\test\\testassets\\ComponentsApp.Server\\ComponentsApp.Server.csproj",
"src\\Components\\test\\testassets\\GlobalizationWasmApp\\GlobalizationWasmApp.csproj",
"src\\Components\\test\\testassets\\LazyTestContentPackage\\LazyTestContentPackage.csproj",
"src\\Components\\test\\testassets\\TestContentPackage\\TestContentPackage.csproj",
"src\\Components\\test\\testassets\\TestServer\\Components.TestServer.csproj",
"src\\DataProtection\\Abstractions\\src\\Microsoft.AspNetCore.DataProtection.Abstractions.csproj",
"src\\DataProtection\\Cryptography.Internal\\src\\Microsoft.AspNetCore.Cryptography.Internal.csproj",
"src\\DataProtection\\Cryptography.KeyDerivation\\src\\Microsoft.AspNetCore.Cryptography.KeyDerivation.csproj",
"src\\DataProtection\\DataProtection\\src\\Microsoft.AspNetCore.DataProtection.csproj",
"src\\DataProtection\\Extensions\\src\\Microsoft.AspNetCore.DataProtection.Extensions.csproj",
"src\\DefaultBuilder\\src\\Microsoft.AspNetCore.csproj",
"src\\Extensions\\Features\\src\\Microsoft.Extensions.Features.csproj",
"src\\Features\\JsonPatch\\src\\Microsoft.AspNetCore.JsonPatch.csproj",
"src\\FileProviders\\Embedded\\src\\Microsoft.Extensions.FileProviders.Embedded.csproj",
"src\\Hosting\\Abstractions\\src\\Microsoft.AspNetCore.Hosting.Abstractions.csproj",
"src\\Hosting\\Hosting\\src\\Microsoft.AspNetCore.Hosting.csproj",
"src\\Hosting\\Server.Abstractions\\src\\Microsoft.AspNetCore.Hosting.Server.Abstractions.csproj",
"src\\Html.Abstractions\\src\\Microsoft.AspNetCore.Html.Abstractions.csproj",
"src\\Http\\Authentication.Abstractions\\src\\Microsoft.AspNetCore.Authentication.Abstractions.csproj",
"src\\Http\\Authentication.Core\\src\\Microsoft.AspNetCore.Authentication.Core.csproj",
"src\\Extensions\\Features\\src\\Microsoft.Extensions.Features.csproj",
"src\\Http\\Headers\\src\\Microsoft.Net.Http.Headers.csproj",
"src\\Http\\Http.Abstractions\\src\\Microsoft.AspNetCore.Http.Abstractions.csproj",
"src\\Http\\Http.Extensions\\src\\Microsoft.AspNetCore.Http.Extensions.csproj",
Expand All @@ -79,6 +83,8 @@
"src\\Identity\\Extensions.Stores\\src\\Microsoft.Extensions.Identity.Stores.csproj",
"src\\Identity\\UI\\src\\Microsoft.AspNetCore.Identity.UI.csproj",
"src\\JSInterop\\Microsoft.JSInterop\\src\\Microsoft.JSInterop.csproj",
"src\\Localization\\Abstractions\\src\\Microsoft.Extensions.Localization.Abstractions.csproj",
"src\\Localization\\Localization\\src\\Microsoft.Extensions.Localization.csproj",
"src\\Middleware\\CORS\\src\\Microsoft.AspNetCore.Cors.csproj",
"src\\Middleware\\Diagnostics.Abstractions\\src\\Microsoft.AspNetCore.Diagnostics.Abstractions.csproj",
"src\\Middleware\\Diagnostics\\src\\Microsoft.AspNetCore.Diagnostics.csproj",
Expand All @@ -96,13 +102,15 @@
"src\\Mvc\\Mvc.Core\\src\\Microsoft.AspNetCore.Mvc.Core.csproj",
"src\\Mvc\\Mvc.Cors\\src\\Microsoft.AspNetCore.Mvc.Cors.csproj",
"src\\Mvc\\Mvc.DataAnnotations\\src\\Microsoft.AspNetCore.Mvc.DataAnnotations.csproj",
"src\\Mvc\\Mvc.Formatters.Json\\src\\Microsoft.AspNetCore.Mvc.Formatters.Json.csproj",
"src\\Mvc\\Mvc.Localization\\src\\Microsoft.AspNetCore.Mvc.Localization.csproj",
"src\\Mvc\\Mvc.NewtonsoftJson\\src\\Microsoft.AspNetCore.Mvc.NewtonsoftJson.csproj",
"src\\Mvc\\Mvc.RazorPages\\src\\Microsoft.AspNetCore.Mvc.RazorPages.csproj",
"src\\Mvc\\Mvc.Razor\\src\\Microsoft.AspNetCore.Mvc.Razor.csproj",
"src\\Mvc\\Mvc.TagHelpers\\src\\Microsoft.AspNetCore.Mvc.TagHelpers.csproj",
"src\\Mvc\\Mvc.ViewFeatures\\src\\Microsoft.AspNetCore.Mvc.ViewFeatures.csproj",
"src\\Mvc\\Mvc\\src\\Microsoft.AspNetCore.Mvc.csproj",
"src\\ObjectPool\\src\\Microsoft.Extensions.ObjectPool.csproj",
"src\\Razor\\Razor.Runtime\\src\\Microsoft.AspNetCore.Razor.Runtime.csproj",
"src\\Razor\\Razor\\src\\Microsoft.AspNetCore.Razor.csproj",
"src\\Security\\Authentication\\Cookies\\src\\Microsoft.AspNetCore.Authentication.Cookies.csproj",
Expand All @@ -128,7 +136,8 @@
"src\\SignalR\\common\\SignalR.Common\\src\\Microsoft.AspNetCore.SignalR.Common.csproj",
"src\\SignalR\\server\\Core\\src\\Microsoft.AspNetCore.SignalR.Core.csproj",
"src\\SignalR\\server\\SignalR\\src\\Microsoft.AspNetCore.SignalR.csproj",
"src\\Testing\\src\\Microsoft.AspNetCore.Testing.csproj"
"src\\Testing\\src\\Microsoft.AspNetCore.Testing.csproj",
"src\\WebEncoders\\src\\Microsoft.Extensions.WebEncoders.csproj"
]
}
}
}
52 changes: 45 additions & 7 deletions src/Components/Web/src/Forms/InputExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -16,23 +16,61 @@ internal static class InputExtensions
{
try
{
if (BindConverter.TryConvertTo<TValue>(value, CultureInfo.CurrentCulture, out var parsedValue))
// We special-case bool values because BindConverter reserves bool conversion for conditional attributes.
if (typeof(TValue) == typeof(bool))
{
if (TryConvertToBool(value, out result))
{
validationErrorMessage = null;
return true;
}
}
else if (typeof(TValue) == typeof(bool?))
{
if (TryConvertToNullableBool(value, out result))
{
validationErrorMessage = null;
return true;
}
}
else if (BindConverter.TryConvertTo<TValue>(value, CultureInfo.CurrentCulture, out var parsedValue))
{
result = parsedValue;
validationErrorMessage = null;
return true;
}
else
{
result = default;
validationErrorMessage = $"The {input.DisplayName ?? input.FieldIdentifier.FieldName} field is not valid.";
return false;
}

result = default;
validationErrorMessage = $"The {input.DisplayName ?? input.FieldIdentifier.FieldName} field is not valid.";
return false;
}
catch (InvalidOperationException ex)
{
throw new InvalidOperationException($"{input.GetType()} does not support the type '{typeof(TValue)}'.", ex);
}
}

private static bool TryConvertToBool<TValue>(string? value, out TValue result)
{
if (bool.TryParse(value, out var @bool))
{
result = (TValue)(object)@bool;
return true;
}

result = default!;
return false;
}

private static bool TryConvertToNullableBool<TValue>(string? value, out TValue result)
{
if (string.IsNullOrEmpty(value))
{
result = default!;
return true;
}

return TryConvertToBool(value, out result);
}
}
}
16 changes: 16 additions & 0 deletions src/Components/Web/src/Forms/InputSelect.cs
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,22 @@ protected override void BuildRenderTree(RenderTreeBuilder builder)
protected override bool TryParseValueFromString(string? value, [MaybeNullWhen(false)] out TValue result, [NotNullWhen(false)] out string? validationErrorMessage)
=> this.TryParseSelectableValueFromString(value, out result, out validationErrorMessage);

/// <inheritdoc />
protected override string? FormatValueAsString(TValue? value)
{
// We special-case bool values because BindConverter reserves bool conversion for conditional attributes.
if (typeof(TValue) == typeof(bool))
{
return (bool)(object)value! ? "true" : "false";
}
else if (typeof(TValue) == typeof(bool?))
{
return value is not null && (bool)(object)value ? "true" : "false";
}

return base.FormatValueAsString(value);
}

private void SetCurrentValueAsStringArray(string?[]? value)
{
CurrentValue = BindConverter.TryConvertTo<TValue>(value, CultureInfo.CurrentCulture, out var result)
Expand Down
1 change: 1 addition & 0 deletions src/Components/Web/src/PublicAPI.Unshipped.txt
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,7 @@ Microsoft.AspNetCore.Components.Web.PageTitle.ChildContent.set -> void
Microsoft.AspNetCore.Components.Web.PageTitle.PageTitle() -> void
override Microsoft.AspNetCore.Components.Forms.InputDate<TValue>.OnParametersSet() -> void
abstract Microsoft.AspNetCore.Components.RenderTree.WebRenderer.AttachRootComponentToBrowser(int componentId, string! domElementSelector) -> void
override Microsoft.AspNetCore.Components.Forms.InputSelect<TValue>.FormatValueAsString(TValue? value) -> string?
override Microsoft.AspNetCore.Components.RenderTree.WebRenderer.Dispose(bool disposing) -> void
override Microsoft.AspNetCore.Components.Routing.FocusOnNavigate.OnAfterRenderAsync(bool firstRender) -> System.Threading.Tasks.Task!
override Microsoft.AspNetCore.Components.Routing.FocusOnNavigate.OnParametersSet() -> void
Expand Down
64 changes: 64 additions & 0 deletions src/Components/test/E2ETest/Tests/FormsTest.cs
Original file line number Diff line number Diff line change
Expand Up @@ -355,6 +355,34 @@ public void InputSelectInteractsWithEditContext()
Browser.Equal(new[] { "The TicketClass field is not valid." }, messagesAccessor);
}

[Fact]
public void InputSelectInteractsWithEditContext_BoolValues()
{
var appElement = MountTypicalValidationComponent();
var ticketClassInput = new SelectElement(appElement.FindElement(By.ClassName("select-bool-values")).FindElement(By.TagName("select")));
var select = ticketClassInput.WrappedElement;
var messagesAccessor = CreateValidationMessagesAccessor(appElement);

// Invalidates on edit
Browser.Equal("valid", () => select.GetAttribute("class"));
ticketClassInput.SelectByText("true");
Browser.Equal("modified invalid", () => select.GetAttribute("class"));
Browser.Equal(new[] { "77 + 33 = 100 is a false statement, unfortunately." }, messagesAccessor);

// Nullable conversion can fail
ticketClassInput.SelectByText("(select)");
Browser.Equal("modified invalid", () => select.GetAttribute("class"));
Browser.Equal(new[]
{
"77 + 33 = 100 is a false statement, unfortunately.",
"The IsSelectMathStatementTrue field is not valid."
}, messagesAccessor);

// Can become valid
ticketClassInput.SelectByText("false");
Browser.Equal("modified valid", () => select.GetAttribute("class"));
}

[Fact]
public void InputSelectInteractsWithEditContext_MultipleAttribute()
{
Expand Down Expand Up @@ -521,6 +549,42 @@ public void InputRadioGroupsWithNamesNestedInteractWithEditContext()
IReadOnlyCollection<IWebElement> FindColorInputs() => group.FindElements(By.Name("color"));
}

[Fact]
public void InputRadioGroupWithBoolValuesInteractsWithEditContext()
{
var appElement = MountTypicalValidationComponent();
var messagesAccessor = CreateValidationMessagesAccessor(appElement);

// Validate selected inputs
Browser.False(() => FindTrueInput().Selected);
Browser.True(() => FindFalseInput().Selected);

// Validates on edit
Browser.Equal("valid", () => FindTrueInput().GetAttribute("class"));
Browser.Equal("valid", () => FindFalseInput().GetAttribute("class"));

FindTrueInput().Click();

Browser.Equal("modified valid", () => FindTrueInput().GetAttribute("class"));
Browser.Equal("modified valid", () => FindFalseInput().GetAttribute("class"));

// Can become invalid
FindFalseInput().Click();

Browser.Equal("modified invalid", () => FindTrueInput().GetAttribute("class"));
Browser.Equal("modified invalid", () => FindFalseInput().GetAttribute("class"));
Browser.Equal(new[] { "7 * 3 = 21 is a true statement." }, messagesAccessor);

IReadOnlyCollection<IWebElement> FindInputs()
=> appElement.FindElement(By.ClassName("radio-group-bool-values")).FindElements(By.TagName("input"));

IWebElement FindTrueInput()
=> FindInputs().First(i => string.Equals("True", i.GetAttribute("value")));

IWebElement FindFalseInput()
=> FindInputs().First(i => string.Equals("False", i.GetAttribute("value")));
}

[Fact]
public void CanWireUpINotifyPropertyChangedToEditContext()
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,14 @@
</InputSelect>
<span>@string.Join(", ", person.HostileStrings)</span>
</p>
<p class="select-bool-values">
T/F: 77 + 33 = 100<br>
<InputSelect @bind-Value="person.IsSelectMathStatementTrue">
<option>(select)</option>
<option value="true">true</option>
<option value="false">false</option>
</InputSelect>
</p>
<p class="airline">
<InputRadioGroup @bind-Value="person.Airline">
Airline:
Expand All @@ -96,6 +104,13 @@
</InputRadioGroup>
</InputRadioGroup>
</p>
<p class="radio-group-bool-values">
T/F: 7 * 3 = 21<br>
<InputRadioGroup @bind-Value="person.IsRadioMathStatementTrue">
<InputRadio Value="true" />true<br>
<InputRadio Value="false" />false<br>
</InputRadioGroup>
</p>
<p class="socks">
Socks color: <InputText @bind-Value="person.SocksColor" />
</p>
Expand Down Expand Up @@ -188,6 +203,12 @@
[Required, EnumDataType(typeof(Country))]
public Country? Country { get; set; } = null;

[Required, Range(typeof(bool), "false", "false", ErrorMessage = "77 + 33 = 100 is a false statement, unfortunately.")]
public bool? IsSelectMathStatementTrue { get; set; } = null;

[Required, Range(typeof(bool), "true", "true", ErrorMessage = "7 * 3 = 21 is a true statement.")]
public bool IsRadioMathStatementTrue { get; set; } = false;

[Required, StringLength(10), CustomValidationClassName(Valid = "valid-socks", Invalid = "invalid-socks")]
public string SocksColor { get; set; }

Expand Down
27 changes: 19 additions & 8 deletions src/Http/Http.Extensions/src/RequestDelegateFactory.cs
Original file line number Diff line number Diff line change
Expand Up @@ -627,8 +627,7 @@ private static Expression GetValueFromProperty(Expression sourceExpression, stri

private static Expression BindParameterFromService(ParameterInfo parameter)
{
var nullability = NullabilityContext.Create(parameter);
var isOptional = parameter.HasDefaultValue || nullability.ReadState == NullabilityState.Nullable;
var isOptional = IsOptionalParameter(parameter);

return isOptional
? Expression.Call(GetServiceMethod.MakeGenericMethod(parameter.ParameterType), RequestServicesExpr)
Expand All @@ -637,8 +636,7 @@ private static Expression BindParameterFromService(ParameterInfo parameter)

private static Expression BindParameterFromValue(ParameterInfo parameter, Expression valueExpression, FactoryContext factoryContext)
{
var nullability = NullabilityContext.Create(parameter);
var isOptional = parameter.HasDefaultValue || nullability.ReadState == NullabilityState.Nullable;
var isOptional = IsOptionalParameter(parameter);

var argument = Expression.Variable(parameter.ParameterType, $"{parameter.Name}_local");

Expand Down Expand Up @@ -671,7 +669,8 @@ private static Expression BindParameterFromValue(ParameterInfo parameter, Expres
}

// Allow nullable parameters that don't have a default value
if (nullability.ReadState == NullabilityState.Nullable && !parameter.HasDefaultValue)
var nullability = NullabilityContext.Create(parameter);
if (nullability.ReadState != NullabilityState.NotNull && !parameter.HasDefaultValue)
{
return valueExpression;
}
Expand Down Expand Up @@ -817,7 +816,7 @@ private static Expression BindParameterFromBindAsync(ParameterInfo parameter, Fa
{
// We reference the boundValues array by parameter index here
var nullability = NullabilityContext.Create(parameter);
var isOptional = parameter.HasDefaultValue || nullability.ReadState == NullabilityState.Nullable;
var isOptional = IsOptionalParameter(parameter);

// Get the BindAsync method
var body = TryParseMethodCache.FindBindAsyncMethod(parameter.ParameterType)!;
Expand Down Expand Up @@ -862,8 +861,7 @@ private static Expression BindParameterFromBody(ParameterInfo parameter, bool al
}
}

var nullability = NullabilityContext.Create(parameter);
var isOptional = parameter.HasDefaultValue || nullability.ReadState == NullabilityState.Nullable;
var isOptional = IsOptionalParameter(parameter);

factoryContext.JsonRequestBodyType = parameter.ParameterType;
factoryContext.AllowEmptyRequestBody = allowEmpty || isOptional;
Expand Down Expand Up @@ -903,6 +901,19 @@ private static Expression BindParameterFromBody(ParameterInfo parameter, bool al
return Expression.Convert(BodyValueExpr, parameter.ParameterType);
}

private static bool IsOptionalParameter(ParameterInfo parameter)
{
// - Parameters representing value or reference types with a default value
// under any nullability context are treated as optional.
// - Value type parameters without a default value in an oblivious
// nullability context are required.
// - Reference type parameters without a default value in an oblivious
// nullability context are optional.
var nullability = NullabilityContext.Create(parameter);
return parameter.HasDefaultValue
|| nullability.ReadState != NullabilityState.NotNull;
}

private static MethodInfo GetMethodInfo<T>(Expression<T> expr)
{
var mc = (MethodCallExpression)expr.Body;
Expand Down
Loading