From 6e2701ba1de61b5be5205c01554a62992915a0fe Mon Sep 17 00:00:00 2001
From: Boromir <boromir@squad.dev>
Date: Fri, 24 Apr 2026 16:38:24 -0700
Subject: [PATCH] =?UTF-8?q?fix(quality):=20resolve=20Web=20infrastructure?=
 =?UTF-8?q?=20warnings=20(CA1062,=20CA1031,=20CA2007)=20=E2=80=94=20closes?=
 =?UTF-8?q?=20#153=20-=20Add=20null-guard=20ArgumentNullException.ThrowIfN?=
 =?UTF-8?q?ull=20for=20external=20parameters=20(CA1062)=20-=20Replace=20ca?=
 =?UTF-8?q?tch(Exception)=20with=20specific=20exception=20types=20(CA1031)?=
 =?UTF-8?q?=20-=20Add=20ConfigureAwait(false)=20to=20async=20calls=20in=20?=
 =?UTF-8?q?handlers=20(CA2007)=20-=20Add=20InternalsVisibleTo=20+=20CLSCom?=
 =?UTF-8?q?pliant(false)=20to=20Web.csproj=20-=20Formatting=20and=20whites?=
 =?UTF-8?q?pace=20alignment=20throughout=20Web=20handlers?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 src/AppHost/AppHost.cs                        |   8 +-
 .../Components/Theme/ThemeProvider.razor.cs   |  14 +-
 src/Web/Data/BlogDbContext.cs                 |   4 +-
 src/Web/Data/BlogPostDto.cs                   |  14 +-
 .../BlogPosts/Create/CreateBlogPostHandler.cs |  44 +--
 .../BlogPosts/Delete/DeleteBlogPostHandler.cs |  56 ++--
 .../BlogPosts/Edit/EditBlogPostHandler.cs     | 116 +++----
 .../BlogPosts/List/GetBlogPostsHandler.cs     |  52 +--
 .../UserManagement/UserManagementHandler.cs   | 312 +++++++++---------
 src/Web/GlobalUsings.cs                       |   2 +
 src/Web/Program.cs                            |   4 +-
 src/Web/Security/RoleClaimsHelper.cs          | 238 ++++++-------
 src/Web/Web.csproj                            |  70 ++--
 13 files changed, 472 insertions(+), 462 deletions(-)

diff --git a/src/AppHost/AppHost.cs b/src/AppHost/AppHost.cs
index 74b89caa..96609fd0 100644
--- a/src/AppHost/AppHost.cs
+++ b/src/AppHost/AppHost.cs
@@ -14,10 +14,10 @@
 var redis = builder.AddRedis("redis");
 
 builder.AddProject<Projects.Web>("web")
-    .WithReference(mongoDb)
-    .WithReference(redis)
-    .WaitFor(mongo)
-    .WaitFor(redis);
+		.WithReference(mongoDb)
+		.WithReference(redis)
+		.WaitFor(mongo)
+		.WaitFor(redis);
 
 builder.Build().Run();
 
diff --git a/src/Web/Components/Theme/ThemeProvider.razor.cs b/src/Web/Components/Theme/ThemeProvider.razor.cs
index f392be4e..557a72a1 100644
--- a/src/Web/Components/Theme/ThemeProvider.razor.cs
+++ b/src/Web/Components/Theme/ThemeProvider.razor.cs
@@ -14,7 +14,7 @@ namespace MyBlog.Web.Components.Theme;
 
 public partial class ThemeProvider : ComponentBase
 {
-	[Inject] private IJSRuntime Js { get; set; } = default!;
+	[Inject] private IJSRuntime Js { get; set; } = null!;
 
 	[Parameter] public RenderFragment? ChildContent { get; set; }
 
@@ -27,18 +27,18 @@ protected override async Task OnAfterRenderAsync(bool firstRender)
 
 		try
 		{
-			CurrentColor = await Js.InvokeAsync<string>("themeManager.getColor");
+			CurrentColor = await Js.InvokeAsync<string>("themeManager.getColor").ConfigureAwait(true);
 		}
-		catch
+		catch (JSException)
 		{
 			// Keep default if localStorage is unavailable
 		}
 
 		try
 		{
-			CurrentBrightness = await Js.InvokeAsync<string>("themeManager.getBrightness");
+			CurrentBrightness = await Js.InvokeAsync<string>("themeManager.getBrightness").ConfigureAwait(true);
 		}
-		catch
+		catch (JSException)
 		{
 			// Keep default if localStorage is unavailable
 		}
@@ -50,13 +50,13 @@ public async Task SetColor(string color)
 	{
 		CurrentColor = color;
 		StateHasChanged();
-		await Js.InvokeVoidAsync("themeManager.setColor", color);
+		await Js.InvokeVoidAsync("themeManager.setColor", color).ConfigureAwait(true);
 	}
 
 	public async Task SetBrightness(string brightness)
 	{
 		CurrentBrightness = brightness;
 		StateHasChanged();
-		await Js.InvokeVoidAsync("themeManager.setBrightness", brightness);
+		await Js.InvokeVoidAsync("themeManager.setBrightness", brightness).ConfigureAwait(true);
 	}
 }
diff --git a/src/Web/Data/BlogDbContext.cs b/src/Web/Data/BlogDbContext.cs
index a49ccba9..e5d6c2ef 100644
--- a/src/Web/Data/BlogDbContext.cs
+++ b/src/Web/Data/BlogDbContext.cs
@@ -11,12 +11,14 @@
 
 namespace MyBlog.Web.Data;
 
