Skip to content

Commit 5ad966e

Browse files
In SSR, supply HttpContext as cascading value (#50253)
Fixes #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<AuthenticationState> 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 `<CascadingValue Name=...>`, 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.
1 parent 61852ce commit 5ad966e

19 files changed

+220
-75
lines changed

src/Components/Components/src/CascadingParameterAttribute.cs

+1-1
Original file line numberDiff line numberDiff line change
@@ -20,5 +20,5 @@ public sealed class CascadingParameterAttribute : CascadingParameterAttributeBas
2020
/// <see cref="CascadingValue{T}"/> that supplies a value with a compatible
2121
/// type.
2222
/// </summary>
23-
public override string? Name { get; set; }
23+
public string? Name { get; set; }
2424
}

src/Components/Components/src/CascadingParameterAttributeBase.cs

-6
Original file line numberDiff line numberDiff line change
@@ -8,12 +8,6 @@ namespace Microsoft.AspNetCore.Components;
88
/// </summary>
99
public abstract class CascadingParameterAttributeBase : Attribute
1010
{
11-
/// <summary>
12-
/// Gets or sets the name for the parameter, which correlates to the name
13-
/// of a cascading value.
14-
/// </summary>
15-
public abstract string? Name { get; set; }
16-
1711
/// <summary>
1812
/// Gets a flag indicating whether the cascading parameter should
1913
/// be supplied only once per component.

src/Components/Components/src/CascadingValueServiceCollectionExtensions.cs

+56
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
// The .NET Foundation licenses this file to you under the MIT license.
33

44
using Microsoft.AspNetCore.Components;
5+
using Microsoft.Extensions.DependencyInjection.Extensions;
56

67
namespace Microsoft.Extensions.DependencyInjection;
78

@@ -50,4 +51,59 @@ public static IServiceCollection AddCascadingValue<TValue>(
5051
public static IServiceCollection AddCascadingValue<TValue>(
5152
this IServiceCollection serviceCollection, Func<IServiceProvider, CascadingValueSource<TValue>> sourceFactory)
5253
=> serviceCollection.AddScoped<ICascadingValueSupplier>(sourceFactory);
54+
55+
/// <summary>
56+
/// Adds a cascading value to the <paramref name="serviceCollection"/>, if none is already registered
57+
/// with the value type. This is equivalent to having a fixed <see cref="CascadingValue{TValue}"/> at
58+
/// the root of the component hierarchy.
59+
/// </summary>
60+
/// <typeparam name="TValue">The value type.</typeparam>
61+
/// <param name="serviceCollection">The <see cref="IServiceCollection"/>.</param>
62+
/// <param name="valueFactory">A callback that supplies a fixed value within each service provider scope.</param>
63+
/// <returns>The <see cref="IServiceCollection"/>.</returns>
64+
public static void TryAddCascadingValue<TValue>(
65+
this IServiceCollection serviceCollection, Func<IServiceProvider, TValue> valueFactory)
66+
{
67+
serviceCollection.TryAddEnumerable(
68+
ServiceDescriptor.Scoped<ICascadingValueSupplier, CascadingValueSource<TValue>>(
69+
sp => new CascadingValueSource<TValue>(() => valueFactory(sp), isFixed: true)));
70+
}
71+
72+
/// <summary>
73+
/// Adds a cascading value to the <paramref name="serviceCollection"/>, if none is already registered
74+
/// with the value type, regardless of the <paramref name="name"/>. This is equivalent to having a fixed
75+
/// <see cref="CascadingValue{TValue}"/> at the root of the component hierarchy.
76+
/// </summary>
77+
/// <typeparam name="TValue">The value type.</typeparam>
78+
/// <param name="serviceCollection">The <see cref="IServiceCollection"/>.</param>
79+
/// <param name="name">A name for the cascading value. If set, <see cref="CascadingParameterAttribute"/> can be configured to match based on this name.</param>
80+
/// <param name="valueFactory">A callback that supplies a fixed value within each service provider scope.</param>
81+
/// <returns>The <see cref="IServiceCollection"/>.</returns>
82+
public static void TryAddCascadingValue<TValue>(
83+
this IServiceCollection serviceCollection, string name, Func<IServiceProvider, TValue> valueFactory)
84+
{
85+
serviceCollection.TryAddEnumerable(
86+
ServiceDescriptor.Scoped<ICascadingValueSupplier, CascadingValueSource<TValue>>(
87+
sp => new CascadingValueSource<TValue>(name, () => valueFactory(sp), isFixed: true)));
88+
}
89+
90+
/// <summary>
91+
/// Adds a cascading value to the <paramref name="serviceCollection"/>, if none is already registered
92+
/// with the value type. This is equivalent to having a fixed <see cref="CascadingValue{TValue}"/> at
93+
/// the root of the component hierarchy.
94+
///
95+
/// With this overload, you can supply a <see cref="CascadingValueSource{TValue}"/> which allows you
96+
/// to notify about updates to the value later, causing recipients to re-render. This overload should
97+
/// only be used if you plan to update the value dynamically.
98+
/// </summary>
99+
/// <typeparam name="TValue">The value type.</typeparam>
100+
/// <param name="serviceCollection">The <see cref="IServiceCollection"/>.</param>
101+
/// <param name="sourceFactory">A callback that supplies a <see cref="CascadingValueSource{TValue}"/> within each service provider scope.</param>
102+
/// <returns>The <see cref="IServiceCollection"/>.</returns>
103+
public static void TryAddCascadingValue<TValue>(
104+
this IServiceCollection serviceCollection, Func<IServiceProvider, CascadingValueSource<TValue>> sourceFactory)
105+
{
106+
serviceCollection.TryAddEnumerable(
107+
ServiceDescriptor.Scoped<ICascadingValueSupplier, CascadingValueSource<TValue>>(sourceFactory));
108+
}
53109
}

src/Components/Components/src/PublicAPI.Unshipped.txt

+3-10
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,4 @@
11
#nullable enable
2-
abstract Microsoft.AspNetCore.Components.CascadingParameterAttributeBase.Name.get -> string?
3-
abstract Microsoft.AspNetCore.Components.CascadingParameterAttributeBase.Name.set -> void
42
abstract Microsoft.AspNetCore.Components.RenderModeAttribute.Mode.get -> Microsoft.AspNetCore.Components.IComponentRenderMode!
53
Microsoft.AspNetCore.Components.CascadingParameterAttributeBase
64
Microsoft.AspNetCore.Components.CascadingParameterAttributeBase.CascadingParameterAttributeBase() -> void
@@ -83,24 +81,19 @@ Microsoft.AspNetCore.Components.Rendering.RenderTreeBuilder.AddComponentParamete
8381
Microsoft.AspNetCore.Components.StreamRenderingAttribute
8482
Microsoft.AspNetCore.Components.StreamRenderingAttribute.Enabled.get -> bool
8583
Microsoft.AspNetCore.Components.StreamRenderingAttribute.StreamRenderingAttribute(bool enabled) -> void
86-
*REMOVED*Microsoft.AspNetCore.Components.CascadingParameterAttribute.Name.get -> string?
87-
*REMOVED*Microsoft.AspNetCore.Components.CascadingParameterAttribute.Name.set -> void
8884
Microsoft.AspNetCore.Components.SupplyParameterFromQueryProviderServiceCollectionExtensions
8985
Microsoft.Extensions.DependencyInjection.CascadingValueServiceCollectionExtensions
90-
override Microsoft.AspNetCore.Components.CascadingParameterAttribute.Name.get -> string?
91-
override Microsoft.AspNetCore.Components.CascadingParameterAttribute.Name.set -> void
9286
override Microsoft.AspNetCore.Components.EventCallback.GetHashCode() -> int
9387
override Microsoft.AspNetCore.Components.EventCallback.Equals(object? obj) -> bool
9488
override Microsoft.AspNetCore.Components.EventCallback<TValue>.GetHashCode() -> int
9589
override Microsoft.AspNetCore.Components.EventCallback<TValue>.Equals(object? obj) -> bool
96-
*REMOVED*Microsoft.AspNetCore.Components.SupplyParameterFromQueryAttribute.Name.get -> string?
97-
*REMOVED*Microsoft.AspNetCore.Components.SupplyParameterFromQueryAttribute.Name.set -> void
98-
override Microsoft.AspNetCore.Components.SupplyParameterFromQueryAttribute.Name.get -> string?
99-
override Microsoft.AspNetCore.Components.SupplyParameterFromQueryAttribute.Name.set -> void
10090
static Microsoft.AspNetCore.Components.SupplyParameterFromQueryProviderServiceCollectionExtensions.AddSupplyValueFromQueryProvider(this Microsoft.Extensions.DependencyInjection.IServiceCollection! services) -> Microsoft.Extensions.DependencyInjection.IServiceCollection!
10191
static Microsoft.Extensions.DependencyInjection.CascadingValueServiceCollectionExtensions.AddCascadingValue<TValue>(this Microsoft.Extensions.DependencyInjection.IServiceCollection! serviceCollection, string! name, System.Func<System.IServiceProvider!, TValue>! valueFactory) -> Microsoft.Extensions.DependencyInjection.IServiceCollection!
10292
static Microsoft.Extensions.DependencyInjection.CascadingValueServiceCollectionExtensions.AddCascadingValue<TValue>(this Microsoft.Extensions.DependencyInjection.IServiceCollection! serviceCollection, System.Func<System.IServiceProvider!, Microsoft.AspNetCore.Components.CascadingValueSource<TValue>!>! sourceFactory) -> Microsoft.Extensions.DependencyInjection.IServiceCollection!
10393
static Microsoft.Extensions.DependencyInjection.CascadingValueServiceCollectionExtensions.AddCascadingValue<TValue>(this Microsoft.Extensions.DependencyInjection.IServiceCollection! serviceCollection, System.Func<System.IServiceProvider!, TValue>! valueFactory) -> Microsoft.Extensions.DependencyInjection.IServiceCollection!
94+
static Microsoft.Extensions.DependencyInjection.CascadingValueServiceCollectionExtensions.TryAddCascadingValue<TValue>(this Microsoft.Extensions.DependencyInjection.IServiceCollection! serviceCollection, string! name, System.Func<System.IServiceProvider!, TValue>! valueFactory) -> void
95+
static Microsoft.Extensions.DependencyInjection.CascadingValueServiceCollectionExtensions.TryAddCascadingValue<TValue>(this Microsoft.Extensions.DependencyInjection.IServiceCollection! serviceCollection, System.Func<System.IServiceProvider!, Microsoft.AspNetCore.Components.CascadingValueSource<TValue>!>! sourceFactory) -> void
96+
static Microsoft.Extensions.DependencyInjection.CascadingValueServiceCollectionExtensions.TryAddCascadingValue<TValue>(this Microsoft.Extensions.DependencyInjection.IServiceCollection! serviceCollection, System.Func<System.IServiceProvider!, TValue>! valueFactory) -> void
10497
virtual Microsoft.AspNetCore.Components.NavigationManager.Refresh(bool forceReload = false) -> void
10598
virtual Microsoft.AspNetCore.Components.Rendering.ComponentState.DisposeAsync() -> System.Threading.Tasks.ValueTask
10699
virtual Microsoft.AspNetCore.Components.RenderTree.Renderer.AddPendingTask(Microsoft.AspNetCore.Components.Rendering.ComponentState? componentState, System.Threading.Tasks.Task! task) -> void

src/Components/Components/src/Reflection/ComponentProperties.cs

+1-2
Original file line numberDiff line numberDiff line change
@@ -184,8 +184,7 @@ private static void ThrowForUnknownIncomingParameterName([DynamicallyAccessedMem
184184
{
185185
throw new InvalidOperationException(
186186
$"Object of type '{targetType.FullName}' has a property matching the name '{parameterName}', " +
187-
$"but it does not have [{nameof(ParameterAttribute)}], [{nameof(CascadingParameterAttribute)}] or " +
188-
$"[SupplyParameterFromFormAttribute] applied.");
187+
$"but it does not have [Parameter], [CascadingParameter], or any other parameter-supplying attribute.");
189188
}
190189
else
191190
{

src/Components/Components/src/SupplyParameterFromQueryAttribute.cs

+1-1
Original file line numberDiff line numberDiff line change
@@ -14,5 +14,5 @@ public sealed class SupplyParameterFromQueryAttribute : CascadingParameterAttrib
1414
/// Gets or sets the name of the querystring parameter. If null, the querystring
1515
/// parameter is assumed to have the same name as the associated property.
1616
/// </summary>
17-
public override string? Name { get; set; }
17+
public string? Name { get; set; }
1818
}

src/Components/Components/src/SupplyParameterFromQueryProviderServiceCollectionExtensions.cs

+2-1
Original file line numberDiff line numberDiff line change
@@ -50,7 +50,8 @@ public bool CanSupplyValue(in CascadingParameterInfo parameterInfo)
5050
UpdateQueryParameters();
5151
}
5252

53-
var queryParameterName = parameterInfo.Attribute.Name ?? parameterInfo.PropertyName;
53+
var attribute = (SupplyParameterFromQueryAttribute)parameterInfo.Attribute; // Must be a valid cast because we check in CanSupplyValue
54+
var queryParameterName = attribute.Name ?? parameterInfo.PropertyName;
5455
return _queryParameterValueSupplier.GetQueryParameterValue(parameterInfo.PropertyType, queryParameterName);
5556
}
5657

