Syntrony 1.4.0

dotnet add package Syntrony --version 1.4.0
                    
NuGet\Install-Package Syntrony -Version 1.4.0
                    
This command is intended to be used within the Package Manager Console in Visual Studio, as it uses the NuGet module's version of Install-Package.
<PackageReference Include="Syntrony" Version="1.4.0" />
                    
For projects that support PackageReference, copy this XML node into the project file to reference the package.
<PackageVersion Include="Syntrony" Version="1.4.0" />
                    
Directory.Packages.props
<PackageReference Include="Syntrony" />
                    
Project file
For projects that support Central Package Management (CPM), copy this XML node into the solution Directory.Packages.props file to version the package.
paket add Syntrony --version 1.4.0
                    
#r "nuget: Syntrony, 1.4.0"
                    
#r directive can be used in F# Interactive and Polyglot Notebooks. Copy this into the interactive tool or source code of the script to reference the package.
#:package Syntrony@1.4.0
                    
#:package directive can be used in C# file-based apps starting in .NET 10 preview 4. Copy this into a .cs file before any lines of code to reference the package.
#addin nuget:?package=Syntrony&version=1.4.0
                    
Install as a Cake Addin
#tool nuget:?package=Syntrony&version=1.4.0
                    
Install as a Cake Tool

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 every Use* extension configures. Services is the IServiceCollection being built; Assemblies is a live view (not a snapshot) of the assemblies added via AddAssembly/AddAssemblyContaining<T>; AddInitializer defers work to application startup.
  • Convention-based registration — a class implementing ITransientDependency, IScopedDependency or ISingletonDependency (Syntrony.DependencyInjection) is registered against itself and its default interfaces automatically, no services.AddScoped<...>() per service. IApplicationService (Syntrony.Application) marks an application service the same way.
  • Deferred initialization pipeline — AddInitializer callbacks run once, in registration order, before the app serves its first request. Consumers without an IHost (tests, console apps, migration tools) call IServiceProvider.InitializeSyntronyAsync() explicitly.
  • SyntronyException and the classification contracts IHasLogSeverity, 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, BusinessException and AuthorizationException are 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. See Result<T> below.
  • LogSeverityExtensions, LoggerExceptionExtensions and LogHelper — translate LogSeverity to Microsoft.Extensions.Logging.LogLevel, log an exception at the severity it declares via IHasLogSeverity (Error if it declares none), and a last-resort static logger for code that can't receive an ILogger by injection. LogHelper.Logger is NullLogger.Instance until something calls LogHelper.Initialize — a sink package's own Use* extension, typically via AddInitializer — 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.System registered 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 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. 
Compatible target framework(s)
Included target framework(s) (in package)
Learn more about Target Frameworks and .NET Standard.

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.

Version Downloads Last Updated
1.4.0 81 10/4/2026
1.3.0 109 10/3/2026
1.2.0 140 9/24/2026
1.1.0 192 9/21/2026
1.0.0 115 9/4/2026