From a8acc607f503b5068d1d702c921208ed5198f45f Mon Sep 17 00:00:00 2001 From: Steve Sanderson Date: Tue, 22 Aug 2023 09:29:50 +0100 Subject: [PATCH 1/5] Move CascadingParameterAttributeBase.Name into subclasses. It shouldn't have been in the base because the meaning varies. --- .../src/CascadingParameterAttribute.cs | 2 +- .../src/CascadingParameterAttributeBase.cs | 6 ------ .../Components/src/PublicAPI.Unshipped.txt | 10 ---------- .../src/Reflection/ComponentProperties.cs | 3 +-- .../src/SupplyParameterFromQueryAttribute.cs | 2 +- ...QueryProviderServiceCollectionExtensions.cs | 3 ++- .../test/CascadingParameterStateTest.cs | 18 ------------------ .../Components/test/CascadingParameterTest.cs | 8 ++------ .../test/ParameterViewTest.Assignment.cs | 2 +- .../SupplyParameterFromFormValueProvider.cs | 5 +++-- src/Components/Web/src/PublicAPI.Unshipped.txt | 4 ++-- .../src/SupplyParameterFromFormAttribute.cs | 6 ++---- 12 files changed, 15 insertions(+), 54 deletions(-) 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/PublicAPI.Unshipped.txt b/src/Components/Components/src/PublicAPI.Unshipped.txt index f892dc2cbd74..b18f7d3addca 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,20 +81,12 @@ 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! 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..9c3dfb928ba0 100644 --- a/src/Components/Components/test/CascadingParameterTest.cs +++ b/src/Components/Components/test/CascadingParameterTest.cs @@ -734,8 +734,6 @@ private class SingleDeliveryValue(string text) private class SingleDeliveryCascadingParameterAttribute : CascadingParameterAttributeBase { - public override string Name { get; set; } - internal override bool SingleDelivery => true; } @@ -852,13 +850,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 +900,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 +911,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) 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/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 From a27e4c4885faede3e3fb1cb94038d2e0e522d797 Mon Sep 17 00:00:00 2001 From: Steve Sanderson Date: Tue, 22 Aug 2023 10:22:32 +0100 Subject: [PATCH 2/5] Supply HttpContext as cascading value --- ...orComponentsServiceCollectionExtensions.cs | 1 + .../src/Rendering/EndpointHtmlRenderer.cs | 2 ++ .../ServerRenderingTests/RenderingTest.cs | 14 +++++++++++++ .../Pages/AccessHttpContext.razor | 20 +++++++++++++++++++ 4 files changed, 37 insertions(+) create mode 100644 src/Components/test/testassets/Components.TestServer/RazorComponents/Pages/AccessHttpContext.razor diff --git a/src/Components/Endpoints/src/DependencyInjection/RazorComponentsServiceCollectionExtensions.cs b/src/Components/Endpoints/src/DependencyInjection/RazorComponentsServiceCollectionExtensions.cs index 3b143bb6cff2..e64dbf40a836 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.AddCascadingValue(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/test/E2ETest/ServerRenderingTests/RenderingTest.cs b/src/Components/test/E2ETest/ServerRenderingTests/RenderingTest.cs index ce616f81d0ee..1739b5b3069d 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; @@ -35,4 +37,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; + } +} From 44f8b9d0ea896bf4766e3bb129c2994f673cf455 Mon Sep 17 00:00:00 2001 From: Steve Sanderson Date: Tue, 22 Aug 2023 13:15:15 +0100 Subject: [PATCH 3/5] Fix tests by supporting TryAddCascadingValue --- ...scadingValueServiceCollectionExtensions.cs | 56 +++++++++++++++++++ .../Components/src/PublicAPI.Unshipped.txt | 3 + .../Components/test/CascadingParameterTest.cs | 48 ++++++++++++++++ ...orComponentsServiceCollectionExtensions.cs | 2 +- 4 files changed, 108 insertions(+), 1 deletion(-) 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 b18f7d3addca..0e534c7e557e 100644 --- a/src/Components/Components/src/PublicAPI.Unshipped.txt +++ b/src/Components/Components/src/PublicAPI.Unshipped.txt @@ -91,6 +91,9 @@ static Microsoft.AspNetCore.Components.SupplyParameterFromQueryProviderServiceCo 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/test/CascadingParameterTest.cs b/src/Components/Components/test/CascadingParameterTest.cs index 9c3dfb928ba0..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; @@ -940,4 +985,7 @@ public void ChangeValue(string newValue) StringValue = newValue; } } + + class Type1 { } + class Type2 { } } diff --git a/src/Components/Endpoints/src/DependencyInjection/RazorComponentsServiceCollectionExtensions.cs b/src/Components/Endpoints/src/DependencyInjection/RazorComponentsServiceCollectionExtensions.cs index e64dbf40a836..9fa6c8c27890 100644 --- a/src/Components/Endpoints/src/DependencyInjection/RazorComponentsServiceCollectionExtensions.cs +++ b/src/Components/Endpoints/src/DependencyInjection/RazorComponentsServiceCollectionExtensions.cs @@ -63,7 +63,7 @@ public static IRazorComponentsBuilder AddRazorComponents(this IServiceCollection services.TryAddScoped(); services.TryAddScoped(sp => sp.GetRequiredService()); services.AddSupplyValueFromQueryProvider(); - services.AddCascadingValue(sp => sp.GetRequiredService().HttpContext); + services.TryAddCascadingValue(sp => sp.GetRequiredService().HttpContext); // Form handling services.AddSupplyValueFromFormProvider(); From afb7bbbf6d40b8ffe9f3ab295fbd088a13be8b6f Mon Sep 17 00:00:00 2001 From: Steve Sanderson Date: Tue, 22 Aug 2023 14:29:11 +0100 Subject: [PATCH 4/5] Fix the build --- .../test/E2ETest/ServerRenderingTests/RenderingTest.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/src/Components/test/E2ETest/ServerRenderingTests/RenderingTest.cs b/src/Components/test/E2ETest/ServerRenderingTests/RenderingTest.cs index 1739b5b3069d..21c3e01d4712 100644 --- a/src/Components/test/E2ETest/ServerRenderingTests/RenderingTest.cs +++ b/src/Components/test/E2ETest/ServerRenderingTests/RenderingTest.cs @@ -27,6 +27,7 @@ public RenderingTest( public override Task InitializeAsync() => InitializeAsync(BrowserFixture.StreamingContext); + [Fact] [QuarantinedTest("https://github.com/dotnet/aspnetcore/issues/49975")] public void CanRenderLargeComponentsWithServerRenderMode() { From ed3539995fc991eac9435ae5a6933be65978a915 Mon Sep 17 00:00:00 2001 From: Steve Sanderson Date: Wed, 23 Aug 2023 13:28:50 +0100 Subject: [PATCH 5/5] Fix more tests --- ...mponentsServiceCollectionExtensionsTest.cs | 35 ++++++++------ .../MvcServiceCollectionExtensionsTest.cs | 46 ++++++++++++++++--- 2 files changed, 60 insertions(+), 21 deletions(-) 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/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}");