All notable changes to the Trellis project will be documented in this file.
The format is based on Keep a Changelog, and this project adheres to Semantic Versioning.
- TRLS003, TRLS004, TRLS006 — The unsafe-access analyzers now recognize ternary conditional expressions (
? :) as valid guards. Previously,maybe.HasValue ? maybe.Value : fallbackand similar patterns forResult.Value/Result.Errorproduced false-positive diagnostics.
ReplaceResourceLoader<TMessage, TResource>— NewIServiceCollectionextension method that removes all existingIResourceLoader<TMessage, TResource>registrations and re-registers the replacement as scoped (matching the production lifetime of resource loaders). Accepts aFunc<IServiceProvider, IResourceLoader>factory. Eliminates the need to manually callRemoveAllbefore re-registering whenAddMockAntiCorruptionLayer()causes duplicate DI registrations.
[StringLength]—RequiredString<TSelf>derivatives now support[StringLength(max)]and[StringLength(max, MinimumLength = min)]for declarative length validation at creation time. The source generator emits.Ensure()length checks inTryCreatewith clear validation error messages (e.g.,"First Name must be 50 characters or fewer.").
MoneyConvention—ApplyTrellisConventionsnow automatically mapsMoneyproperties as owned types with{PropertyName}(decimal 18,3) +{PropertyName}Currency(nvarchar 3) columns. Scale 3 accommodates all ISO 4217 minor units (BHD, KWD, OMR, TND). NoOwnsOneconfiguration needed. ExplicitOwnsOnetakes precedence.
Money— Added private parameterless constructor and private setters onAmount/Currencyfor EF Core materialization support. No public API changes.
Lightweight authorization primitives with zero dependencies beyond Trellis.Results:
Actor— Sealed record representing an authenticated user (Id+Permissions) withHasPermission,HasAllPermissions,HasAnyPermissionhelpersIActorProvider— Abstraction for resolving the current actor (implement in API layer)IAuthorize— Marker interface for static permission requirements (AND logic)IAuthorizeResource<TResource>— Resource-based authorization with a loaded resource viaAuthorize(Actor, TResource)IResourceLoader<TMessage, TResource>— Loads the resource required for resource-based authorizationResourceLoaderById<TMessage, TResource, TId>— Convenience base class for ID-based resource loading
Usable with or without CQRS — no Mediator dependency.
Result-aware pipeline behaviors for martinothamar/Mediator v3:
ValidationBehavior— Short-circuits onIValidate.Validate()failureAuthorizationBehavior— ChecksIAuthorize.RequiredPermissionsviaIActorProviderResourceAuthorizationBehavior<TMessage, TResource, TResponse>— Loads resource viaIResourceLoader, delegates toIAuthorizeResource<TResource>.Authorize(Actor, TResource). Auto-discovered viaAddResourceAuthorization(Assembly)or registered explicitly for AOT.LoggingBehavior— Structured logging with duration and Result outcomeTracingBehavior— OpenTelemetry activity span with Result statusExceptionBehavior— Catches unhandled exceptions →Error.UnexpectedServiceCollectionExtensions—PipelineBehaviorsarray andAddTrellisBehaviors()DI registration
IFailureFactory<TSelf>— Static abstract interface for AOT-friendly typed failure creation in generic pipeline behaviorsResult<TValue>now implementsIFailureFactory<Result<TValue>>
Specification<T> is a new DDD building block for encapsulating business rules as composable, storage-agnostic expression trees:
Specification<T>— Abstract base class withToExpression(),IsSatisfiedBy(T), andAnd/Or/Notcomposition- Expression-tree based — Works with EF Core 8+ for server-side filtering via
IQueryable - Implicit conversion to
Expression<Func<T, bool>>for seamless LINQ integration - In-memory evaluation via
IsSatisfiedBy(T)for domain logic and testing
// Define a specification
public class HighValueOrderSpec(decimal threshold) : Specification<Order>
{
public override Expression<Func<Order, bool>> ToExpression() =>
order => order.TotalAmount > threshold;
}
// Compose specifications
var spec = new OverdueOrderSpec(now).And(new HighValueOrderSpec(500m));
var orders = await dbContext.Orders.Where(spec).ToListAsync();Maybe<T> now has a notnull constraint and new transformation methods, making it a proper domain-level optionality type:
notnullconstraint —Maybe<T> where T : notnullprevents wrapping nullable typesMap<TResult>— Transform the inner value:maybe.Map(url => url.Value)returnsMaybe<string>Match<TResult>— Pattern match:maybe.Match(url => url.Value, () => "none")- Implicit operator —
Maybe<Url> m = url;works naturally
Full support for optional value object properties in DTOs:
MaybeScalarValueJsonConverter<TValue,TPrimitive>— JSON deserialization:null→Maybe.None, valid →Maybe.From(validated), invalid → validation error collectedMaybeScalarValueJsonConverterFactory— Auto-discoversMaybe<T>properties on DTOsMaybeModelBinder<TValue,TPrimitive>— MVC model binding: absent/empty →Maybe.None, valid →Maybe.From(result), invalid → ModelState errorMaybeSuppressChildValidationMetadataProvider— Prevents MVC from requiring child properties onMaybe<T>(fixes MVC crash)ScalarValueTypeHelperadditions —IsMaybeScalarValue(),GetMaybeInnerType(),GetMaybePrimitiveType()- SampleWeb apps updated —
Maybe<Url> Websiteon User/RegisterUserDto,Maybe<FirstName> AssignedToon UpdateOrderDto
Maybe<T>now requireswhere T : notnull— see Migration Guide for details
A comprehensive suite of 18 Roslyn analyzers to enforce Railway Oriented Programming best practices at compile time:
Safety Rules (Warnings):
- TRLS001: Detect unhandled Result return values
- TRLS003: Prevent unsafe
Result.Valueaccess withoutIsSuccesscheck - TRLS004: Prevent unsafe
Result.Erroraccess withoutIsFailurecheck - TRLS006: Prevent unsafe
Maybe.Valueaccess withoutHasValuecheck - TRLS007: Suggest
Create()instead ofTryCreate().Valuefor clearer intent - TRLS008: Detect
Result<Result<T>>double wrapping - TRLS009: Prevent blocking on
Task<Result<T>>with.Resultor.Wait() - TRLS011: Detect
Maybe<Maybe<T>>double wrapping - TRLS014: Detect async lambda used with sync method (Map instead of MapAsync)
- TRLS015: Don't throw exceptions in Result chains (defeats ROP purpose)
- TRLS016: Empty error messages provide no debugging context
- TRLS017: Don't compare Result/Maybe to null (they're structs)
- TRLS018: Unsafe
.Valueaccess in LINQ without filtering first
Best Practice Rules (Info):
- TRLS002: Suggest
Bindinstead ofMapwhen lambda returns Result - TRLS005: Suggest
MatchErrorfor type-safe error discrimination - TRLS010: Suggest specific error types instead of base
Errorclass - TRLS012: Suggest
Result.Combine()for multiple Result checks - TRLS013: Suggest
GetValueOrDefault/Matchinstead of ternary operator
Benefits:
- ✅ Catch common ROP mistakes at compile time
- ✅ Guide developers toward best practices
- ✅ Improve code quality and maintainability
- ✅ 149 comprehensive tests ensuring accuracy
Installation:
dotnet add package Trellis.AnalyzersDocumentation: Analyzer Documentation