diff --git a/eng/scripts/CodeCheck.ps1 b/eng/scripts/CodeCheck.ps1
index c3e02ff55716..73691561e618 100644
--- a/eng/scripts/CodeCheck.ps1
+++ b/eng/scripts/CodeCheck.ps1
@@ -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)
}
}
diff --git a/src/Components/Components.slnf b/src/Components/Components.slnf
index 14878520e4d5..7bd92c90828f 100644
--- a/src/Components/Components.slnf
+++ b/src/Components/Components.slnf
@@ -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",
@@ -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",
@@ -96,6 +102,7 @@
"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",
@@ -103,6 +110,7 @@
"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",
@@ -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"
]
}
-}
+}
\ No newline at end of file
diff --git a/src/Components/Web/src/Forms/InputExtensions.cs b/src/Components/Web/src/Forms/InputExtensions.cs
index f0065f0611ff..32f8b46c838d 100644
--- a/src/Components/Web/src/Forms/InputExtensions.cs
+++ b/src/Components/Web/src/Forms/InputExtensions.cs
@@ -16,23 +16,61 @@ internal static class InputExtensions
{
try
{
- if (BindConverter.TryConvertTo(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(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(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(string? value, out TValue result)
+ {
+ if (string.IsNullOrEmpty(value))
+ {
+ result = default!;
+ return true;
+ }
+
+ return TryConvertToBool(value, out result);
+ }
}
}
diff --git a/src/Components/Web/src/Forms/InputSelect.cs b/src/Components/Web/src/Forms/InputSelect.cs
index 477c8d69326b..cc4ec970b571 100644
--- a/src/Components/Web/src/Forms/InputSelect.cs
+++ b/src/Components/Web/src/Forms/InputSelect.cs
@@ -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);
+ ///
+ 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(value, CultureInfo.CurrentCulture, out var result)
diff --git a/src/Components/Web/src/PublicAPI.Unshipped.txt b/src/Components/Web/src/PublicAPI.Unshipped.txt
index 3463104ba853..5f28157c5bb1 100644
--- a/src/Components/Web/src/PublicAPI.Unshipped.txt
+++ b/src/Components/Web/src/PublicAPI.Unshipped.txt
@@ -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.OnParametersSet() -> void
abstract Microsoft.AspNetCore.Components.RenderTree.WebRenderer.AttachRootComponentToBrowser(int componentId, string! domElementSelector) -> void
+override Microsoft.AspNetCore.Components.Forms.InputSelect.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
diff --git a/src/Components/test/E2ETest/Tests/FormsTest.cs b/src/Components/test/E2ETest/Tests/FormsTest.cs
index d1bbdf36f910..c95813707567 100644
--- a/src/Components/test/E2ETest/Tests/FormsTest.cs
+++ b/src/Components/test/E2ETest/Tests/FormsTest.cs
@@ -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()
{
@@ -521,6 +549,42 @@ public void InputRadioGroupsWithNamesNestedInteractWithEditContext()
IReadOnlyCollection 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 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()
{
diff --git a/src/Components/test/testassets/BasicTestApp/FormsTest/TypicalValidationComponent.razor b/src/Components/test/testassets/BasicTestApp/FormsTest/TypicalValidationComponent.razor
index 458759f9ab62..3fb9217a5fbd 100644
--- a/src/Components/test/testassets/BasicTestApp/FormsTest/TypicalValidationComponent.razor
+++ b/src/Components/test/testassets/BasicTestApp/FormsTest/TypicalValidationComponent.razor
@@ -70,6 +70,14 @@
@string.Join(", ", person.HostileStrings)
+
+ T/F: 77 + 33 = 100
+
+
+
+
+
+
Airline:
@@ -96,6 +104,13 @@
+
+ T/F: 7 * 3 = 21
+
+ true
+ false
+
+
Socks color:
@@ -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; }
diff --git a/src/Http/Http.Abstractions/src/Metadata/IAcceptsMetadata.cs b/src/Http/Http.Abstractions/src/Metadata/IAcceptsMetadata.cs
new file mode 100644
index 000000000000..a3325158a7ac
--- /dev/null
+++ b/src/Http/Http.Abstractions/src/Metadata/IAcceptsMetadata.cs
@@ -0,0 +1,25 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+
+using System.Collections.Generic;
+
+namespace Microsoft.AspNetCore.Http.Metadata
+{
+ ///
+ /// Interface for accepting request media types.
+ ///
+ public interface IAcceptsMetadata
+ {
+ ///
+ /// Gets a list of the allowed request content types.
+ /// If the incoming request does not have a Content-Type with one of these values, the request will be rejected with a 415 response.
+ ///
+ IReadOnlyList ContentTypes { get; }
+
+ ///
+ /// Gets the type being read from the request.
+ ///
+ Type? RequestType { get; }
+ }
+}
diff --git a/src/Http/Http.Abstractions/src/PublicAPI.Unshipped.txt b/src/Http/Http.Abstractions/src/PublicAPI.Unshipped.txt
index 9338ae940214..d689bce14e4b 100644
--- a/src/Http/Http.Abstractions/src/PublicAPI.Unshipped.txt
+++ b/src/Http/Http.Abstractions/src/PublicAPI.Unshipped.txt
@@ -7,6 +7,9 @@
*REMOVED*abstract Microsoft.AspNetCore.Http.HttpRequest.ContentType.get -> string!
Microsoft.AspNetCore.Http.IResult
Microsoft.AspNetCore.Http.IResult.ExecuteAsync(Microsoft.AspNetCore.Http.HttpContext! httpContext) -> System.Threading.Tasks.Task!
+Microsoft.AspNetCore.Http.Metadata.IAcceptsMetadata
+Microsoft.AspNetCore.Http.Metadata.IAcceptsMetadata.ContentTypes.get -> System.Collections.Generic.IReadOnlyList!
+Microsoft.AspNetCore.Http.Metadata.IAcceptsMetadata.RequestType.get -> System.Type?
Microsoft.AspNetCore.Http.Metadata.IFromBodyMetadata
Microsoft.AspNetCore.Http.Metadata.IFromBodyMetadata.AllowEmpty.get -> bool
Microsoft.AspNetCore.Http.Metadata.IFromHeaderMetadata
@@ -18,6 +21,10 @@ Microsoft.AspNetCore.Http.Metadata.IFromRouteMetadata.Name.get -> string?
Microsoft.AspNetCore.Http.Metadata.IFromServiceMetadata
Microsoft.AspNetCore.Http.Endpoint.Endpoint(Microsoft.AspNetCore.Http.RequestDelegate? requestDelegate, Microsoft.AspNetCore.Http.EndpointMetadataCollection? metadata, string? displayName) -> void
Microsoft.AspNetCore.Http.Endpoint.RequestDelegate.get -> Microsoft.AspNetCore.Http.RequestDelegate?
+Microsoft.AspNetCore.Http.RequestDelegateResult
+Microsoft.AspNetCore.Http.RequestDelegateResult.EndpointMetadata.get -> System.Collections.Generic.IReadOnlyList