Skip to content

Commit 319e87f

Browse files
authored
SupplyParameterFromTempData support for Blazor (#65306)
1 parent df6d8dd commit 319e87f

20 files changed

Lines changed: 733 additions & 13 deletions
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
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+
namespace Microsoft.AspNetCore.Components;
5+
6+
/// <summary>
7+
/// This class represents an active subscription to a cascading parameter. The implementation is responsible for providing the current value and notifying about changes by causing the subscriber component to re-render when necessary.
8+
/// </summary>
9+
public abstract class CascadingParameterSubscription : IDisposable
10+
{
11+
/// <summary>
12+
/// Function that returns the current value for the subscription.
13+
/// </summary>
14+
public abstract object? GetCurrentValue();
15+
16+
/// <inheritdoc/>
17+
public abstract void Dispose();
18+
}
Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
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.Infrastructure;
5+
using Microsoft.AspNetCore.Components.Rendering;
6+
7+
namespace Microsoft.AspNetCore.Components;
8+
9+
internal class CascadingParameterValueProvider<TAttribute> : ICascadingValueSupplier
10+
where TAttribute : CascadingParameterAttributeBase
11+
{
12+
private readonly Dictionary<ComponentSubscriptionKey, CascadingParameterSubscription> _subscriptions = new();
13+
private readonly Func<ComponentState, TAttribute, CascadingParameterInfo, CascadingParameterSubscription> _subscribeFactory;
14+
15+
public CascadingParameterValueProvider(Func<ComponentState, TAttribute, CascadingParameterInfo, CascadingParameterSubscription> subscribeFactory)
16+
{
17+
_subscribeFactory = subscribeFactory;
18+
}
19+
20+
public bool IsFixed => false;
21+
22+
public bool CanSupplyValue(in CascadingParameterInfo parameterInfo)
23+
=> parameterInfo.Attribute is TAttribute;
24+
25+
public object? GetCurrentValue(object? key, in CascadingParameterInfo parameterInfo)
26+
{
27+
var subscriptionKey = new ComponentSubscriptionKey((ComponentState)key!, parameterInfo.PropertyName);
28+
if (_subscriptions.TryGetValue(subscriptionKey, out var subscription))
29+
{
30+
return subscription.GetCurrentValue();
31+
}
32+
return null;
33+
}
34+
35+
public void Subscribe(ComponentState subscriber, in CascadingParameterInfo parameterInfo)
36+
{
37+
var key = new ComponentSubscriptionKey(subscriber, parameterInfo.PropertyName);
38+
_subscriptions[key] = _subscribeFactory(subscriber, (TAttribute)parameterInfo.Attribute, parameterInfo);
39+
}
40+
41+
public void Unsubscribe(ComponentState subscriber, in CascadingParameterInfo parameterInfo)
42+
{
43+
var key = new ComponentSubscriptionKey(subscriber, parameterInfo.PropertyName);
44+
if (_subscriptions.Remove(key, out var subscription))
45+
{
46+
subscription.Dispose();
47+
}
48+
}
49+
}

src/Components/Components/src/CascadingValueServiceCollectionExtensions.cs

Lines changed: 20 additions & 1 deletion
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.AspNetCore.Components.Rendering;
56
using Microsoft.Extensions.DependencyInjection.Extensions;
67

78
namespace Microsoft.Extensions.DependencyInjection;
@@ -52,6 +53,24 @@ public static IServiceCollection AddCascadingValue<TValue>(
5253
this IServiceCollection serviceCollection, Func<IServiceProvider, CascadingValueSource<TValue>> sourceFactory)
5354
=> serviceCollection.AddScoped<ICascadingValueSupplier>(sourceFactory);
5455

56+
/// <summary>
57+
/// Adds a cascading value supplier to the <paramref name="serviceCollection"/>. This allows supplying cascading values based on custom attributes, which can be used to support multiple cascading values of the same type with different matching criteria.
58+
/// </summary>
59+
/// <typeparam name="TAttribute">The attribute type.</typeparam>
60+
/// <param name="serviceCollection">The <see cref="IServiceCollection"/>.</param>
61+
/// <param name="subscribeFactory">A callback that supplies a <see cref="CascadingParameterSubscription"/> that handles subscriptions for attribute.</param>
62+
/// <returns>The <see cref="IServiceCollection"/>.</returns>
63+
public static IServiceCollection TryAddCascadingValueSupplier<TAttribute>(
64+
this IServiceCollection serviceCollection,
65+
Func<IServiceProvider, Func<ComponentState, TAttribute, CascadingParameterInfo, CascadingParameterSubscription>> subscribeFactory)
66+
where TAttribute : CascadingParameterAttributeBase
67+
{
68+
serviceCollection.TryAddEnumerable(
69+
ServiceDescriptor.Scoped<ICascadingValueSupplier, CascadingParameterValueProvider<TAttribute>>(
70+
sp => new CascadingParameterValueProvider<TAttribute>(subscribeFactory(sp))));
71+
return serviceCollection;
72+
}
73+
5574
/// <summary>
5675
/// Adds a cascading value to the <paramref name="serviceCollection"/>, if none is already registered
5776
/// with the value type. This is equivalent to having a fixed <see cref="CascadingValue{TValue}"/> at
@@ -91,7 +110,7 @@ public static void TryAddCascadingValue<TValue>(
91110
/// Adds a cascading value to the <paramref name="serviceCollection"/>, if none is already registered
92111
/// with the value type. This is equivalent to having a fixed <see cref="CascadingValue{TValue}"/> at
93112
/// the root of the component hierarchy.
94-
///
113+
///
95114
/// With this overload, you can supply a <see cref="CascadingValueSource{TValue}"/> which allows you
96115
/// to notify about updates to the value later, causing recipients to re-render. This overload should
97116
/// only be used if you plan to update the value dynamically.

src/Components/Components/src/Microsoft.AspNetCore.Components.csproj

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,8 @@
1818
<Compile Include="$(ComponentsSharedSourceRoot)src\JsonSerializerOptionsProvider.cs" />
1919
<Compile Include="$(ComponentsSharedSourceRoot)src\HotReloadManager.cs" LinkBase="HotReload" />
2020
<Compile Include="$(ComponentsSharedSourceRoot)src\RootTypeCache.cs" LinkBase="Shared" />
21+
<Compile Include="$(ComponentsSharedSourceRoot)src\Reflection\PropertyGetter.cs" LinkBase="Shared" />
22+
<Compile Include="$(ComponentsSharedSourceRoot)src\Reflection\PropertySetter.cs" LinkBase="Shared" />
2123
<Compile Include="$(SharedSourceRoot)LinkerFlags.cs" LinkBase="Shared" />
2224
<Compile Include="$(SharedSourceRoot)PooledArrayBufferWriter.cs" LinkBase="Shared" />
2325
<Compile Include="$(SharedSourceRoot)QueryStringEnumerable.cs" LinkBase="Shared" />

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

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,8 @@
11
#nullable enable
2+
abstract Microsoft.AspNetCore.Components.CascadingParameterSubscription.Dispose() -> void
3+
abstract Microsoft.AspNetCore.Components.CascadingParameterSubscription.GetCurrentValue() -> object?
4+
Microsoft.AspNetCore.Components.CascadingParameterSubscription
5+
Microsoft.AspNetCore.Components.CascadingParameterSubscription.CascadingParameterSubscription() -> void
26
static Microsoft.AspNetCore.Components.NavigationManagerExtensions.GetUriWithHash(this Microsoft.AspNetCore.Components.NavigationManager! navigationManager, string! hash) -> string!
37
Microsoft.AspNetCore.Components.NavigationOptions.RelativeToCurrentUri.get -> bool
48
Microsoft.AspNetCore.Components.NavigationOptions.RelativeToCurrentUri.init -> void
@@ -7,6 +11,7 @@ Microsoft.AspNetCore.Components.IComponentPropertyActivator.GetActivator(System.
711
*REMOVED*Microsoft.AspNetCore.Components.ResourceAsset.ResourceAsset(string! url, System.Collections.Generic.IReadOnlyList<Microsoft.AspNetCore.Components.ResourceAssetProperty!>? properties) -> void
812
*REMOVED*Microsoft.AspNetCore.Components.Routing.Router.PreferExactMatches.get -> bool
913
*REMOVED*Microsoft.AspNetCore.Components.Routing.Router.PreferExactMatches.set -> void
14+
static Microsoft.Extensions.DependencyInjection.CascadingValueServiceCollectionExtensions.TryAddCascadingValueSupplier<TAttribute>(this Microsoft.Extensions.DependencyInjection.IServiceCollection! serviceCollection, System.Func<System.IServiceProvider!, System.Func<Microsoft.AspNetCore.Components.Rendering.ComponentState!, TAttribute!, Microsoft.AspNetCore.Components.CascadingParameterInfo, Microsoft.AspNetCore.Components.CascadingParameterSubscription!>!>! subscribeFactory) -> Microsoft.Extensions.DependencyInjection.IServiceCollection!
1015
Microsoft.AspNetCore.Components.BrowserConfiguration
1116
Microsoft.AspNetCore.Components.BrowserConfiguration.BrowserConfiguration() -> void
1217
Microsoft.AspNetCore.Components.BrowserConfiguration.LogLevel.get -> int?

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

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -74,6 +74,9 @@ public static IRazorComponentsBuilder AddRazorComponents(this IServiceCollection
7474
services.TryAddCascadingValue(sp => sp.GetRequiredService<EndpointHtmlRenderer>().HttpContext);
7575
services.TryAddScoped<ResourcePreloadService>();
7676
services.AddTempData();
77+
services.TryAddScoped<TempDataCascadingValueSupplier>();
78+
services.TryAddCascadingValueSupplier<SupplyParameterFromTempDataAttribute>(
79+
sp => sp.GetRequiredService<TempDataCascadingValueSupplier>().CreateSubscription);
7780

7881
services.TryAddScoped<ResourceCollectionProvider>();
7982
RegisterPersistentComponentStateServiceCollectionExtensions.AddPersistentServiceRegistration<ResourceCollectionProvider>(services, RenderMode.InteractiveWebAssembly);

src/Components/Endpoints/src/DependencyInjection/TempDataService.cs

Lines changed: 6 additions & 0 deletions
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.Http;
5+
using Microsoft.Extensions.DependencyInjection;
56

67
namespace Microsoft.AspNetCore.Components.Endpoints;
78

@@ -26,6 +27,11 @@ public TempData CreateEmpty(HttpContext httpContext)
2627

2728
public void Save(HttpContext httpContext, TempData tempData)
2829
{
30+
if (httpContext.RequestServices.GetService<TempDataCascadingValueSupplier>() is { } supplier)
31+
{
32+
supplier.PersistValues(tempData);
33+
}
34+
2935
if (!tempData.WasLoaded)
3036
{
3137
return;

src/Components/Endpoints/src/Microsoft.AspNetCore.Components.Endpoints.csproj

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,8 @@
3737
<Compile Include="$(SharedSourceRoot)ChunkingCookieManager\**\*.cs" LinkBase="Internal" />
3838
<Compile Include="$(SharedSourceRoot)Components\ComponentsActivityLinkStore.cs" LinkBase="Shared" />
3939
<Compile Include="$(ComponentsSharedSourceRoot)src\HotReloadManager.cs" LinkBase="HotReload" />
40+
<Compile Include="$(ComponentsSharedSourceRoot)src\Reflection\PropertyGetter.cs" />
41+
<Compile Include="$(ComponentsSharedSourceRoot)src\Reflection\PropertySetter.cs" />
4042

4143
<Compile Include="$(SharedSourceRoot)PropertyHelper\**\*.cs" />
4244

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

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -122,6 +122,11 @@ internal async Task InitializeStandardComponentServicesAsync(
122122
antiforgery.SetRequestContext(httpContext);
123123
}
124124

125+
if (httpContext.RequestServices.GetService<TempDataCascadingValueSupplier>() is {} tempDataSupplier)
126+
{
127+
tempDataSupplier.SetRequestContext(httpContext);
128+
}
129+
125130
// It's important that this is initialized since a component might try to restore state during prerendering
126131
// (which will obviously not work, but should not fail)
127132
var componentApplicationLifetime = httpContext.RequestServices.GetRequiredService<ComponentStatePersistenceManager>();
Lines changed: 175 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,175 @@
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 System.Reflection;
6+
using Microsoft.AspNetCore.Components.Reflection;
7+
using Microsoft.AspNetCore.Components.Rendering;
8+
using Microsoft.AspNetCore.Components.Web;
9+
using Microsoft.AspNetCore.Http;
10+
using Microsoft.Extensions.Logging;
11+
12+
namespace Microsoft.AspNetCore.Components.Endpoints;
13+
14+
internal partial class TempDataCascadingValueSupplier
15+
{
16+
private static readonly ConcurrentDictionary<(Type, string), PropertyGetter> _propertyGetterCache = new();
17+
private HttpContext? _httpContext;
18+
private readonly Dictionary<string, Func<object?>> _valueCallbacks = new(StringComparer.OrdinalIgnoreCase);
19+
private readonly ILogger<TempDataCascadingValueSupplier> _logger;
20+
21+
public TempDataCascadingValueSupplier(ILogger<TempDataCascadingValueSupplier> logger)
22+
{
23+
_logger = logger;
24+
}
25+
26+
internal void SetRequestContext(HttpContext httpContext)
27+
{
28+
_httpContext = httpContext;
29+
}
30+
31+
internal CascadingParameterSubscription CreateSubscription(
32+
ComponentState componentState,
33+
SupplyParameterFromTempDataAttribute attribute,
34+
CascadingParameterInfo parameterInfo)
35+
{
36+
var tempDataKey = attribute.Name ?? parameterInfo.PropertyName;
37+
var componentType = componentState.Component.GetType();
38+
var getter = _propertyGetterCache.GetOrAdd((componentType, parameterInfo.PropertyName), PropertyGetterFactory);
39+
Func<object?> valueGetter = () => getter.GetValue(componentState.Component);
40+
RegisterValueCallback(tempDataKey, valueGetter);
41+
return new TempDataSubscription(this, tempDataKey, parameterInfo.PropertyType, valueGetter);
42+
}
43+
44+
private static PropertyGetter PropertyGetterFactory((Type type, string propertyName) key)
45+
{
46+
var (type, propertyName) = key;
47+
var propertyInfo = type.GetProperty(propertyName, BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic);
48+
if (propertyInfo is null)
49+
{
50+
throw new InvalidOperationException($"A property '{propertyName}' on component type '{type.FullName}' wasn't found.");
51+
}
52+
return new PropertyGetter(type, propertyInfo);
53+
}
54+
55+
internal ITempData? GetTempData() => _httpContext is null
56+
? null
57+
: TempDataProviderServiceCollectionExtensions.GetOrCreateTempData(_httpContext);
58+
59+
internal void RegisterValueCallback(string tempDataKey, Func<object?> valueGetter)
60+
{
61+
if (!_valueCallbacks.TryAdd(tempDataKey, valueGetter))
62+
{
63+
throw new InvalidOperationException($"A callback is already registered for the TempData key '{tempDataKey}'. Multiple components cannot use the same TempData key for multiple [SupplyParameterFromTempData] attributes.");
64+
}
65+
}
66+
67+
internal void PersistValues(ITempData tempData)
68+
{
69+
if (_valueCallbacks.Count == 0)
70+
{
71+
return;
72+
}
73+
74+
foreach (var (key, valueGetter) in _valueCallbacks)
75+
{
76+
object? value = null;
77+
try
78+
{
79+
value = valueGetter();
80+
}
81+
catch (Exception ex)
82+
{
83+
Log.TempDataPersistFail(_logger, ex);
84+
continue;
85+
}
86+
tempData[key] = value;
87+
}
88+
}
89+
90+
internal void DeleteValueCallback(string tempDataKey)
91+
{
92+
_valueCallbacks.Remove(tempDataKey);
93+
}
94+
95+
private static partial class Log
96+
{
97+
[LoggerMessage(1, LogLevel.Warning, "Persisting of the TempData element failed.", EventName = "TempDataPersistFail")]
98+
public static partial void TempDataPersistFail(ILogger logger, Exception exception);
99+
100+
[LoggerMessage(2, LogLevel.Warning, "Deserialization of the element from TempData failed.", EventName = "TempDataDeserializeFail")]
101+
public static partial void TempDataDeserializeFail(ILogger logger, Exception exception);
102+
}
103+
104+
internal partial class TempDataSubscription : CascadingParameterSubscription
105+
{
106+
private readonly TempDataCascadingValueSupplier _owner;
107+
private readonly string _tempDataKey;
108+
private readonly Type _underlyingType;
109+
private readonly bool _isEnum;
110+
private readonly Func<object?> _currentValueGetter;
111+
private bool _delivered;
112+
113+
public TempDataSubscription(
114+
TempDataCascadingValueSupplier owner,
115+
string tempDataKey,
116+
Type propertyType,
117+
Func<object?> currentValueGetter)
118+
{
119+
_owner = owner;
120+
_tempDataKey = tempDataKey;
121+
_underlyingType = Nullable.GetUnderlyingType(propertyType) ?? propertyType;
122+
_isEnum = _underlyingType.IsEnum;
123+
_currentValueGetter = currentValueGetter;
124+
}
125+
126+
public override object? GetCurrentValue()
127+
{
128+
if (_delivered)
129+
{
130+
// After the first delivery, return the component's current property value
131+
// to avoid overriding modifications the component made during rendering.
132+
return _currentValueGetter();
133+
}
134+
135+
_delivered = true;
136+
137+
var tempData = _owner.GetTempData();
138+
if (tempData is null)
139+
{
140+
return null;
141+
}
142+
143+
try
144+
{
145+
var value = tempData.Get(_tempDataKey);
146+
if (value is null)
147+
{
148+
return null;
149+
}
150+
151+
if (_isEnum && value is int intValue)
152+
{
153+
return Enum.ToObject(_underlyingType, intValue);
154+
}
155+
156+
if (!_underlyingType.IsAssignableFrom(value.GetType()))
157+
{
158+
return null;
159+
}
160+
161+
return value;
162+
}
163+
catch (Exception ex)
164+
{
165+
Log.TempDataDeserializeFail(_owner._logger, ex);
166+
return null;
167+
}
168+
}
169+
170+
public override void Dispose()
171+
{
172+
_owner.DeleteValueCallback(_tempDataKey);
173+
}
174+
}
175+
}

0 commit comments

Comments
 (0)