8000 [Blazor] Support for declaratively persisting component and services … · dotnet/aspnetcore@90cc8cd · GitHub
[go: up one dir, main page]

Skip to content

Commit 90cc8cd

Browse files
authored
[Blazor] Support for declaratively persisting component and services state (#60634)
# Adds a declarative model for persistent component and services state This PR augments the persistent component state feature with a declarative model that allows the developer to place an attribute on components and services properties to indicate that they should be persisted during prerendering so that it is accessible when the application becomes interactive. ## Scenarios ### Serializing state for a component ```razor @page "/counter" <h1>Counter</h1> <p>Current count: @CurrentCount</p> <button class="btn btn-primary" @OnClick="IncrementCount">Click me</button> @code { [SupplyParameterFromPersistentComponentState] private int CurrentCount { get; set; } private void IncrementCount() { CurrentCount++; } } ``` * Properties annotated with `[SupplyParameterFromPersistentComponentState]` will be serialized and deserialized during prerendering. ### Serializing state for multiple components of the same type **ParentComponent.razor** ```razor @page "/parent" @foreach (var element in elements) { <ChildComponent @key="element.Name" /> } ``` **ChildComponent.razor** ```razor <div> <p>Current count: @Element.CurrentCount</p> <button class="btn btn-primary" @OnClick="IncrementCount">Click me</button> </div> @code { [SupplyParameterFromPersistentComponentState] public State Element { get; set; } private void IncrementCount() { Element.CurrentCount++; } protected override void OnInitialized() { Element ??= new State(); } private class State { public int CurrentCount { get; set; } } } ``` * Properties annotated with `[SupplyParameterFromPersistentComponentState]` will be serialized and deserialized during prerendering. * The `@key` directive is used to ensure that the state is correctly associated with the component instance. * The `Element` property is initialized in the `OnInitialized` method to avoid null reference exceptions similarly to how we do it for query parameters and form data. ### Serializing state for a service **CounterService.cs** ```csharp public class CounterService { [SupplyParameterFromPersistentComponentState] public int CurrentCount { get; set; } public void IncrementCount() { CurrentCount++; } } ``` **Program.cs** ``` builder.Services.AddRazorComponents().RegisterPersistentService<CounterService>(RenderMode.InteractiveAuto); ``` * Properties annotated with `[SupplyParameterFromPersistentComponentState]` will be serialized during prerendering and deserialized when the application becomes interactive. * The `AddPersistentService` method is used to register the service for persistence. * The render mode is required as can't be inferred from the service type. * `RenderMode.Server` - The service will be available for interactive server mode. * `RenderMode.Webassembly` - The service will be available for interactive webassembly mode. * `RenderMode.InteractiveAuto` - The service will be available for both interactive server and webassembly modes if a component renders in any of those modes. * The service will be resolved during interactive mode initialization and the properties annotated with `[SupplyParameterFromPersistentComponentState]` will be deserialized. ## Implementation details ### Key Computation #### For components We need to generate a unique key for each property that needs to be persisted. For components, this key is computed based on: - The parent component type - The component type - The property name - The `@key` directive if present and serializable (e.g., `Guid`, `DateOnly`, `TimeOnly`, and primitive types) The key computation ensures that even if multiple instances of the same component are present on the page (for example, in a loop), each instance's state can be uniquely identified and persisted. The key computation algorithm only takes into account a small subset of a component hierarchy for performance reasons. This limits the ability to persist state on recursive component hierarchies. Our recommendation for those scenarios is to persist the state at the top level of the hierarchy. #### For services Only persisting scoped services is supported. We need to generate a unique key for each property that needs to be persisted. The key for services is derived from: - The type used to register the persistent service - Assembly - Full type name - Property name Properties to be serialized are identified from the actual service instance. - This approach allows marking an abstraction as a persistent service. - Enables actual implementations to be internal or different types. - Supports shared code in different assemblies. - Each instance must expose the same properties. ### Serialization and Deserialization By default properties are serialized using the System.Text.Json serializer with default settings. Note that this method is not trimmer safe and requires the user to ensure that the types used are preserved through some other means. This is consistent with our usage of System.Text.Json across other areas of the product, like root component parameters or JSInterop.
1 parent a848bfa commit 90cc8cd

File tree

69 files changed

+2822
-475
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

69 files changed

+2822
-475
lines changed

AspNetCore.sln

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1772,6 +1772,10 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Microsoft.AspNetCore.Http.R
17721772
EndProject
17731773
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Microsoft.AspNetCore.Http.ValidationsGenerator", "src\Http\Http.Extensions\gen\Microsoft.AspNetCore.Http.ValidationsGenerator\Microsoft.AspNetCore.Http.ValidationsGenerator.csproj", "{7899F5DD-AA7C-4561-BAC4-E2EC78B7D157}"
17741774
EndProject
1775+
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Endpoints", "Endpoints", "{02EA681E-C7D8-13C7-8484-4AC65E1B71E8}"
1776+
EndProject
1777+
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "CustomElements", "CustomElements", "{E22DD5A6-06E2-490E-BD32-88D629FD6668}"
1778+
EndProject
17751779
Global
17761780
GlobalSection(SolutionConfigurationPlatforms) = preSolution
17771781
Debug|Any CPU = Debug|Any CPU
@@ -11768,6 +11772,15 @@ Global
1176811772
{7324770C-0871-4D73-BE3D-5E2F3E9E1B1E} = {D30A658D-61F6-444B-9AC7-F66A1A1B86B6}
1176911773
{B54A8F61-60DE-4AD9-87CA-D102F230678E} = {D30A658D-61F6-444B-9AC7-F66A1A1B86B6}
1177011774
{D30A658D-61F6-444B-9AC7-F66A1A1B86B6} = {5E46DC83-C39C-4E3A-B242-C064607F4367}
11775+
{76C3E22D-092B-4E8A-81F0-DCF071BFF4CD} = {E22DD5A6-06E2-490E-BD32-88D629FD6668}
11776+
{A05652B3-953E-4915-9D7F-0E361D988815} = {0CE1CC26-98CE-4022-A81C-E32AAFC9B819}
11777+
{AE4D272D-6F13-42C8-9404-C149188AFA33} = {02EA681E-C7D8-13C7-8484-4AC65E1B71E8}
11778+
{5D438258-CB19-4282-814F-974ABBC71411} = {02EA681E-C7D8-13C7-8484-4AC65E1B71E8}
11779+
{F5AE525F-F435-40F9-A567-4D5EC3B50D6E} = {5FE1FBC1-8CE3-4355-9866-44FE1307C5F1}
11780+
{87D58D50-20D1-4091-88C5-8D88DCCC2DE3} = {6126DCE4-9692-4EE2-B240-C65743572995}
11781+
{433F91E4-E39D-4EB0-B798-2998B3969A2C} = {6126DCE4-9692-4EE2-B240-C65743572995}
11782+
{8A021D6D-7935-4AB3-BB47-38D4FF9B0D13} = {6126DCE4-9692-4EE2-B240-C65743572995}
11783+
{757CBDE0-5D0A-4FD8-99F3-6C20BDDD4E63} = {5FE1FBC1-8CE3-4355-9866-44FE1307C5F1}
1177111784
{96EC4DD3-028E-6E27-5B14-08C21B07CE89} = {017429CC-C5FB-48B4-9C46-034E29EE2F06}
1177211785
{1BBD75D2-429D-D565-A98E-36437448E8C0} = {96EC4DD3-028E-6E27-5B14-08C21B07CE89}
1177311786
{C10EB67A-F43E-4B85-AEFD-7064C9B3DBE2} = {1BBD75D2-429D-D565-A98E-36437448E8C0}
@@ -11777,6 +11790,8 @@ Global
1177711790
{01A75167-DF5A-AF38-8700-C3FBB2C2CFF5} = {225AEDCF-7162-4A86-AC74-06B84660B379}
1177811791
{E6D564C0-4CA5-411C-BF40-9802AF7900CB} = {01A75167-DF5A-AF38-8700-C3FBB2C2CFF5}
1177911792
{7899F5DD-AA7C-4561-BAC4-E2EC78B7D157} = {01A75167-DF5A-AF38-8700-C3FBB2C2CFF5}
11793+
{02EA681E-C7D8-13C7-8484-4AC65E1B71E8} = {60D51C98-2CC0-40DF-B338-44154EFEE2FF}
11794+
{E22DD5A6-06E2-490E-BD32-88D629FD6668} = {60D51C98-2CC0-40DF-B338-44154EFEE2FF}
1178011795
EndGlobalSection
1178111796
GlobalSection(ExtensibilityGlobals) = postSolution
1178211797
SolutionGuid = {3E8720B3-DBDD-498C-B383-2CC32A054E8F}

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/CascadingValue.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -140,7 +140,7 @@ bool ICascadingValueSupplier.CanSupplyValue(in CascadingParameterInfo parameterI
140140
|| string.Equals(requestedName, Name, StringComparison.OrdinalIgnoreCase); // Also match on name
141141
}
142142

143-
object? ICascadingValueSupplier.GetCurrentValue(in CascadingParameterInfo parameterInfo)
143+
object? ICascadingValueSupplier.GetCurrentValue(object? key, in CascadingParameterInfo parameterInfo)
144144
{
145145
return Value;
146146
}

src/Components/Components/src/CascadingValueSource.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -149,7 +149,7 @@ bool ICascadingValueSupplier.CanSupplyValue(in CascadingParameterInfo parameterI
149149
|| string.Equals(requestedName, _name, StringComparison.OrdinalIgnoreCase); // Also match on name
150150
}
151151

152-
object? ICascadingValueSupplier.GetCurrentValue(in CascadingParameterInfo parameterInfo)
152+
object? ICascadingValueSupplier.GetCurrentValue(object? key, in CascadingParameterInfo parameterInfo)
153153
{
154154
if (_initialValueFactory is not null)
155155
{

src/Components/Components/src/ICascadingValueSupplier.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ internal interface ICascadingValueSupplier
1111

1212
bool CanSupplyValue(in CascadingParameterInfo parameterInfo);
1313

14-
object? GetCurrentValue(in CascadingParameterInfo parameterInfo);
14+
object? GetCurrentValue(object? key, in CascadingParameterInfo parameterInfo);
1515

1616
void Subscribe(ComponentState subscriber, in CascadingParameterInfo parameterInfo);
1717

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

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717
<Compile Include="$(ComponentsSharedSourceRoot)src\ArrayBuilder.cs" LinkBase="RenderTree" />
1818
<Compile Include="$(ComponentsSharedSourceRoot)src\JsonSerializerOptionsProvider.cs" />
1919
<Compile Include="$(ComponentsSharedSourceRoot)src\HotReloadManager.cs" LinkBase="HotReload" />
20+
<Compile Include="$(ComponentsSharedSourceRoot)src\RootTypeCache.cs" LinkBase="Shared" />
2021
<Compile Include="$(SharedSourceRoot)LinkerFlags.cs" LinkBase="Shared" />
2122
<Compile Include="$(SharedSourceRoot)QueryStringEnumerable.cs" LinkBase="Shared" />
2223
<Compile Include="$(SharedSourceRoot)Debugger\DictionaryItemDebugView.cs" LinkBase="Shared" />

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: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,11 @@ public PersistingComponentStateSubscription RegisterOnPersisting(Func<Task> call
5656
{
5757
ArgumentNullException.ThrowIfNull(callback);
5858

59+
if (PersistingState)
60+
{
61+
throw new InvalidOperationException("Registering a callback while persisting state is not allowed.");
62+
}
63+
5964
var persistenceCallback = new PersistComponentStateRegistration(callback, renderMode);
6065

6166
_registeredCallbacks.Add(persistenceCallback);
@@ -87,6 +92,24 @@ public PersistingComponentStateSubscription RegisterOnPersisting(Func<Task> call
8792
_currentState.Add(key, JsonSerializer.SerializeToUtf8Bytes(instance, JsonSerializerOptionsProvider.Options));
8893
}
8994

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

140+
[RequiresUnreferencedCode("JSON serialization and deserialization might require types that cannot be statically analyzed.")]
141+
internal bool TryTakeFromJson(string key, [DynamicallyAccessedMembers(JsonSerialized)] Type type, [MaybeNullWhen(false)] out object? instance)
142+
{
143+
ArgumentNullException.ThrowIfNull(type);
144+
ArgumentNullException.ThrowIfNull(key);
145+
if (TryTake(key, out var data))
146+
{
147+
var reader = new Utf8JsonReader(data);
148+
instance = JsonSerializer.Deserialize(ref reader, type, JsonSerializerOptionsProvider.Options);
149+
return true;
150+
}
151+
else
152+
{
153+
instance = default;
154+
return false;
155+
}
156+
}
157+
117158
private bool TryTake(string key, out byte[]? value)
118159
{
119160
ArgumentNullException.ThrowIfNull(key);

src/Components/Components/src/Infrastructure/ComponentStatePersistenceManager.cs renamed to src/Components/Components/src/PersistentState/ComponentStatePersistenceManager.cs

Lines changed: 94 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -15,17 +15,35 @@ public class ComponentStatePersistenceManager
1515
private readonly ILogger<ComponentStatePersistenceManager> _logger;
1616

1717
private bool _stateIsPersisted;
18+
private readonly PersistentServicesRegistry? _servicesRegistry;
1819
private readonly Dictionary<string, byte[]> _currentState = new(StringComparer.Ordinal);
1920

2021
/// <summary>
2122
/// Initializes a new instance of <see cref="ComponentStatePersistenceManager"/>.
2223
/// </summary>
24+
/// <param name="logger"></param>
2325
public ComponentStatePersistenceManager(ILogger<ComponentStatePersistenceManager> logger)
2426
{
2527
State = new PersistentComponentState(_currentState, _registeredCallbacks);
2628
_logger = logger;
2729
}
2830

31+
/// <summary>
32+
/// Initializes a new instance of <see cref="ComponentStatePersistenceManager"/>.
33+
/// </summary>
34+
/// <param name="logger"></param>
35+
/// <param name="serviceProvider"></param>
36+
public ComponentStatePersistenceManager(ILogger<ComponentStatePersistenceManager> logger, IServiceProvider serviceProvider) : this(logger)
37+
{
38+
_servicesRegistry = new PersistentServicesRegistry(serviceProvider);
39+
}
40+
41+
// For testing purposes only
42+
internal PersistentServicesRegistry? ServicesRegistry => _servicesRegistry;
43+
44+
// For testing purposes only
45+
internal List<PersistComponentStateRegistration> RegisteredCallbacks => _registeredCallbacks;
46+
2947
/// <summary>
3048
/// Gets the <see cref="ComponentStatePersistenceManager"/> associated with the <see cref="ComponentStatePersistenceManager"/>.
3149
/// </summary>
@@ -40,6 +58,7 @@ public async Task RestoreStateAsync(IPersistentComponentStateStore store)
4058
{
4159
var data = await store.GetPersistedStateAsync();
4260
State.InitializeExistingState(data);
61+
_servicesRegistry?.Restore(State);
4362
}
4463

4564
/// <summary>
@@ -59,6 +78,9 @@ public Task PersistStateAsync(IPersistentComponentStateStore store, Renderer ren
5978

6079
async Task PauseAndPersistState()
6180
{
81+
// Ensure that we register the services before we start persisting the state.
82+
_servicesRegistry?.RegisterForPersistence(State);
83+
6284
State.PersistingState = true;
6385

6486
if (store is IEnumerable<IPersistentComponentStateStore> compositeStore)
@@ -72,24 +94,53 @@ async Task PauseAndPersistState()
7294
// the next store can start with a clean slate.
7395
foreach (var store in compositeStore)
7496
{
75-
await PersistState(store);
97+
var result = await TryPersistState(store);
98+
if (!result)
99+
{
100+
break;
101+
}
76102
_currentState.Clear();
77103
}
78104
}
79105
else
80106
{
81-
await PersistState(store);
107+
await TryPersistState(store);
82108
}
83109

84110
State.PersistingState = false;
85111
_stateIsPersisted = true;
86112
}
87113

88-
async Task PersistState(IPersistentComponentStateStore store)
114+
async Task<bool> TryPersistState(IPersistentComponentStateStore store)
89115
{
90-
await PauseAsync(store);
116+
if (!await TryPauseAsync(store))
117+
{
118+
_currentState.Clear();
119+
return false;
120+
}
121+
91122
await store.PersistStateAsync(_currentState);
123+
return true;
124+
}
125+
}
126+
127+
/// <summary>
128+
/// Initializes the render mode for state pe 10000 rsisted by the platform.
129+
/// </summary>
130+
/// <param name="renderMode">The render mode to use for state persisted by the platform.</param>
131+
/// <exception cref="InvalidOperationException">when the render mode is already set.</exception>
132+
public void SetPlatformRenderMode(IComponentRenderMode renderMode)
133+
{
134+
if (_servicesRegistry == null)
135+
{
136+
return;
137+
}
138+
else if (_servicesRegistry?.RenderMode != null)
139+
{
140+
throw new InvalidOperationException("Render mode already set.");
92141
}
142+
143+
_servicesRegistry!.RenderMode = renderMode;
93144
}
94145

95146
private void InferRenderModes(Renderer renderer)
@@ -125,11 +176,17 @@ private void InferRenderModes(Renderer renderer)
125176
}
126177
}
127178

128-
internal Task PauseAsync(IPersistentComponentStateStore store)
179+
internal Task<bool> TryPauseAsync(IPersistentComponentStateStore store)
129180
{
130-
List<Task>? pendingCallbackTasks = null;
181+
List<Task<bool>>? pendingCallbackTasks = null;
131182

132-
for (var i = 0; i < _registeredCallbacks.Count; i++)
183+
// We are iterating backwards to allow the callbacks to remove themselves from the list.
184+
// Otherwise, we would have to make a copy of the list to avoid running into situations
185+
// where we don't run all the callbacks because the count of the list changed while we
186+
// were iterating over it.
187+
// It is not allowed to register a callback while we are persisting the state, so we don't
188+
// need to worry about new callbacks being added to the list.
189+
for (var i = _registeredCallbacks.Count - 1; i >= 0; i--)
133190
{
134191
var registration = _registeredCallbacks[i];
135192

@@ -142,31 +199,38 @@ internal Task PauseAsync(IPersistentComponentStateStore store)
142199
continue;
143200
}
144201

145-
var result = ExecuteCallback(registration.Callback, _logger);
202+
var result = TryExecuteCallback(registration.Callback, _logger);
146203
if (!result.IsCompletedSuccessfully)
147204
{
148-
pendingCallbackTasks ??= new();
205+
pendingCallbackTasks ??= [];
149206
pendingCallbackTasks.Add(result);
150207
}
208+
else
209+
{
210+
if (!result.Result)
211+
{
212+
return Task.FromResult(false);
213+
}
214+
}
151215
}
152216

153217
if (pendingCallbackTasks != null)
154218
{
155-
return Task.WhenAll(pendingCallbackTasks);
219+
return AnyTaskFailed(pendingCallbackTasks);
156220
}
157221
else
158222
{
159-
return Task.CompletedTask;
223+
return Task.FromResult(true);
160224
}
161225

162-
static Task ExecuteCallback(Func<Task> callback, ILogger<ComponentStatePersistenceManager> logger)
226+
static Task<bool> TryExecuteCallback(Func<Task> callback, ILogger<ComponentStatePersistenceManager> logger)
163227
{
164228
try
165229
{
166230
var current = callback();
167231
if (current.IsCompletedSuccessfully)
168232
{
169-
return current;
233+
return Task.FromResult(true);
170234
}
171235
else
172236
{
@@ -176,21 +240,35 @@ static Task ExecuteCallback(Func<Task> callback, ILogger<ComponentStatePersisten
176240
catch (Exception ex)
177241
{
178242
logger.LogError(new EventId(1000, "PersistenceCallbackError"), ex, "There was an error executing a callback while pausing the application.");
179-
return Task.CompletedTask;
243+
return Task.FromResult(false);
180244
}
181245

182-
static async Task Awaited(Task task, ILogger<ComponentStatePersistenceManager> logger)
246+
static async Task<bool> Awaited(Task task, ILogger<ComponentStatePersistenceManager> logger)
183247
{
184248
try
185249
{
186250
await task;
251+
return true;
187252
}
188253
catch (Exception ex)
189254
{
190255
logger.LogError(new EventId(1000, "PersistenceCallbackError"), ex, "There was an error executing a callback while pausing the application.");
191-
return;
256+
return false;
192257
}
193258
}
194259
}
260+
261+
static async Task<bool> AnyTaskFailed(List<Task<bool>> pendingCallbackTasks)
262+
{
263+
foreach (var result in await Task.WhenAll(pendingCallbackTasks))
264+
{
265+
if (!result)
266+
{
267+
return false;
268+
}
269+
}
270+
271+
return true;
272+
}
195273
}
196274
}

0 commit comments

Comments
 (0)
0