Skip to content

Commit fdfb0d2

Browse files
Root-level cascading values + CascadingAuthenticationState at root (#49204)
1 parent 2983a98 commit fdfb0d2

File tree

13 files changed

+716
-2
lines changed

13 files changed

+716
-2
lines changed
Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
// Licensed to the .NET Foundation under one or more agreements.
2+
// The .NET Foundation licenses this file to you under the MIT license.
3+
4+
using Microsoft.AspNetCore.Components;
5+
using Microsoft.AspNetCore.Components.Authorization;
6+
7+
namespace Microsoft.Extensions.DependencyInjection;
8+
9+
/// <summary>
10+
/// Extension methods for configuring cascading authentication state on a service collection.
11+
/// </summary>
12+
public static class CascadingAuthenticationStateServiceCollectionExtensions
13+
{
14+
/// <summary>
15+
/// Adds cascading authentication state to the <paramref name="serviceCollection"/>. This is equivalent to
16+
/// having a <see cref="CascadingAuthenticationState"/> component at the root of your component hierarchy.
17+
/// </summary>
18+
/// <param name="serviceCollection">The <see cref="IServiceCollection"/>.</param>
19+
/// <returns>The <see cref="IServiceCollection"/>.</returns>
20+
public static IServiceCollection AddCascadingAuthenticationState(this IServiceCollection serviceCollection)
21+
{
22+
return serviceCollection.AddCascadingValue<Task<AuthenticationState>>(services =>
23+
{
24+
var authenticationStateProvider = services.GetRequiredService<AuthenticationStateProvider>();
25+
return new AuthenticationStateCascadingValueSource(authenticationStateProvider);
26+
});
27+
}
28+
29+
private sealed class AuthenticationStateCascadingValueSource : CascadingValueSource<Task<AuthenticationState>>, IDisposable
30+
{
31+
// This is intended to produce identical behavior to having a <CascadingAuthenticationStateProvider>
32+
// wrapped around the root component.
33+
34+
private readonly AuthenticationStateProvider _authenticationStateProvider;
35+
36+
public AuthenticationStateCascadingValueSource(AuthenticationStateProvider authenticationStateProvider)
37+
: base(authenticationStateProvider.GetAuthenticationStateAsync, isFixed: false)
38+
{
39+
_authenticationStateProvider = authenticationStateProvider;
40+
_authenticationStateProvider.AuthenticationStateChanged += HandleAuthenticationStateChanged;
41+
}
42+
43+
private void HandleAuthenticationStateChanged(Task<AuthenticationState> newAuthStateTask)
44+
{
45+
// It's OK to discard the task because this only represents the duration of the dispatch to sync context.
46+
// It handles any exceptions internally by dispatching them to the renderer within the context of whichever
47+
// component threw when receiving the update. This is the same as how a CascadingValue doesn't get notified
48+
// about exceptions that happen inside the recipients of value notifications.
49+
_ = NotifyChangedAsync(newAuthStateTask);
50+
}
51+
52+
public void Dispose()
53+
{
54+
_authenticationStateProvider.AuthenticationStateChanged -= HandleAuthenticationStateChanged;
55+
}
56+
}
57+
}
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1 +1,3 @@
11
#nullable enable
2+
Microsoft.Extensions.DependencyInjection.CascadingAuthenticationStateServiceCollectionExtensions
3+
static Microsoft.Extensions.DependencyInjection.CascadingAuthenticationStateServiceCollectionExtensions.AddCascadingAuthenticationState(this Microsoft.Extensions.DependencyInjection.IServiceCollection! serviceCollection) -> Microsoft.Extensions.DependencyInjection.IServiceCollection!

src/Components/Components/src/CascadingParameterState.cs

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,7 @@ public static IReadOnlyList<CascadingParameterState> FindCascadingParameters(Com
5757

5858
private static ICascadingValueSupplier? GetMatchingCascadingValueSupplier(in CascadingParameterInfo info, ComponentState componentState)
5959
{
60+
// First scan up through the component hierarchy
6061
var candidate = componentState;
6162
do
6263
{
@@ -68,6 +69,15 @@ public static IReadOnlyList<CascadingParameterState> FindCascadingParameters(Com
6869
candidate = candidate.LogicalParentComponentState;
6970
} while (candidate != null);
7071

72+
// We got to the root and found no match, so now look at the providers registered in DI
73+
foreach (var valueSupplier in componentState.Renderer.ServiceProviderCascadingValueSuppliers)
74+
{
75+
if (valueSupplier.CanSupplyValue(info))
76+
{
77+
return valueSupplier;
78+
}
79+
}
80+
7181
// No match
7282
return null;
7383
}
Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
// Licensed to the .NET Foundation under one or more agreements.
2+
// The .NET Foundation licenses this file to you under the MIT license.
3+
4+
using Microsoft.AspNetCore.Components;
5+
6+
namespace Microsoft.Extensions.DependencyInjection;
7+
8+
/// <summary>
9+
/// Extension methods for configuring cascading values on an <see cref="IServiceCollection"/>.
10+
/// </summary>
11+
public static class CascadingValueServiceCollectionExtensions
12+
{
13+
/// <summary>
14+
/// Adds a cascading value to the <paramref name="serviceCollection"/>. This is equivalent to having
15+
/// a fixed <see cref="CascadingValue{TValue}"/> at the root of the component hierarchy.
16+
/// </summary>
17+
/// <typeparam name="TValue">The value type.</typeparam>
18+
/// <param name="serviceCollection">The <see cref="IServiceCollection"/>.</param>
19+
/// <param name="valueFactory">A callback that supplies a fixed value within each service provider scope.</param>
20+
/// <returns>The <see cref="IServiceCollection"/>.</returns>
21+
public static IServiceCollection AddCascadingValue<TValue>(
22+
this IServiceCollection serviceCollection, Func<IServiceProvider, TValue> valueFactory)
23+
=> serviceCollection.AddScoped<ICascadingValueSupplier>(sp => new CascadingValueSource<TValue>(() => valueFactory(sp), isFixed: true));
24+
25+
/// <summary>
26+
/// Adds a cascading value to the <paramref name="serviceCollection"/>. This is equivalent to having
27+
/// a fixed <see cref="CascadingValue{TValue}"/> at the root of the component hierarchy.
28+
/// </summary>
29+
/// <typeparam name="TValue">The value type.</typeparam>
30+
/// <param name="serviceCollection">The <see cref="IServiceCollection"/>.</param>
31+
/// <param name="name">A name for the cascading value. If set, <see cref="CascadingParameterAttribute"/> can be configured to match based on this name.</param>
32+
/// <param name="valueFactory">A callback that supplies a fixed value within each service provider scope.</param>
33+
/// <returns>The <see cref="IServiceCollection"/>.</returns>
34+
public static IServiceCollection AddCascadingValue<TValue>(
35+
this IServiceCollection serviceCollection, string name, Func<IServiceProvider, TValue> valueFactory)
36+
=> serviceCollection.AddScoped<ICascadingValueSupplier>(sp => new CascadingValueSource<TValue>(name, () => valueFactory(sp), isFixed: true));
37+
38+
/// <summary>
39+
/// Adds a cascading value to the <paramref name="serviceCollection"/>. This is equivalent to having
40+
/// a <see cref="CascadingValue{TValue}"/> at the root of the component hierarchy.
41+
///
42+
/// With this overload, you can supply a <see cref="CascadingValueSource{TValue}"/> which allows you
43+
/// to notify about updates to the value later, causing recipients to re-render. This overload should
44+
/// only be used if you plan to update the value dynamically.
45+
/// </summary>
46+
/// <typeparam name="TValue">The value type.</typeparam>
47+
/// <param name="serviceCollection">The <see cref="IServiceCollection"/>.</param>
48+
/// <param name="sourceFactory">A callback that supplies a <see cref="CascadingValueSource{TValue}"/> within each service provider scope.</param>
49+
/// <returns>The <see cref="IServiceCollection"/>.</returns>
50+
public static IServiceCollection AddCascadingValue<TValue>(
51+
this IServiceCollection serviceCollection, Func<IServiceProvider, CascadingValueSource<TValue>> sourceFactory)
52+
=> serviceCollection.AddScoped<ICascadingValueSupplier>(sourceFactory);
53+
}
Lines changed: 177 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,177 @@
1+
// Licensed to the .NET Foundation under one or more agreements.
2+
// The .NET Foundation licenses this file to you under the MIT license.
3+
4+
using System.Collections.Concurrent;
5+
using Microsoft.AspNetCore.Components.Rendering;
6+
7+
namespace Microsoft.AspNetCore.Components;
8+
9+
/// <summary>
10+
/// Supplies a cascading value that can be received by components using
11+
/// <see cref="CascadingParameterAttribute"/>.
12+
/// </summary>
13+
public class CascadingValueSource<TValue> : ICascadingValueSupplier
14+
{
15+
// By *not* making this sealed, people who want to deal with value disposal can subclass this,
16+
// add IDisposable, and then do what they want during shutdown
17+
18+
private readonly ConcurrentDictionary<Dispatcher, List<ComponentState>>? _subscribers;
19+
private readonly bool _isFixed;
20+
private readonly string? _name;
21+
22+
// You can either provide an initial value to the constructor, or a func to provide one lazily
23+
private TValue? _currentValue;
24+
private Func<TValue>? _initialValueFactory;
25+
26+
/// <summary>
27+
/// Constructs an instance of <see cref="CascadingValueSource{TValue}"/>.
28+
/// </summary>
29+
/// <param name="value">The initial value.</param>
30+
/// <param name="isFixed">A flag to indicate whether the value is fixed. If false, all receipients will subscribe for update notifications, which you can issue by calling <see cref="NotifyChangedAsync()"/>. These subscriptions come at a performance cost, so if the value will not change, set <paramref name="isFixed"/> to true.</param>
31+
public CascadingValueSource(TValue value, bool isFixed) : this(isFixed)
32+
{
33+
_currentValue = value;
34+
}
35+
36+
/// <summary>
37+
/// Constructs an instance of <see cref="CascadingValueSource{TValue}"/>.
38+
/// </summary>
39+
/// <param name="name">A name for the cascading value. If set, <see cref="CascadingParameterAttribute"/> can be configured to match based on this name.</param>
40+
/// <param name="value">The initial value.</param>
41+
/// <param name="isFixed">A flag to indicate whether the value is fixed. If false, all receipients will subscribe for update notifications, which you can issue by calling <see cref="NotifyChangedAsync()"/>. These subscriptions come at a performance cost, so if the value will not change, set <paramref name="isFixed"/> to true.</param>
42+
public CascadingValueSource(string name, TValue value, bool isFixed) : this(value, isFixed)
43+
{
44+
ArgumentNullException.ThrowIfNull(name);
45+
_name = name;
46+
}
47+
48+
/// <summary>
49+
/// Constructs an instance of <see cref="CascadingValueSource{TValue}"/>.
50+
/// </summary>
51+
/// <param name="valueFactory">A callback that produces the initial value when first required.</param>
52+
/// <param name="isFixed">A flag to indicate whether the value is fixed. If false, all receipients will subscribe for update notifications, which you can issue by calling <see cref="NotifyChangedAsync()"/>. These subscriptions come at a performance cost, so if the value will not change, set <paramref name="isFixed"/> to true.</param>
53+
public CascadingValueSource(Func<TValue> valueFactory, bool isFixed) : this(isFixed)
54+
{
55+
_initialValueFactory = valueFactory;
56+
}
57+
58+
/// <summary>
59+
/// Constructs an instance of <see cref="CascadingValueSource{TValue}"/>.
60+
/// </summary>
61+
/// <param name="name">A name for the cascading value. If set, <see cref="CascadingParameterAttribute"/> can be configured to match based on this name.</param>
62+
/// <param name="valueFactory">A callback that produces the initial value when first required.</param>
63+
/// <param name="isFixed">A flag to indicate whether the value is fixed. If false, all receipients will subscribe for update notifications, which you can issue by calling <see cref="NotifyChangedAsync()"/>. These subscriptions come at a performance cost, so if the value will not change, set <paramref name="isFixed"/> to true.</param>
64+
public CascadingValueSource(string name, Func<TValue> valueFactory, bool isFixed) : this(valueFactory, isFixed)
65+
{
66+
ArgumentNullException.ThrowIfNull(name);
67+
_name = name;
68+
}
69+
70+
private CascadingValueSource(bool isFixed)
71+
{
72+
_isFixed = isFixed;
73+
74+
if (!_isFixed)
75+
{
76+
_subscribers = new();
77+
}
78+
}
79+
80+
/// <summary>
81+
/// Notifies subscribers that the value has changed (for example, if it has been mutated).
82+
/// </summary>
83+
/// <returns>A <see cref="Task"/> that completes when the notifications have been issued.</returns>
84+
public Task NotifyChangedAsync()
85+
{
86+
if (_isFixed)
87+
{
88+
throw new InvalidOperationException($"Cannot notify about changes because the {GetType()} is configured as fixed.");
89+
}
90+
91+
if (_subscribers?.Count > 0)
92+
{
93+
var tasks = new List<Task>();
94+
95+
foreach (var (dispatcher, subscribers) in _subscribers)
96+
{
97+
tasks.Add(dispatcher.InvokeAsync(() =>
98+
{
99+
foreach (var subscriber in subscribers)
100+
{
101+
subscriber.NotifyCascadingValueChanged(ParameterViewLifetime.Unbound);
102+
}
103+
}));
104+
}
105+
106+
return Task.WhenAll(tasks);
107+
}
108+
else
109+
{
110+
return Task.CompletedTask;
111+
}
112+
}
113+
114+
/// <summary>
115+
/// Notifies subscribers that the value has changed, supplying a new value.
116+
/// </summary>
117+
/// <param name="newValue"></param>
118+
/// <returns>A <see cref="Task"/> that completes when the notifications have been issued.</returns>
119+
public Task NotifyChangedAsync(TValue newValue)
120+
{
121+
_currentValue = newValue;
122+
_initialValueFactory = null; // This definitely won't be used now
123+
124+
return NotifyChangedAsync();
125+
}
126+
127+
bool ICascadingValueSupplier.IsFixed => _isFixed;
128+
129+
bool ICascadingValueSupplier.CanSupplyValue(in CascadingParameterInfo parameterInfo)
130+
{
131+
if (parameterInfo.Attribute is not CascadingParameterAttribute cascadingParameterAttribute || !parameterInfo.PropertyType.IsAssignableFrom(typeof(TValue)))
132+
{
133+
return false;
134+
}
135+
136+
// We only consider explicitly requested names, not the property name.
137+
var requestedName = cascadingParameterAttribute.Name;
138+
return (requestedName == null && _name == null) // Match on type alone
139+
|| string.Equals(requestedName, _name, StringComparison.OrdinalIgnoreCase); // Also match on name
140+
}
141+
142+
object? ICascadingValueSupplier.GetCurrentValue(in CascadingParameterInfo parameterInfo)
143+
{
144+
if (_initialValueFactory is not null)
145+
{
146+
_currentValue = _initialValueFactory();
147+
_initialValueFactory = null;
148+
}
149+
150+
return _currentValue;
151+
}
152+
153+
void ICascadingValueSupplier.Subscribe(ComponentState subscriber, in CascadingParameterInfo parameterInfo)
154+
{
155+
Dispatcher dispatcher = subscriber.Renderer.Dispatcher;
156+
dispatcher.AssertAccess();
157+
158+
// The .Add is threadsafe because we are in the sync context for this dispatcher
159+
_subscribers?.GetOrAdd(dispatcher, _ => new()).Add(subscriber);
160+
}
161+
162+
void ICascadingValueSupplier.Unsubscribe(ComponentState subscriber, in CascadingParameterInfo parameterInfo)
163+
{
164+
Dispatcher dispatcher = subscriber.Renderer.Dispatcher;
165+
dispatcher.AssertAccess();
166+
167+
if (_subscribers?.TryGetValue(dispatcher, out var subscribersForDispatcher) == true)
168+
{
169+
// Threadsafe because we're in the sync context for this dispatcher
170+
subscribersForDispatcher.Remove(subscriber);
171+
if (subscribersForDispatcher.Count == 0)
172+
{
173+
_subscribers.Remove(dispatcher, out _);
174+
}
175+
}
176+
}
177+
}

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

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,13 @@ Microsoft.AspNetCore.Components.CascadingParameterInfo.Attribute.get -> Microsof
4646
Microsoft.AspNetCore.Components.CascadingParameterInfo.CascadingParameterInfo() -> void
4747
Microsoft.AspNetCore.Components.CascadingParameterInfo.PropertyName.get -> string!
4848
Microsoft.AspNetCore.Components.CascadingParameterInfo.PropertyType.get -> System.Type!
49+
Microsoft.AspNetCore.Components.CascadingValueSource<TValue>
50+
Microsoft.AspNetCore.Components.CascadingValueSource<TValue>.CascadingValueSource(string! name, System.Func<TValue>! valueFactory, bool isFixed) -> void
51+
Microsoft.AspNetCore.Components.CascadingValueSource<TValue>.CascadingValueSource(string! name, TValue value, bool isFixed) -> void
52+
Microsoft.AspNetCore.Components.CascadingValueSource<TValue>.CascadingValueSource(System.Func<TValue>! valueFactory, bool isFixed) -> void
53+
Microsoft.AspNetCore.Components.CascadingValueSource<TValue>.CascadingValueSource(TValue value, bool isFixed) -> void
54+
Microsoft.AspNetCore.Components.CascadingValueSource<TValue>.NotifyChangedAsync() -> System.Threading.Tasks.Task!
55+
Microsoft.AspNetCore.Components.CascadingValueSource<TValue>.NotifyChangedAsync(TValue newValue) -> System.Threading.Tasks.Task!
4956
Microsoft.AspNetCore.Components.ComponentBase.DispatchExceptionAsync(System.Exception! exception) -> System.Threading.Tasks.Task!
5057
Microsoft.AspNetCore.Components.IComponentRenderMode
5158
Microsoft.AspNetCore.Components.Infrastructure.ComponentStatePersistenceManager.PersistStateAsync(Microsoft.AspNetCore.Components.IPersistentComponentStateStore! store, Microsoft.AspNetCore.Components.Dispatcher! dispatcher) -> System.Threading.Tasks.Task!
@@ -109,6 +116,7 @@ Microsoft.AspNetCore.Components.StreamRenderingAttribute.Enabled.get -> bool
109116
Microsoft.AspNetCore.Components.StreamRenderingAttribute.StreamRenderingAttribute(bool enabled) -> void
110117
*REMOVED*Microsoft.AspNetCore.Components.CascadingParameterAttribute.Name.get -> string?
111118
*REMOVED*Microsoft.AspNetCore.Components.CascadingParameterAttribute.Name.set -> void
119+
Microsoft.Extensions.DependencyInjection.CascadingValueServiceCollectionExtensions
112120
override Microsoft.AspNetCore.Components.CascadingParameterAttribute.Name.get -> string?
113121
override Microsoft.AspNetCore.Components.CascadingParameterAttribute.Name.set -> void
114122
override Microsoft.AspNetCore.Components.EventCallback.GetHashCode() -> int
@@ -119,6 +127,9 @@ override Microsoft.AspNetCore.Components.EventCallback<TValue>.Equals(object? ob
119127
*REMOVED*Microsoft.AspNetCore.Components.SupplyParameterFromQueryAttribute.Name.set -> void
120128
override Microsoft.AspNetCore.Components.SupplyParameterFromQueryAttribute.Name.get -> string?
121129
override Microsoft.AspNetCore.Components.SupplyParameterFromQueryAttribute.Name.set -> void
130+
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!
131+
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!
132+
static Microsoft.Extensions.DependencyInjection.CascadingValueServiceCollectionExtensions.AddCascadingValue<TValue>(this Microsoft.Extensions.DependencyInjection.IServiceCollection! serviceCollection, System.Func<System.IServiceProvider!, TValue>! valueFactory) -> Microsoft.Extensions.DependencyInjection.IServiceCollection!
122133
virtual Microsoft.AspNetCore.Components.Binding.CascadingModelBindingProvider.Subscribe(Microsoft.AspNetCore.Components.Rendering.ComponentState! subscriber) -> void
123134
virtual Microsoft.AspNetCore.Components.Binding.CascadingModelBindingProvider.Unsubscribe(Microsoft.AspNetCore.Components.Rendering.ComponentState! subscriber) -> void
124135
virtual Microsoft.AspNetCore.Components.Rendering.ComponentState.DisposeAsync() -> System.Threading.Tasks.ValueTask

0 commit comments

Comments
 (0)