Skip to content

Commit 33a24f5

Browse files
authored
Merge branch 'neozhu:main' into main
2 parents 0a46b8a + 085aa04 commit 33a24f5

602 files changed

Lines changed: 7081 additions & 35203 deletions

File tree

Some content is hidden

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

docker-compose.yml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -17,9 +17,9 @@ services:
1717
- DatabaseSettings__DBProvider=${DB_PROVIDER}
1818
- DatabaseSettings__ConnectionString=${DB_CONNECTION_STRING}
1919
- AI__GeminiApiKey=${GEMINI_API_KEY}
20-
- SmtpClientOptions__User=${SMTP_USER}
20+
- SmtpClientOptions__UserName=${SMTP_USERNAME}
2121
- SmtpClientOptions__Port=${SMTP_PORT}
22-
- SmtpClientOptions__Server=${SMTP_SERVER}
22+
- SmtpClientOptions__Host=${SMTP_HOST}
2323
- SmtpClientOptions__Password=${SMTP_PASSWORD}
2424
- SmtpClientOptions__DefaultFromEmail=${SMTP_DEFAULT_FROM}
2525
- Authentication__Microsoft__ClientId=${MS_CLIENT_ID}
Lines changed: 130 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,130 @@
1+
## OneOf Refactor Discussion and Plan (2025-09-20)
2+
3+
### Context
4+
- Project: CleanArchitectureWithBlazorServer
5+
- Current result pattern: `Result` / `Result<T>` with `Succeeded`, `Errors` (strings), `Match/Map/Bind` helpers.
6+
- UI usages: `Products.razor`, `ProductFormDialog.razor` consume `Result/Result<T>`.
7+
- Exception handling: MediatR `IRequestExceptionHandler` implementations for:
8+
- `DbExceptionHandler<TRequest, TResponse, TException>`
9+
- `ValidationExceptionHandler<TRequest, TResponse, TException>`
10+
- `NotFoundExceptionHandler<TRequest, TResponse, TException>`
11+
- `FallbackExceptionHandler<TRequest, TResponse, TException>`
12+
- Goal: Adopt `OneOf`-style discriminated unions to enable strong, exhaustive, multi-branch results without pervasive try/catch in handlers.
13+
14+
### Problems Identified
15+
- `Result` is binary (success/failure) and errors are plain strings, limiting type-safety and exhaustiveness.
16+
- `Result.Failure` signatures currently using `params IEnumerable<string>` conflict with handlers that reflect for `Failure(string[])`.
17+
- Handlers must branch on strings instead of strong error types.
18+
- Try/catch is used (or would be needed) to map EF exceptions; we want interceptor-based mapping.
19+
20+
### Design Direction
21+
Adopt `OneOf` with a pair of reusable, generic result types for the whole application:
22+
- `AppResult` (no payload)
23+
- `AppResult<T>` (payload)
24+
25+
And a small, reusable set of error union cases:
26+
- `Success` (marker success)
27+
- `ValidationFailed` (collection of messages)
28+
- `NotFound`
29+
- `Conflict` (unique constraint, duplicates, business conflict)
30+
- `HasDependents` (FK reference prevents deletion)
31+
- `Unexpected` (fallback)
32+
33+
This avoids defining a distinct `XxxResult` per entity/feature, while preserving strong typing and exhaustive matching.
34+
35+
### Phased Plan
36+
1) Stabilize current `Result` (backward-compatible)
37+
- Change method signatures to align with existing exception handlers' reflection:
38+
- `public static Result Failure(params string[] errors)`
39+
- `public static Result<T> Failure(params string[] errors)`
40+
- And corresponding `FailureAsync(params string[] errors)` methods.
41+
- Optional convenience additions:
42+
- `Match<TResult>(Func<TResult> onSuccess, Func<string, TResult> onFailure)`
43+
- `Switch(Action onSuccess, Action<string> onFailure)`
44+
- `TryPickSuccess(out T)` / `TryPickFailure(out IReadOnlyList<string>)` for `Result<T>`.
45+
46+
2) Introduce OneOf
47+
- Add NuGet packages in Application project:
48+
- `OneOf`
49+
- `OneOf.SourceGenerator` (optional but recommended)
50+
- Define common error types in `Application/Common/Errors/`:
51+
```csharp
52+
public readonly record struct Success;
53+
public readonly record struct NotFound(string Message);
54+
public readonly record struct ValidationFailed(IReadOnlyList<string> Messages);
55+
public readonly record struct Conflict(string Message);
56+
public readonly record struct HasDependents(string Message);
57+
public readonly record struct Unexpected(string Message);
58+
```
59+
- Define reusable result unions in `Application/Common/Results/`:
60+
```csharp
61+
[GenerateOneOf]
62+
public partial class AppResult : OneOfBase<
63+
Success, ValidationFailed, NotFound, Conflict, HasDependents, Unexpected> { }
64+
65+
[GenerateOneOf]
66+
public partial class AppResult<T> : OneOfBase<
67+
T, ValidationFailed, NotFound, Conflict, HasDependents, Unexpected> { }
68+
```
69+
70+
3) OneOf-based Exception Handlers (no try/catch in handlers)
71+
- Add parallel MediatR exception handlers targeting `AppResult` / `AppResult<T>`:
72+
- Validation → `new ValidationFailed(errors)`
73+
- NotFound → `new NotFound(message)`
74+
- DbUpdateException family → `new Conflict(...)`, `new HasDependents(...)`, or `new ValidationFailed(...)` as appropriate
75+
- Fallback → `new Unexpected(ex.Message)`
76+
- Keep existing `IResult`-based handlers for legacy `Result` usage.
77+
- Register both; MediatR will pick handlers based on `TResponse`.
78+
79+
4) Pilot Migration (Products feature)
80+
- Change handler signatures to return `AppResult<T>` / `AppResult`.
81+
- Remove explicit try/catch in handlers; rely on OneOf exception handlers.
82+
- Update `ProductFormDialog.razor` to exhaustively match OneOf results:
83+
```csharp
84+
AppResult<int> r = await Mediator.Send(_model);
85+
r.Match(
86+
id => { MudDialog.Close(DialogResult.Ok(true)); Snackbar.Add(ConstantString.SaveSuccess, Severity.Info); },
87+
v => Snackbar.Add(string.Join("\n", v.Messages), Severity.Error),
88+
nf => Snackbar.Add(nf.Message, Severity.Error),
89+
c => Snackbar.Add(c.Message, Severity.Error),
90+
hd => Snackbar.Add(hd.Message, Severity.Error),
91+
u => Snackbar.Add(u.Message, Severity.Error)
92+
);
93+
```
94+
- Keep `Products.razor` export/query paths on `Result<byte[]>` initially; migrate later.
95+
96+
5) Gradual Rollout
97+
- Adopt `AppResult` / `AppResult<T>` across other features incrementally.
98+
- Optionally provide extension bridge methods to convert between `Result<T>` and `AppResult<T>` during transition.
99+
100+
### File References (current)
101+
- `src/Application/Common/Models/Result.cs`
102+
- `src/Application/Common/Interfaces/IResult.cs`
103+
- `src/Application/Common/ExceptionHandlers/DbExceptionHandler.cs`
104+
- `src/Application/Common/ExceptionHandlers/ValidationExceptionHandler.cs`
105+
- `src/Application/Common/ExceptionHandlers/NotFoundExceptionHandler.cs`
106+
- `src/Application/Common/ExceptionHandlers/FallbackExceptionHandler.cs`
107+
- `src/Application/Features/Products/Commands/AddEdit/AddEditProductCommand.cs`
108+
- `src/Application/Features/Products/Commands/Delete/DeleteProductCommand.cs`
109+
- `src/Server.UI/Pages/Products/Products.razor`
110+
- `src/Server.UI/Pages/Products/Components/ProductFormDialog.razor`
111+
112+
### Notes and Rationale
113+
- Using two generic OneOf-based result types allows reuse across all entities without creating per-entity result classes.
114+
- Strongly-typed error cases improve readability, localization, and testability.
115+
- Exhaustive matching in UI enforces handling of all branches, avoiding silent fallbacks.
116+
- OneOf exception handlers eliminate repetitive try/catch in handlers, centralizing mapping of EF and domain exceptions.
117+
118+
### Next Actions (Proposed)
119+
1) Update `Result.cs` failure signatures to `params string[]` and add convenience APIs.
120+
2) Add `OneOf` packages and define `AppResult` / `AppResult<T>` and common error types.
121+
3) Implement OneOf-based MediatR exception handlers for `AppResult` / `AppResult<T>`.
122+
4) Pilot migrate Products feature (handlers + `ProductFormDialog.razor`).
123+
5) Roll out to other features.
124+
125+
---
126+
127+
This document captures the discussion and actionable plan for adopting OneOf while maintaining backward compatibility and leveraging MediatR exception handling to avoid per-handler try/catch blocks.
128+
129+
130+
Lines changed: 93 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,93 @@
1+
# User Cache Management Refactoring Summary
2+
3+
## Overview
4+
This document describes the refactoring of scattered user cache management across multiple services into a centralized, simplified approach.
5+
6+
## Problem Statement
7+
- Cache keys for ApplicationUser and userManager-related operations were scattered across multiple services
8+
- Inconsistent naming conventions for cache keys
9+
- Duplicate cache clearing logic in multiple services
10+
- Hard to maintain and track all cache operations
11+
12+
## Solution Architecture
13+
14+
### Final Implementation: Simplified Static Approach
15+
Based on user requirements for simplicity ("保存结构简单"), we implemented a minimal, static-only approach:
16+
17+
#### UserCacheKeys.cs
18+
A static class that centralizes all user-related cache key generation:
19+
20+
```csharp
21+
public static class UserCacheKeys
22+
{
23+
// Specific cache key methods
24+
public static string UserContext(string userId) => $"User:Context:{userId}";
25+
public static string UserProfile(string userId) => $"User:Profile:{userId}";
26+
public static string UserApplication(string userId) => $"User:Application:{userId}";
27+
public static string UserClaims(string userId) => $"User:Claims:{userId}";
28+
public static string UserRoles(string userId) => $"User:Roles:{userId}";
29+
public static string UserPermissions(string userId) => $"User:Permissions:{userId}";
30+
public static string RoleClaims(string roleId) => $"Role:Claims:{roleId}";
31+
32+
// Helper methods
33+
public static string GetCacheKey(string userId, UserCacheType cacheType) { ... }
34+
public static string[] AllUserKeys(string userId) { ... }
35+
}
36+
```
37+
38+
## Usage Examples
39+
40+
### Getting Cache Keys
41+
```csharp
42+
// Get specific cache key
43+
var cacheKey = UserCacheKeys.UserContext(userId);
44+
var key = UserCacheKeys.GetCacheKey(userId, UserCacheType.Context);
45+
46+
// Get all cache keys for a user
47+
var allKeys = UserCacheKeys.AllUserKeys(userId);
48+
```
49+
50+
### Cache Operations
51+
Services now handle cache operations directly with IFusionCache:
52+
53+
```csharp
54+
// Clear single cache
55+
var cacheKey = UserCacheKeys.GetCacheKey(userId, UserCacheType.Context);
56+
await _fusionCache.RemoveAsync(cacheKey);
57+
58+
// Clear multiple caches
59+
var cacheTypes = new[] { UserCacheType.Claims, UserCacheType.Permissions, UserCacheType.Context };
60+
var tasks = cacheTypes.Select(cacheType =>
61+
{
62+
var cacheKey = UserCacheKeys.GetCacheKey(userId, cacheType);
63+
return _fusionCache.RemoveAsync(cacheKey).AsTask();
64+
});
65+
await Task.WhenAll(tasks);
66+
```
67+
68+
## Benefits of This Approach
69+
70+
1. **Simplicity**: Single static class with no interfaces or complex abstractions
71+
2. **No Dependencies**: No need for DI registration or service injection
72+
3. **Centralized Keys**: All cache keys defined in one place
73+
4. **Consistency**: Standardized naming pattern "User:{Type}:{Id}"
74+
5. **Flexibility**: Services can use IFusionCache directly for cache operations
75+
6. **Performance**: No abstraction overhead, direct cache operations
76+
77+
## Migration Summary
78+
79+
### Files Updated
80+
1. `UserCacheKeys.cs` - Simplified to key generation only
81+
2. `UserContextLoader.cs` - Updated to use direct cache operations
82+
3. `UserProfileState.cs` - Updated cache clearing logic
83+
4. `UserPermissionAssignmentService.cs` - Updated to handle multiple cache types
84+
5. `TenantSwitchService.cs` - Updated context cache clearing
85+
6. `PermissionHelper.cs` - Already using the key generation methods
86+
87+
### Key Changes
88+
- Removed all cache management methods from UserCacheKeys
89+
- Services now use IFusionCache directly for cache operations
90+
- Simplified background cache clearing with Task.Run
91+
- Maintained all existing cache functionality while simplifying the API
92+
93+
This refactoring achieved the goal of consolidating scattered cache management while keeping the structure simple and maintainable.
Lines changed: 103 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,103 @@
1+
# User Cache Management Refactoring Summary
2+
3+
## Overview
4+
This document describes the refactoring of scattered user cache management across multiple services into a centralized, simplified approach.
5+
6+
## Problem Statement
7+
- Cache keys for ApplicationUser and userManager-related operations were scattered across multiple services
8+
- Inconsistent naming conventions for cache keys
9+
- Duplicate cache clearing logic in multiple services
10+
- Hard to maintain and track all cache operations
11+
12+
## Solution Architecture
13+
14+
### Final Implementation: Simplified Static Approach
15+
Based on user requirements for simplicity ("保存结构简单"), we implemented a minimal, static-only approach:
16+
17+
#### UserCacheKeys.cs (Application/Common/Constants/Cache/)
18+
A static class that centralizes all user-related cache key generation:
19+
20+
```csharp
21+
namespace CleanArchitecture.Blazor.Application.Common.Constants.Cache;
22+
23+
public static class UserCacheKeys
24+
{
25+
// Specific cache key methods
26+
public static string UserContext(string userId) => $"User:Context:{userId}";
27+
public static string UserProfile(string userId) => $"User:Profile:{userId}";
28+
public static string UserApplication(string userId) => $"User:Application:{userId}";
29+
public static string UserClaims(string userId) => $"User:Claims:{userId}";
30+
public static string UserRoles(string userId) => $"User:Roles:{userId}";
31+
public static string UserPermissions(string userId) => $"User:Permissions:{userId}";
32+
public static string RoleClaims(string roleId) => $"Role:Claims:{roleId}";
33+
34+
// Helper methods
35+
public static string GetCacheKey(string userId, UserCacheType cacheType) { ... }
36+
public static string[] AllUserKeys(string userId) { ... }
37+
}
38+
```
39+
40+
## Usage Examples
41+
42+
### Getting Cache Keys
43+
```csharp
44+
// Get specific cache key
45+
var cacheKey = UserCacheKeys.UserContext(userId);
46+
var key = UserCacheKeys.GetCacheKey(userId, UserCacheType.Context);
47+
48+
// Get all cache keys for a user
49+
var allKeys = UserCacheKeys.AllUserKeys(userId);
50+
```
51+
52+
### Cache Operations
53+
Services now handle cache operations directly with IFusionCache:
54+
55+
```csharp
56+
// Clear single cache
57+
var cacheKey = UserCacheKeys.GetCacheKey(userId, UserCacheType.Context);
58+
await _fusionCache.RemoveAsync(cacheKey);
59+
60+
// Clear multiple caches
61+
var cacheTypes = new[] { UserCacheType.Claims, UserCacheType.Permissions, UserCacheType.Context };
62+
var tasks = cacheTypes.Select(cacheType =>
63+
{
64+
var cacheKey = UserCacheKeys.GetCacheKey(userId, cacheType);
65+
return _fusionCache.RemoveAsync(cacheKey).AsTask();
66+
});
67+
await Task.WhenAll(tasks);
68+
```
69+
70+
## Benefits of This Approach
71+
72+
1. **Simplicity**: Single static class with no interfaces or complex abstractions
73+
2. **No Dependencies**: No need for DI registration or service injection
74+
3. **Centralized Keys**: All cache keys defined in one place
75+
4. **Consistency**: Standardized naming pattern "User:{Type}:{Id}"
76+
5. **Flexibility**: Services can use IFusionCache directly for cache operations
77+
6. **Performance**: No abstraction overhead, direct cache operations
78+
79+
## Migration Summary
80+
81+
### Files Updated
82+
1. `UserCacheKeys.cs` - **Moved from Infrastructure to Application layer** (Application/Common/Constants/Cache/)
83+
2. `UserContextLoader.cs` - Updated to use direct cache operations
84+
3. `UserProfileState.cs` - Updated cache clearing logic
85+
4. `UserPermissionAssignmentService.cs` - Updated to handle multiple cache types
86+
5. `TenantSwitchService.cs` - Updated context cache clearing and added IUserContextLoader dependency
87+
6. `PermissionHelper.cs` - Already using the key generation methods
88+
89+
### Key Changes
90+
- **Moved UserCacheKeys to Application layer** for better architecture alignment
91+
- Removed all cache management methods from UserCacheKeys
92+
- Services now use IFusionCache directly for cache operations
93+
- Simplified background cache clearing with Task.Run
94+
- Added IUserContextLoader.ClearUserContextCache() method and integration
95+
- Maintained all existing cache functionality while simplifying the API
96+
97+
### Architecture Benefits
98+
- **Proper Layer Separation**: Cache keys (business concepts) in Application, cache operations in Infrastructure
99+
- **Dependency Direction**: Infrastructure can reference Application constants, following Clean Architecture principles
100+
- **Consistency**: Aligns with existing project structure (other constants in Application layer)
101+
- **Reusability**: Application layer constants can be used across all layers
102+
103+
This refactoring achieved the goal of consolidating scattered cache management while keeping the structure simple and maintainable.