-public sealed class BlogDbContext(DbContextOptions<BlogDbContext> options) : DbContext(options)
+internal sealed class BlogDbContext(DbContextOptions<BlogDbContext> options) : DbContext(options)
 {
 	public DbSet<BlogPost> BlogPosts => Set<BlogPost>();
 
 	protected override void OnModelCreating(ModelBuilder modelBuilder)
 	{
+		ArgumentNullException.ThrowIfNull(modelBuilder);
+
 		var entity = modelBuilder.Entity<BlogPost>();
 		entity.ToCollection("blogposts");
 		entity.HasKey(p => p.Id);
diff --git a/src/Web/Data/BlogPostDto.cs b/src/Web/Data/BlogPostDto.cs
index 77cd7b11..37943351 100644
--- a/src/Web/Data/BlogPostDto.cs
+++ b/src/Web/Data/BlogPostDto.cs
@@ -10,10 +10,10 @@
 namespace MyBlog.Web.Data;
 
 internal sealed record BlogPostDto(
-    Guid Id,
-    string Title,
-    string Content,
-    string Author,
-    DateTime CreatedAt,
-    DateTime? UpdatedAt,
-    bool IsPublished);
+		Guid Id,
+		string Title,
+		string Content,
+		string Author,
+		DateTime CreatedAt,
+		DateTime? UpdatedAt,
+		bool IsPublished);
diff --git a/src/Web/Features/BlogPosts/Create/CreateBlogPostHandler.cs b/src/Web/Features/BlogPosts/Create/CreateBlogPostHandler.cs
index b9d0a1f2..bc0cafbe 100644
--- a/src/Web/Features/BlogPosts/Create/CreateBlogPostHandler.cs
+++ b/src/Web/Features/BlogPosts/Create/CreateBlogPostHandler.cs
@@ -16,28 +16,28 @@ internal sealed class CreateBlogPostHandler(
 IBlogPostRepository repo,
 IBlogPostCacheService cache) : IRequestHandler<CreateBlogPostCommand, Result<Guid>>
 {
-public async Task<Result<Guid>> Handle(CreateBlogPostCommand request, CancellationToken cancellationToken)
-{
-try
-{
-var post = BlogPost.Create(request.Title, request.Content, request.Author);
-await repo.AddAsync(post, cancellationToken).ConfigureAwait(false);
-await cache.InvalidateAllAsync(cancellationToken).ConfigureAwait(false);
-return Result.Ok<Guid>(post.Id);
-}
-catch (OperationCanceledException)
-{
-throw;
-}
-catch (InvalidOperationException ex)
-{
-return Result.Fail<Guid>(ex.Message);
-}
+	public async Task<Result<Guid>> Handle(CreateBlogPostCommand request, CancellationToken cancellationToken)
+	{
+		try
+		{
+			var post = BlogPost.Create(request.Title, request.Content, request.Author);
+			await repo.AddAsync(post, cancellationToken).ConfigureAwait(false);
+			await cache.InvalidateAllAsync(cancellationToken).ConfigureAwait(false);
+			return Result.Ok<Guid>(post.Id);
+		}
+		catch (OperationCanceledException)
+		{
+			throw;
+		}
+		catch (InvalidOperationException ex)
+		{
+			return Result.Fail<Guid>(ex.Message);
+		}
 #pragma warning disable CA1031 // Intentional: top-level handler converts unexpected failures to Result to keep UI stable
-catch (Exception)
-{
-return Result.Fail<Guid>("An unexpected error occurred.");
-}
+		catch (Exception)
+		{
+			return Result.Fail<Guid>("An unexpected error occurred.");
+		}
 #pragma warning restore CA1031
-}
+	}
 }
diff --git a/src/Web/Features/BlogPosts/Delete/DeleteBlogPostHandler.cs b/src/Web/Features/BlogPosts/Delete/DeleteBlogPostHandler.cs
index ec45bd13..2dfef6bd 100644
--- a/src/Web/Features/BlogPosts/Delete/DeleteBlogPostHandler.cs
+++ b/src/Web/Features/BlogPosts/Delete/DeleteBlogPostHandler.cs
@@ -16,34 +16,34 @@ internal sealed class DeleteBlogPostHandler(
 IBlogPostRepository repo,
 IBlogPostCacheService cache) : IRequestHandler<DeleteBlogPostCommand, Result>
 {
-public async Task<Result> Handle(DeleteBlogPostCommand request, CancellationToken cancellationToken)
-{
-try
-{
-await repo.DeleteAsync(request.Id, cancellationToken).ConfigureAwait(false);
-await cache.InvalidateAllAsync(cancellationToken).ConfigureAwait(false);
-await cache.InvalidateByIdAsync(request.Id, cancellationToken).ConfigureAwait(false);
-return Result.Ok();
-}
-catch (DbUpdateConcurrencyException)
-{
-return Result.Fail(
-"This post was modified by another user. Please reload and try again.",
-ResultErrorCode.Concurrency);
-}
-catch (OperationCanceledException)
-{
-throw;
-}
-catch (InvalidOperationException ex)
-{
-return Result.Fail(ex.Message);
-}
+	public async Task<Result> Handle(DeleteBlogPostCommand request, CancellationToken cancellationToken)
+	{
+		try
+		{
+			await repo.DeleteAsync(request.Id, cancellationToken).ConfigureAwait(false);
+			await cache.InvalidateAllAsync(cancellationToken).ConfigureAwait(false);
+			await cache.InvalidateByIdAsync(request.Id, cancellationToken).ConfigureAwait(false);
+			return Result.Ok();
+		}
+		catch (DbUpdateConcurrencyException)
+		{
+			return Result.Fail(
+			"This post was modified by another user. Please reload and try again.",
+			ResultErrorCode.Concurrency);
+		}
+		catch (OperationCanceledException)
+		{
+			throw;
+		}
+		catch (InvalidOperationException ex)
+		{
+			return Result.Fail(ex.Message);
+		}
 #pragma warning disable CA1031 // Intentional: top-level handler converts unexpected failures to Result to keep UI stable
-catch (Exception)
-{
-return Result.Fail("An unexpected error occurred.");
-}
+		catch (Exception)
+		{
+			return Result.Fail("An unexpected error occurred.");
+		}
 #pragma warning restore CA1031
-}
+	}
 }
diff --git a/src/Web/Features/BlogPosts/Edit/EditBlogPostHandler.cs b/src/Web/Features/BlogPosts/Edit/EditBlogPostHandler.cs
index 88278245..e7cd4c25 100644
--- a/src/Web/Features/BlogPosts/Edit/EditBlogPostHandler.cs
+++ b/src/Web/Features/BlogPosts/Edit/EditBlogPostHandler.cs
@@ -18,67 +18,67 @@ internal sealed class EditBlogPostHandler(
 : IRequestHandler<EditBlogPostCommand, Result>,
 IRequestHandler<GetBlogPostByIdQuery, Result<BlogPostDto?>>
 {
-public async Task<Result> Handle(EditBlogPostCommand request, CancellationToken cancellationToken)
-{
-try
-{
-var post = await repo.GetByIdAsync(request.Id, cancellationToken).ConfigureAwait(false);
-if (post is null)
-return Result.Fail($"BlogPost {request.Id} not found.");
-post.Update(request.Title, request.Content);
-await repo.UpdateAsync(post, cancellationToken).ConfigureAwait(false);
-await cache.InvalidateAllAsync(cancellationToken).ConfigureAwait(false);
-await cache.InvalidateByIdAsync(request.Id, cancellationToken).ConfigureAwait(false);
-return Result.Ok();
-}
-catch (DbUpdateConcurrencyException)
-{
-return Result.Fail(
-"This post was modified by another user. Please reload and try again.",
-ResultErrorCode.Concurrency);
-}
-catch (OperationCanceledException)
-{
-throw;
-}
-catch (InvalidOperationException ex)
-{
-return Result.Fail(ex.Message);
-}
+	public async Task<Result> Handle(EditBlogPostCommand request, CancellationToken cancellationToken)
+	{
+		try
+		{
+			var post = await repo.GetByIdAsync(request.Id, cancellationToken).ConfigureAwait(false);
+			if (post is null)
+				return Result.Fail($"BlogPost {request.Id} not found.");
+			post.Update(request.Title, request.Content);
+			await repo.UpdateAsync(post, cancellationToken).ConfigureAwait(false);
+			await cache.InvalidateAllAsync(cancellationToken).ConfigureAwait(false);
+			await cache.InvalidateByIdAsync(request.Id, cancellationToken).ConfigureAwait(false);
+			return Result.Ok();
+		}
+		catch (DbUpdateConcurrencyException)
+		{
+			return Result.Fail(
+			"This post was modified by another user. Please reload and try again.",
+			ResultErrorCode.Concurrency);
+		}
+		catch (OperationCanceledException)
+		{
+			throw;
+		}
+		catch (InvalidOperationException ex)
+		{
+			return Result.Fail(ex.Message);
+		}
 #pragma warning disable CA1031 // Intentional: top-level handler converts unexpected failures to Result to keep UI stable
-catch (Exception)
-{
-return Result.Fail("An unexpected error occurred.");
-}
+		catch (Exception)
+		{
+			return Result.Fail("An unexpected error occurred.");
+		}
 #pragma warning restore CA1031
-}
+	}
 
-public async Task<Result<BlogPostDto?>> Handle(GetBlogPostByIdQuery request, CancellationToken cancellationToken)
-{
-try
-{
-var dto = await cache.GetOrFetchByIdAsync(
-request.Id,
-async () =>
-{
-var post = await repo.GetByIdAsync(request.Id, cancellationToken).ConfigureAwait(false);
-return post?.ToDto();
-}, cancellationToken).ConfigureAwait(false);
-return Result.Ok<BlogPostDto?>(dto);
-}
-catch (OperationCanceledException)
-{
-throw;
-}
-catch (InvalidOperationException ex)
-{
-return Result.Fail<BlogPostDto?>(ex.Message);
-}
+	public async Task<Result<BlogPostDto?>> Handle(GetBlogPostByIdQuery request, CancellationToken cancellationToken)
+	{
+		try
+		{
+			var dto = await cache.GetOrFetchByIdAsync(
+			request.Id,
+			async () =>
+			{
+				var post = await repo.GetByIdAsync(request.Id, cancellationToken).ConfigureAwait(false);
+				return post?.ToDto();
+			}, cancellationToken).ConfigureAwait(false);
+			return Result.Ok<BlogPostDto?>(dto);
+		}
+		catch (OperationCanceledException)
+		{
+			throw;
+		}
+		catch (InvalidOperationException ex)
+		{
+			return Result.Fail<BlogPostDto?>(ex.Message);
+		}
 #pragma warning disable CA1031 // Intentional: top-level handler converts unexpected failures to Result to keep UI stable
-catch (Exception)
-{
-return Result.Fail<BlogPostDto?>("An unexpected error occurred.");
-}
+		catch (Exception)
+		{
+			return Result.Fail<BlogPostDto?>("An unexpected error occurred.");
+		}
 #pragma warning restore CA1031
-}
+	}
 }
diff --git a/src/Web/Features/BlogPosts/List/GetBlogPostsHandler.cs b/src/Web/Features/BlogPosts/List/GetBlogPostsHandler.cs
index 18276f74..601be879 100644
--- a/src/Web/Features/BlogPosts/List/GetBlogPostsHandler.cs
+++ b/src/Web/Features/BlogPosts/List/GetBlogPostsHandler.cs
@@ -16,32 +16,32 @@ internal sealed class GetBlogPostsHandler(
 IBlogPostRepository repo,
 IBlogPostCacheService cache) : IRequestHandler<GetBlogPostsQuery, Result<IReadOnlyList<BlogPostDto>>>
 {
-public async Task<Result<IReadOnlyList<BlogPostDto>>> Handle(
-GetBlogPostsQuery request, CancellationToken cancellationToken)
-{
-try
-{
-var result = await cache.GetOrFetchAllAsync(
-async () =>
-{
-var all = await repo.GetAllAsync(cancellationToken).ConfigureAwait(false);
-return (IReadOnlyList<BlogPostDto>)all.Select(p => p.ToDto()).ToList();
-}, cancellationToken).ConfigureAwait(false);
-return Result.Ok<IReadOnlyList<BlogPostDto>>(result);
-}
-catch (OperationCanceledException)
-{
-throw;
-}
-catch (InvalidOperationException ex)
-{
-return Result.Fail<IReadOnlyList<BlogPostDto>>(ex.Message);
-}
+	public async Task<Result<IReadOnlyList<BlogPostDto>>> Handle(
+	GetBlogPostsQuery request, CancellationToken cancellationToken)
+	{
+		try
+		{
+			var result = await cache.GetOrFetchAllAsync(
+			async () =>
+			{
+				var all = await repo.GetAllAsync(cancellationToken).ConfigureAwait(false);
+				return (IReadOnlyList<BlogPostDto>)all.Select(p => p.ToDto()).ToList();
+			}, cancellationToken).ConfigureAwait(false);
+			return Result.Ok<IReadOnlyList<BlogPostDto>>(result);
+		}
+		catch (OperationCanceledException)
+		{
+			throw;
+		}
+		catch (InvalidOperationException ex)
+		{
+			return Result.Fail<IReadOnlyList<BlogPostDto>>(ex.Message);
+		}
 #pragma warning disable CA1031 // Intentional: top-level handler converts unexpected failures to Result to keep UI stable
-catch (Exception)
-{
-return Result.Fail<IReadOnlyList<BlogPostDto>>("An unexpected error occurred.");
-}
+		catch (Exception)
+		{
+			return Result.Fail<IReadOnlyList<BlogPostDto>>("An unexpected error occurred.");
+		}
 #pragma warning restore CA1031
-}
+	}
 }
diff --git a/src/Web/Features/UserManagement/UserManagementHandler.cs b/src/Web/Features/UserManagement/UserManagementHandler.cs
index 9797286c..044eb8ef 100644
--- a/src/Web/Features/UserManagement/UserManagementHandler.cs
+++ b/src/Web/Features/UserManagement/UserManagementHandler.cs
@@ -22,174 +22,174 @@ internal sealed class UserManagementHandler(
 IRequestHandler<RemoveRoleCommand, Result>,
 IRequestHandler<GetAvailableRolesQuery, Result<IReadOnlyList<RoleDto>>>
 {
-public async Task<Result<IReadOnlyList<UserWithRolesDto>>> Handle(
-GetUsersWithRolesQuery request, CancellationToken cancellationToken)
-{
-try
-{
-var client = await GetManagementClientAsync(cancellationToken).ConfigureAwait(false);
-var usersPager = await client.Users.ListAsync(new ListUsersRequestParameters(), cancellationToken: cancellationToken).ConfigureAwait(false);
-var result = new List<UserWithRolesDto>();
-await foreach (var user in usersPager)
-{
-var rolesPager = await client.Users.Roles.ListAsync(
-user.UserId ?? string.Empty, new ListUserRolesRequestParameters(), cancellationToken: cancellationToken).ConfigureAwait(false);
-var roles = new List<string>();
-await foreach (var role in rolesPager)
-{
-roles.Add(role.Name ?? string.Empty);
-}
-result.Add(new UserWithRolesDto(
-user.UserId ?? string.Empty,
-user.Email ?? string.Empty,
-user.Name ?? user.Email ?? string.Empty,
-roles));
-}
-return Result.Ok<IReadOnlyList<UserWithRolesDto>>(result);
-}
-catch (OperationCanceledException)
-{
-throw;
-}
-catch (InvalidOperationException ex)
-{
-return Result.Fail<IReadOnlyList<UserWithRolesDto>>(ex.Message);
-}
-catch (HttpRequestException ex)
-{
-return Result.Fail<IReadOnlyList<UserWithRolesDto>>(ex.Message);
-}
+	public async Task<Result<IReadOnlyList<UserWithRolesDto>>> Handle(
+	GetUsersWithRolesQuery request, CancellationToken cancellationToken)
+	{
+		try
+		{
+			var client = await GetManagementClientAsync(cancellationToken).ConfigureAwait(false);
+			var usersPager = await client.Users.ListAsync(new ListUsersRequestParameters(), cancellationToken: cancellationToken).ConfigureAwait(false);
+			var result = new List<UserWithRolesDto>();
+			await foreach (var user in usersPager)
+			{
+				var rolesPager = await client.Users.Roles.ListAsync(
+				user.UserId ?? string.Empty, new ListUserRolesRequestParameters(), cancellationToken: cancellationToken).ConfigureAwait(false);
+				var roles = new List<string>();
+				await foreach (var role in rolesPager)
+				{
+					roles.Add(role.Name ?? string.Empty);
+				}
+				result.Add(new UserWithRolesDto(
+				user.UserId ?? string.Empty,
+				user.Email ?? string.Empty,
+				user.Name ?? user.Email ?? string.Empty,
+				roles));
+			}
+			return Result.Ok<IReadOnlyList<UserWithRolesDto>>(result);
+		}
+		catch (OperationCanceledException)
+		{
+			throw;
+		}
+		catch (InvalidOperationException ex)
+		{
+			return Result.Fail<IReadOnlyList<UserWithRolesDto>>(ex.Message);
+		}
+		catch (HttpRequestException ex)
+		{
+			return Result.Fail<IReadOnlyList<UserWithRolesDto>>(ex.Message);
+		}
 #pragma warning disable CA1031 // Intentional: top-level handler converts unexpected failures to Result to keep UI stable
-catch (Exception)
-{
-return Result.Fail<IReadOnlyList<UserWithRolesDto>>("An unexpected error occurred.");
-}
+		catch (Exception)
+		{
+			return Result.Fail<IReadOnlyList<UserWithRolesDto>>("An unexpected error occurred.");
+		}
 #pragma warning restore CA1031
-}
+	}
 
-public async Task<Result> Handle(AssignRoleCommand request, CancellationToken cancellationToken)
-{
-try
-{
-var client = await GetManagementClientAsync(cancellationToken).ConfigureAwait(false);
-await client.Users.Roles.AssignAsync(
-request.UserId,
-new AssignUserRolesRequestContent { Roles = [request.RoleId] },
-cancellationToken: cancellationToken).ConfigureAwait(false);
-return Result.Ok();
-}
-catch (OperationCanceledException)
-{
-throw;
-}
-catch (InvalidOperationException ex)
-{
-return Result.Fail(ex.Message);
-}
-catch (HttpRequestException ex)
-{
-return Result.Fail(ex.Message);
-}
+	public async Task<Result> Handle(AssignRoleCommand request, CancellationToken cancellationToken)
+	{
+		try
+		{
+			var client = await GetManagementClientAsync(cancellationToken).ConfigureAwait(false);
+			await client.Users.Roles.AssignAsync(
+			request.UserId,
+			new AssignUserRolesRequestContent { Roles = [request.RoleId] },
+			cancellationToken: cancellationToken).ConfigureAwait(false);
+			return Result.Ok();
+		}
+		catch (OperationCanceledException)
+		{
+			throw;
+		}
+		catch (InvalidOperationException ex)
+		{
+			return Result.Fail(ex.Message);
+		}
+		catch (HttpRequestException ex)
+		{
+			return Result.Fail(ex.Message);
+		}
 #pragma warning disable CA1031 // Intentional: top-level handler converts unexpected failures to Result to keep UI stable
-catch (Exception)
-{
-return Result.Fail("An unexpected error occurred.");
-}
+		catch (Exception)
+		{
+			return Result.Fail("An unexpected error occurred.");
+		}
 #pragma warning restore CA1031
-}
+	}
 
-public async Task<Result> Handle(RemoveRoleCommand request, CancellationToken cancellationToken)
-{
-try
-{
-var client = await GetManagementClientAsync(cancellationToken).ConfigureAwait(false);
-await client.Users.Roles.DeleteAsync(
-request.UserId,
-new DeleteUserRolesRequestContent { Roles = [request.RoleId] },
-cancellationToken: cancellationToken).ConfigureAwait(false);
-return Result.Ok();
-}
-catch (OperationCanceledException)
-{
-throw;
-}
-catch (InvalidOperationException ex)
-{
-return Result.Fail(ex.Message);
-}
-catch (HttpRequestException ex)
-{
-return Result.Fail(ex.Message);
-}
+	public async Task<Result> Handle(RemoveRoleCommand request, CancellationToken cancellationToken)
+	{
+		try
+		{
+			var client = await GetManagementClientAsync(cancellationToken).ConfigureAwait(false);
+			await client.Users.Roles.DeleteAsync(
+			request.UserId,
+			new DeleteUserRolesRequestContent { Roles = [request.RoleId] },
+			cancellationToken: cancellationToken).ConfigureAwait(false);
+			return Result.Ok();
+		}
+		catch (OperationCanceledException)
+		{
+			throw;
+		}
+		catch (InvalidOperationException ex)
+		{
+			return Result.Fail(ex.Message);
+		}
+		catch (HttpRequestException ex)
+		{
+			return Result.Fail(ex.Message);
+		}
 #pragma warning disable CA1031 // Intentional: top-level handler converts unexpected failures to Result to keep UI stable
-catch (Exception)
-{
-return Result.Fail("An unexpected error occurred.");
-}
+		catch (Exception)
+		{
+			return Result.Fail("An unexpected error occurred.");
+		}
 #pragma warning restore CA1031
-}
+	}
 
-public async Task<Result<IReadOnlyList<RoleDto>>> Handle(GetAvailableRolesQuery request, CancellationToken cancellationToken)
-{
-try
-{
-var client = await GetManagementClientAsync(cancellationToken).ConfigureAwait(false);
-var rolesPager = await client.Roles.ListAsync(new ListRolesRequestParameters(), cancellationToken: cancellationToken).ConfigureAwait(false);
-var roles = new List<RoleDto>();
-await foreach (var role in rolesPager)
-{
-roles.Add(new RoleDto(role.Id ?? string.Empty, role.Name ?? string.Empty));
-}
-return Result.Ok<IReadOnlyList<RoleDto>>(roles);
-}
-catch (OperationCanceledException)
-{
-throw;
-}
-catch (InvalidOperationException ex)
-{
-return Result.Fail<IReadOnlyList<RoleDto>>(ex.Message);
-}
-catch (HttpRequestException ex)
-{
-return Result.Fail<IReadOnlyList<RoleDto>>(ex.Message);
-}
+	public async Task<Result<IReadOnlyList<RoleDto>>> Handle(GetAvailableRolesQuery request, CancellationToken cancellationToken)
+	{
+		try
+		{
+			var client = await GetManagementClientAsync(cancellationToken).ConfigureAwait(false);
+			var rolesPager = await client.Roles.ListAsync(new ListRolesRequestParameters(), cancellationToken: cancellationToken).ConfigureAwait(false);
+			var roles = new List<RoleDto>();
+			await foreach (var role in rolesPager)
+			{
+				roles.Add(new RoleDto(role.Id ?? string.Empty, role.Name ?? string.Empty));
+			}
+			return Result.Ok<IReadOnlyList<RoleDto>>(roles);
+		}
+		catch (OperationCanceledException)
+		{
+			throw;
+		}
+		catch (InvalidOperationException ex)
+		{
+			return Result.Fail<IReadOnlyList<RoleDto>>(ex.Message);
+		}
+		catch (HttpRequestException ex)
+		{
+			return Result.Fail<IReadOnlyList<RoleDto>>(ex.Message);
+		}
 #pragma warning disable CA1031 // Intentional: top-level handler converts unexpected failures to Result to keep UI stable
-catch (Exception)
-{
-return Result.Fail<IReadOnlyList<RoleDto>>("An unexpected error occurred.");
-}
+		catch (Exception)
+		{
+			return Result.Fail<IReadOnlyList<RoleDto>>("An unexpected error occurred.");
+		}
 #pragma warning restore CA1031
-}
+	}
 
-private async Task<ManagementApiClient> GetManagementClientAsync(CancellationToken cancellationToken)
-{
-var domain = configuration["Auth0:ManagementApiDomain"]
-?? throw new InvalidOperationException("Auth0:ManagementApiDomain not configured.");
-var clientId = configuration["Auth0:ManagementApiClientId"]
-?? throw new InvalidOperationException("Auth0:ManagementApiClientId not configured.");
-var clientSecret = configuration["Auth0:ManagementApiClientSecret"]
-?? throw new InvalidOperationException("Auth0:ManagementApiClientSecret not configured.");
+	private async Task<ManagementApiClient> GetManagementClientAsync(CancellationToken cancellationToken)
+	{
+		var domain = configuration["Auth0:ManagementApiDomain"]
+		?? throw new InvalidOperationException("Auth0:ManagementApiDomain not configured.");
+		var clientId = configuration["Auth0:ManagementApiClientId"]
+		?? throw new InvalidOperationException("Auth0:ManagementApiClientId not configured.");
+		var clientSecret = configuration["Auth0:ManagementApiClientSecret"]
+		?? throw new InvalidOperationException("Auth0:ManagementApiClientSecret not configured.");
 
-var httpClient = httpClientFactory.CreateClient();
-var tokenResponse = await httpClient.PostAsJsonAsync(
-$"https://{domain}/oauth/token",
-new
-{
-client_id = clientId,
-client_secret = clientSecret,
-audience = $"https://{domain}/api/v2/",
-grant_type = "client_credentials"
-}, cancellationToken).ConfigureAwait(false);
-tokenResponse.EnsureSuccessStatusCode();
-var tokenData = await tokenResponse.Content.ReadFromJsonAsync<TokenResponse>(cancellationToken).ConfigureAwait(false);
-return new ManagementApiClient(
-token: tokenData!.AccessToken,
-clientOptions: new ClientOptions { BaseUrl = $"https://{domain}/api/v2" });
-}
+		var httpClient = httpClientFactory.CreateClient();
+		var tokenResponse = await httpClient.PostAsJsonAsync(
+		$"https://{domain}/oauth/token",
+		new
+		{
+			client_id = clientId,
+			client_secret = clientSecret,
+			audience = $"https://{domain}/api/v2/",
+			grant_type = "client_credentials"
+		}, cancellationToken).ConfigureAwait(false);
+		tokenResponse.EnsureSuccessStatusCode();
+		var tokenData = await tokenResponse.Content.ReadFromJsonAsync<TokenResponse>(cancellationToken).ConfigureAwait(false);
+		return new ManagementApiClient(
+		token: tokenData!.AccessToken,
+		clientOptions: new ClientOptions { BaseUrl = $"https://{domain}/api/v2" });
+	}
 
-private sealed class TokenResponse
-{
-public string AccessToken { get; init; } = string.Empty;
-}
+	private sealed class TokenResponse
+	{
+		public string AccessToken { get; init; } = string.Empty;
+	}
 }
diff --git a/src/Web/GlobalUsings.cs b/src/Web/GlobalUsings.cs
index 551ae07b..8e4b29eb 100644
--- a/src/Web/GlobalUsings.cs
+++ b/src/Web/GlobalUsings.cs
@@ -8,9 +8,11 @@
 //=======================================================
 
 global using MediatR;
+
 global using Microsoft.EntityFrameworkCore;
 global using Microsoft.Extensions.Caching.Distributed;
 global using Microsoft.Extensions.Caching.Memory;
+
 global using MyBlog.Domain.Entities;
 global using MyBlog.Domain.Interfaces;
 global using MyBlog.Web.Data;
diff --git a/src/Web/Program.cs b/src/Web/Program.cs
index 14bf56e9..a18e7059 100644
--- a/src/Web/Program.cs
+++ b/src/Web/Program.cs
@@ -115,7 +115,7 @@
 // MediatR — scans Web assembly for all handlers
 builder.Services.AddMediatR(cfg =>
 {
-    cfg.RegisterServicesFromAssembly(typeof(Program).Assembly);
+	cfg.RegisterServicesFromAssembly(typeof(Program).Assembly);
 });
 
 // FluentValidation — scans Web assembly for all validators
@@ -207,4 +207,4 @@ static async Task MapTestLoginEndpoint(HttpContext ctx, string? role)
 
 // Exclude the compiler-generated Program class (top-level bootstrap statements) from coverage.
 [ExcludeFromCodeCoverage(Justification = "Application bootstrap entry-point — not business logic")]
-public partial class Program { }
+internal partial class Program { }
diff --git a/src/Web/Security/RoleClaimsHelper.cs b/src/Web/Security/RoleClaimsHelper.cs
index 02895781..22241270 100644
--- a/src/Web/Security/RoleClaimsHelper.cs
+++ b/src/Web/Security/RoleClaimsHelper.cs
@@ -14,123 +14,123 @@ namespace MyBlog.Web.Security;
 
 internal static class RoleClaimsHelper
 {
-    public static readonly string[] DefaultRoleClaimTypes =
-    [
-        "https://myblog/roles",
-        "roles",
-        "role"
-    ];
-
-    public static IReadOnlyList<string> GetRoleClaimTypes(IConfiguration configuration)
-    {
-        var configured = configuration.GetSection("Auth0:RoleClaimTypes").Get<string[]>();
-
-        return configured is { Length: > 0 }
-            ? configured.Where(value => !string.IsNullOrWhiteSpace(value))
-                .Distinct(StringComparer.OrdinalIgnoreCase)
-                .ToArray()
-            : DefaultRoleClaimTypes;
-    }
-
-    public static bool IsRoleClaimType(string? claimType)
-    {
-        if (string.IsNullOrWhiteSpace(claimType))
-        {
-            return false;
-        }
-
-        if (claimType.Equals(ClaimTypes.Role, StringComparison.OrdinalIgnoreCase)
-            || claimType.Equals("roles", StringComparison.OrdinalIgnoreCase)
-            || claimType.Equals("role", StringComparison.OrdinalIgnoreCase))
-        {
-            return true;
-        }
-
-        var lastSlash = claimType.LastIndexOf('/');
-        var lastColon = claimType.LastIndexOf(':');
-        var separatorIndex = Math.Max(lastSlash, lastColon);
-        var tail = separatorIndex >= 0 ? claimType[(separatorIndex + 1)..] : claimType;
-
-        return tail.Equals("roles", StringComparison.OrdinalIgnoreCase)
-            || tail.Equals("role", StringComparison.OrdinalIgnoreCase);
-    }
-
-    private static string[] GetEffectiveRoleClaimTypes(IEnumerable<Claim> claims, IEnumerable<string>? roleClaimTypes)
-    {
-        return (roleClaimTypes ?? DefaultRoleClaimTypes)
-            .Append(ClaimTypes.Role)
-            .Concat(claims.Select(claim => claim.Type).Where(IsRoleClaimType))
-            .Distinct(StringComparer.OrdinalIgnoreCase)
-            .ToArray();
-    }
-
-    public static IReadOnlyList<string> ExpandRoleValues(string? claimValue)
-    {
-        if (string.IsNullOrWhiteSpace(claimValue))
-        {
-            return [];
-        }
-
-        var trimmed = claimValue.Trim();
-
-        if (trimmed.StartsWith("[", StringComparison.Ordinal))
-        {
-            try
-            {
-                using var document = JsonDocument.Parse(trimmed);
-
-                if (document.RootElement.ValueKind == JsonValueKind.Array)
-                {
-                    return document.RootElement
-                        .EnumerateArray()
-                        .Select(element => element.GetString())
-                        .Where(role => !string.IsNullOrWhiteSpace(role))
-                        .Cast<string>()
-                        .ToArray();
-                }
-            }
-            catch (JsonException)
-            {
-                return [];
-            }
-        }
-
-        if (trimmed.Contains(',', StringComparison.Ordinal))
-        {
-            return trimmed.Split(',', StringSplitOptions.TrimEntries | StringSplitOptions.RemoveEmptyEntries);
-        }
-
-        return [trimmed];
-    }
-
-    public static void AddRoleClaims(ClaimsIdentity identity, IEnumerable<string> roleClaimTypes)
-    {
-        ArgumentNullException.ThrowIfNull(identity);
-
-        foreach (var roleClaimType in GetEffectiveRoleClaimTypes(identity.Claims, roleClaimTypes))
-        {
-            foreach (var claim in identity.FindAll(roleClaimType).ToList())
-            {
-                foreach (var role in ExpandRoleValues(claim.Value))
-                {
-                    if (!identity.HasClaim(ClaimTypes.Role, role))
-                    {
-                        identity.AddClaim(new Claim(ClaimTypes.Role, role));
-                    }
-                }
-            }
-        }
-    }
-
-    public static IReadOnlyList<string> GetRoles(ClaimsPrincipal user, IEnumerable<string>? roleClaimTypes = null)
-    {
-        var types = GetEffectiveRoleClaimTypes(user.Claims, roleClaimTypes);
-
-        return user.Claims
-            .Where(claim => types.Contains(claim.Type, StringComparer.OrdinalIgnoreCase))
-            .SelectMany(claim => ExpandRoleValues(claim.Value))
-            .Distinct(StringComparer.OrdinalIgnoreCase)
-            .OrderBy(role => role)
-            .ToList();
-    }
+	public static readonly string[] DefaultRoleClaimTypes =
+	[
+			"https://myblog/roles",
+				"roles",
+				"role"
+	];
+
+	public static IReadOnlyList<string> GetRoleClaimTypes(IConfiguration configuration)
+	{
+		var configured = configuration.GetSection("Auth0:RoleClaimTypes").Get<string[]>();
+
+		return configured is { Length: > 0 }
+				? configured.Where(value => !string.IsNullOrWhiteSpace(value))
+						.Distinct(StringComparer.OrdinalIgnoreCase)
+						.ToArray()
+				: DefaultRoleClaimTypes;
+	}
+
+	public static bool IsRoleClaimType(string? claimType)
+	{
+		if (string.IsNullOrWhiteSpace(claimType))
+		{
+			return false;
+		}
+
+		if (claimType.Equals(ClaimTypes.Role, StringComparison.OrdinalIgnoreCase)
+				|| claimType.Equals("roles", StringComparison.OrdinalIgnoreCase)
+				|| claimType.Equals("role", StringComparison.OrdinalIgnoreCase))
+		{
+			return true;
+		}
+
+		var lastSlash = claimType.LastIndexOf('/');
+		var lastColon = claimType.LastIndexOf(':');
+		var separatorIndex = Math.Max(lastSlash, lastColon);
+		var tail = separatorIndex >= 0 ? claimType[(separatorIndex + 1)..] : claimType;
+
+		return tail.Equals("roles", StringComparison.OrdinalIgnoreCase)
+				|| tail.Equals("role", StringComparison.OrdinalIgnoreCase);
+	}
+
+	private static string[] GetEffectiveRoleClaimTypes(IEnumerable<Claim> claims, IEnumerable<string>? roleClaimTypes)
+	{
+		return (roleClaimTypes ?? DefaultRoleClaimTypes)
+				.Append(ClaimTypes.Role)
+				.Concat(claims.Select(claim => claim.Type).Where(IsRoleClaimType))
+				.Distinct(StringComparer.OrdinalIgnoreCase)
+				.ToArray();
+	}
+
+	public static IReadOnlyList<string> ExpandRoleValues(string? claimValue)
+	{
+		if (string.IsNullOrWhiteSpace(claimValue))
+		{
+			return [];
+		}
+
+		var trimmed = claimValue.Trim();
+
+		if (trimmed.StartsWith("[", StringComparison.Ordinal))
+		{
+			try
+			{
+				using var document = JsonDocument.Parse(trimmed);
+
+				if (document.RootElement.ValueKind == JsonValueKind.Array)
+				{
+					return document.RootElement
+							.EnumerateArray()
+							.Select(element => element.GetString())
+							.Where(role => !string.IsNullOrWhiteSpace(role))
+							.Cast<string>()
+							.ToArray();
+				}
+			}
+			catch (JsonException)
+			{
+				return [];
+			}
+		}
+
+		if (trimmed.Contains(',', StringComparison.Ordinal))
+		{
+			return trimmed.Split(',', StringSplitOptions.TrimEntries | StringSplitOptions.RemoveEmptyEntries);
+		}
+
+		return [trimmed];
+	}
+
+	public static void AddRoleClaims(ClaimsIdentity identity, IEnumerable<string> roleClaimTypes)
+	{
+		ArgumentNullException.ThrowIfNull(identity);
+
+		foreach (var roleClaimType in GetEffectiveRoleClaimTypes(identity.Claims, roleClaimTypes))
+		{
+			foreach (var claim in identity.FindAll(roleClaimType).ToList())
+			{
+				foreach (var role in ExpandRoleValues(claim.Value))
+				{
+					if (!identity.HasClaim(ClaimTypes.Role, role))
+					{
+						identity.AddClaim(new Claim(ClaimTypes.Role, role));
+					}
+				}
+			}
+		}
+	}
+
+	public static IReadOnlyList<string> GetRoles(ClaimsPrincipal user, IEnumerable<string>? roleClaimTypes = null)
+	{
+		var types = GetEffectiveRoleClaimTypes(user.Claims, roleClaimTypes);
+
+		return user.Claims
+				.Where(claim => types.Contains(claim.Type, StringComparer.OrdinalIgnoreCase))
+				.SelectMany(claim => ExpandRoleValues(claim.Value))
+				.Distinct(StringComparer.OrdinalIgnoreCase)
+				.OrderBy(role => role)
+				.ToList();
+	}
 }
diff --git a/src/Web/Web.csproj b/src/Web/Web.csproj
index 98488f4d..4649ed1a 100644
--- a/src/Web/Web.csproj
+++ b/src/Web/Web.csproj
@@ -1,40 +1,46 @@
 <Project Sdk="Microsoft.NET.Sdk.Web">
 
-  <ItemGroup>
-    <ProjectReference Include="..\ServiceDefaults\ServiceDefaults.csproj" />
-    <ProjectReference Include="..\Domain\Domain.csproj" />
-  </ItemGroup>
+	<PropertyGroup>
+		<TargetFramework>net10.0</TargetFramework>
+		<Nullable>enable</Nullable>
+		<ImplicitUsings>enable</ImplicitUsings>
+		<BlazorDisableThrowNavigationException>true</BlazorDisableThrowNavigationException>
+		<RootNamespace>MyBlog.Web</RootNamespace>
+		<UserSecretsId>a1b2c3d4-e5f6-7890-abcd-ef1234567890</UserSecretsId>
+		<!-- Ensure Blazor static assets are generated during build for E2E tests -->
+		<PublishTrimmed>false</PublishTrimmed>
+		<InvariantGlobalization>false</InvariantGlobalization>
+		<IncludeNETCoreAppRuntime>false</IncludeNETCoreAppRuntime>
+	</PropertyGroup>
 
-  <ItemGroup>
-    <PackageReference Include="Aspire.MongoDB.Driver" />
-    <PackageReference Include="Aspire.StackExchange.Redis.DistributedCaching" />
-    <PackageReference Include="Auth0.AspNetCore.Authentication" />
-    <PackageReference Include="Auth0.ManagementApi" />
-    <PackageReference Include="FluentValidation.AspNetCore" />
-    <PackageReference Include="FluentValidation.DependencyInjectionExtensions" />
-    <PackageReference Include="MediatR" />
-    <PackageReference Include="Microsoft.Bcl.AsyncInterfaces" />
-    <PackageReference Include="MongoDB.EntityFrameworkCore" />
-  </ItemGroup>
+	<ItemGroup>
+		<ProjectReference Include="..\ServiceDefaults\ServiceDefaults.csproj"/>
+		<ProjectReference Include="..\Domain\Domain.csproj"/>
+	</ItemGroup>
 
-  <PropertyGroup>
-    <TargetFramework>net10.0</TargetFramework>
-    <Nullable>enable</Nullable>
-    <ImplicitUsings>enable</ImplicitUsings>
-    <BlazorDisableThrowNavigationException>true</BlazorDisableThrowNavigationException>
-    <RootNamespace>MyBlog.Web</RootNamespace>
-    <UserSecretsId>a1b2c3d4-e5f6-7890-abcd-ef1234567890</UserSecretsId>
-    <!-- Ensure Blazor static assets are generated during build for E2E tests -->
-    <PublishTrimmed>false</PublishTrimmed>
-    <InvariantGlobalization>false</InvariantGlobalization>
-    <IncludeNETCoreAppRuntime>false</IncludeNETCoreAppRuntime>
-  </PropertyGroup>
-
-  <!-- Build Tailwind CSS (skip on CI environments where npm is not installed) -->
-  <Target Name="BuildTailwind" BeforeTargets="Build" Condition="'$(CI)' != 'true'">
-    <Exec Command="npm run tw:build" WorkingDirectory="$(MSBuildProjectDirectory)/../.." />
-  </Target>
+	<ItemGroup>
+		<PackageReference Include="Aspire.MongoDB.Driver"/>
+		<PackageReference Include="Aspire.StackExchange.Redis.DistributedCaching"/>
+		<PackageReference Include="Auth0.AspNetCore.Authentication"/>
+		<PackageReference Include="Auth0.ManagementApi"/>
+		<PackageReference Include="FluentValidation.AspNetCore"/>
+		<PackageReference Include="FluentValidation.DependencyInjectionExtensions"/>
+		<PackageReference Include="MediatR"/>
+		<PackageReference Include="Microsoft.Bcl.AsyncInterfaces"/>
+		<PackageReference Include="MongoDB.EntityFrameworkCore"/>
+	</ItemGroup>
 
+	<ItemGroup>
+		<InternalsVisibleTo Include="Architecture.Tests"/>
+		<InternalsVisibleTo Include="Domain.Tests"/>
+		<InternalsVisibleTo Include="Web.Tests"/>
+		<InternalsVisibleTo Include="Web.Tests.Bunit"/>
+		<InternalsVisibleTo Include="Web.Tests.Integration"/>
+	</ItemGroup>
 
+	<!-- Build Tailwind CSS (skip on CI environments where npm is not installed) -->
+	<Target Name="BuildTailwind" BeforeTargets="Build" Condition="'$(CI)' != 'true'">
+		<Exec Command="npm run tw:build" WorkingDirectory="$(MSBuildProjectDirectory)/../.."/>
+	</Target>
 
 </Project>
