Prepare for Blazor Server/Auto migration by documenting current WASM-specific culture initialization approach and providing detailed migration checklist in COPILOT_CONTEXT.md.
16 KiB
EnvelopeGenerator — AI Context Reference
Purpose
Digital document signing system with unified Blazor WASM frontend for both Senders and Receivers. Senders create envelopes and place signature fields. Receivers view PDFs, sign documents, export stamped PDFs.
Primary Libraries: DevExpress + PDF.js (PSPDFKit removed)
Deployment Architecture
Two Presentation Projects (Both Required):
-
EnvelopeGenerator.API (ASP.NET Core Web API)
- Runs independently (development & production)
- YARP Reverse Proxy configured via
yarp.json - Proxies requests to:
EnvelopeGenerator.ReceiverUI(Blazor WASM)- External Auth.API service
- Serves as single entry point for all requests
-
EnvelopeGenerator.ReceiverUI (Blazor WebAssembly)
- Runs on separate host/port
- Accessed only through API proxy (not directly)
- Serves static files (HTML, JS, CSS, WASM)
Request Flow:
Client ? API:8088 (YARP Proxy) ? ReceiverUI:52936 (Blazor WASM)
? Auth.API:9090 (External Auth Service)
Configuration: EnvelopeGenerator.API/yarp.json
ReceiverUI Route Structure
Root Route
| Route | File | Purpose |
|---|---|---|
/ |
Index.razor |
Application entry point (landing page). |
Sender Routes
| Route | File | Purpose |
|---|---|---|
/sender/login |
LoginSenderPage.razor |
Username/password authentication |
/sender |
EnvelopeSenderPage.razor |
Sender dashboard (envelope list) |
Receiver Routes
| Route | File | Purpose |
|---|---|---|
/envelope/login/{EnvelopeKey} |
LoginReceiverPage.razor |
Access code authentication for specific envelope |
/envelope/{EnvelopeKey} |
EnvelopeReceiverPage.razor |
View & sign envelope (PDF.js viewer) |
Multi-Envelope Support: Receivers can login to multiple envelopes simultaneously (per-envelope cookie authentication).
Architecture Evolution
Old Architecture (Deprecated)
- Sender UI:
EnvelopeGenerator.Web(Razor Pages + PSPDFKit) - Receiver UI:
EnvelopeGenerator.ReceiverUI(Blazor WASM + PDF.js) - Backend:
EnvelopeGenerator.API
Current Architecture
- Unified Frontend:
EnvelopeGenerator.ReceiverUI(Blazor WASM) — Both Senders & Receivers - Backend:
EnvelopeGenerator.API— Both Senders & Receivers - Libraries: DevExpress + PDF.js
- PSPDFKit: REMOVED
Solution Structure
| Project | Target | Purpose |
|---|---|---|
EnvelopeGenerator.API |
net8.0 | ASP.NET Core Web API. Backend for both Senders & Receivers. Auth, PDF serving, signature endpoints. |
EnvelopeGenerator.ReceiverUI |
net8.0 WASM | Unified Blazor WebAssembly Frontend. UI for both Senders & Receivers. YARP proxy to API. |
EnvelopeGenerator.Web |
net7/8/9 | DEPRECATED. Legacy Razor Pages (Sender UI). No longer used. |
EnvelopeGenerator.Application |
multi | MediatR CQRS handlers. Business logic. |
EnvelopeGenerator.Domain |
multi | Domain models, constants, interfaces. |
EnvelopeGenerator.Infrastructure |
multi | EF Core repos, DB context. |
EnvelopeGenerator.PdfEditor |
multi | iText7 utilities (NOT used in ReceiverUI). |
EnvelopeGenerator.DependencyInjection |
multi | DI registration helpers. |
| VB.NET projects (Service/Form/BBTests) | net462 | Legacy. Do NOT touch. |
Localization & Culture Management
Current Architecture: Blazor WebAssembly (client-side culture management)
Implementation Details
Culture Storage:
- Culture preference stored in browser's
localStorage(key:AppCulture) - Managed by
CultureService.cs(ReceiverUI/Services) - Supported cultures:
de-DE,en-US,fr-FR
Culture Initialization:
- Location:
Program.cs(lines 53-57) - Sets
CultureInfo.DefaultThreadCurrentCulture/UICulturebefore app runs - WASM-Safe: Each user has isolated browser instance
Language Selector:
- Component:
LanguageSelector.razor(ReceiverUI/Shared) - Displays flag icon + language name
- Changes culture via
CultureService.SetCultureAsync() - Navigates with
forceLoad: false(smooth transition, no page reload)
⚠️ MIGRATION WARNING: Blazor Server/Auto
Current approach is WASM-specific and will break in Server/Auto render modes!
Why it breaks:
Program.cs:53-57sets globalDefaultThreadCurrentCulture- In Server/Auto, one app instance serves all users
- User A selects German → User B sees German too (shared state)
- Thread-safety issues and culture conflicts
Migration Checklist (when moving to Server/Auto):
-
Remove global culture initialization from
Program.cs(lines 53-57)- See detailed warning comment in the code
-
Add RequestLocalizationMiddleware (Server-side approach):
app.UseRequestLocalization(options => { options.SupportedCultures = new[] { "de-DE", "en-US", "fr-FR" }; options.SupportedUICultures = options.SupportedCultures; options.RequestCultureProviders.Insert(0, new CookieRequestCultureProvider()); }); -
OR Use per-circuit culture (Blazor Server approach):
- Store culture in circuit-scoped service
- Use
CascadingParameterto distribute to components - See: https://learn.microsoft.com/aspnet/core/blazor/globalization-localization
-
Update
LanguageSelector.razor:- Remove manual
CultureInfo.DefaultThreadCurrentCultureassignment - Use middleware/circuit culture provider instead
- Remove manual
-
Update
CultureService.cs:- Integrate with Server-side culture provider
- May need to store in cookies instead of localStorage
References:
- Microsoft Docs: Blazor Globalization/Localization
- Current implementation:
Program.cs,CultureService.cs,LanguageSelector.razor
Key Files & Routes
| File | Route/Purpose |
|---|---|
ReceiverUI/Pages/Index.razor |
/ — Application entry point (landing page). |
ReceiverUI/Pages/EnvelopeSenderPage.razor |
/sender — Sender dashboard (envelope list). |
ReceiverUI/Pages/EnvelopeReceiverPage.razor |
/envelope/{key} — Receiver PDF viewer & signing. |
ReceiverUI/Pages/LoginSenderPage.razor |
/sender/login — Sender username/password auth. |
ReceiverUI/Pages/LoginReceiverPage.razor |
/envelope/login/{EnvelopeKey} — Receiver access code auth. |
ReceiverUI/wwwroot/js/pdf-viewer.js |
PDF.js wrapper (zoom, pagination, thumbnails). |
ReceiverUI/wwwroot/js/receiver-signature.js |
Signature pad (draw/type/image). |
ReceiverUI/wwwroot/css/envelope-viewer.css |
EnvelopeViewer styles. |
ReceiverUI/Services/AuthService.cs |
Receiver + Sender authentication. |
ReceiverUI/Services/SignatureCacheService.cs |
Signature caching (Redis/SQL). |
API/Controllers/CacheController.cs |
Signature cache endpoints. |
Coordinate System — CRITICAL
Database Format: INCHES (GdPicture14 native)
Origin: Top-left corner
Axes: X right, Y down
Conversion Formulas
| From INCHES to | Formula | Example |
|---|---|---|
| DevExpress DX | x_DX = x_inches * 100 |
1.5" ? 150 DX |
| PDF Points | x_pt = x_inches * 72 |
1.5" ? 108 pt |
| PDF.js Pixels | Normalize ? scale | (x_inches / pageWidth) * canvasWidth * scale |
A4 Dimensions:
- Width: 8.27" = 595pt = 827 DX
- Height: 11.69" = 842pt = 1169 DX
Unit Systems
| System | Unit | Origin | Y-Axis |
|---|---|---|---|
| Database (GdPicture14) | Inches | Top-left | Down |
| PDF.js | Pixels | Top-left | Down |
| iText7 PDF | Points (1/72") | Bottom-left | Up (flip required) |
| REMOVED |
EnvelopeReceiver — PDF.js Viewer & Signing
Route: /envelope/{EnvelopeKey}
Tech: PDF.js 3.11.174 + Blazor WASM + configurable quality
File: ReceiverUI/Pages/EnvelopeReceiverPage.razor
Key Features
- HiDPI/Retina support (4x quality)
- Configurable quality (
appsettings.json) - Unlimited zoom (50%-300%)
- Ctrl+Wheel global zoom
- Resizable thumbnail sidebar (150-400px, localStorage)
- Responsive (desktop/mobile)
Configuration
File: ReceiverUI/wwwroot/appsettings.json
{
"PdfViewer": {
"ThumbnailBaseScale": 0.75,
"ThumbnailEnableHiDPI": true,
"MainCanvasEnableHiDPI": true,
"ZoomStepPercentage": 5
}
}
JavaScript API
File: ReceiverUI/wwwroot/js/pdf-viewer.js
window.pdfViewer = {
initialize(canvasId, pdfDataUrl, dotNetRef),
renderPage(num),
renderSignatureButtons(signatures, pageNum, dotNetRef),
applySignature(signatureId, dataUrl, fullName, position, place),
zoomIn(), zoomOut(), dispose()
}
Signature Workflow — EnvelopeReceiver
IMPORTANT: iText7 NOT used (GPL license issue). Client-side overlay system only.
Workflow Steps
-
Page Load:
- Check
SignatureCacheServicefor cached signature - If cached ? skip popup, load signature
- If not ? show automatic popup (mandatory)
- Check
-
Signature Popup (DxPopup):
- Cannot close (no X, no ESC, no outside-click)
- 3 Tabs: Draw (canvas) / Text (font select) / Image (upload)
- Required: Full name, Place
- Optional: Position
- Save ? Store in
_capturedSignature, cache via API
-
Signature Buttons:
- Render purple "Unterschreiben" buttons at signature field positions
- Coordinates: INCHES ? POINTS ? Pixels (scaled)
- File:
pdf-viewer.js?renderSignatureButtons()
-
Apply Signature (Click "Unterschreiben"):
- JS: Remove button, create HTML overlay
- Format: Image + separator + text (Name, Position, Place, Date)
- NOT stamped on PDF bytes (visual overlay only)
-
Re-rendering:
- Zoom/Page change ? recalculate button positions
- Session state:
_capturedSignature(lost on refresh)
Data Model
File: ReceiverUI/Models/SignatureCaptureDto.cs
public sealed record SignatureCaptureDto {
public required string DataUrl { get; init; } // base64 PNG
public required string FullName { get; init; }
public string Position { get; init; } = ""; // Optional
public required string Place { get; init; }
}
Signature Caching
Purpose: Persist signature across page refreshes (distributed cache: Redis/SQL)
API Endpoints
Controller: API/Controllers/CacheController.cs
POST /api/Cache/SignatureCapture/{envelopeKey}— SaveGET /api/Cache/SignatureCapture/{envelopeKey}— LoadDELETE /api/Cache/SignatureCapture/{envelopeKey}— Delete
Cache Key Format:
signature:91751687-8ae6-4777-bf5f-b8846085e62e:{envelopeKey}
Configuration: appsettings.json
{
"Cache": {
"SignatureCacheExpiration": null // or "02:00:00" for 2h
}
}
Service
File: ReceiverUI/Services/SignatureCacheService.cs
public class SignatureCacheService {
Task SaveSignatureAsync(string envelopeKey, SignatureCaptureDto signature);
Task<SignatureCaptureDto?> GetSignatureAsync(string envelopeKey);
Task DeleteSignatureAsync(string envelopeKey);
}
Error Handling: Fire-and-forget saves, graceful degradation on load failure.
Sender Login
Route: /sender/login
File: ReceiverUI/Pages/LoginSenderPage.razor
Tech: Bootstrap 5 + DevExpress Blazing Berry theme
AuthService Extension
File: ReceiverUI/Services/AuthService.cs
public enum SenderLoginResult { Success, InvalidCredentials, Error }
public async Task<SenderLoginResult> LoginSenderAsync(string username, string password) {
var response = await http.PostAsJsonAsync(
$"{_api.BaseUrl}/api/auth?cookie=true",
new { username, password });
return response.StatusCode switch {
HttpStatusCode.OK => SenderLoginResult.Success,
HttpStatusCode.Unauthorized => SenderLoginResult.InvalidCredentials,
_ => SenderLoginResult.Error
};
}
API Integration
Endpoint: POST /api/auth?cookie=true
Request:
{ "username": "TekH", "password": "***" }
Response:
200 OK? Cookie set, redirect to/sender401 Unauthorized? Show error: "Ungültige Anmeldedaten"- Other ? Show error: "Serverfehler"
Cookie: HTTP-only, Secure (HTTPS), SameSite=Strict
UI Flow
- User enters username + password
- Click "Anmelden" or press Enter
- Call
AuthService.LoginSenderAsync() - Success ?
Navigation.NavigateTo("/sender", forceLoad: true) - Error ? Display alert
Receiver Login
Route: /envelope/login/{EnvelopeKey}
File: ReceiverUI/Pages/LoginReceiverPage.razor
Multi-Envelope Support: Cookies are stored per-envelope (e.g., AuthTokenSignFLOWReceiver.{envelopeKey}), allowing simultaneous authentication for multiple envelopes in the same browser session.
AuthService Method
public enum EnvelopeLoginResult { Success, InvalidCode, NotFound, Error }
public async Task<EnvelopeLoginResult> LoginEnvelopeReceiverAsync(string key, string accessCode) {
var form = new MultipartFormDataContent();
form.Add(new StringContent(accessCode), "AccessCode");
var response = await http.PostAsync(
$"{_api.BaseUrl}/api/Auth/envelope-receiver/{Uri.EscapeDataString(key)}", form);
return response.StatusCode switch {
HttpStatusCode.OK => EnvelopeLoginResult.Success,
HttpStatusCode.Unauthorized => EnvelopeLoginResult.InvalidCode,
HttpStatusCode.NotFound => EnvelopeLoginResult.NotFound,
_ => EnvelopeLoginResult.Error
};
}
Success: Redirect to /envelope/{key}
NuGet Packages (ReceiverUI)
| Package | Version | Purpose |
|---|---|---|
DevExpress.Blazor.* |
25.2.3 | UI components (grids, popups, etc.) |
SkiaSharp.* |
3.119.1 | WASM rendering |
itext |
NOT USED (GPL license) |
External CDN:
- PDF.js 3.11.174:
https://cdnjs.cloudflare.com/ajax/libs/pdf.js/3.11.174/pdf.min.js
Mistakes History — Do NOT Repeat
| Mistake | Why Wrong |
|---|---|
| Using iText7 in EnvelopeReceiver | GPL license issue. Use overlay system instead. |
| Using PSPDFKit | Removed from architecture. Use PDF.js + DevExpress. |
| Hardcoded quality values in PDF.js | Use appsettings.json for configurability. |
| Complex toolbar layouts | User wants simplicity. Keep horizontal layout. |
| Over-designed UI (gradients/badges) | User prefers simple text labels. |
| Ignoring "revert" instructions | Revert HTML structure, not just CSS. |
BottomMarginBand for signatures |
Repeats on every page. Use DetailBand. |
imageY = (page-1) * 1169 + ann.Y |
Inflates DetailBand. Calculate per-page. |
Development Notes
Deprecated Projects
DO NOT USE:
EnvelopeGenerator.Web(Razor Pages) — Replaced by unified ReceiverUI- PSPDFKit — Removed, use PDF.js + DevExpress instead
Legacy Projects (VB.NET)
DO NOT TOUCH: EnvelopeGenerator.Service, EnvelopeGenerator.Form, EnvelopeGenerator.BBTests
Signature Coordinate Evidence
File: EnvelopeGenerator.Form/frmFieldEditor.vb (VB.NET)
Private Const SIGNATURE_WIDTH As Single = 1.77 ' inches
Private Const SIGNATURE_HEIGHT As Single = 1.96 ' inches
Sub LoadAnnotation(pElement As Signature, ...)
oAnnotation.Left = CSng(pElement.X) ' Direct INCHES assignment
oAnnotation.Top = CSng(pElement.Y)
End Sub
Proves database uses INCHES natively.
Quick Reference
When working with coordinates:
- Database ? UI: INCHES × 72 = PDF Points
- UI ? Display: Points × scale = Pixels
- iText7 stamping: Flip Y-axis (top-down ? bottom-up)
When adding features:
- Check
Mistakes Historyfirst - Prefer simplicity over complexity
- Use
appsettings.jsonfor configuration - Keep consistent with existing design (Bootstrap 5 + Blazing Berry)
- Unified frontend: ReceiverUI serves both Senders and Receivers
When debugging:
- Coordinates: Always check unit system (inches/points/pixels)
- Authentication: Check cookie name/domain/SameSite
- Cache: Check Redis/SQL connection + key format
- Frontend confusion: Only use ReceiverUI (Web is deprecated)
Last Updated: Session 19 (Razor file naming convention + Index route proxy)