AyCode.Core/docs/ARCHITECTURE.md

8.4 KiB

Architecture

Framework vs. Consumer Boundary

This solution is Layer 0 — Core framework. Consumers (plural, unknown) reference it.

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. A consumer CAN reference this framework; this framework CAN NEVER reference a 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

Well-designed framework → minimal consumer setup. Aim for:

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

Verbose consumer code = framework incomplete. Promote recurring patterns via extension methods, default-providing base classes, or options classes.

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)

Framework design follows "write the base first, derive the specific later" — when planning a new feature, first consider whether the generic part fits the framework, only then implement consumer-specific derived 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: the prefix marks code as framework-bequeathed. When a product class is named Order, an LLM (or human) reading consuming code immediately knows: this is product-domain concrete code, not framework abstraction. Cross-repo reference patterns become self-documenting: AcDalBase<Order> reads as "framework abstraction parameterized by a product concrete type" without needing to look up either side.

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 layers.

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.
  • 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 — applies to AyCode.* family, Mango.Nop.* family, FruitBank, FruitBankHybridApp, and all future projects in this workspace.

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 by default

  • Most code is genuinely shared (DTOs, common logic). A separate .Client project for the rare client-only case keeps the workspace project count manageable.
  • Project-count discipline: doubling every shared project to a .Client + .Server pair would balloon the workspace without proportional benefit.
  • Create .Client only when client-only code exists that doesn't belong in the shared Foo (rare).

Why no security risk in shared code seeing server context

  • The boundary is directional: server may reference shared code; client must not reference .Server code.
  • Enforcement is at the project-graph + DLL reference level, not the suffix. A client project that accidentally references Foo.Server will fail to build — manual review catches the rest.
  • A server seeing the shared (client-relevant) code is fine — the shared code is, by design, safe to expose to both sides.

When a new project is added

Default: one shared Foo project. Add Foo.Server only when server-only code accumulates that genuinely cannot live shared. Add Foo.Client only in the rare case described 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> { ... }

This allows the framework to define relationships without knowing concrete types.