Iyu.MainServer
0.9.0
dotnet add package Iyu.MainServer --version 0.9.0
NuGet\Install-Package Iyu.MainServer -Version 0.9.0
<PackageReference Include="Iyu.MainServer" Version="0.9.0" />
<PackageVersion Include="Iyu.MainServer" Version="0.9.0" />
<PackageReference Include="Iyu.MainServer" />
paket add Iyu.MainServer --version 0.9.0
#r "nuget: Iyu.MainServer, 0.9.0"
#:package Iyu.MainServer@0.9.0
#addin nuget:?package=Iyu.MainServer&version=0.9.0
#tool nuget:?package=Iyu.MainServer&version=0.9.0
iyu-framework-v5
Runtime library for the Iyu stack. Consumed by apps generated from M3L models
via mdd-booster. Provides a single
AddIyuMainServer entry point that wires EF Core, OData, and GraphQL on top of
generator-produced entities, plus optional modules for identity, attachments,
chat, and scheduled reports.
Targets .NET 10. All eight projects share one version and ship as separate NuGet packages.
Layers
| Project | Role |
|---|---|
Iyu.Core |
IyuEntity base class, marker attributes ([Lookup], [Rollup], [Computed], [Reference]), value objects (PhoneNumber, EmailAddress, WebUrl), identity contracts, and the attachment contracts (IAttachmentStorage, FileAccessToken, FileAccessTokenService) |
Iyu.Data |
IyuDbContext base + IyuTimestampInterceptor (automatic CreatedAt/UpdatedAt) + EF Core ValueConverters for the value objects |
Iyu.Server.OData |
IyuEdmModelBuilder.AddEntityPair<TRead,TWrite>(setName) + generic IyuODataController<TRead,TWrite> (CRUD), $search binder |
Iyu.Server.GraphQL |
IyuGraphQLSchemaBuilder.AddEntityPair<TRead,TWrite>(queryName, mutationPrefix) (HotChocolate-based) |
Iyu.MainServer |
Composite — AddIyuMainServer / UseIyuMainServer; also AddIyuIdentity / MapIyuIdentity (cookie + JWT bearer, OAuth2 client_credentials service clients) |
Iyu.FileServer |
AddIyuFileGateway / MapIyuFileGateway — token-gated byte gateway with Azure Blob and local filesystem backends |
Iyu.Server.Chat |
AddIyuChat / UseIyuChat — bare-chat adapter |
Iyu.VaultAi |
AddVaultAiReports / UseVaultAiReports — scheduled report generation |
Minimum consumer
using Iyu.MainServer;
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddIyuMainServer<AppDbContext>(
configureDb: db => db.UseSqlServer(builder.Configuration.GetConnectionString("Default")),
configure: options =>
{
options.ODataModel.AddEntityPair<OrderExt, Order>("Orders");
options.GraphQL .AddEntityPair<OrderExt, Order>("orders", "order");
// ...additional pairs, or a generated RegisterEntities(options) call
});
var app = builder.Build();
app.UseIyuMainServer();
app.Run();
Resulting endpoints:
GET /$data/$metadata— OData EDM documentGET /$data/Orders?$filter=Status eq 'confirmed'— OData queryPOST /graphqlwith{ orders { ... } }— GraphQL query
Read/Write pair model
Each logical entity has two CLR types:
- Write type (e.g.
Order) — mapped to the base SQL table. Contains only stored fields. Used for POST/PATCH/DELETE inside the controller. - Read type (e.g.
OrderExt) — mapped to a SQL view. Contains stored fields plus lookups/rollups/computed fields. Exposed as the OData entity set and GraphQL query field.
The controller copies overlapping properties from the read body to a fresh
write entity using reflection; extras are dropped. CreatedAt/UpdatedAt/Id
are explicitly excluded because they are owned by the interceptor or the
caller's explicit assignment.
Integration testing (TestServer)
The generated OData controllers live in the server assembly, but MVC discovers
controllers by walking the entry assembly's closure. In production the entry
assembly is the server, so discovery finds them. Under a test host the entry
assembly is testhost, whose closure does not include the server — so without
help every endpoint silently returns 404.
AddIyuMainServer handles this automatically: it registers the TContext
assembly and the registration callback's declaring assembly as application
parts. The standard method-group form therefore just works over TestServer:
var builder = WebApplication.CreateBuilder();
builder.WebHost.UseTestServer();
builder.Services.AddIyuMainServer<AppDbContext>(
configureDb: db => db.UseSqlite(conn),
configure: ApiRegistration.RegisterGeneratedEntities); // method group → server assembly
If your controllers live in yet another assembly, or you pass the callback as a lambda wrapper (whose declaring assembly is the caller, not the server), name the controller-hosting assemblies explicitly — registration is deduplicated, so this never double-registers:
configure: options =>
{
options.ControllerAssemblies.Add(typeof(SomeGeneratedController).Assembly);
ApiRegistration.RegisterGeneratedEntities(options);
}
File gateway (Iyu.FileServer)
A standalone host that moves bytes and nothing else. It holds no database: every
request carries an HMAC-SHA256-signed FileAccessToken minted by whoever owns
the attachment metadata, and the token names the storage key, so the gateway
cannot be redirected to another object.
builder.Services.AddIyuFileGateway(
gw =>
{
gw.SigningKey = builder.Configuration["Files:SigningKey"]!; // ≥32 bytes, shared with the minter
gw.MaxBytes = 50L * 1024 * 1024;
gw.AllowedContentTypes = ["application/pdf", "image/png"]; // empty = allow all
},
(FileSystemOptions fs) => fs.RootPath = "/var/attachments"); // or AzureBlobOptions
var app = builder.Build();
app.MapIyuFileGateway(); // PUT / GET / DELETE at gw.RoutePrefix (default "/files")
Behaviour worth knowing before deploying it:
MaxBytesis the authority on its own endpoint. The upload handler aligns the server's per-request body limit to it, so the host's global default (30,000,000 bytes on Kestrel, HTTP.sys and IIS in-process) does not silently cap uploads below it. Ceilings the gateway cannot raise still apply and must be configured by the operator: IIS out-of-process (maxAllowedContentLength) and any reverse proxy.- Rejections are structured, so a caller can tell them apart:
413with{"Error":"too_large"};400with{"Error":"content_type_not_allowed"}or{"Error":"content_type_required"}. An oversized body answers413whichever layer noticed it — the header check, the gateway's stream ceiling, or the host's guard. AllowedContentTypeschecks the type the token declares, not the bytes that arrive. It enforces a policy the minter committed to; it does not sniff the payload. Matching comparestype/subtypeand ignores parameters, soimage/jpeg; charset=binarysatisfies an entry ofimage/jpeg; wildcards are not expanded. Once non-empty it fails closed — a token declaring no content type is rejected rather than waved through.- Downloads support
Range, so a large transfer can resume and a media client can seek. This is verified end-to-end against the filesystem backend; on Azure Blob it depends on the SDK's read stream reporting its length when opened, which is untested here (see Status). Uploads are a single request with no resume: a dropped connection restarts from zero, which is why the default limit is sized for document attachments rather than bulk media. - A missing object is 404, not 500. Absence is a normal state — a key can be deleted while a still-valid token is in flight.
- Every rejection is logged under the category
Iyu.FileServer.FileGateway(FileGatewayExtensions.LogCategory) so it can be filtered independently. Tokens are never logged. Successful transfers are not logged either — that is the host's request log.
Migrating to 0.9.0
Three breaking changes, all in the attachment layer:
IAttachmentStorage.OpenReadAsyncnow returnsTask<Stream?>, wherenullmeans "nothing is stored at this key". CustomIAttachmentStorageimplementations must be updated: normalise your backend's own not-found signal intonullinstead of letting it throw. Callers get a compiler error until they handle thenull, which is the point — the previous behaviour surfaced a missing object as an unhandled exception and a 500.AllowedContentTypesnow fails closed. If you configure an allowlist, tokens must declare a content type; previously such tokens bypassed the allowlist entirely. SetFileAccessToken.ContentTypewhen minting.- An oversized upload answers
413instead of400. The body is unchanged ({"Error":"too_large"}), so a client that reads the payload needs no change; one that branches on the status code does. The other two rejections stay400. Allowlist matching also became parameter-insensitive, which only ever accepts more than before — nothing that used to upload will start failing.
Build & test
dotnet build IyuFramework.slnx
dotnet test IyuFramework.slnx
All warnings are treated as errors across every project in the solution.
Status
Version 0.9.0. 207 unit and integration tests passing; build clean with no warnings. The OData/GraphQL runtime, identity, attachments, chat, and reports modules are all in place and consumed in production.
Known gaps, in rough priority order:
- The Azure Blob storage backend has no automated coverage — the test suite uses a fake and the local filesystem backend. Backend-specific failure modes are therefore not caught here.
- Resumable (chunked) upload is not supported. A GB-scale upload that drops restarts from zero. Adding it means a resumable protocol plus session state, which the gateway deliberately does not have today.
- The gateway has no request-timeout or minimum-data-rate policy of its own, so
a slow but legitimate upload is subject to the host's slow-POST defences
(Kestrel's
MinRequestBodyDataRate, 240 bytes/second by default).
| Product | Versions Compatible and additional computed target framework versions. |
|---|---|
| .NET | net10.0 is compatible. net10.0-android was computed. net10.0-browser was computed. net10.0-ios was computed. net10.0-maccatalyst was computed. net10.0-macos was computed. net10.0-tvos was computed. net10.0-windows was computed. |
-
net10.0
- Iyu.Core (>= 0.9.0)
- Iyu.Data (>= 0.9.0)
- Iyu.Server.GraphQL (>= 0.9.0)
- Iyu.Server.OData (>= 0.9.0)
- Microsoft.AspNetCore.Authentication.JwtBearer (>= 10.0.0)
- System.IdentityModel.Tokens.Jwt (>= 8.19.1)
NuGet packages
This package is not used by any NuGet packages.
GitHub repositories
This package is not used by any popular GitHub repositories.