Revised Plan for Shared Features Across CHS, MFG, and DPR
Executive decision
Use shared libraries, but do not begin by moving every apparently similar utility into a common project.
Recommended order:
- Extract stable shared Blazor UI into a Razor Class Library.
- Extract only the contracts required by that UI.
- Consolidate genuinely identical cross-cutting infrastructure after behavioral comparison and verification.
- Use a local project reference during development and an internal NuGet package when the shared library is stable.
- Keep the three product applications and their business APIs independent.
A monorepo is optional. A shared microservice is not required for this problem.
What was validated
The three repositories have the same broad architecture and several duplicated seams:
- Blazor Web
Sharedcomponents - Login and 2FA workflows
PostLoginServiceandTwoFactorAuthServiceFooter, validation components, navigation shells, and CSS- Authentication setup and local storage registration
- API user-context logging middleware
- Web API rate-limit handlers
- Serilog bootstrapping
However, the implementations are not identical:
- MFG, CHS, and DPR use different model namespaces and named HTTP clients.
- Sidebars contain different branding, routes, menu entries, and visibility rules.
MainLayoutdiffers in error handling, notifications, and collapse behavior.UserContextHeaderHandlerbehavior differs; for example, some applications add subdomain context while DPR currently only adds user context.- Encryption key derivation is not the same across the applications.
ValidateIdentifieris not currently a common helper in all three codebases.- Serilog setup is duplicated, but each
Program.cshas product-specific configuration and registration differences.
Evidence locations include:
- MFG Web Program
- CHS Web Program
- DPR Web Program
- MFG API encryption
- CHS API encryption
- DPR API encryption
- CHS identifier validation
Target architecture
Use separate libraries with narrow responsibilities:
ErpCrystal.Shared.Contracts
Common authentication DTOs
Login and 2FA interfaces
Navigation contracts
ErpCrystal.Shared.UI
Razor components
Shared CSS and static assets
UI service-registration extension
ErpCrystal.Shared.Infrastructure (later, only if justified)
Verified common middleware
Verified HTTP handlers
Logging extensions with explicit optionsThe product applications remain responsible for:
ErpCrystal_CHS.Web / Api
ErpCrystal_MFG.Web / Api
ErpCrystal_DPR.Web / Api
Product models
Product API clients and endpoints
Database rules
Product navigation
Product branding
Product-specific configurationDo not create a shared project that references all three product projects. Shared code must point toward stable contracts, not toward MFG, CHS, or DPR implementations.
Phase 1: Establish the baseline
Before moving code, create a comparison list for each candidate feature:
- Public interface
- Consumers and call sites
- DTOs and model dependencies
- Configuration keys and environment variables
- HTTP client names and endpoint paths
- Middleware ordering
- Security assumptions
- Product-specific behavior
Record manual acceptance checks because the repositories currently have no test projects:
- Login and logout
- Session restoration
- Session expiry
- 2FA enabled and disabled
- Google Authenticator flow
- Email OTP flow
- Invalid session handling
- Sidebar rendering and collapse
- Rate-limit response handling
- User context in logs
- Email credential encryption/decryption
No shared feature should be deleted from a product project until the corresponding check passes in all three applications.
Phase 2: Extract the lowest-risk shared UI
Start with direct or near-direct candidates:
Footer- Shared CSS that is genuinely identical
CustomValidation- Small presentational components
Create an RCL using the Razor SDK. Each application references it during development.
Shared static assets under the RCL wwwroot are consumed through:
_content/{PACKAGE ID}/{path}Global CSS and JavaScript must still be explicitly referenced by each consuming application. Do not assume that placing a file in the RCL automatically loads it.
Phase 3: Extract authentication contracts
Create shared contracts for the stable parts of login and 2FA, for example:
public interface ILoginContextService
{
Task<PostLoginContext?> LoadAsync();
Task<bool> ValidateSessionAsync(PostLoginContext context);
}
public interface ITwoFactorClient
{
Task<TwoFactorSession> GetSessionAsync(string userDb);
Task UpdateSessionAsync(TwoFactorSession session, string userDb);
Task<HttpResponseMessage> ValidateAsync(TwoFactorSession session);
}The exact contracts should be derived from actual consumers rather than copied from the existing service interfaces.
Each product implements the contracts through an adapter using its own named API client:
MFG → mfgapi
CHS → chsapi
DPR → dprapiThe shared UI must not know these client names, product namespaces, product routes, or database-specific rules.
Phase 4: Extract the 2FA and login UI
Move the common workflow into the RCL only after the contracts exist.
The shared components may own:
- Authorized/unauthorized display
- Session state presentation
- 2FA prompts
- OTP validation presentation
- Session-expiry messaging
- Common navigation callbacks
The host application must provide:
- Login context service
- 2FA adapter
- Product navigation target
- Product-specific error routes
- Product-specific notifications
- Branding and text where needed
Do not move LoginDisplay.razor directly from MFG into the RCL. The current file contains product-specific services, models, routes, and notification dependencies.
Phase 5: Extract the sidebar shell
Share only the stable shell:
- Authorization visibility
- Collapse/expand state
- Layout markup
- Common icons/styles where appropriate
- Rendering of supplied navigation items
Each product supplies its navigation definition:
IReadOnlyList<NavigationItem> GetNavigation(PostLoginContext context);Do not place MFG, CHS, and DPR branches inside one shared sidebar. That recreates duplication as conditional complexity.
Keep each product’s MainLayout initially. Share layout primitives only after differences in error boundaries, notifications, footer placement, and responsive behavior are deliberately resolved.
Phase 6: Review shared API infrastructure
The team proposal is correct that some API/Web infrastructure may be reusable, but these items require separate decisions.
User-context logging middleware
This is a good candidate for ErpCrystal.Shared.Infrastructure, but first compare:
- User extraction priority
- Route parameter names
- Header trust model
HttpContext.Itemsvalues- Serilog property names
- Middleware namespace and registration order
- Any per-database caching behavior
Move it only after these behaviors are intentionally standardized. User and database context are security-sensitive logging data; a shared implementation must not silently change audit behavior.
User-context HTTP handler
The handler is not identical across products. Some versions add X-Subdomain, while DPR currently does not. Share a parameterized implementation only if the header contract is documented and required by all APIs. Otherwise keep thin product adapters.
Rate-limit handler
The RateLimitHandler implementation is small and appears broadly reusable. It can be shared with a common RateLimitExceededException contract, provided all three applications agree on:
- Which status codes are handled
- Whether the response is disposed
- The exception type
- User-facing error behavior
- Retry information such as
Retry-After
This is a reasonable early infrastructure extraction after the UI work.
Serilog configuration
Serilog setup is a good candidate for a small extension method, but do not move the complete Program.cs block blindly.
The shared method should own only stable mechanics, such as common enrichers and sink setup. Each application should provide:
- Application name
- Environment-specific paths
- Email subject and recipients
- Minimum levels
- Product-specific sinks or overrides
- Secret/configuration access
Prefer an options object or explicit parameters over hidden environment-variable assumptions. Validate that logging still starts when optional email configuration is absent.
Security headers
The current repository scan did not establish a single existing security-header implementation shared by all three APIs. Do not add a generic security-header library merely because it was listed in the team document. First identify the actual policies required by each deployed application.
Phase 7: Encryption and identifier validation — defer and handle carefully
Encryption service
Do not move the current EncryptionService into a shared core library as-is.
The applications currently derive keys differently. MFG uses an environment-provided key, while CHS and DPR use machine/user-derived values with product-specific key material. Moving or unifying this code could make existing encrypted email credentials unreadable or weaken the security boundary.
If encryption must eventually be standardized:
- Document the current formats and key sources.
- Decide on a supported key-management strategy.
- Add versioned ciphertext or migration handling.
- Test decrypting existing production data.
- Migrate deliberately, with backup and rollback procedures.
This is a security and data-compatibility project, not a simple code-sharing task.
ValidateIdentifier
The scan found ValidateIdentifier as a private method in CHS’s UtilityMethodsRepository, not as an established common implementation in all three repositories.
Do not move or relax it into shared code yet. First compare every dynamic SQL identifier call site and preserve the strictest safe behavior. If it becomes common, place a tested implementation in a narrowly named security/contracts library and treat changes as security-sensitive.
Distribution strategy
Development
Use a project reference to the shared RCL and contracts library for fast debugging and immediate feedback.
Do not depend on an untracked absolute path such as D:\erpcrystal_common. Use a reproducible repository layout, a submodule, or a workspace configuration that CI can reproduce.
Git submodule
Use a submodule only if the team wants source-level sharing while retaining separate repositories. A submodule pins a commit; it does not automatically update all consumers. Every application must update its submodule pointer deliberately.
Internal NuGet package
Use an internal NuGet feed after the shared API is stable. This is the preferred distribution model for separate repositories because it provides versioning, rollback, and controlled adoption.
Developers can continue using a project reference or local package feed during active development. NuGet does not need to be repacked for every local CSS experiment.
Monorepo
A monorepo is optional, not a prerequisite. It is appropriate if the same team owns all products, wants atomic changes, and accepts more coupled CI/CD and release coordination.
Do not migrate to a monorepo solely to avoid copying files; a shared repository plus RCL/NuGet provides the same reuse with less disruption.
Explicit non-goals
Do not initially:
- Build a shared microservice for UI or utility reuse
- Merge the three product APIs
- Move all product models into one shared model project
- Centralize all
Program.csregistrations - Share the complete
MainLayout - Share the complete product sidebar definition
- Standardize encryption without a data migration plan
- Add a security-header abstraction without confirmed common policy
- Introduce a monorepo before the shared contracts are proven
A microservice should be considered only for a genuinely centralized external capability, such as a separately operated notification or document-processing service. It is not an efficient replacement for a class library.
Acceptance criteria for the first release
The first shared-library release is ready when:
- All three applications build from a reproducible checkout.
- Shared components render in all three applications.
- Login, logout, session expiry, and 2FA checks pass in all three products.
- Product-specific routes and menu entries remain outside the shared library.
- No shared project references a product project.
- Static assets work in development and published output.
- Shared and product-specific service registrations are explicit.
- A shared-library version can be upgraded or rolled back independently.
- Existing encrypted credentials are unaffected.
- CI builds at least the shared libraries and all three consuming applications.
Final recommendation
Adopt the team member’s RCL/shared-library direction, but narrow it:
- Shared RCL for UI.
- Shared contracts for stable authentication and navigation boundaries.
- Shared infrastructure only after middleware and logging behavior is compared and verified.
- Defer encryption and identifier-validation consolidation.
- Use project references for extraction, then internal NuGet packages for controlled distribution.
- Treat monorepo migration as an optional organizational decision, not a technical requirement.
This addresses the immediate duplication problem while avoiding a new shared project that accidentally owns product-specific behavior, security assumptions, or incompatible data formats.