src/Application/Application.csproj

Lines changed: 10 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -10,17 +10,19 @@
1010
<ItemGroup>
1111
<PackageReference Include="AutoMapper" Version="14.0.0" />
1212
<PackageReference Include="SixLabors.ImageSharp" Version="3.1.11" />
13-
<PackageReference Include="Ardalis.Specification" Version="9.3.0" />
14-
<PackageReference Include="Ardalis.Specification.EntityFrameworkCore" Version="9.3.0" />
13+
<PackageReference Include="Ardalis.Specification" Version="9.3.1" />
14+
<PackageReference Include="Ardalis.Specification.EntityFrameworkCore" Version="9.3.1" />
1515
<PackageReference Include="ClosedXML" Version="0.105.0" />
16-
<PackageReference Include="jcamp.FluentEmail.Core" Version="3.8.0" />
17-
<PackageReference Include="jcamp.FluentEmail.MailKit" Version="3.8.0" />
18-
<PackageReference Include="jcamp.FluentEmail.Razor" Version="3.8.0" />
16+
17+
<PackageReference Include="MailKit" Version="4.14.0" />
18+
<PackageReference Include="MimeKit" Version="4.14.0" />
19+
<PackageReference Include="Scriban" Version="6.4.0" />
20+
1921
<PackageReference Include="FluentValidation" Version="12.0.0" />
2022
<PackageReference Include="FluentValidation.DependencyInjectionExtensions" Version="12.0.0" />
21-
<PackageReference Include="Microsoft.AspNetCore.Components.Web" Version="9.0.8" />
22-
<PackageReference Include="Microsoft.Extensions.Localization.Abstractions" Version="9.0.8" />
23-
<PackageReference Include="Newtonsoft.Json" Version="13.0.3" />
23+
<PackageReference Include="Microsoft.AspNetCore.Components.Web" Version="9.0.9" />
24+
<PackageReference Include="Microsoft.Extensions.Localization.Abstractions" Version="9.0.9" />
25+
<PackageReference Include="Newtonsoft.Json" Version="13.0.4" />
2426
<PackageReference Include="Hangfire.Core" Version="1.8.21" />
2527
<PackageReference Include="ZiggyCreatures.FusionCache" Version="2.4.0" />
2628

0 commit comments

Comments
 (0)