src/Components/Components/test/CascadingParameterStateTest.cs

-18
Original file line numberDiff line numberDiff line change
@@ -476,8 +476,6 @@ class ComponentWithNamedCascadingParam : TestComponentBase
476476

477477
class SupplyParameterWithSingleDeliveryAttribute : CascadingParameterAttributeBase
478478
{
479-
public override string Name { get; set; }
480-
481479
internal override bool SingleDelivery => true;
482480
}
483481

@@ -523,19 +521,3 @@ public TestNavigationManager()
523521
}
524522
}
525523
}
526-
527-
[AttributeUsage(AttributeTargets.Property, AllowMultiple = false, Inherited = true)]
528-
public sealed class SupplyParameterFromFormAttribute : CascadingParameterAttributeBase
529-
{
530-
/// <summary>
531-
/// Gets or sets the name for the parameter. The name is used to match
532-
/// the form data and decide whether or not the value needs to be bound.
533-
/// </summary>
534-
public override string Name { get; set; }
535-
536-
/// <summary>
537-
/// Gets or sets the name for the handler. The name is used to match
538-
/// the form data and decide whether or not the value needs to be bound.
539-
/// </summary>
540-
public string Handler { get; set; }
541-
}

src/Components/Components/test/CascadingParameterTest.cs

