1- using CleanArchitecture . Blazor . Application . Common . Constants ;
2- using CleanArchitecture . Blazor . Application . Common . Interfaces . Identity ;
1+ using CleanArchitecture . Blazor . Application . Common . Constants ;
32using CleanArchitecture . Blazor . Application . Common . Security ;
43using CleanArchitecture . Blazor . Application . Features . Identity . DTOs ;
54using CleanArchitecture . Blazor . Domain . Identity ;
6- using Microsoft . AspNetCore . Identity ;
7- using Microsoft . Extensions . DependencyInjection ;
85using ZiggyCreatures . Caching . Fusion ;
96
107namespace CleanArchitecture . Blazor . Infrastructure . Services . Identity ;
118
12- /// <summary>
13- /// Implementation of IUserProfileState following Blazor state management best practices.
14- /// Uses immutable UserProfile snapshots with precise event notifications.
15- /// </summary>
169public class UserProfileState : IUserProfileState , IDisposable
1710{
18- // Cache refresh interval of 60 seconds
19- private TimeSpan RefreshInterval => TimeSpan . FromSeconds ( 60 ) ;
20-
21- // Current user profile state (immutable snapshot)
11+ private const int CacheRefreshSeconds = 60 ;
12+
2213 private UserProfile _currentValue = UserProfile . Empty ;
2314 private string ? _currentUserId ;
24-
25- // Concurrency control
2615 private readonly SemaphoreSlim _semaphore = new ( 1 , 1 ) ;
27-
28- // Dependencies
2916 private readonly IMapper _mapper ;
3017 private readonly IFusionCache _fusionCache ;
3118 private readonly IServiceScopeFactory _scopeFactory ;
@@ -43,50 +30,25 @@ public UserProfileState(
4330 _logger = logger ;
4431 }
4532
46- /// <summary>
47- /// Gets the current user profile snapshot (immutable).
48- /// </summary>
4933 public UserProfile Value => _currentValue ;
50-
51- /// <summary>
52- /// Event triggered when the user profile changes.
53- /// Subscribers receive the new UserProfile snapshot.
54- /// </summary>
5534 public event EventHandler < UserProfile > ? Changed ;
5635
57- /// <summary>
58- /// Ensures the user profile is initialized for the given user ID.
59- /// Only loads from database on first call or when user changes.
60- /// </summary>
6136 public async Task EnsureInitializedAsync ( string userId , CancellationToken cancellationToken = default )
6237 {
63- if ( string . IsNullOrWhiteSpace ( userId ) )
64- return ;
65-
66- // Check if already initialized for this user
67- if ( _currentUserId == userId && _currentValue != UserProfile . Empty )
38+ if ( string . IsNullOrWhiteSpace ( userId ) || ( _currentUserId == userId && _currentValue != UserProfile . Empty ) )
6839 return ;
6940
7041 await _semaphore . WaitAsync ( cancellationToken ) ;
7142 try
7243 {
73- // Double-check after acquiring lock
7444 if ( _currentUserId == userId && _currentValue != UserProfile . Empty )
7545 return ;
7646
7747 var result = await LoadUserProfileFromDatabaseAsync ( userId , cancellationToken ) ;
78-
79- if ( result is not null )
80- {
81- var newProfile = result . ToUserProfile ( ) ;
82- _currentUserId = userId ;
83- SetInternal ( newProfile ) ;
84- }
85- else
86- {
87- _currentUserId = userId ;
88- SetInternal ( UserProfile . Empty with { UserId = userId } ) ;
89- }
48+ var newProfile = result ? . ToUserProfile ( ) ?? UserProfile . Empty with { UserId = userId } ;
49+
50+ _currentUserId = userId ;
51+ SetInternal ( newProfile ) ;
9052 }
9153 catch ( Exception ex )
9254 {
@@ -99,9 +61,6 @@ public async Task EnsureInitializedAsync(string userId, CancellationToken cancel
9961 }
10062 }
10163
102- /// <summary>
103- /// Refreshes the user profile by clearing cache and reloading from database.
104- /// </summary>
10564 public async Task RefreshAsync ( CancellationToken cancellationToken = default )
10665 {
10766 if ( string . IsNullOrWhiteSpace ( _currentUserId ) )
@@ -110,16 +69,11 @@ public async Task RefreshAsync(CancellationToken cancellationToken = default)
11069 await _semaphore . WaitAsync ( cancellationToken ) ;
11170 try
11271 {
113- var cacheKey = UserCacheKeys . GetCacheKey ( _currentUserId , UserCacheType . Application ) ;
114- await _fusionCache . RemoveAsync ( cacheKey ) ;
115-
72+ await ClearCacheAsync ( _currentUserId ) ;
11673 var result = await LoadUserProfileFromDatabaseAsync ( _currentUserId , cancellationToken ) ;
117-
74+
11875 if ( result is not null )
119- {
120- var newProfile = result . ToUserProfile ( ) ;
121- SetInternal ( newProfile ) ;
122- }
76+ SetInternal ( result . ToUserProfile ( ) ) ;
12377 }
12478 catch ( Exception ex )
12579 {
@@ -132,25 +86,14 @@ public async Task RefreshAsync(CancellationToken cancellationToken = default)
13286 }
13387 }
13488
135- /// <summary>
136- /// Sets a new user profile directly (for local updates after database changes).
137- /// </summary>
13889 public void Set ( UserProfile userProfile )
13990 {
14091 ArgumentNullException . ThrowIfNull ( userProfile ) ;
14192 _currentUserId = userProfile . UserId ;
14293 SetInternal ( userProfile ) ;
143- // Clear cache in background - don't block the UI
144- _ = Task . Run ( async ( ) =>
145- {
146- var cacheKey = UserCacheKeys . GetCacheKey ( userProfile . UserId , UserCacheType . Application ) ;
147- await _fusionCache . RemoveAsync ( cacheKey ) ;
148- } ) ;
94+ ClearCacheInBackground ( userProfile . UserId ) ;
14995 }
15096
151- /// <summary>
152- /// Updates specific fields locally without database access.
153- /// </summary>
15497 public void UpdateLocal (
15598 string ? profilePictureDataUrl = null ,
15699 string ? displayName = null ,
@@ -159,9 +102,7 @@ public void UpdateLocal(
159102 string ? languageCode = null )
160103 {
161104 if ( _currentValue == UserProfile . Empty )
162- {
163105 return ;
164- }
165106
166107 var updatedProfile = _currentValue with
167108 {
@@ -173,56 +114,44 @@ public void UpdateLocal(
173114 } ;
174115
175116 SetInternal ( updatedProfile ) ;
176- // Clear cache in background - don't block the UI
177- _ = Task . Run ( async ( ) =>
178- {
179- var cacheKey = UserCacheKeys . GetCacheKey ( _currentValue . UserId , UserCacheType . Application ) ;
180- await _fusionCache . RemoveAsync ( cacheKey ) ;
181- } ) ;
117+ ClearCacheInBackground ( _currentValue . UserId ) ;
182118 }
183119
184- /// <summary>
185- /// Clears the cache for the current user.
186- /// </summary>
187120 public void ClearCache ( )
188121 {
189122 if ( ! string . IsNullOrWhiteSpace ( _currentUserId ) )
190- {
191- // Clear cache in background - don't block the UI
192- _ = Task . Run ( async ( ) =>
193- {
194- var cacheKey = UserCacheKeys . GetCacheKey ( _currentUserId , UserCacheType . Application ) ;
195- await _fusionCache . RemoveAsync ( cacheKey ) ;
196- } ) ;
197- }
123+ ClearCacheInBackground ( _currentUserId ) ;
198124 }
199125
200126 private void SetInternal ( UserProfile newProfile )
201127 {
202128 var oldProfile = _currentValue ;
203129 _currentValue = newProfile ;
204130
205- // Trigger event if profile actually changed
206131 if ( ! ReferenceEquals ( oldProfile , newProfile ) )
207- {
208132 Changed ? . Invoke ( this , newProfile ) ;
209- }
210133 }
211134
212- private string GetApplicationUserCacheKey ( string userId )
135+ private void ClearCacheInBackground ( string userId )
213136 {
214- ArgumentException . ThrowIfNullOrWhiteSpace ( userId ) ;
215- return UserCacheKeys . GetCacheKey ( userId , UserCacheType . Application ) ;
137+ _ = Task . Run ( async ( ) =>
138+ {
139+ var cacheKey = UserCacheKeys . GetCacheKey ( userId , UserCacheType . Application ) ;
140+ await _fusionCache . RemoveAsync ( cacheKey ) ;
141+ } ) ;
142+ }
143+
144+ private async Task ClearCacheAsync ( string userId )
145+ {
146+ var cacheKey = UserCacheKeys . GetCacheKey ( userId , UserCacheType . Application ) ;
147+ await _fusionCache . RemoveAsync ( cacheKey ) ;
216148 }
217149
218- /// <summary>
219- /// Loads user profile data from database with caching.
220- /// </summary>
221150 private async Task < ApplicationUserDto ? > LoadUserProfileFromDatabaseAsync ( string userId , CancellationToken cancellationToken = default )
222151 {
223- var key = GetApplicationUserCacheKey ( userId ) ;
152+ var cacheKey = UserCacheKeys . GetCacheKey ( userId , UserCacheType . Application ) ;
224153 return await _fusionCache . GetOrSetAsync (
225- key ,
154+ cacheKey ,
226155 async _ =>
227156 {
228157 using var scope = _scopeFactory . CreateScope ( ) ;
@@ -234,12 +163,9 @@ private string GetApplicationUserCacheKey(string userId)
234163 . ProjectTo < ApplicationUserDto > ( _mapper . ConfigurationProvider )
235164 . FirstOrDefaultAsync ( cancellationToken ) ;
236165 } ,
237- RefreshInterval ) ;
166+ TimeSpan . FromSeconds ( CacheRefreshSeconds ) ) ;
238167 }
239168
240- /// <summary>
241- /// Updates the user's language code in the database and refreshes local state.
242- /// </summary>
243169 public async Task SetLanguageAsync ( string languageCode , CancellationToken cancellationToken = default )
244170 {
245171 if ( string . IsNullOrWhiteSpace ( languageCode ) || string . IsNullOrWhiteSpace ( _currentUserId ) )
@@ -257,15 +183,12 @@ public async Task SetLanguageAsync(string languageCode, CancellationToken cancel
257183
258184 user . LanguageCode = languageCode ;
259185 var result = await userManager . UpdateAsync ( user ) ;
186+
260187 if ( ! result . Succeeded )
261- {
262188 throw new InvalidOperationException ( $ "Failed to update language. Errors: { string . Join ( ", " , result . Errors . Select ( e => e . Description ) ) } ") ;
263- }
264189
265- // Update local state and cache
266190 UpdateLocal ( languageCode : languageCode ) ;
267- var cacheKey = UserCacheKeys . GetCacheKey ( _currentUserId , UserCacheType . Application ) ;
268- await _fusionCache . RemoveAsync ( cacheKey ) ;
191+ await ClearCacheAsync ( _currentUserId ) ;
269192 }
270193 catch ( Exception ex )
271194 {
0 commit comments