AyCode.Core/docs/ARCHITECTURE.md

7.6 KiB

Architecture

Framework vs. Consumer Boundary

Layer 0 (Core framework). Consumers (plural, unknown) reference downward only.

Layer hierarchy

Layer 0 — Core framework           this solution (AyCode.Core)
Layer 1 — UI framework             e.g. AyCode.Blazor — Blazor/MAUI bases
Layer 2 — Domain framework         e.g. Mango.Nop.Core — NopCommerce-plugin bases
Layer 3 — Consumer application     the actual business app

Dependencies flow downward only — framework never references consumer.

What belongs here vs. in a consumer

Yes, framework:

  • Abstract base classes with hooks for consumer override
  • Interfaces and contracts
  • Options classes for consumer configuration
  • Generic logic parameterized by consumer types

No, consumer only:

  • Business logic
  • Consumer-named types or namespaces
  • Hardcoded URLs, tenants, or product IDs

Minimum-boilerplate ideal

Aim for minimal consumer setup:

// Consumer Program.cs — ideal pattern
services.Configure<AcXxxOptions>(config.GetSection("AyCode:Xxx"));
services.AddAcXxxFactory<MyConcreteType>();

Verbose consumer code → incomplete framework. Promote recurring patterns to extension methods, base classes, or options.

Promotion pattern

When a pattern appears in 2+ consumer projects:

  1. Identify generic vs. consumer-specific parts
  2. Move generic part → appropriate framework layer (abstract base, options, or extension)
  3. Leave specific part in consumer (override or configure)

Pattern: "write the base first, derive the specific later" — plan the framework abstraction before consumer-specific code.

Class prefix — framework-only mandate

Workspace-wide convention — every framework-typed repo (@repo.type = "framework" in .github/copilot-instructions.md) MUST prefix its public types with a stable repo-family prefix:

Family Prefix Example types
AyCode.* Ac AcDalBase, AcBinarySerializer, IAcUserDbSet
Mango.Nop.* Mg MgEntityBase, MgDbTableBase, MgOrderDto

Product/Consumer repos (@repo.type = "product" or "consumer") MUST NOT prefix their domain types. Product types are concrete derivations of framework abstractions and use natural domain names (e.g. Order, ShippingItem, OrderItemPallet — no product-specific prefix).

Why: prefix marks code as framework-bequeathed. Order (un-prefixed) = product concrete; AcDalBase = framework abstraction. Cross-repo patterns self-document: AcDalBase<Order> reads as "framework abstraction over product concrete" without lookup.

Dependency Graph

AyCode.Utils (zero dependencies)
    ↑
AyCode.Interfaces → AyCode.Entities → AyCode.Models
    ↑                    ↑                  ↑
AyCode.Core ─────────────┘                  │
    ↑                                       │
AyCode.Database ────────────────────────────┘
    ↑
AyCode.Services ← AyCode.Services.Server

Rule: Dependencies flow upward only — lower layers never reference higher.

Project Roles

Foundation Layer

  • AyCode.Utils — Zero-dependency utilities. String/DateTime extensions, lock wrappers.
  • AyCode.Interfaces — Pure interfaces. IId<T> is the root abstraction.
  • AyCode.Entities — Abstract generic entity classes. Never instantiated directly.
  • AyCode.Models — DTOs and view models for service boundaries.

Core Layer

  • AyCode.Core — Serializers (Binary, JSON, Toon), compression (Brotli, GZip, LZ4), logging framework, constants, validation.
  • AyCode.Core.Serializers.SourceGenerator — Roslyn incremental generator. Targets netstandard2.0. Generates IGeneratedBinaryWriter / IGeneratedBinaryReader for [AcBinarySerializable] types.

Data Layer

  • AyCode.Database — EF Core with generic DAL pattern. Session for reads, Transaction for writes. DAL pooling via PooledDal.

Service Layer

  • AyCode.Services — Client-side: SignalR client, login service, loggers, ASP.NET Core MVC formatters for AcBinary (Mvc/temporarily disabled, see AyCode.Services/Mvc/README.md).
  • AyCode.Services.Server — Server-side: SignalR hub with custom binary protocol, email (SendGrid), JWT auth.
  • AyCode.Models.Server/DynamicMethods — Reflection-based tag→method dispatch used by the SignalR hub.

SignalR Dispatch: Both directions use a single method OnReceiveMessage(int messageTag, int? requestId, SignalParams signalParams, object data) with integer tag-based routing instead of standard Hub methods. Write path: zero-copy via AcBinarySerializer.Serialize(value, output) directly to pipe. Read path: protocol eagerly deserializes data to typed object via SignalParams.SignalDataType, or returns raw byte[] for IsRawBytesData/byte[] fast-path. See AyCode.Services/docs/SIGNALR/README.md for full details.

Server Extensions

  • AyCode.Core.Server, AyCode.Interfaces.Server, AyCode.Entities.Server, AyCode.Models.Server — Server-only additions that don't belong in shared code.

Project Layout — Shared/Server Split

Workspace-wide convention. Each logical project gets one or two physical projects, named by suffix:

Suffix Contains Visibility
Foo Code shared by client and server (DTOs, interfaces, common logic) Both
Foo.Server Server-only code (data access, hosting, server-side services) Server only
Foo.Client RARE — only when truly client-only code exists Client only

Examples already in this repo: AyCode.Core + AyCode.Core.Server, AyCode.Interfaces + AyCode.Interfaces.Server, AyCode.Entities + AyCode.Entities.Server, AyCode.Models + AyCode.Models.Server, AyCode.Services + AyCode.Services.Server. No .Client projects exist by default.

Why no symmetric .Client

  • Most client-relevant code is genuinely shared (DTOs, common logic).
  • Doubling every project to .Client + .Server would balloon the workspace without proportional benefit.
  • Create .Client only for true client-only code (rare).

Why no security risk

  • The boundary is directional: server may reference shared; client must not reference .Server.
  • Enforcement at the project-graph + DLL reference level, not the suffix — a client referencing .Server fails to build.
  • Shared code is by design safe for both sides.

When a new project is added

Default: one shared Foo project. Add Foo.Server when server-only code accumulates. Add Foo.Client only for the rare case above.

Serialization Architecture

Three serializers share a common infrastructure but serve different goals:

Serializer Primary Goal Use Case
AcBinary Speed Wire protocol, SignalR, storage
AcJson Compatibility REST APIs, debugging, interop
Toon LLM Accuracy AI context, schema documentation

Generic Entity Pattern

Entities use composition via generic type parameters:

// Interface layer
interface IAcUser<TProfile, TCompany> : IId<Guid> { ... }

// Entity layer (abstract)
abstract class AcUser<TProfile, TCompany, TUserToCompany, TAddress> { ... }

// Consuming project (concrete)
class User : AcUser<Profile, Company, UserToCompany, Address> { ... }

Framework defines relationships without knowing concrete types.