162 lines
8.4 KiB
Markdown
162 lines
8.4 KiB
Markdown
# 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:
|
|
|
|
```csharp
|
|
// 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:
|
|
|
|
```csharp
|
|
// 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.
|