Revised Plan for Shared Features Across CHS, MFG, and DPR

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:

  1. Extract stable shared Blazor UI into a Razor Class Library.
  2. Extract only the contracts required by that UI.
  3. Consolidate genuinely identical cross-cutting infrastructure after behavioral comparison and verification.
  4. Use a local project reference during development and an internal NuGet package when the shared library is stable.
  5. 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 Shared components
  • Login and 2FA workflows
  • PostLoginService and TwoFactorAuthService
  • Footer, 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.
  • MainLayout differs in error handling, notifications, and collapse behavior.
  • UserContextHeaderHandler behavior 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.
  • ValidateIdentifier is not currently a common helper in all three codebases.
  • Serilog setup is duplicated, but each Program.cs has product-specific configuration and registration differences.

Evidence locations include:

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 options

The 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 configuration

Do 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:

  1. Footer
  2. Shared CSS that is genuinely identical
  3. CustomValidation
  4. 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 → dprapi

The 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.Items values
  • 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:

  1. Document the current formats and key sources.
  2. Decide on a supported key-management strategy.
  3. Add versioned ciphertext or migration handling.
  4. Test decrypting existing production data.
  5. 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.cs registrations
  • 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:

  1. Shared RCL for UI.
  2. Shared contracts for stable authentication and navigation boundaries.
  3. Shared infrastructure only after middleware and logging behavior is compared and verified.
  4. Defer encryption and identifier-validation consolidation.
  5. Use project references for extraction, then internal NuGet packages for controlled distribution.
  6. 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.

References