+50-6
Original file line numberDiff line numberDiff line change
@@ -727,15 +727,58 @@ public void OmitsSingleDeliveryCascadingParametersWhenUpdatingDirectParameters()
727727
});
728728
}
729729

730+
[Fact]
731+
public void CanUseTryAddPatternForCascadingValuesInServiceCollection_ValueFactory()
732+
{
733+
// Arrange
734+
var services = new ServiceCollection();
735+
736+
// Act
737+
services.TryAddCascadingValue(_ => new Type1());
738+
services.TryAddCascadingValue(_ => new Type1());
739+
services.TryAddCascadingValue(_ => new Type2());
740+
741+
// Assert
742+
Assert.Equal(2, services.Count());
743+
}
744+
745+
[Fact]
746+
public void CanUseTryAddPatternForCascadingValuesInServiceCollection_NamedValueFactory()
747+
{
748+
// Arrange
749+
var services = new ServiceCollection();
750+
751+
// Act
752+
services.TryAddCascadingValue("Name1", _ => new Type1());
753+
services.TryAddCascadingValue("Name2", _ => new Type1());
754+
services.TryAddCascadingValue("Name3", _ => new Type2());
755+
756+
// Assert
757+
Assert.Equal(2, services.Count());
758+
}
759+
760+
[Fact]
761+
public void CanUseTryAddPatternForCascadingValuesInServiceCollection_CascadingValueSource()
762+
{
763+
// Arrange
764+
var services = new ServiceCollection();
765+
766+
// Act
767+
services.TryAddCascadingValue(_ => new CascadingValueSource<Type1>("Name1", new Type1(), false));
768+
services.TryAddCascadingValue(_ => new CascadingValueSource<Type1>("Name2", new Type1(), false));
769+
services.TryAddCascadingValue(_ => new CascadingValueSource<Type2>("Name3", new Type2(), false));
770+
771+
// Assert
772+
Assert.Equal(2, services.Count());
773+
}
774+
730775
private class SingleDeliveryValue(string text)
731776
{
732777
public string Text => text;
733778
}
734779

