Fly.Shared.Core
1.9.0
dotnet add package Fly.Shared.Core --version 1.9.0
NuGet\Install-Package Fly.Shared.Core -Version 1.9.0
<PackageReference Include="Fly.Shared.Core" Version="1.9.0" />
<PackageVersion Include="Fly.Shared.Core" Version="1.9.0" />
<PackageReference Include="Fly.Shared.Core" />
paket add Fly.Shared.Core --version 1.9.0
#r "nuget: Fly.Shared.Core, 1.9.0"
#:package Fly.Shared.Core@1.9.0
#addin nuget:?package=Fly.Shared.Core&version=1.9.0
#tool nuget:?package=Fly.Shared.Core&version=1.9.0
Fly.Shared.Core
Core shared library for building FlyOS-compatible business app microservices on .NET 9.
Installation
<PackageReference Include="Fly.Shared.Core" Version="1.*" />
What's Included
| Module | Namespace | What It Provides |
|---|---|---|
| Multi-tenancy | Fly.Shared.Core.MultiTenancy |
ITenantContext, TenantContext, TenantEntity, AuditableEntity, TenantMiddleware, MultiTenantDbContext with automatic global query filters |
| Authentication | Fly.Shared.Core.Auth |
ClaimsExtensions for extracting tenant/user from JWT, FlyAuthorizeAttribute for Cerbos-based authorization |
| Caching | Fly.Shared.Core.Caching |
IFlyCache / RedisFlyCache — Redis-backed cache with consistent key conventions |
| i18n | Fly.Shared.Core.I18n |
TranslatableEntity<T> base class for multilingual entities |
| Middleware | Fly.Shared.Core.Middleware |
CorrelationIdMiddleware — injects/propagates X-Correlation-ID on every request |
| API Models | Fly.Shared.Core.Models |
ApiResponse<T>, ApiResponse, ApiError, PagedRequest — standard envelope types |
| Swagger / OpenAPI | Fly.Shared.Core.Swagger |
IncludeFlyOpenApiXmlComments() — registers entry-assembly + every Fly.Shared.*.xml doc-comments file with Swashbuckle. ApiDocsManifest record — declares OpenAPI document location for the gateway aggregator (sent on POST /api/apps/{appId}/onboard). ApiDocsPathValidator.IsValid(path, out reason) — shared SSRF-safe path validator with fixed-point decode loop; used by Controller onboarding (write-time) and Gateway upstream proxy (fetch-time defence-in-depth). |
Quick Start
Service Registration
var builder = WebApplication.CreateBuilder(args);
// Registers TenantContext, IFlyCache (Redis), IHttpContextAccessor, MemoryCache
builder.Services.AddFlyCore();
// Add your DbContext using the shared multi-tenant base
builder.Services.AddDbContext<MyAppDbContext>(options =>
options.UseNpgsql(builder.Configuration.GetConnectionString("Default")));
var app = builder.Build();
// Resolves tenant from JWT sub-claim and populates ITenantContext
app.UseMiddleware<TenantMiddleware>();
app.UseMiddleware<CorrelationIdMiddleware>();
app.MapControllers();
app.Run();
Multi-Tenant Entity
using Fly.Shared.Core.MultiTenancy;
public class Project : TenantEntity
{
public string Name { get; set; } = string.Empty;
public bool IsActive { get; set; } = true;
}
Multi-Tenant DbContext
using Fly.Shared.Core.MultiTenancy;
public class MyAppDbContext : MultiTenantDbContext
{
public MyAppDbContext(DbContextOptions<MyAppDbContext> options, ITenantContext tenant)
: base(options, tenant) { }
public DbSet<Project> Projects => Set<Project>();
}
MultiTenantDbContext automatically applies a global query filter e.TenantId == currentTenantId on all TenantEntity sets, and stamps TenantId on SaveChanges.
API Response Envelope
[ApiController]
[Route("api/projects")]
public class ProjectsController : ControllerBase
{
[HttpGet]
public IActionResult GetAll()
{
var projects = _service.GetAll();
return Ok(ApiResponse<List<ProjectDto>>.Ok(projects));
}
[HttpPost]
public IActionResult Create(CreateProjectRequest req)
{
if (!ModelState.IsValid)
return BadRequest(ApiResponse.Fail("VALIDATION_ERROR", "Invalid input"));
var project = _service.Create(req);
return Created($"api/projects/{project.Id}", ApiResponse<ProjectDto>.Ok(project));
}
}
Caching
public class ProjectService
{
private readonly IFlyCache _cache;
public async Task<Project?> GetByIdAsync(Guid id)
{
return await _cache.GetOrSetAsync(
$"project:{id}",
() => _db.Projects.FindAsync(id).AsTask(),
TimeSpan.FromMinutes(15));
}
}
Swagger / OpenAPI
AddFlyApiDocs is the bundled "good defaults" registration — single call, replaces the SwaggerDoc / XML / Bearer-security boilerplate every backend would otherwise repeat:
using Fly.Shared.Core.Swagger;
builder.Services.AddFlyApiDocs(opts =>
{
opts.Title = "My API"; // optional; defaults to entry-assembly name
opts.Description = "Optional summary shown in Swagger UI";
});
What that one call does:
- registers
SwaggerDoc(opts.Version, ...)withOpenApiInfopopulated fromTitle/Version/Description; - calls
IncludeFlyOpenApiXmlComments()so the entry-assembly XML and everyFly.Shared.*.xmlnext to the binary are picked up; - when
AddSecurityScheme = true(default) registers a Bearer JWT security definition + global requirement so the Swagger UI Authorize dialog accepts a token; - finally invokes
opts.PostConfigure?.Invoke(swaggerGenOptions)so callers can layer on tags, schema filters, OAuth2 flows, or anything else without giving up the bundled defaults.
For an explicitly anonymous service (e.g. a public landing doc) opt out of the Bearer scheme:
builder.Services.AddFlyApiDocs(opts => opts.AddSecurityScheme = false);
Lower-level access remains available when something more bespoke is needed:
builder.Services.AddSwaggerGen(c =>
{
c.SwaggerDoc("v1", new() { Title = "My API", Version = "v1" });
c.IncludeFlyOpenApiXmlComments();
});
The consuming project must enable <GenerateDocumentationFile>true</GenerateDocumentationFile> and <CopyDocumentationFileToOutputDirectory>true</CopyDocumentationFileToOutputDirectory> in its csproj for the entry-assembly XML to exist at runtime.
Declaring your OpenAPI document for the gateway aggregator
Build an ApiDocsManifest and pass it on the trailing ApiDocs parameter of AppOnboardingRequest when your service onboards. The Controller persists it next to your gateway route hints; the gateway-side aggregator reads it on its next poll cycle to populate the unified Swagger UI dropdown.
using Fly.Shared.Core.Swagger;
// AppOnboardingRequest has many positional parameters; use named args so this
// snippet compiles as written. Pass null for any sections you don't reconcile
// in this onboarding call.
var manifest = new AppOnboardingRequest(
Permissions: null,
Roles: null,
NotificationTemplates: null,
ServiceAccess: null,
GatewayRoutes: null,
HelpArticles: null,
ApiDocs: new ApiDocsManifest(
Path: "/swagger/v1/swagger.json",
Title: "Circles API",
Version: "1.0.0",
RequiredScopes: new() { "fly-api" }));
To remove a previously published descriptor, send new ApiDocsManifest("") — the empty-path tombstone sentinel.
The Swagger/ namespace will grow as the gateway aggregator and richer Swashbuckle wiring land in upcoming releases.
Scoped inter-service calls
InterServiceTokenHandler (registered automatically by AddFlyServiceClient) attaches a cached client_credentials Bearer token to every outbound request. By default the token is acquired with the catch-all scope inter-service and cached per client_id. Callers that need a different scope set on a single call attach a hint to the HttpRequestMessage:
using Fly.Shared.Core.Http;
var req = new HttpRequestMessage(HttpMethod.Get, "https://controller/api/something");
req.WithInterServiceScopes("inter-service", "circles");
using var resp = await flyClient.HttpClient.SendAsync(req, ct);
The handler:
- normalises the supplied set (deduplicated, case-insensitive, sorted) so
["A", "b"]and["b", "A"]reach the same cache slot; - keys the token cache as
fly:inter-service-token:{clientId}:{sortedScopes}so a default-scope token and a custom-scope token never overwrite each other; - requests the joined scope string from OpenIddict on cache miss, falling back to
"inter-service"when no hint is set.
The stored OpenIddict client must already have been granted every requested scope — otherwise the token endpoint returns OpenIddict ID2051 and the call fails.
Configuration
Add to appsettings.json:
{
"Redis": {
"ConnectionString": "localhost:6379"
}
}
Dependencies
Microsoft.AspNetCore.App(framework reference)Microsoft.EntityFrameworkCore9.xMicrosoft.EntityFrameworkCore.Relational9.xMicrosoft.Extensions.Caching.StackExchangeRedis9.xStackExchange.Redis2.xFluentValidation.AspNetCore11.xOpenTelemetry.Extensions.Hosting1.xOpenTelemetry.Instrumentation.AspNetCore1.x
Related Packages
- Fly.Shared.Messaging — MassTransit/RabbitMQ wiring and platform event contracts. Depends on this package.
| 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
- AspNetCore.HealthChecks.NpgSql (>= 9.0.0)
- AspNetCore.HealthChecks.Rabbitmq (>= 9.0.0)
- AspNetCore.HealthChecks.Redis (>= 9.0.0)
- Azure.Extensions.AspNetCore.DataProtection.Blobs (>= 1.4.0)
- Azure.Extensions.AspNetCore.DataProtection.Keys (>= 1.4.0)
- Azure.Identity (>= 1.17.2)
- Azure.Security.KeyVault.Keys (>= 4.7.0)
- Azure.Storage.Blobs (>= 12.26.0)
- FluentValidation.AspNetCore (>= 11.3.1)
- Microsoft.AspNetCore.DataProtection.StackExchangeRedis (>= 10.0.8)
- Microsoft.EntityFrameworkCore (>= 10.0.8)
- Microsoft.EntityFrameworkCore.Relational (>= 10.0.8)
- Microsoft.Extensions.Caching.StackExchangeRedis (>= 10.0.8)
- Microsoft.Extensions.Http.Resilience (>= 10.6.0)
- Npgsql (>= 9.0.5)
- OpenTelemetry.Extensions.Hosting (>= 1.15.3)
- OpenTelemetry.Instrumentation.AspNetCore (>= 1.15.2)
- StackExchange.Redis (>= 2.12.8)
- Swashbuckle.AspNetCore (>= 10.1.7)
NuGet packages (1)
Showing the top 1 NuGet packages that depend on Fly.Shared.Core:
| Package | Downloads |
|---|---|
|
Fly.Shared.Messaging
MassTransit/RabbitMQ wiring and platform event/command contracts for FlyOS business app integration. Provides AddFlyMessaging() extension, exponential retry, in-memory outbox, and all platform event records (TenantCreated, UserCreated, LicenseUpdated, etc.). |
GitHub repositories
This package is not used by any popular GitHub repositories.