Skip to content

Commit 5bb3215

Browse files
committed
Initial support for persisting component state
1 parent a2fd165 commit 5bb3215

32 files changed

+798
-92
lines changed

src/Components/Components/src/CascadingParameterState.cs

Lines changed: 6 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -13,17 +13,16 @@
1313
namespace Microsoft.AspNetCore.Components;
1414

1515
internal readonly struct CascadingParameterState
16+
(in CascadingParameterInfo parameterInfo, ICascadingValueSupplier valueSupplier, object? key)
1617
{
1718
private static readonly ConcurrentDictionary<Type, CascadingParameterInfo[]> _cachedInfos = new();
1819

19-
public CascadingParameterInfo ParameterInfo { get; }
20-
public ICascadingValueSupplier ValueSupplier { get; }
20+
public CascadingParameterInfo ParameterInfo { get; } = parameterInfo;
21+
public ICascadingValueSupplier ValueSupplier { get; } = valueSupplier;
22+
public object? Key { get; } = key;
2123

2224
public CascadingParameterState(in CascadingParameterInfo parameterInfo, ICascadingValueSupplier valueSupplier)
23-
{
24-
ParameterInfo = parameterInfo;
25-
ValueSupplier = valueSupplier;
26-
}
25+
: this(parameterInfo, valueSupplier, key: null) { }
2726

2827
public static IReadOnlyList<CascadingParameterState> FindCascadingParameters(ComponentState componentState, out bool hasSingleDeliveryParameters)
2928
{
@@ -55,7 +54,7 @@ public static IReadOnlyList<CascadingParameterState> FindCascadingParameters(Com
5554
{
5655
// Although not all parameters might be matched, we know the maximum number
5756
resultStates ??= new List<CascadingParameterState>(infos.Length - infoIndex);
58-
resultStates.Add(new CascadingParameterState(info, supplier));
57+
resultStates.Add(new CascadingParameterState(info, supplier, componentState));
5958

6059
if (info.Attribute.SingleDelivery)
6160
{

src/Components/Components/src/ICascadingValueSupplier.cs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,8 @@ internal interface ICascadingValueSupplier
1313

1414
object? GetCurrentValue(in CascadingParameterInfo parameterInfo);
1515

16+
object? GetCurrentValue(object? key, in CascadingParameterInfo parameterInfo) => GetCurrentValue(parameterInfo);
17+
1618
void Subscribe(ComponentState subscriber, in CascadingParameterInfo parameterInfo);
1719

1820
void Unsubscribe(ComponentState subscriber, in CascadingParameterInfo parameterInfo);

src/Components/Components/src/Infrastructure/ComponentStatePersistenceManager.cs

Lines changed: 15 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.Components.RenderTree;
5+
using Microsoft.Extensions.DependencyInjection;
56
using Microsoft.Extensions.Logging;
67

78
namespace Microsoft.AspNetCore.Components.Infrastructure;
@@ -15,17 +16,30 @@ public class ComponentStatePersistenceManager
1516
private readonly ILogger<ComponentStatePersistenceManager> _logger;
1617

1718
private bool _stateIsPersisted;
19+
private readonly PersistentServicesRegistry? _servicesRegistry;
1820
private readonly Dictionary<string, byte[]> _currentState = new(StringComparer.Ordinal);
1921

2022
/// <summary>
2123
/// Initializes a new instance of <see cref="ComponentStatePersistenceManager"/>.
2224
/// </summary>
25+
/// <param name="logger"></param>
2326
public ComponentStatePersistenceManager(ILogger<ComponentStatePersistenceManager> logger)
2427
{
2528
State = new PersistentComponentState(_currentState, _registeredCallbacks);
2629
_logger = logger;
2730
}
2831

32+
/// <summary>
33+
/// Initializes a new instance of <see cref="ComponentStatePersistenceManager"/>.
34+
/// </summary>
35+
/// <param name="logger"></param>
36+
/// <param name="serviceProvider"></param>
37+
public ComponentStatePersistenceManager(ILogger<ComponentStatePersistenceManager> logger, IServiceProvider serviceProvider) : this(logger)
38+
{
39+
_servicesRegistry = serviceProvider.GetService<PersistentServicesRegistry>();
40+
_servicesRegistry?.RegisterForPersistence(State);
41+
}
42+
2943
/// <summary>
3044
/// Gets the <see cref="ComponentStatePersistenceManager"/> associated with the <see cref="ComponentStatePersistenceManager"/>.
3145
/// </summary>
@@ -40,6 +54,7 @@ public async Task RestoreStateAsync(IPersistentComponentStateStore store)
4054
{
4155
var data = await store.GetPersistedStateAsync();
4256
State.InitializeExistingState(data);
57+
_servicesRegistry?.Restore(State);
4358
}
4459

4560
/// <summary>

src/Components/Components/src/ParameterView.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -437,7 +437,7 @@ public bool MoveNext()
437437
_currentIndex = nextIndex;
438438

439439
var state = _cascadingParameters[_currentIndex];
440-
var currentValue = state.ValueSupplier.GetCurrentValue(state.ParameterInfo);
440+
var currentValue = state.ValueSupplier.GetCurrentValue(state.Key, state.ParameterInfo);
441441
_current = new ParameterValue(state.ParameterInfo.PropertyName, currentValue!, true);
442442
return true;
443443
}

src/Components/Components/src/PersistentComponentState.cs

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -87,6 +87,24 @@ public PersistingComponentStateSubscription RegisterOnPersisting(Func<Task> call
8787
_currentState.Add(key, JsonSerializer.SerializeToUtf8Bytes(instance, JsonSerializerOptionsProvider.Options));
8888
}
8989

90+
[RequiresUnreferencedCode("JSON serialization and deserialization might require types that cannot be statically analyzed.")]
91+
internal void PersistAsJson(string key, object instance, [DynamicallyAccessedMembers(JsonSerialized)] Type type)
92+
{
93+
ArgumentNullException.ThrowIfNull(key);
94+
95+
if (!PersistingState)
96+
{
97+
throw new InvalidOperationException("Persisting state is only allowed during an OnPersisting callback.");
98+
}
99+
100+
if (_currentState.ContainsKey(key))
101+
{
102+
throw new ArgumentException($"There is already a persisted object under the same key '{key}'");
103+
}
104+
105+
_currentState.Add(key, JsonSerializer.SerializeToUtf8Bytes(instance, type, JsonSerializerOptionsProvider.Options));
106+
}
107+
90108
/// <summary>
91109
/// Tries to retrieve the persisted state as JSON with the given <paramref name="key"/> and deserializes it into an
92110
/// instance of type <typeparamref name="TValue"/>.
@@ -114,6 +132,24 @@ public PersistingComponentStateSubscription RegisterOnPersisting(Func<Task> call
114132
}
115133
}
116134

135+
[RequiresUnreferencedCode("JSON serialization and deserialization might require types that cannot be statically analyzed.")]
136+
internal bool TryTakeFromJson(string key, [DynamicallyAccessedMembers(JsonSerialized)] Type type, [MaybeNullWhen(false)] out object? instance)
137+
{
138+
ArgumentNullException.ThrowIfNull(type);
139+
ArgumentNullException.ThrowIfNull(key);
140+
if (TryTake(key, out var data))
141+
{
142+
var reader = new Utf8JsonReader(data);
143+
instance = JsonSerializer.Deserialize(ref reader, type, JsonSerializerOptionsProvider.Options);
144+
return true;
145+
}
146+
else
147+
{
148+
instance = default;
149+
return false;
150+
}
151+
}
152+
117153
private bool TryTake(string key, out byte[]? value)
118154
{
119155
ArgumentNullException.ThrowIfNull(key);
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
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+
internal interface IPersistentComponentRegistration
7+
{
8+
public string Assembly { get; }
9+
public string FullTypeName { get; }
10+
}
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
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.Diagnostics;
5+
6+
namespace Microsoft.AspNetCore.Components;
7+
8+
[DebuggerDisplay($"{{{nameof(GetDebuggerDisplay)}(),nq}}")]
9+
internal class PersistentComponentRegistration<TService> : IPersistentComponentRegistration
10+
{
11+
public string Assembly => typeof(TService).Assembly.GetName().Name!;
12+
public string FullTypeName => typeof(TService).FullName!;
13+
14+
private string GetDebuggerDisplay() => $"{Assembly}::{FullTypeName}";
15+
}
Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,82 @@
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.Diagnostics.CodeAnalysis;
6+
using System.Reflection;
7+
8+
namespace Microsoft.AspNetCore.Components;
9+
10+
// A cache for registered persistent services. This is similar to the `RootComponentTypeCache`.
11+
internal sealed class PersistentServiceTypeCache
12+
{
13+
private readonly ConcurrentDictionary<Key, Type?> _typeToKeyLookUp = new();
14+
15+
public Type? GetPersistentService(string assembly, string type)
16+
{
17+
var key = new Key(assembly, type);
18+
if (_typeToKeyLookUp.TryGetValue(key, out var resolvedType))
19+
{
20+
return resolvedType;
21+
}
22+
else
23+
{
24+
return _typeToKeyLookUp.GetOrAdd(key, ResolveType, AppDomain.CurrentDomain.GetAssemblies());
25+
}
26+
}
27+
28+
[UnconditionalSuppressMessage("Trimming", "IL2026", Justification = "Types in this cache are added by calling `AddPersistentService` and are expected to be preserved.")]
29+
private static Type? ResolveType(Key key, Assembly[] assemblies)
30+
{
31+
Assembly? assembly = null;
32+
for (var i = 0; i < assemblies.Length; i++)
33+
{
34+
var current = assemblies[i];
35+
if (current.GetName().Name == key.Assembly)
36+
{
37+
assembly = current;
38+
break;
39+
}
40+
}
41+
42+
if (assembly == null)
43+
{
44+
// It might be that the assembly is not loaded yet, this can happen if the root component is defined in a
45+
// different assembly than the app and there is no reference from the app assembly to any type in the class
46+
// library that has been used yet.
47+
// In this case, try and load the assembly and look up the type again.
48+
// We only need to do this in the browser because its a different process, in the server the assembly will already
49+
// be loaded.
50+
if (OperatingSystem.IsBrowser())
51+
{
52+
try
53+
{
54+
assembly = Assembly.Load(key.Assembly);
55+
}
56+
catch
57+
{
58+
// It's fine to ignore the exception, since we'll return null below.
59+
}
60+
}
61+
}
62+
63+
return assembly?.GetType(key.Type, throwOnError: false, ignoreCase: false);
64+
}
65+
66+
private readonly struct Key : IEquatable<Key>
67+
{
68+
public Key(string assembly, string type) =>
69+
(Assembly, Type) = (assembly, type);
70+
71+
public string Assembly { get; }
72+
73+
public string Type { get; }
74+
75+
public override bool Equals(object? obj) => obj is Key key && Equals(key);
76+
77+
public bool Equals(Key other) => string.Equals(Assembly, other.Assembly, StringComparison.Ordinal) &&
78+
string.Equals(Type, other.Type, StringComparison.Ordinal);
79+
80+
public override int GetHashCode() => HashCode.Combine(Assembly, Type);
81+
}
82+
}

0 commit comments

Comments
 (0)