From 5ad966e31103542883f297799a318b284ac10308 Mon Sep 17 00:00:00 2001 From: Steve Sanderson Date: Wed, 23 Aug 2023 16:50:59 +0100 Subject: [PATCH] In SSR, supply HttpContext as cascading value (#50253) Fixes https://github.com/dotnet/aspnetcore/issues/48769 ### Usage ```cs [CascadingParameter] public HttpContext? Context { get; set; } ``` In SSR, this will receive the `HttpContext`. In other rendering modes where there is no HTTP context, no value will be supplied so the property will remain `null`. ### Alternative design considered In #48769 I suggested using `[SupplyParameterFromHttpContext]` but that turns out not to be practical unless we either (a) make `ICascadingValueSupplier` public, or (b) add an IVT from `M.A.Components` to `.Endpoints`. I'm not keen on doing either of the above on a whim. Plus, use of `[CascadingValue]` has advantages: * It's consistent with the existing pattern for authentication state (we have `[CascadingParameter] Task AuthenticationStateTask { get; set; }`). * Longer term, if we add more things like this, it would be nice not to add a separate special attribute for each one when `[CascadingParameter]` is already descriptive enough. Special attributes are needed only when the type of thing being supplied might reasonably clash with something else the application is doing (for example, we do need it for form/query, as they supply arbitrary types). ## Review notes It's best to look at the two commits in this PR completely separately: 1. The first commit fixes an API design problem I discovered while considering how to do this. I realised that when we added `CascadingParameterAttributeBase`, we made a design mistake: * We put the `Name` property on the abstract base class just because `CascadingParameterAttribute`, `SupplyParameterFromQuery`, and `SupplyParameterFromForm` all have a `Name`. * However, in all three cases there, the `Name` has completely different meanings. For `CascadingParameterAttribute`, it's the name associated with ``, whereas for form it's the `Request.Form` entry or fall back on property name, and for query it's the `Request.Query` entry or fall back on property name. In general there's no reason why a `CascadingParameterAttributeBase` subclass should have a `Name` at all (`SupplyParameterFromHttpContext` wasn't going to), and if it does have one, its semantics are specific to it. So these should not be the same properties. * The change we made to make `CascadingParameterAttribute.Name` virtual might even be breaking (see https://learn.microsoft.com/en-us/dotnet/core/compatibility/library-change-rules stating *DISALLOWED: Adding the virtual keyword to a member*). So it's good we can revert that here. 2. The second commit is the completely trivial implementation of supplying `HttpContext` as a cascading value, with an E2E test. --- .../src/CascadingParameterAttribute.cs | 2 +- .../src/CascadingParameterAttributeBase.cs | 6 -- ...scadingValueServiceCollectionExtensions.cs | 56 +++++++++++++++++++ .../Components/src/PublicAPI.Unshipped.txt | 13 +---- .../src/Reflection/ComponentProperties.cs | 3 +- .../src/SupplyParameterFromQueryAttribute.cs | 2 +- ...ueryProviderServiceCollectionExtensions.cs | 3 +- .../test/CascadingParameterStateTest.cs | 18 ------ .../Components/test/CascadingParameterTest.cs | 56 +++++++++++++++++-- .../test/ParameterViewTest.Assignment.cs | 2 +- ...orComponentsServiceCollectionExtensions.cs | 1 + .../src/Rendering/EndpointHtmlRenderer.cs | 2 + ...mponentsServiceCollectionExtensionsTest.cs | 35 +++++++----- .../SupplyParameterFromFormValueProvider.cs | 5 +- .../Web/src/PublicAPI.Unshipped.txt | 4 +- .../src/SupplyParameterFromFormAttribute.cs | 6 +- .../ServerRenderingTests/RenderingTest.cs | 15 +++++ .../Pages/AccessHttpContext.razor | 20 +++++++ .../MvcServiceCollectionExtensionsTest.cs | 46 +++++++++++++-- 19 files changed, 220 insertions(+), 75 deletions(-) create mode 100644 src/Components/test/testassets/Components.TestServer/RazorComponents/Pages/AccessHttpContext.razor diff --git a/src/Components/Components/src/CascadingParameterAttribute.cs b/src/Components/Components/src/CascadingParameterAttribute.cs index bb9be43a5b08..becc2ce1cb57 100644 --- a/src/Components/Components/src/CascadingParameterAttribute.cs +++ b/src/Components/Components/src/CascadingParameterAttribute.cs @@ -20,5 +20,5 @@ public sealed class CascadingParameterAttribute : CascadingParameterAttributeBas /// that supplies a value with a compatible /// type. /// - public override string? Name { get; set; } + public string? Name { get; set; } } diff --git a/src/Components/Components/src/CascadingParameterAttributeBase.cs b/src/Components/Components/src/CascadingParameterAttributeBase.cs index 307743c890cb..47a50edce641 100644 --- a/src/Components/Components/src/CascadingParameterAttributeBase.cs +++ b/src/Components/Components/src/CascadingParameterAttributeBase.cs @@ -8,12 +8,6 @@ namespace Microsoft.AspNetCore.Components; /// public abstract class CascadingParameterAttributeBase : Attribute { - /// - /// Gets or sets the name for the parameter, which correlates to the name - /// of a cascading value. - /// - public abstract string? Name { get; set; } - /// /// Gets a flag indicating whether the cascading parameter should /// be supplied only once per component. diff --git a/src/Components/Components/src/CascadingValueServiceCollectionExtensions.cs b/src/Components/Components/src/CascadingValueServiceCollectionExtensions.cs index 07e0ae985b58..bcdfbc4f35e9 100644 --- a/src/Components/Components/src/CascadingValueServiceCollectionExtensions.cs +++ b/src/Components/Components/src/CascadingValueServiceCollectionExtensions.cs @@ -2,6 +2,7 @@ // The .NET Foundation licenses this file to you under the MIT license. using Microsoft.AspNetCore.Components; +using Microsoft.Extensions.DependencyInjection.Extensions; namespace Microsoft.Extensions.DependencyInjection; @@ -50,4 +51,59 @@ public static IServiceCollection AddCascadingValue( public static IServiceCollection AddCascadingValue( this IServiceCollection serviceCollection, Func> sourceFactory) => serviceCollection.AddScoped(sourceFactory); + + /// + /// Adds a cascading value to the , if none is already registered + /// with the value type. This is equivalent to having a fixed at + /// the root of the component hierarchy. + /// + /// The value type. + /// The . + /// A callback that supplies a fixed value within each service provider scope. + /// The . + public static void TryAddCascadingValue( + this IServiceCollection serviceCollection, Func valueFactory) + { + serviceCollection.TryAddEnumerable( + ServiceDescriptor.Scoped>( + sp => new CascadingValueSource(() => valueFactory(sp), isFixed: true))); + } + + /// + /// Adds a cascading value to the , if none is already registered + /// with the value type, regardless of the . This is equivalent to having a fixed + /// at the root of the component hierarchy. + /// + /// The value type. + /// The . + /// A name for the cascading value. If set, can be configured to match based on this name. + /// A callback that supplies a fixed value within each service provider scope. + /// The . + public static void TryAddCascadingValue( + this IServiceCollection serviceCollection, string name, Func valueFactory) + { + serviceCollection.TryAddEnumerable( + ServiceDescriptor.Scoped>( + sp => new CascadingValueSource(name, () => valueFactory(sp), isFixed: true))); + } + + /// + /// Adds a cascading value to the , if none is already registered + /// with the value type. This is equivalent to having a fixed at + /// the root of the component hierarchy. + /// + /// With this overload, you can supply a which allows you + /// to notify about updates to the value later, causing recipients to re-render. This overload should + /// only be used if you plan to update the value dynamically. + /// + /// The value type. + /// The . + /// A callback that supplies a within each service provider scope. + /// The . + public static void TryAddCascadingValue( + this IServiceCollection serviceCollection, Func> sourceFactory) + { + serviceCollection.TryAddEnumerable( + ServiceDescriptor.Scoped>(sourceFactory)); + } } diff --git a/src/Components/Components/src/PublicAPI.Unshipped.txt b/src/Components/Components/src/PublicAPI.Unshipped.txt index f892dc2cbd74..0e534c7e557e 100644 --- a/src/Components/Components/src/PublicAPI.Unshipped.txt +++ b/src/Components/Components/src/PublicAPI.Unshipped.txt @@ -1,6 +1,4 @@ #nullable enable -abstract Microsoft.AspNetCore.Components.CascadingParameterAttributeBase.Name.get -> string? -abstract Microsoft.AspNetCore.Components.CascadingParameterAttributeBase.Name.set -> void abstract Microsoft.AspNetCore.Components.RenderModeAttribute.Mode.get -> Microsoft.AspNetCore.Components.IComponentRenderMode! Microsoft.AspNetCore.Components.CascadingParameterAttributeBase Microsoft.AspNetCore.Components.CascadingParameterAttributeBase.CascadingParameterAttributeBase() -> void @@ -83,24 +81,19 @@ Microsoft.AspNetCore.Components.Rendering.RenderTreeBuilder.AddComponentParamete Microsoft.AspNetCore.Components.StreamRenderingAttribute Microsoft.AspNetCore.Components.StreamRenderingAttribute.Enabled.get -> bool Microsoft.AspNetCore.Components.StreamRenderingAttribute.StreamRenderingAttribute(bool enabled) -> void -*REMOVED*Microsoft.AspNetCore.Components.CascadingParameterAttribute.Name.get -> string? -*REMOVED*Microsoft.AspNetCore.Components.CascadingParameterAttribute.Name.set -> void Microsoft.AspNetCore.Components.SupplyParameterFromQueryProviderServiceCollectionExtensions Microsoft.Extensions.DependencyInjection.CascadingValueServiceCollectionExtensions -override Microsoft.AspNetCore.Components.CascadingParameterAttribute.Name.get -> string? -override Microsoft.AspNetCore.Components.CascadingParameterAttribute.Name.set -> void override Microsoft.AspNetCore.Components.EventCallback.GetHashCode() -> int override Microsoft.AspNetCore.Components.EventCallback.Equals(object? obj) -> bool override Microsoft.AspNetCore.Components.EventCallback.GetHashCode() -> int override Microsoft.AspNetCore.Components.EventCallback.Equals(object? obj) -> bool -*REMOVED*Microsoft.AspNetCore.Components.SupplyParameterFromQueryAttribute.Name.get -> string? -*REMOVED*Microsoft.AspNetCore.Components.SupplyParameterFromQueryAttribute.Name.set -> void -override Microsoft.AspNetCore.Components.SupplyParameterFromQueryAttribute.Name.get -> string? -override Microsoft.AspNetCore.Components.SupplyParameterFromQueryAttribute.Name.set -> void static Microsoft.AspNetCore.Components.SupplyParameterFromQueryProviderServiceCollectionExtensions.AddSupplyValueFromQueryProvider(this Microsoft.Extensions.DependencyInjection.IServiceCollection! services) -> Microsoft.Extensions.DependencyInjection.IServiceCollection! static Microsoft.Extensions.DependencyInjection.CascadingValueServiceCollectionExtensions.AddCascadingValue(this Microsoft.Extensions.DependencyInjection.IServiceCollection! serviceCollection, string! name, System.Func! valueFactory) -> Microsoft.Extensions.DependencyInjection.IServiceCollection! static Microsoft.Extensions.DependencyInjection.CascadingValueServiceCollectionExtensions.AddCascadingValue(this Microsoft.Extensions.DependencyInjection.IServiceCollection! serviceCollection, System.Func!>! sourceFactory) -> Microsoft.Extensions.DependencyInjection.IServiceCollection! static Microsoft.Extensions.DependencyInjection.CascadingValueServiceCollectionExtensions.AddCascadingValue(this Microsoft.Extensions.DependencyInjection.IServiceCollection! serviceCollection, System.Func! valueFactory) -> Microsoft.Extensions.DependencyInjection.IServiceCollection! +static Microsoft.Extensions.DependencyInjection.CascadingValueServiceCollectionExtensions.TryAddCascadingValue(this Microsoft.Extensions.DependencyInjection.IServiceCollection! serviceCollection, string! name, System.Func! valueFactory) -> void +static Microsoft.Extensions.DependencyInjection.CascadingValueServiceCollectionExtensions.TryAddCascadingValue(this Microsoft.Extensions.DependencyInjection.IServiceCollection! serviceCollection, System.Func!>! sourceFactory) -> void +static Microsoft.Extensions.DependencyInjection.CascadingValueServiceCollectionExtensions.TryAddCascadingValue(this Microsoft.Extensions.DependencyInjection.IServiceCollection! serviceCollection, System.Func! valueFactory) -> void virtual Microsoft.AspNetCore.Components.NavigationManager.Refresh(bool forceReload = false) -> void virtual Microsoft.AspNetCore.Components.Rendering.ComponentState.DisposeAsync() -> System.Threading.Tasks.ValueTask virtual Microsoft.AspNetCore.Components.RenderTree.Renderer.AddPendingTask(Microsoft.AspNetCore.Components.Rendering.ComponentState? componentState, System.Threading.Tasks.Task! task) -> void diff --git a/src/Components/Components/src/Reflection/ComponentProperties.cs b/src/Components/Components/src/Reflection/ComponentProperties.cs index d49cfad7978a..9a0b3cbdfdb4 100644 --- a/src/Components/Components/src/Reflection/ComponentProperties.cs +++ b/src/Components/Components/src/Reflection/ComponentProperties.cs @@ -184,8 +184,7 @@ private static void ThrowForUnknownIncomingParameterName([DynamicallyAccessedMem { throw new InvalidOperationException( $"Object of type '{targetType.FullName}' has a property matching the name '{parameterName}', " + - $"but it does not have [{nameof(ParameterAttribute)}], [{nameof(CascadingParameterAttribute)}] or " + - $"[SupplyParameterFromFormAttribute] applied."); + $"but it does not have [Parameter], [CascadingParameter], or any other parameter-supplying attribute."); } else { diff --git a/src/Components/Components/src/SupplyParameterFromQueryAttribute.cs b/src/Components/Components/src/SupplyParameterFromQueryAttribute.cs index ffae75576ff7..9177a8d652c2 100644 --- a/src/Components/Components/src/SupplyParameterFromQueryAttribute.cs +++ b/src/Components/Components/src/SupplyParameterFromQueryAttribute.cs @@ -14,5 +14,5 @@ public sealed class SupplyParameterFromQueryAttribute : CascadingParameterAttrib /// Gets or sets the name of the querystring parameter. If null, the querystring /// parameter is assumed to have the same name as the associated property. /// - public override string? Name { get; set; } + public string? Name { get; set; } } diff --git a/src/Components/Components/src/SupplyParameterFromQueryProviderServiceCollectionExtensions.cs b/src/Components/Components/src/SupplyParameterFromQueryProviderServiceCollectionExtensions.cs index 3de3ab99ee9a..21cb8c98db19 100644 --- a/src/Components/Components/src/SupplyParameterFromQueryProviderServiceCollectionExtensions.cs +++ b/src/Components/Components/src/SupplyParameterFromQueryProviderServiceCollectionExtensions.cs @@ -50,7 +50,8 @@ public bool CanSupplyValue(in CascadingParameterInfo parameterInfo) UpdateQueryParameters(); } - var queryParameterName = parameterInfo.Attribute.Name ?? parameterInfo.PropertyName; + var attribute = (SupplyParameterFromQueryAttribute)parameterInfo.Attribute; // Must be a valid cast because we check in CanSupplyValue + var queryParameterName = attribute.Name ?? parameterInfo.PropertyName; return _queryParameterValueSupplier.GetQueryParameterValue(parameterInfo.PropertyType, queryParameterName); } diff --git a/src/Components/Components/test/CascadingParameterStateTest.cs b/src/Components/Components/test/CascadingParameterStateTest.cs index ed6420fffb25..e055b9c70801 100644 --- a/src/Components/Components/test/CascadingParameterStateTest.cs +++ b/src/Components/Components/test/CascadingParameterStateTest.cs @@ -476,8 +476,6 @@ class ComponentWithNamedCascadingParam : TestComponentBase class SupplyParameterWithSingleDeliveryAttribute : CascadingParameterAttributeBase { - public override string Name { get; set; } - internal override bool SingleDelivery => true; } @@ -523,19 +521,3 @@ public TestNavigationManager() } } } - -[AttributeUsage(AttributeTargets.Property, AllowMultiple = false, Inherited = true)] -public sealed class SupplyParameterFromFormAttribute : CascadingParameterAttributeBase -{ - /// - /// Gets or sets the name for the parameter. The name is used to match - /// the form data and decide whether or not the value needs to be bound. - /// - public override string Name { get; set; } - - /// - /// Gets or sets the name for the handler. The name is used to match - /// the form data and decide whether or not the value needs to be bound. - /// - public string Handler { get; set; } -} diff --git a/src/Components/Components/test/CascadingParameterTest.cs b/src/Components/Components/test/CascadingParameterTest.cs index 9ce74e19708b..a99baeb96833 100644 --- a/src/Components/Components/test/CascadingParameterTest.cs +++ b/src/Components/Components/test/CascadingParameterTest.cs @@ -727,6 +727,51 @@ public void OmitsSingleDeliveryCascadingParametersWhenUpdatingDirectParameters() }); } + [Fact] + public void CanUseTryAddPatternForCascadingValuesInServiceCollection_ValueFactory() + { + // Arrange + var services = new ServiceCollection(); + + // Act + services.TryAddCascadingValue(_ => new Type1()); + services.TryAddCascadingValue(_ => new Type1()); + services.TryAddCascadingValue(_ => new Type2()); + + // Assert + Assert.Equal(2, services.Count()); + } + + [Fact] + public void CanUseTryAddPatternForCascadingValuesInServiceCollection_NamedValueFactory() + { + // Arrange + var services = new ServiceCollection(); + + // Act + services.TryAddCascadingValue("Name1", _ => new Type1()); + services.TryAddCascadingValue("Name2", _ => new Type1()); + services.TryAddCascadingValue("Name3", _ => new Type2()); + + // Assert + Assert.Equal(2, services.Count()); + } + + [Fact] + public void CanUseTryAddPatternForCascadingValuesInServiceCollection_CascadingValueSource() + { + // Arrange + var services = new ServiceCollection(); + + // Act + services.TryAddCascadingValue(_ => new CascadingValueSource("Name1", new Type1(), false)); + services.TryAddCascadingValue(_ => new CascadingValueSource("Name2", new Type1(), false)); + services.TryAddCascadingValue(_ => new CascadingValueSource("Name3", new Type2(), false)); + + // Assert + Assert.Equal(2, services.Count()); + } + private class SingleDeliveryValue(string text) { public string Text => text; @@ -734,8 +779,6 @@ private class SingleDeliveryValue(string text) private class SingleDeliveryCascadingParameterAttribute : CascadingParameterAttributeBase { - public override string Name { get; set; } - internal override bool SingleDelivery => true; } @@ -852,13 +895,11 @@ class SecondCascadingParameterConsumerComponent : CascadingParameterCons [AttributeUsage(AttributeTargets.Property, AllowMultiple = false, Inherited = true)] class CustomCascadingParameter1Attribute : CascadingParameterAttributeBase { - public override string Name { get; set; } } [AttributeUsage(AttributeTargets.Property, AllowMultiple = false, Inherited = true)] class CustomCascadingParameter2Attribute : CascadingParameterAttributeBase { - public override string Name { get; set; } } class CustomCascadingValueProducer : AutoRenderComponent, ICascadingValueSupplier @@ -904,7 +945,7 @@ void ICascadingValueSupplier.Unsubscribe(ComponentState subscriber, in Cascading class CustomCascadingValueConsumer1 : AutoRenderComponent { - [CustomCascadingParameter1(Name = nameof(Value))] + [CustomCascadingParameter1] public object Value { get; set; } protected override void BuildRenderTree(RenderTreeBuilder builder) @@ -915,7 +956,7 @@ protected override void BuildRenderTree(RenderTreeBuilder builder) class CustomCascadingValueConsumer2 : AutoRenderComponent { - [CustomCascadingParameter2(Name = nameof(Value))] + [CustomCascadingParameter2] public object Value { get; set; } protected override void BuildRenderTree(RenderTreeBuilder builder) @@ -944,4 +985,7 @@ public void ChangeValue(string newValue) StringValue = newValue; } } + + class Type1 { } + class Type2 { } } diff --git a/src/Components/Components/test/ParameterViewTest.Assignment.cs b/src/Components/Components/test/ParameterViewTest.Assignment.cs index 262f9584c4e4..f0308c0182a0 100644 --- a/src/Components/Components/test/ParameterViewTest.Assignment.cs +++ b/src/Components/Components/test/ParameterViewTest.Assignment.cs @@ -183,7 +183,7 @@ public void IncomingParameterMatchesPropertyNotDeclaredAsParameter_Throws() Assert.Equal(default, target.IntProp); Assert.Equal( $"Object of type '{typeof(HasPropertyWithoutParameterAttribute).FullName}' has a property matching the name '{nameof(HasPropertyWithoutParameterAttribute.IntProp)}', " + - $"but it does not have [{nameof(ParameterAttribute)}], [{nameof(CascadingParameterAttribute)}] or [{nameof(SupplyParameterFromFormAttribute)}] applied.", + "but it does not have [Parameter], [CascadingParameter], or any other parameter-supplying attribute.", ex.Message); } diff --git a/src/Components/Endpoints/src/DependencyInjection/RazorComponentsServiceCollectionExtensions.cs b/src/Components/Endpoints/src/DependencyInjection/RazorComponentsServiceCollectionExtensions.cs index 3b143bb6cff2..9fa6c8c27890 100644 --- a/src/Components/Endpoints/src/DependencyInjection/RazorComponentsServiceCollectionExtensions.cs +++ b/src/Components/Endpoints/src/DependencyInjection/RazorComponentsServiceCollectionExtensions.cs @@ -63,6 +63,7 @@ public static IRazorComponentsBuilder AddRazorComponents(this IServiceCollection services.TryAddScoped(); services.TryAddScoped(sp => sp.GetRequiredService()); services.AddSupplyValueFromQueryProvider(); + services.TryAddCascadingValue(sp => sp.GetRequiredService().HttpContext); // Form handling services.AddSupplyValueFromFormProvider(); diff --git a/src/Components/Endpoints/src/Rendering/EndpointHtmlRenderer.cs b/src/Components/Endpoints/src/Rendering/EndpointHtmlRenderer.cs index 1118dfeb5825..0c46325988a1 100644 --- a/src/Components/Endpoints/src/Rendering/EndpointHtmlRenderer.cs +++ b/src/Components/Endpoints/src/Rendering/EndpointHtmlRenderer.cs @@ -52,6 +52,8 @@ public EndpointHtmlRenderer(IServiceProvider serviceProvider, ILoggerFactory log _services = serviceProvider; } + internal HttpContext? HttpContext => _httpContext; + private void SetHttpContext(HttpContext httpContext) { if (_httpContext is null) diff --git a/src/Components/Endpoints/test/RazorComponentsServiceCollectionExtensionsTest.cs b/src/Components/Endpoints/test/RazorComponentsServiceCollectionExtensionsTest.cs index 819e6b8b4294..74aa7caad748 100644 --- a/src/Components/Endpoints/test/RazorComponentsServiceCollectionExtensionsTest.cs +++ b/src/Components/Endpoints/test/RazorComponentsServiceCollectionExtensionsTest.cs @@ -1,6 +1,7 @@ // 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 Microsoft.AspNetCore.Components; using Microsoft.AspNetCore.Components.Forms.Mapping; @@ -28,8 +29,8 @@ public void AddRazorComponents_RegistersServices() } else { - // 'multi-registration' services should only have one *instance* of each implementation registered. - AssertContainsSingle(services, service.ServiceType, service.ImplementationType); + // 'multi-registration' services should not have any duplicate implementation types + AssertAllImplementationTypesAreDistinct(services, service.ServiceType); } } } @@ -55,8 +56,8 @@ public void AddRazorComponentsTwice_DoesNotDuplicateServices() } else { - // 'multi-registration' services should only have one *instance* of each implementation registered. - AssertContainsSingle(services, service.ServiceType, service.ImplementationType); + // 'multi-registration' services should not have any duplicate implementation types + AssertAllImplementationTypesAreDistinct(services, service.ServiceType); } } } @@ -104,28 +105,32 @@ private void AssertServiceCountEquals( $" time(s) but was actually registered {actual} time(s)."); } - private void AssertContainsSingle( + private void AssertAllImplementationTypesAreDistinct( IServiceCollection services, - Type serviceType, - Type implementationType) + Type serviceType) { - var matches = services - .Where(sd => - sd.ServiceType == serviceType && - sd.ImplementationType == implementationType) + var serviceProvider = services.BuildServiceProvider(); + var implementationTypes = services + .Where(sd => sd.ServiceType == serviceType) + .Select(service => service switch + { + { ImplementationType: { } type } => type, + { ImplementationInstance: { } instance } => instance.GetType(), + { ImplementationFactory: { } factory } => factory(serviceProvider).GetType(), + }) .ToArray(); - if (matches.Length == 0) + if (implementationTypes.Length == 0) { Assert.True( false, - $"Could not find an instance of {implementationType} registered as {serviceType}"); + $"Could not find an implementation type for {serviceType}"); } - else if (matches.Length > 1) + else if (implementationTypes.Length != implementationTypes.Distinct().Count()) { Assert.True( false, - $"Found multiple instances of {implementationType} registered as {serviceType}"); + $"Found duplicate implementation types for {serviceType}. Implementation types: {string.Join(", ", implementationTypes.Select(x => x.ToString()))}"); } } } diff --git a/src/Components/Web/src/Forms/Mapping/SupplyParameterFromFormValueProvider.cs b/src/Components/Web/src/Forms/Mapping/SupplyParameterFromFormValueProvider.cs index 010f2a49f1b0..123b14622680 100644 --- a/src/Components/Web/src/Forms/Mapping/SupplyParameterFromFormValueProvider.cs +++ b/src/Components/Web/src/Forms/Mapping/SupplyParameterFromFormValueProvider.cs @@ -73,8 +73,9 @@ void ICascadingValueSupplier.Unsubscribe(ComponentState subscriber, in Cascading { Debug.Assert(mappingContext != null); - var parameterName = parameterInfo.Attribute.Name ?? parameterInfo.PropertyName; - var restrictToFormName = ((SupplyParameterFromFormAttribute)parameterInfo.Attribute).Handler; + var attribute = (SupplyParameterFromFormAttribute)parameterInfo.Attribute; // Must be a valid cast because we check in CanSupplyValue + var parameterName = attribute.Name ?? parameterInfo.PropertyName; + var restrictToFormName = attribute.Handler; Action errorHandler = string.IsNullOrEmpty(restrictToFormName) ? mappingContext.AddError : (name, message, value) => mappingContext.AddError(restrictToFormName, parameterName, message, value); diff --git a/src/Components/Web/src/PublicAPI.Unshipped.txt b/src/Components/Web/src/PublicAPI.Unshipped.txt index 341bc7e08f50..51a388b43767 100644 --- a/src/Components/Web/src/PublicAPI.Unshipped.txt +++ b/src/Components/Web/src/PublicAPI.Unshipped.txt @@ -65,6 +65,8 @@ Microsoft.AspNetCore.Components.HtmlRendering.Infrastructure.StaticHtmlRenderer. Microsoft.AspNetCore.Components.SupplyParameterFromFormAttribute Microsoft.AspNetCore.Components.SupplyParameterFromFormAttribute.Handler.get -> string? Microsoft.AspNetCore.Components.SupplyParameterFromFormAttribute.Handler.set -> void +Microsoft.AspNetCore.Components.SupplyParameterFromFormAttribute.Name.get -> string? +Microsoft.AspNetCore.Components.SupplyParameterFromFormAttribute.Name.set -> void Microsoft.AspNetCore.Components.SupplyParameterFromFormAttribute.SupplyParameterFromFormAttribute() -> void Microsoft.AspNetCore.Components.Web.AutoRenderMode Microsoft.AspNetCore.Components.Web.AutoRenderMode.AutoRenderMode() -> void @@ -111,8 +113,6 @@ override Microsoft.AspNetCore.Components.Forms.Editor.OnParametersSet() -> vo override Microsoft.AspNetCore.Components.HtmlRendering.Infrastructure.StaticHtmlRenderer.Dispatcher.get -> Microsoft.AspNetCore.Components.Dispatcher! override Microsoft.AspNetCore.Components.HtmlRendering.Infrastructure.StaticHtmlRenderer.HandleException(System.Exception! exception) -> void override Microsoft.AspNetCore.Components.HtmlRendering.Infrastructure.StaticHtmlRenderer.UpdateDisplayAsync(in Microsoft.AspNetCore.Components.RenderTree.RenderBatch renderBatch) -> System.Threading.Tasks.Task! -override Microsoft.AspNetCore.Components.SupplyParameterFromFormAttribute.Name.get -> string? -override Microsoft.AspNetCore.Components.SupplyParameterFromFormAttribute.Name.set -> void override Microsoft.AspNetCore.Components.Web.RenderModeAutoAttribute.Mode.get -> Microsoft.AspNetCore.Components.IComponentRenderMode! override Microsoft.AspNetCore.Components.Web.RenderModeServerAttribute.Mode.get -> Microsoft.AspNetCore.Components.IComponentRenderMode! override Microsoft.AspNetCore.Components.Web.RenderModeWebAssemblyAttribute.Mode.get -> Microsoft.AspNetCore.Components.IComponentRenderMode! diff --git a/src/Components/Web/src/SupplyParameterFromFormAttribute.cs b/src/Components/Web/src/SupplyParameterFromFormAttribute.cs index 94820b9d0957..8aba004415b5 100644 --- a/src/Components/Web/src/SupplyParameterFromFormAttribute.cs +++ b/src/Components/Web/src/SupplyParameterFromFormAttribute.cs @@ -11,11 +11,9 @@ namespace Microsoft.AspNetCore.Components; public sealed class SupplyParameterFromFormAttribute : CascadingParameterAttributeBase { /// - /// Gets or sets the name for the parameter. The name is used to determine - /// the prefix to use to match the form data and decide whether or not the - /// value needs to be bound. + /// Gets or sets the name for the form value. If not specified, the property name will be used. /// - public override string? Name { get; set; } + public string? Name { get; set; } /// /// Gets or sets the name for the handler. The name is used to match diff --git a/src/Components/test/E2ETest/ServerRenderingTests/RenderingTest.cs b/src/Components/test/E2ETest/ServerRenderingTests/RenderingTest.cs index ce616f81d0ee..21c3e01d4712 100644 --- a/src/Components/test/E2ETest/ServerRenderingTests/RenderingTest.cs +++ b/src/Components/test/E2ETest/ServerRenderingTests/RenderingTest.cs @@ -1,6 +1,8 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. +using System.Net; +using System.Net.Http; using Components.TestServer.RazorComponents; using Microsoft.AspNetCore.Components.E2ETest.Infrastructure; using Microsoft.AspNetCore.Components.E2ETest.Infrastructure.ServerFixtures; @@ -25,6 +27,7 @@ public RenderingTest( public override Task InitializeAsync() => InitializeAsync(BrowserFixture.StreamingContext); + [Fact] [QuarantinedTest("https://github.com/dotnet/aspnetcore/issues/49975")] public void CanRenderLargeComponentsWithServerRenderMode() { @@ -35,4 +38,16 @@ public void CanRenderLargeComponentsWithServerRenderMode() Assert.Equal(result, Browser.FindElement(By.Id("server-prerender")).Text); Assert.Equal(result, Browser.FindElement(By.Id("server-prerender")).Text); } + + [Fact] + public async Task CanUseHttpContextRequestAndResponse() + { + Navigate($"{ServerPathBase}/httpcontext"); + Browser.Equal("GET", () => Browser.FindElement(By.Id("request-method")).Text); + Browser.Equal("/httpcontext", () => Browser.FindElement(By.Id("request-path")).Text); + + // We can't see the response status code using Selenium, so make a direct request + var response = await new HttpClient().GetAsync(Browser.Url); + Assert.Equal(HttpStatusCode.Created, response.StatusCode); + } } diff --git a/src/Components/test/testassets/Components.TestServer/RazorComponents/Pages/AccessHttpContext.razor b/src/Components/test/testassets/Components.TestServer/RazorComponents/Pages/AccessHttpContext.razor new file mode 100644 index 000000000000..96c89cfd0da4 --- /dev/null +++ b/src/Components/test/testassets/Components.TestServer/RazorComponents/Pages/AccessHttpContext.razor @@ -0,0 +1,20 @@ +@page "/httpcontext" + +

HttpContext

+ +

+ Request method: @Ctx.Request.Method +

+

+ Request path: @Ctx.Request.Path +

+ +@code { + [CascadingParameter] public HttpContext Ctx { get; set; } + + protected override void OnInitialized() + { + // Show we can change the response status code + Ctx.Response.StatusCode = 201; + } +} diff --git a/src/Mvc/Mvc/test/MvcServiceCollectionExtensionsTest.cs b/src/Mvc/Mvc/test/MvcServiceCollectionExtensionsTest.cs index 78e32dbaebff..3520e30a01ee 100644 --- a/src/Mvc/Mvc/test/MvcServiceCollectionExtensionsTest.cs +++ b/src/Mvc/Mvc/test/MvcServiceCollectionExtensionsTest.cs @@ -162,6 +162,7 @@ public void AddMvc_Twice_DoesNotAddDuplicates() { // Arrange var services = new ServiceCollection(); + services.AddLogging(); services.AddSingleton(GetHostingEnvironment()); // Act @@ -177,6 +178,7 @@ public void AddControllersAddRazorPages_Twice_DoesNotAddDuplicates() { // Arrange var services = new ServiceCollection(); + services.AddLogging(); services.AddSingleton(GetHostingEnvironment()); // Act @@ -194,6 +196,7 @@ public void AddControllersWithViews_Twice_DoesNotAddDuplicates() { // Arrange var services = new ServiceCollection(); + services.AddLogging(); services.AddSingleton(GetHostingEnvironment()); // Act @@ -209,6 +212,7 @@ public void AddRazorPages_Twice_DoesNotAddDuplicates() { // Arrange var services = new ServiceCollection(); + services.AddLogging(); services.AddSingleton(GetHostingEnvironment()); // Act @@ -251,6 +255,7 @@ public void AddControllersWithViews_AddsDocumentedServices() private void VerifyAllServices(IServiceCollection services) { var singleRegistrationServiceTypes = SingleRegistrationServiceTypes; + var serviceProvider = services.BuildServiceProvider(); foreach (var service in services) { if (singleRegistrationServiceTypes.Contains(service.ServiceType)) @@ -258,14 +263,24 @@ private void VerifyAllServices(IServiceCollection services) // 'single-registration' services should only have one implementation registered. AssertServiceCountEquals(services, service.ServiceType, 1); } - else if (service.ImplementationType != null && !service.ImplementationType.Assembly.FullName.Contains("Mvc")) - { - // Ignore types that don't come from MVC - } else { - // 'multi-registration' services should only have one *instance* of each implementation registered. - AssertContainsSingle(services, service.ServiceType, service.ImplementationType); + var implementationType = service switch + { + { ImplementationType: { } type } => type, + { ImplementationInstance: { } instance } => instance.GetType(), + { ImplementationFactory: { } factory } => factory(serviceProvider).GetType(), + }; + + if (implementationType != null && !implementationType.Assembly.FullName.Contains("Mvc")) + { + // Ignore types that don't come from MVC + } + else + { + // 'multi-registration' services should only have one *instance* of each implementation registered. + AssertContainsSingle(services, service.ServiceType, service.ImplementationType); + } } } } @@ -625,6 +640,25 @@ private void AssertContainsSingle( } else if (matches.Length > 1) { + var implementations = new List(); + var sp = services.BuildServiceProvider(); + foreach ( var service in matches ) + { + if (service.ImplementationType is not null) + { + implementations.Add(service.ImplementationType); + } + else if (service.ImplementationInstance is not null) + { + implementations.Add(service.ImplementationInstance.GetType()); + } + else if (service.ImplementationFactory is not null) + { + var instance = service.ImplementationFactory(sp); + implementations.Add(instance.GetType()); + } + } + Assert.True( false, $"Found multiple instances of {implementationType} registered as {serviceType}");