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:
- Identify generic vs. consumer-specific parts
- Move generic part → appropriate framework layer (abstract base, options, or extension)
- 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/IGeneratedBinaryReaderfor[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, seeAyCode.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 viaAcBinarySerializer.Serialize(value, output)directly to pipe. Read path: protocol eagerly deserializesdatato typed object viaSignalParams.SignalDataType, or returns rawbyte[]forIsRawBytesData/byte[] fast-path. SeeAyCode.Services/docs/SIGNALR/README.mdfor 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+.Serverwould balloon the workspace without proportional benefit. - Create
.Clientonly 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
.Serverfails 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.