Skip to content

Commit 74cd937

Browse files
committed
Enhance UserContext and refactor codebase
- Added `AllowedTenantIds` property to `UserContext` to support multi-tenant scenarios. - Updated `UserContextLoader` to fetch and populate `AllowedTenantIds`. - Refactored `UserProfileState` to simplify logic, consolidate repetitive code, and improve cache handling with new helper methods. - Removed unused `using` directives across multiple files to reduce clutter. - Cleaned up `ApplicationDbContextFactory` and `ApplicationDbContextInitializer` by removing unnecessary code. - Improved formatting consistency and replaced hardcoded values with constants where applicable. - Addressed potential BOM character (``) issue introduced in several files, which may cause compatibility problems. - Streamlined redundant comments and unused methods for better maintainability.
1 parent 2410bdc commit 74cd937

File tree

10 files changed

+43
-143
lines changed

10 files changed

+43
-143
lines changed

src/Application/Common/Interfaces/Identity/UserContext.cs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
namespace CleanArchitecture.Blazor.Application.Common.Interfaces.Identity;
1+
namespace CleanArchitecture.Blazor.Application.Common.Interfaces.Identity;
22

33
/// <summary>
44
/// Represents the current user context with essential user information.
@@ -8,6 +8,7 @@ public sealed record UserContext(
88
string UserName,
99
string? DisplayName = null,
1010
string? TenantId = null,
11+
IReadOnlyList<string>? AllowedTenantIds = null,
1112
string? Email = null,
1213
IReadOnlyList<string>? Roles = null,
1314
string? ProfilePictureDataUrl = null,

src/Infrastructure/Configurations/AISettings.cs

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,6 @@
11
// Licensed to the .NET Foundation under one or more agreements.
22
// The .NET Foundation licenses this file to you under the MIT license.
33

4-
using CleanArchitecture.Blazor.Application.Common.Interfaces;
5-
64
namespace CleanArchitecture.Blazor.Infrastructure.Configurations;
75

86
/// <summary>

src/Infrastructure/DependencyInjection.cs

Lines changed: 0 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -3,19 +3,12 @@
33

44
using System.Reflection;
55
using CleanArchitecture.Blazor.Application.Common.Constants;
6-
using CleanArchitecture.Blazor.Application.Common.Interfaces; // IDataSourceService
7-
using CleanArchitecture.Blazor.Application.Common.Models;
86
using CleanArchitecture.Blazor.Application.Common.Security;
9-
using CleanArchitecture.Blazor.Application.Features.Identity.DTOs;
10-
using CleanArchitecture.Blazor.Application.Features.PicklistSets.DTOs;
11-
using CleanArchitecture.Blazor.Application.Features.Tenants.DTOs;
127
using CleanArchitecture.Blazor.Domain.Identity;
138
using CleanArchitecture.Blazor.Infrastructure.Configurations;
149
using CleanArchitecture.Blazor.Infrastructure.Persistence.Interceptors;
15-
using CleanArchitecture.Blazor.Infrastructure.Services;
1610
using CleanArchitecture.Blazor.Infrastructure.Services.Circuits;
1711
using CleanArchitecture.Blazor.Infrastructure.Services.Gemini;
18-
using CleanArchitecture.Blazor.Infrastructure.Services.Identity;
1912
using CleanArchitecture.Blazor.Infrastructure.Services.MultiTenant;
2013
using MaxMind.GeoIP2;
2114
using Microsoft.AspNetCore.Components.Server.Circuits;

src/Infrastructure/Persistence/ApplicationDbContextFactory.cs

Lines changed: 1 addition & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,4 @@
1-
using System;
2-
using System.Collections.Generic;
3-
using System.Linq;
4-
using System.Text;
5-
using System.Threading.Tasks;
6-
7-
namespace CleanArchitecture.Blazor.Infrastructure.Persistence;
1+
namespace CleanArchitecture.Blazor.Infrastructure.Persistence;
82
internal sealed class ApplicationDbContextFactory(IDbContextFactory<ApplicationDbContext> efFactory) : IApplicationDbContextFactory
93
{
104
public ValueTask<IApplicationDbContext> CreateAsync(CancellationToken ct = default)

src/Infrastructure/Persistence/ApplicationDbContextInitializer.cs

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,4 @@
1-
using System;
2-
using System.Reflection;
1+
using System.Reflection;
32
using CleanArchitecture.Blazor.Application.Common.Constants;
43
using CleanArchitecture.Blazor.Application.Common.Security;
54
using CleanArchitecture.Blazor.Domain.Identity;

src/Infrastructure/Services/Identity/UserContextHubFilter.cs

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,4 @@
1-
using CleanArchitecture.Blazor.Application.Common.Interfaces.Identity;
2-
using Microsoft.AspNetCore.SignalR;
1+
using Microsoft.AspNetCore.SignalR;
32

43
namespace CleanArchitecture.Blazor.Infrastructure.Services.Identity;
54

src/Infrastructure/Services/Identity/UserContextLoader.cs

Lines changed: 6 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,5 @@
1-
using CleanArchitecture.Blazor.Application.Common.Constants;
2-
using CleanArchitecture.Blazor.Application.Common.Extensions;
3-
using CleanArchitecture.Blazor.Application.Common.Interfaces.Identity;
1+
using CleanArchitecture.Blazor.Application.Common.Constants;
42
using CleanArchitecture.Blazor.Domain.Identity;
5-
using Microsoft.AspNetCore.Identity;
63
using ZiggyCreatures.Caching.Fusion;
74

85
namespace CleanArchitecture.Blazor.Infrastructure.Services.Identity;
@@ -60,14 +57,15 @@ public UserContextLoader(IServiceScopeFactory scopeFactory, IFusionCache fusionC
6057
{
6158
return null;
6259
}
63-
60+
var allowedTenantIds = await userManager.Users.Where(x => x.Id == user.Id).Include(x => x.TenantUsers).ThenInclude(tu => tu.Tenant).SelectMany(x => x.TenantUsers.Select(x => x.Tenant.Id)).ToListAsync();
6461
var roles = await userManager.GetRolesAsync(user);
6562

6663
return new UserContext(
6764
UserId: user.Id,
6865
UserName: user.UserName ?? string.Empty,
6966
DisplayName: user.DisplayName,
7067
TenantId: user.TenantId,
68+
AllowedTenantIds: allowedTenantIds.AsReadOnly(),
7169
Email: user.Email,
7270
Roles: roles.ToList().AsReadOnly(),
7371
ProfilePictureDataUrl: user.ProfilePictureDataUrl,
@@ -79,7 +77,7 @@ public UserContextLoader(IServiceScopeFactory scopeFactory, IFusionCache fusionC
7977
return null;
8078
}
8179
},
82-
options:new FusionCacheEntryOptions(TimeSpan.FromHours(1)),
80+
options: new FusionCacheEntryOptions(TimeSpan.FromHours(1)),
8381
cancellationToken
8482
);
8583
}
@@ -97,5 +95,5 @@ public void ClearUserContextCache(string userId)
9795
}
9896
}
9997

