Syntrony 1.4.0
dotnet add package Syntrony --version 1.4.0
NuGet\Install-Package Syntrony -Version 1.4.0
<PackageReference Include="Syntrony" Version="1.4.0" />
<PackageVersion Include="Syntrony" Version="1.4.0" />
<PackageReference Include="Syntrony" />
paket add Syntrony --version 1.4.0
#r "nuget: Syntrony, 1.4.0"
#:package Syntrony@1.4.0
#addin nuget:?package=Syntrony&version=1.4.0
#tool nuget:?package=Syntrony&version=1.4.0
Syntrony
Core Syntrony Framework — a single entry point, services.AddSyntrony(options => {...}), that every other
Syntrony package builds its own Use* extension on top of. Native DI, no service locator, no container swap.
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddControllers();
builder.Services.AddHttpContextAccessor();
builder.Services.AddSyntrony(options =>
{
options.AddApplicationServices();
options.AddInfrastructureServices();
options.UseWrapper();
options.UseMapping();
options.UseUnitOfWork();
options.UseSyntronyWeb();
});
var app = builder.Build();
// ... pipeline
app.Run();
AddApplicationServices/AddInfrastructureServices aren't part of the core — they're this solution's own
extension methods, one per project, each registering its own assembly for convention-based scanning:
// MyApp.Application/ApplicationServiceExtensions.cs
namespace MyApp.Application;
public static class ApplicationServiceExtensions
{
public static SyntronyOptions AddApplicationServices(this SyntronyOptions options)
{
options.AddAssemblyContaining<ApplicationServiceExtensions>();
return options;
}
}
Program.cs never needs to name a type from another project just to point at its assembly — each project
owns that detail about itself. This is the same mechanism a Syntrony package's own Use* extension uses
(Services and AddAssemblyContaining<T>() are both public on SyntronyOptions); nothing distinguishes a
framework package's extension from your own project's. Add* here matches the ASP.NET Core convention for
service-registration extensions (AddControllers, AddDbContext, ...); Use* is reserved for the Syntrony
packages that configure actual framework capabilities.
What's in the core
AddSyntrony/SyntronyOptions— the entry point and the object everyUse*extension configures.Servicesis theIServiceCollectionbeing built;Assembliesis a live view (not a snapshot) of the assemblies added viaAddAssembly/AddAssemblyContaining<T>;AddInitializerdefers work to application startup.- Convention-based registration — a class implementing
ITransientDependency,IScopedDependencyorISingletonDependency(Syntrony.DependencyInjection) is registered against itself and its default interfaces automatically, noservices.AddScoped<...>()per service.IApplicationService(Syntrony.Application) marks an application service the same way. - Deferred initialization pipeline —
AddInitializercallbacks run once, in registration order, before the app serves its first request. Consumers without anIHost(tests, console apps, migration tools) callIServiceProvider.InitializeSyntronyAsync()explicitly. SyntronyExceptionand the classification contractsIHasLogSeverity,IHasHttpStatusCode,IHasErrorCode— let any package's exception declare how it should be logged and mapped to HTTP without depending on the packages that do the logging or the mapping.UserFriendlyException,BusinessExceptionandAuthorizationExceptionare the framework's own exceptions built on those contracts — see Exceptions below.Result<T>(Syntrony.Results) — the uniform response contract: the same shape a Syntrony service returns and a Syntrony client consumes. SeeResult<T>below.LogSeverityExtensions,LoggerExceptionExtensionsandLogHelper— translateLogSeveritytoMicrosoft.Extensions.Logging.LogLevel, log an exception at the severity it declares viaIHasLogSeverity(Errorif it declares none), and a last-resort static logger for code that can't receive anILoggerby injection.LogHelper.LoggerisNullLogger.Instanceuntil something callsLogHelper.Initialize— a sink package's ownUse*extension, typically viaAddInitializer— and never throws on its own.ICurrentUser— lazily-evaluated, scoped access to the authenticated user's claims. Always anonymous outside a web request; never touches a database.TimeProvider.Systemregistered by default (TryAddSingleton), substitutable in tests.
No dependency on ASP.NET Core or any other Syntrony package — see Hosting model below for
why that matters, and ICurrentUser for how the pieces that would
otherwise need it are split.
Hosting model
AddSyntrony returns IServiceCollection, the same collection you passed in. Nothing is swapped, wrapped, or
rebuilt into a different container. That one fact is what makes the core compatible with every ASP.NET Core
hosting model, including the minimal one — WebApplication.CreateBuilder(args) — which most systems built on
a module framework that replaces the container (Castle Windsor via AbpModule, Autofac modules, etc.) cannot
use, because those frameworks need ConfigureServices to return a different IServiceProvider, a signature
the minimal and generic hosts don't support.
Migrating off a module framework replaces however many bootstrap classes it required with one Program.cs:
// Before: Program.cs + Startup.cs + N module classes, each doing RegisterAssemblyByConvention,
// module-specific configuration (AutoMapper, EF Core, application parts), and returning
// a replacement IServiceProvider from ConfigureServices.
// After: one Program.cs, no module classes — each project registers its own assembly
// through its own extension method, same as the example at the top of this README.
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddControllers();
builder.Services.AddHttpContextAccessor();
builder.Services.AddSyntrony(options =>
{
options.AddApplicationServices();
options.AddInfrastructureServices();
options.UseWrapper();
options.UseMapping();
options.UseUnitOfWork();
options.UseSyntronyWeb();
});
var app = builder.Build();
// ... pipeline
app.Run();
AddSyntrony also works unchanged under the legacy WebHost.CreateDefaultBuilder().UseStartup<T>() model, as
long as Startup.ConfigureServices returns void/IServiceCollection rather than swapping the container —
it only needs an IServiceCollection, nothing about the surrounding host. That matters during a migration:
adopting Syntrony's packages is not gated on migrating the hosting model first: both can happen in either order,
or at the same time.
Initializers registered via AddInitializer are guaranteed to finish before the application serves its first
request, regardless of hosting model or how many other hosted services are registered before AddSyntrony is
called — see the deferred initialization pipeline above.
Result<T>
Syntrony.Results.Result<T> is the uniform response contract: the same shape a Syntrony service returns and a
Syntrony client consumes, so the JSON on the wire never changes between the two ends.
using Syntrony.Results;
public Result<UserDto> GetUser(int id)
{
var user = _users.FirstOrDefault(u => u.Id == id);
return user is null
? Result<UserDto>.Failure("User not found", HttpStatusCode.NotFound)
: Result<UserDto>.Success(user, HttpStatusCode.OK);
}
Construction always goes through Success/Failure so an impossible state (IsSuccess true with Error
populated) can't be produced — Value, Error, StatusCode and IsSuccess still have public setters, and
Result<T> has a public parameterless constructor, only so System.Text.Json can deserialize it on the client
side; application code should never call new Result<T>() directly. Failure(error, value, statusCode) keeps
a partial value alongside the error when one is available. There's no non-generic Result — a void/Task
action returns Result<object?>.Success(null, statusCode).
Exceptions
UserFriendlyException, BusinessException and AuthorizationException (namespace Syntrony) all derive
from SyntronyException and declare their own HTTP status and log severity via the classification contracts,
so a package that maps exceptions to responses (like Syntrony.Wrapper) never needs to know the concrete type:
| Exception | IHasHttpStatusCode |
IHasLogSeverity |
IHasErrorCode |
|---|---|---|---|
UserFriendlyException |
400 BadRequest |
Warn |
yes |
BusinessException |
409 Conflict |
Warn |
yes |
AuthorizationException |
403 Forbidden |
Warn |
no |
None of the three is sealed — derive BusinessException for your own domain and inherit the status without
reimplementing anything. AuthorizationException declares 403 by default; a request-aware handler (e.g.
Syntrony.Wrapper's) downgrades it to 401 when the caller turns out to be anonymous — the exception itself
never inspects auth state. Code defaults to 0, meaning "no business error code"; a handler typically omits
it from the Result<T> it builds when it's 0. See ICurrentUser
below for a usage example of AuthorizationException.
EntityNotFoundException and ConcurrencyException are not part of this hierarchy — they're data-access
concerns that live in Syntrony.Repositories instead.
ICurrentUser and the authenticated user
ICurrentUser reads the current ClaimsPrincipal lazily: reading a claim is free, so a service that never
touches ICurrentUser pays nothing for it. It never queries a database and never throws for an
anonymous or missing principal — outside a web request (a background job, a console tool, a test) it's simply
anonymous.
That's a deliberate scope limit, not an oversight. A pattern worth avoiding: resolving the current user inside the constructor of a shared base class, with its own synchronous database read and its own unit of work, and an authorization exception thrown from that constructor. Besides the extra I/O paid by every instantiation whether or not the method needs it, an exception thrown from a constructor is fragile to map to the right HTTP status — how it surfaces depends on how the container happens to unwrap it.
If your application needs to confirm the authenticated user still exists and is active, do it explicitly where it matters:
public async Task<GenericOutput> SetUserRolesAsync(SetUserRolesInput input)
{
var actor = await _users.FirstOrDefaultAsync(u => u.Id == _currentUser.Id && u.IsActive)
?? throw new AuthorizationException("User does not exist or is inactive.");
...
}
If that check must apply to every endpoint, put it in an IAuthorizationHandler or a filter — it runs once per
request, asynchronously, inside the platform's authorization pipeline, with 401/403 mapping already handled.
ICurrentUser is registered scoped — never inject it into a singleton; it will capture the first request's
scope and serve that data to every later one.
Outside Syntrony.Web, ICurrentUser resolves to an always-anonymous ClaimsPrincipal-less accessor, so
consumers with no web layer never fail to resolve it. Syntrony.Web's UseSyntronyWeb() supplies the
HttpContext-backed accessor.
| Product | Versions Compatible and additional computed target framework versions. |
|---|---|
| .NET | net8.0 is compatible. net8.0-android was computed. net8.0-browser was computed. net8.0-ios was computed. net8.0-maccatalyst was computed. net8.0-macos was computed. net8.0-tvos was computed. net8.0-windows was computed. net9.0 was computed. net9.0-android was computed. net9.0-browser was computed. net9.0-ios was computed. net9.0-maccatalyst was computed. net9.0-macos was computed. net9.0-tvos was computed. net9.0-windows was computed. net10.0 was computed. 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. |
-
net8.0
- Microsoft.Extensions.DependencyInjection.Abstractions (>= 8.0.2)
- Microsoft.Extensions.Hosting.Abstractions (>= 8.0.1)
- Microsoft.Extensions.Logging.Abstractions (>= 8.0.3)
NuGet packages (7)
Showing the top 5 NuGet packages that depend on Syntrony:
| Package | Downloads |
|---|---|
|
Syntrony.HttpClient
Reusable HTTP client for Syntrony's .NET services: default configuration via IHttpClientFactory, typed helpers with Result<T>, multipart/streaming support and optional resilience policies with Polly. |
|
|
Syntrony.Logging
Syntrony structured logging package: unified UseLogging configuration with pluggable sinks and OpenTelemetry-based telemetry. |
|
|
Syntrony.Web
Exposes Syntrony application services as HTTP endpoints without hand-written controllers: an IApplicationFeatureProvider turns every IApplicationService into a controller and an IApplicationModelConvention derives route, HTTP verb and parameter binding from the method name, replacing ABP's CreateControllersForAppServices. |
|
|
Syntrony.Repositories
Contrato de repositorio generico y agnostico de motor de persistencia, equivalente a IRepository<TEntity, TKey> de ABP recortado a lo que se usa, junto con las interfaces de entidad (IEntity<TKey>) y las convenciones de auditoria/soft-delete que las implementaciones deben respetar. Sin dependencias de EF Core, Mongo ni ASP.NET Core. |
|
|
Syntrony.Wrapper
Applies Syntrony's Result<T> response contract to the ASP.NET Core pipeline: an IAsyncResultFilter wraps successful responses (opt-out via [DontWrap]) and an IExceptionHandler builds the failure Result<T> from IHasHttpStatusCode, with ProblemDetails (RFC 7807) available as an alternative error format. |
GitHub repositories
This package is not used by any popular GitHub repositories.