735780
private class SingleDeliveryCascadingParameterAttribute : CascadingParameterAttributeBase
736781
{
737-
public override string Name { get; set; }
738-
739782
internal override bool SingleDelivery => true;
740783
}
741784

@@ -852,13 +895,11 @@ class SecondCascadingParameterConsumerComponent<T1, T2> : CascadingParameterCons
852895
[AttributeUsage(AttributeTargets.Property, AllowMultiple = false, Inherited = true)]
853896
class CustomCascadingParameter1Attribute : CascadingParameterAttributeBase
854897
{
855-
public override string Name { get; set; }
856898
}
857899

858900
[AttributeUsage(AttributeTargets.Property, AllowMultiple = false, Inherited = true)]
859901
class CustomCascadingParameter2Attribute : CascadingParameterAttributeBase
860902
{
861-
public override string Name { get; set; }
862903
}
863904

864905
class CustomCascadingValueProducer<TAttribute> : AutoRenderComponent, ICascadingValueSupplier
@@ -904,7 +945,7 @@ void ICascadingValueSupplier.Unsubscribe(ComponentState subscriber, in Cascading
904945

905946
class CustomCascadingValueConsumer1 : AutoRenderComponent
906947
{
907-
[CustomCascadingParameter1(Name = nameof(Value))]
948+
[CustomCascadingParameter1]
908949
public object Value { get; set; }
909950

910951
protected override void BuildRenderTree(RenderTreeBuilder builder)
@@ -915,7 +956,7 @@ protected override void BuildRenderTree(RenderTreeBuilder builder)
915956

916957
class CustomCascadingValueConsumer2 : AutoRenderComponent
917958
{
918-
[CustomCascadingParameter2(Name = nameof(Value))]
959+
[CustomCascadingParameter2]
919960
public object Value { get; set; }
920961

921962
protected override void BuildRenderTree(RenderTreeBuilder builder)
@@ -944,4 +985,7 @@ public void ChangeValue(string newValue)
944985
StringValue = newValue;
945986
}
946987
}
988+
989+
class Type1 { }
990+
class Type2 { }
947991
}