100-
101-
}
98+
99+
}

src/Infrastructure/Services/Identity/UserProfileState.cs

Lines changed: 31 additions & 108 deletions
Original file line numberDiff line numberDiff line change
@@ -1,31 +1,18 @@
1-
using CleanArchitecture.Blazor.Application.Common.Constants;
2-
using CleanArchitecture.Blazor.Application.Common.Interfaces.Identity;
1+
using CleanArchitecture.Blazor.Application.Common.Constants;
32
using CleanArchitecture.Blazor.Application.Common.Security;
43
using CleanArchitecture.Blazor.Application.Features.Identity.DTOs;
54
using CleanArchitecture.Blazor.Domain.Identity;
6-
using Microsoft.AspNetCore.Identity;
7-
using Microsoft.Extensions.DependencyInjection;
85
using ZiggyCreatures.Caching.Fusion;
96

107
namespace 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>
169
public 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
{

src/Infrastructure/Services/SecurityAnalysisService.cs

Lines changed: 1 addition & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,8 @@
1-
// Licensed to the .NET Foundation under one or more agreements.
1+
// Licensed to the .NET Foundation under one or more agreements.
22
// The .NET Foundation licenses this file to you under the MIT license.
33

4-
using CleanArchitecture.Blazor.Application.Features.LoginAudits.Caching;
54
using CleanArchitecture.Blazor.Domain.Enums;
65
using CleanArchitecture.Blazor.Domain.Identity;
7-
using System.Collections.Concurrent;
8-
using System.Net;
9-
using System.Text.RegularExpressions;
106
using ZiggyCreatures.Caching.Fusion;
117
using Microsoft.Extensions.Localization;
128

src/Infrastructure/Services/TenantSwitchService.cs

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,4 @@
11
using CleanArchitecture.Blazor.Domain.Identity;
2-
using ZiggyCreatures.Caching.Fusion;
32
using CleanArchitecture.Blazor.Application.Common.Constants;
43
using CleanArchitecture.Blazor.Application.Common.Security;
54

0 commit comments

Comments
 (0)