src/Components/Components/test/ParameterViewTest.Assignment.cs

+1-1
Original file line numberDiff line numberDiff line change
@@ -183,7 +183,7 @@ public void IncomingParameterMatchesPropertyNotDeclaredAsParameter_Throws()
183183
Assert.Equal(default, target.IntProp);
184184
Assert.Equal(
185185
$"Object of type '{typeof(HasPropertyWithoutParameterAttribute).FullName}' has a property matching the name '{nameof(HasPropertyWithoutParameterAttribute.IntProp)}', " +
186-
$"but it does not have [{nameof(ParameterAttribute)}], [{nameof(CascadingParameterAttribute)}] or [{nameof(SupplyParameterFromFormAttribute)}] applied.",
186+
"but it does not have [Parameter], [CascadingParameter], or any other parameter-supplying attribute.",
187187
ex.Message);
188188
}
189189

src/Components/Endpoints/src/DependencyInjection/RazorComponentsServiceCollectionExtensions.cs

+1
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,7 @@ public static IRazorComponentsBuilder AddRazorComponents(this IServiceCollection
6363
services.TryAddScoped<EndpointRoutingStateProvider>();
6464
services.TryAddScoped<IRoutingStateProvider>(sp => sp.GetRequiredService<EndpointRoutingStateProvider>());
6565
services.AddSupplyValueFromQueryProvider();
66+
services.TryAddCascadingValue(sp => sp.GetRequiredService<EndpointHtmlRenderer>().HttpContext);
6667

6768
// Form handling
6869
services.AddSupplyValueFromFormProvider();

src/Components/Endpoints/src/Rendering/EndpointHtmlRenderer.cs

+2
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,8 @@ public EndpointHtmlRenderer(IServiceProvider serviceProvider, ILoggerFactory log
5252
_services = serviceProvider;
5353
}
5454

55+
internal HttpContext? HttpContext => _httpContext;
56+
5557
private void SetHttpContext(HttpContext httpContext)
5658
{
5759
if (_httpContext is null)

0 commit comments

Comments
 (0)