Trellis.Analyzers — API Reference
- Package:
Trellis.Analyzers - Namespace:
Trellis.Analyzers - Purpose: Roslyn analyzers and code fixes that enforce correct Trellis
Result<T>,Maybe<T>, EF Core, and value-object usage.
See also: trellis-api-cookbook.md — recipes using this package.
Use this file when
- A build emits a
TRLS###diagnostic and you need the exact meaning, likely fix, or suppression constant. - You are writing docs, templates, or examples and want analyzer-backed anti-pattern guidance.
- You need to map source-generator diagnostics (
TRLS031+) to the owning generator.
Patterns Index
| Symptom | Canonical fix | Diagnostic |
|---|---|---|
| Result return value ignored | Return, await, match, bind, or assign the result | TRLS001 |
Lambda returns Result<T> inside Map |
Use Bind / BindAsync |
TRLS002 |
Maybe<T>.Value access can throw |
Gate with HasValue, use TryGetValue, convert to Result, or use EF helpers in queries |
TRLS003, TRLS013 |
Result<Result<T>> or Maybe<Maybe<T>> appears |
Use Bind / flatten the operation |
TRLS004, TRLS007 |
| Sync ROP method receives async lambda | Use the *Async variant |
TRLS009 |
EF query over Maybe<T> uses unsafe value/sentinel access |
Use MaybeQueryableExtensions.WhereXxx or register AddTrellisInterceptors() |
TRLS013 |
Direct SaveChangesAsync in non-UoW repository code |
Use SaveChangesResultAsync / SaveChangesResultUnitAsync, or let AddTrellisUnitOfWork<TContext>() own commits |
TRLS015 |
EF index points at a Maybe<T> CLR property |
Use HasTrellisIndex(...) |
TRLS016 |
Value object uses System.ComponentModel.DataAnnotations.StringLength / Range |
Use Trellis attributes from namespace Trellis |
TRLS017 |
[OwnedEntity] has init-only properties |
Use { get; private set; } for EF-owned value objects |
TRLS022 |
Suppression guidance
Prefer fixing the code over suppressing diagnostics. When a suppression is genuinely intentional, use TrellisDiagnosticIds constants instead of string literals and include a justification.
Diagnostics
| ID | Severity | Title | Description |
|---|---|---|---|
TRLS001 |
Warning | Result return value is not handled | Result |
TRLS002 |
Info | Use Bind instead of Map when lambda returns Result | When the transformation function returns a Result |
TRLS003 |
Error | Unsafe access to Maybe.Value | Maybe.Value throws an InvalidOperationException if the Maybe has no value. Check HasValue first, use TryGetValue, GetValueOrDefault, or convert to Result with ToResult. Maybe<T>.Value is hidden from IntelliSense as polish; this analyzer is the enforcement mechanism. |
TRLS004 |
Warning | Result is double-wrapped | Result should not be wrapped inside another Result. This creates Result<Result |
TRLS005 |
Warning | Incorrect async Result usage | Task<Result |
TRLS007 |
Warning | Maybe is double-wrapped | Maybe should not be wrapped inside another Maybe. This creates Maybe<Maybe |
TRLS008 |
Info | Consider using Result.Combine | When combining multiple Result |
TRLS009 |
Warning | Use async method variant for async lambda | When using an async lambda with Map, Bind, Tap, or Ensure, use the async variant (MapAsync, BindAsync, etc.) to properly handle the async operation. Using sync methods with async lambdas causes the Task to not be awaited. |
TRLS010 |
Warning | Don't throw exceptions in Result chains | Throwing exceptions inside Bind, Map, Tap, or Ensure lambdas defeats the purpose of Railway Oriented Programming. Return Result.Fail |
TRLS013 |
Warning | Unsafe access to Maybe.Value in LINQ projection | .Value on Maybe<T> inside Select-family LINQ projections (Select/SelectMany/OrderBy*/ThenBy*/GroupBy/ToDictionary/ToLookup) throws for None elements unless an earlier .Where(x => x.HasValue) makes the access safe. For EF Core IQueryable predicates over a Maybe<T> property, either register AddTrellisInterceptors() (which rewrites .HasValue/.Value/GetValueOrDefault(d) into EF.Property/null-checks/COALESCE) or use Trellis.EntityFrameworkCore.MaybeQueryableExtensions (WhereHasValue/WhereNone/WhereEquals/WhereLessThan/WhereLessThanOrEqual/WhereGreaterThan/WhereGreaterThanOrEqual) explicitly. |
TRLS014 |
Error | Combine chain exceeds maximum supported tuple size | Combine supports up to 9 elements. Downstream methods (Bind, Map, Tap, Match) also only support tuples up to 9 elements. Group related fields into intermediate value objects or sub-results, then combine those groups. |
TRLS015 |
Warning | Use SaveChangesResultAsync instead of SaveChangesAsync | In non-UoW contexts, direct SaveChanges/SaveChangesAsync calls bypass the Result pipeline and turn database errors into unhandled exceptions; use SaveChangesResultAsync (returns Result<int>) or SaveChangesResultUnitAsync (returns Result<Unit>). Under AddTrellisUnitOfWork<TContext> the TransactionalCommandBehavior owns commit — repositories should stage changes via DbContext APIs (Add/Update/Remove) and not invoke SaveChanges at all. |
TRLS016 |
Warning | HasIndex references a Maybe |
HasIndex with a Maybe |
TRLS017 |
Warning | Wrong [StringLength] or [Range] attribute namespace | Trellis [StringLength] and [Range] attributes share names with System.ComponentModel.DataAnnotations versions. Using the wrong namespace compiles silently but the Trellis source generator ignores them, resulting in value objects without the expected validation constraints. Use the Trellis versions (namespace Trellis) instead. |
TRLS018 |
Warning | Result |
Reading the value position of a Result<T> deconstruction (var (success, value, error) = result;) without first checking success/error returns the default value when the result is in failure. Gate the read with the success bool, an error is null check, or an early return on failure. |
TRLS019 |
Warning | Avoid default(Result), default(Result<T>), and default(Maybe<T>) |
default(Result) and default(Result<T>) are typed failures carrying the new Error.Unexpected("default_initialized") sentinel — never silent successes. default(Maybe<T>) equals Maybe<T>.None but the explicit literal obscures intent. Construct via Result.Ok(...) / Result.Fail(...) or Maybe<T>.None / Maybe.From(...). Suppress with [SuppressMessage("Trellis", TrellisDiagnosticIds.DefaultResultOrMaybe)] or #pragma warning disable TRLS019 for sanctioned sentinel/test-helper sites. |
TRLS020 |
Warning | Composite value object DTO property is missing JSON converter | Composite [OwnedEntity] value objects exposed through request/response DTO surfaces must carry [JsonConverter(typeof(CompositeValueObjectJsonConverter<T>))] so JSON binding round-trips through TryCreate. |
TRLS021 |
Warning | EF configuration duplicates Trellis conventions | Flags HasConversion, OwnsOne, and Ignore calls on Maybe<T> or [OwnedEntity] properties when ApplyTrellisConventions(...) / ApplyTrellisConventionsFor<TContext>() is wired. Remove the manual mapping and let Trellis conventions own the property. |
TRLS022 |
Warning | [OwnedEntity] property uses init-only setter |
Flags { get; init; } properties on classes annotated with [OwnedEntity]. EF Core materializes owned entities through a generator-emitted private parameterless constructor; init-only setters are not covered by Trellis tests today and round-trip behavior is not guaranteed. Use { get; private set; }. |
TRLS031 |
Warning | Unsupported base type for RequiredPartialClassGenerator |
Emitted by the Primitives source generator when a Required*-derived value object inherits from an unsupported base. Supported bases: RequiredGuid, RequiredString, RequiredInt, RequiredDecimal, RequiredLong, RequiredBool, RequiredDateTime, RequiredEnum. (formerly TRLSGEN001) |
TRLS032 |
Error | MinimumLength exceeds MaximumLength |
Emitted by the Primitives source generator when a [StringLength] attribute has MinimumLength > MaximumLength. Adjust the attribute values so the range is non-empty. (formerly TRLSGEN002) |
TRLS033 |
Error | Range minimum exceeds maximum |
Emitted by the Primitives source generator when a [Range] attribute on int/long/decimal has Min > Max. Adjust the attribute values so the range is non-empty. (formerly TRLSGEN003) |
TRLS034 |
Error | Decimal range exceeds decimal bounds |
Emitted by the Primitives source generator when a [Range] attribute on decimal exceeds the CLR decimal value range. Use a tighter range. (formerly TRLSGEN004) |
TRLS035 |
Warning | Maybe<T> property should be partial |
Emitted by the EF Core generator (MaybePartialPropertyGenerator) for non-partial auto-properties of type Maybe<T> whose containing type is partial. Declare the property partial so the generator can emit the backing field and storage member. (formerly TRLSGEN100) |
TRLS036 |
Error | [OwnedEntity] type should be partial |
Emitted by the EF Core generator (OwnedEntityGenerator) when [OwnedEntity] is applied to a non-partial type. Declare the type partial so the generator can emit the private parameterless constructor. (formerly TRLSGEN101) |
TRLS037 |
Warning | [OwnedEntity] type already has a parameterless constructor |
Emitted by the EF Core generator when [OwnedEntity] is applied to a type that already has a parameterless constructor. Remove the existing constructor or remove [OwnedEntity]. (formerly TRLSGEN102) |
TRLS038 |
Error | [OwnedEntity] type must inherit from ValueObject |
Emitted by the EF Core generator when [OwnedEntity] is applied to a type that does not inherit from Trellis.ValueObject. (formerly TRLSGEN103) |
TRLS039 |
Warning | Unsupported scalar value primitive for AOT-safe JSON converter | Emitted by ScalarValueJsonConverterGenerator (Trellis.AspSourceGenerator) when a value object inherits from ScalarValueObject<TSelf, TPrimitive> with a TPrimitive outside the AOT-safe set (string, int, long, short, byte, bool, float, double, decimal, Guid, DateTime, DateTimeOffset). The generator skips the converter for that type to avoid emitting reflection-based JsonSerializer.Deserialize/Serialize calls (IL2026/IL3050 under PublishAot=true); provide a custom JsonConverter<TSelf> or pick a supported primitive. |
Constants — TrellisDiagnosticIds
The public static class Trellis.TrellisDiagnosticIds (in the Trellis.Analyzers assembly) exposes every diagnostic ID above as a public const string. Use it from [SuppressMessage] attributes and rule sets to avoid magic strings:
[SuppressMessage("Trellis", TrellisDiagnosticIds.UnsafeMaybeValueAccess,
Justification = "guarded by HasValue check earlier in the pipeline")]
public string GetCity(Maybe<Address> address) => address.Value.City;
Generator IDs (TRLS031–TRLS039) are also exposed as constants on the same class so consumers have a single canonical reference for the unified namespace.
Constant → diagnostic ID → emitter
Every public const string field on TrellisDiagnosticIds, the diagnostic ID it carries, and the analyzer (or generator) that emits it. Use the constant name in [SuppressMessage] and the diagnostic ID in #pragma warning disable.
| C# constant | Diagnostic ID | Emitted by |
|---|---|---|
ResultNotHandled |
TRLS001 |
ResultNotHandledAnalyzer |
UseBindInsteadOfMap |
TRLS002 |
UseBindInsteadOfMapAnalyzer |
UnsafeMaybeValueAccess |
TRLS003 |
UnsafeValueAccessAnalyzer |
ResultDoubleWrapping |
TRLS004 |
ResultDoubleWrappingAnalyzer |
AsyncResultMisuse |
TRLS005 |
AsyncResultMisuseAnalyzer |
MaybeDoubleWrapping |
TRLS007 |
MaybeDoubleWrappingAnalyzer |
UseResultCombine |
TRLS008 |
UseResultCombineAnalyzer |
UseAsyncMethodVariant |
TRLS009 |
AsyncLambdaWithSyncMethodAnalyzer |
ThrowInResultChain |
TRLS010 |
ThrowInResultChainAnalyzer |
UnsafeMaybeValueInLinq |
TRLS013 |
UnsafeValueInLinqAnalyzer |
CombineChainTooLong |
TRLS014 |
CombineLimitAnalyzer |
UseSaveChangesResult |
TRLS015 |
UseSaveChangesResultAnalyzer |
HasIndexMaybeProperty |
TRLS016 |
HasIndexMaybePropertyAnalyzer |
WrongAttributeNamespace |
TRLS017 |
WrongAttributeNamespaceAnalyzer |
UnsafeResultDeconstruction |
TRLS018 |
UnsafeResultDeconstructionAnalyzer |
DefaultResultOrMaybe |
TRLS019 |
DefaultResultOrMaybeAnalyzer |
CompositeValueObjectDtoMissingJsonConverter |
TRLS020 |
CompositeValueObjectDtoConverterAnalyzer |
RedundantEfConfiguration |
TRLS021 |
RedundantEfConfigurationAnalyzer |
OwnedEntityInitOnlyProperty |
TRLS022 |
OwnedEntityInitOnlyPropertyAnalyzer |
UnsupportedRequiredBaseType |
TRLS031 |
RequiredPartialClassGenerator (Trellis.Core.Generator) |
InvalidStringLengthRange |
TRLS032 |
RequiredPartialClassGenerator (Trellis.Core.Generator) |
InvalidRangeMinExceedsMax |
TRLS033 |
RequiredPartialClassGenerator (Trellis.Core.Generator) |
DecimalRangeExceedsDecimalRange |
TRLS034 |
RequiredPartialClassGenerator (Trellis.Core.Generator) |
MaybePropertyShouldBePartial |
TRLS035 |
MaybePartialPropertyGenerator (Trellis.EntityFrameworkCore.Generator) |
OwnedEntityShouldBePartial |
TRLS036 |
OwnedEntityGenerator (Trellis.EntityFrameworkCore.Generator) |
OwnedEntityAlreadyHasParameterlessCtor |
TRLS037 |
OwnedEntityGenerator (Trellis.EntityFrameworkCore.Generator) |
OwnedEntityMustInheritValueObject |
TRLS038 |
OwnedEntityGenerator (Trellis.EntityFrameworkCore.Generator) |
UnsupportedScalarValuePrimitiveForAotJson |
TRLS039 |
ScalarValueJsonConverterGenerator (Trellis.AspSourceGenerator) |
Descriptors — DiagnosticDescriptors
The public static class Trellis.Analyzers.DiagnosticDescriptors exposes one public static readonly DiagnosticDescriptor field per analyzer-emitted diagnostic. Analyzer implementations register these via SupportedDiagnostics; consumers normally don't reference them directly, but the field names are stable API and can be used in tests or in custom Roslyn tooling that re-exports the rules.
| Field | Backing ID | Default severity | Category |
|---|---|---|---|
ResultNotHandled |
TRLS001 |
Warning | Trellis.Result |
UseBindInsteadOfMap |
TRLS002 |
Info | Trellis.Result |
UnsafeMaybeValueAccess |
TRLS003 |
Error | Trellis.Maybe |
ResultDoubleWrapping |
TRLS004 |
Warning | Trellis.Result |
AsyncResultMisuse |
TRLS005 |
Warning | Trellis.Result |
MaybeDoubleWrapping |
TRLS007 |
Warning | Trellis.Maybe |
UseResultCombine |
TRLS008 |
Info | Trellis.Result |
UseAsyncMethodVariant |
TRLS009 |
Warning | Trellis.Result |
ThrowInResultChain |
TRLS010 |
Warning | Trellis.Result |
UnsafeValueInLinq |
TRLS013 |
Warning | Trellis.Maybe |
CombineChainTooLong |
TRLS014 |
Error | Trellis.Result |
UseSaveChangesResult |
TRLS015 |
Warning | Trellis.EntityFrameworkCore |
HasIndexMaybeProperty |
TRLS016 |
Warning | Trellis.EntityFrameworkCore |
WrongAttributeNamespace |
TRLS017 |
Warning | Trellis.Primitives |
UnsafeResultDeconstruction |
TRLS018 |
Warning | Trellis.Result |
DefaultResultOrMaybe |
TRLS019 |
Warning | Trellis.Result |
CompositeValueObjectDtoMissingJsonConverter |
TRLS020 |
Warning | Trellis.Asp |
RedundantEfConfiguration |
TRLS021 |
Warning | Trellis.EntityFrameworkCore |
OwnedEntityInitOnlyProperty |
TRLS022 |
Warning | Trellis.EntityFrameworkCore |
Note: Generator-emitted diagnostics (
TRLS031–TRLS039) are constructed inline by the source generators and are not exposed as fields onDiagnosticDescriptors. Use theTrellisDiagnosticIdsconstants instead for those IDs.
// Re-exporting an analyzer rule in a custom analyzer:
public override ImmutableArray<DiagnosticDescriptor> SupportedDiagnostics =>
ImmutableArray.Create(Trellis.Analyzers.DiagnosticDescriptors.ResultNotHandled);
Analyzer classes
Result and Maybe flow
ResultNotHandledAnalyzer — TRLS001
- Flags expression statements that discard a
Result<T>. - Also flags discarded
awaitexpressions when the awaited type isTask<Result<T>>orValueTask<Result<T>>. - Unwraps
await someCall.ConfigureAwait(false)before checking the awaited type. - No code fix.
UseBindInsteadOfMapAnalyzer — TRLS002
- Flags Trellis
MapandMapAsyncinvocations when the first argument returns:Result<T>Task<Result<T>>ValueTask<Result<T>>
- Covers lambda expressions, method groups, and member-access method groups.
- Purpose: prevent
Result<Result<T>>. - Code fix:
UseBindInsteadOfMapCodeFixProvider.
UnsafeValueAccessAnalyzer — TRLS003
TRLS003: flagsmaybe.Valuewhen the analyzer cannot prove the access is guarded by presence checks.- Recognized safe patterns include:
if/ ternary checks onHasValue/HasNoValueTryGetValuebranches, including negated formsmaybe.HasValue && maybe.Value ...short-circuit- safe lambda parameters inside Trellis Maybe APIs such as
Bind,Map,Tap,Ensure,Match - prior assignment from
Maybe.From(...)whenTis a non-nullable value type and the variable is not reassigned
- Inside
Expression<Func<...>>lambdas (EF Core, Specifications, FluentValidation): the rule is not relaxed. Use theHasValue && Valueshort-circuit idiom — e.g.e => e.SubmittedAt.HasValue && e.SubmittedAt.Value < cutoff. EF Core needs theHasValuepredicate to translate toIS NOT NULL, and the short-circuit form keeps the analyzer satisfied without#pragmasuppressions. - Code fix:
AddResultGuardCodeFixProvider.
Result accessors: The
UnsafeValueAccessAnalyzerpreviously also coveredResult<T>.ValueandResult<T>.Error. Both branches were deleted because (a)Result<T>.Valueno longer exists, and (b)Result<T>.Erroris nowError?, so unsafe access is caught natively by C# nullable-reference-type analysis.
UseMatchErrorAnalyzer — TRLS005 (removed from the current API)
This analyzer was deleted from the current API. With the closed-ADT Error, switch over an Error reference is exhaustive at the language level — the C# compiler verifies that every nested case is handled — so manual error-type discrimination is the recommended pattern. Replace any remaining result.MatchError(onValidation: ..., onNotFound: ..., ...) calls with:
result.Match(
onSuccess: value => ...,
onFailure: error => error switch
{
Error.NotFound nf => ...,
Error.UnprocessableContent uc => ...,
Error.Conflict c => ...,
_ => ...,
});
TryCreateValueAccessAnalyzer — TRLS007 (removed from the current API)
This analyzer was deleted from the current API. The pattern TryCreate(...).Value no longer compiles because Result<T>.Value was removed (see TRLS003). Call Create(...) directly when the input is known-good, or handle the Result returned by TryCreate(...) explicitly via TryGetValue / Match / Bind.
ResultDoubleWrappingAnalyzer — TRLS004
- Flags declared or inferred
Result<Result<T>>in:- variable declarations
- properties
- method return types
- parameters
- Also flags
Result.Ok(existingResult)andResult.Fail(existingResult)when the argument is already aResult<T>. - No code fix.
AsyncResultMisuseAnalyzer — TRLS005
- Flags blocking access on
Task<Result<T>>andValueTask<Result<T>>:.Result.Wait().GetAwaiter().GetResult()
- Handles both
TaskandValueTask. - No code fix.
MaybeDoubleWrappingAnalyzer — TRLS007
- Flags declared
Maybe<Maybe<T>>in variable declarations, properties, method return types, and parameters. - No code fix.
UseResultCombineAnalyzer — TRLS008
- Flags conditional logic that manually combines two or more Result-state checks:
&&chains over.IsSuccess||chains over.IsFailure
- Uses operation analysis, so it looks at semantic property access rather than raw text.
- No code fix.
TernaryValueOrDefaultAnalyzer — TRLS013 (removed from the current API)
This analyzer was deleted from the current API. The result.IsSuccess ? result.Value : fallback shape no longer compiles because Result<T>.Value was removed. Use result.GetValueOrDefault(fallback) or result.Match(onSuccess: v => v, onFailure: _ => fallback).
AsyncLambdaWithSyncMethodAnalyzer — TRLS009
- Flags synchronous Trellis methods called with async work:
MapBindTapEnsureTapOnFailure
- Reports when any argument is:
- an
asynclambda - a non-async lambda whose converted return type is
TaskorValueTask - a method group returning
TaskorValueTask
- an
- Verifies the receiver is a Trellis
Result,Maybe, or async-result receiver. - Code fix:
UseAsyncMethodVariantCodeFixProvider.
ThrowInResultChainAnalyzer — TRLS010
- Flags
throwstatements andthrowexpressions inside lambdas passed to Trellis result-chain APIs:Bind,BindAsyncMap,MapAsyncTap,TapAsyncEnsure,EnsureAsyncTapOnFailure,TapOnFailureAsyncMapOnFailure,MapOnFailureAsyncRecoverOnFailure,RecoverOnFailureAsyncDebugOnFailure,DebugOnFailureAsync
- No code fix.
UnsafeValueInLinqAnalyzer — TRLS013
- Flags
.Valueinside LINQ projection/order/grouping lambdas for:SelectSelectManyToDictionaryToLookupGroupByOrderByOrderByDescendingThenByThenByDescending
- Reports only when
.Valueis accessed on aMaybe<T>lambda parameter. The Result-side branch was removed along withResult<T>.Value. - Suppresses the diagnostic when an earlier
.Where(maybe => maybe.HasValue)in the same chain proves the access is safe. - For EF Core IQueryable predicates over a
Maybe<T>property, either registerAddTrellisInterceptors()(which rewrites.HasValue/.Value/GetValueOrDefault(d)intoEF.Property/null-checks/COALESCE) or useTrellis.EntityFrameworkCore.MaybeQueryableExtensions(WhereHasValue/WhereNone/WhereEquals/WhereLessThan/WhereLessThanOrEqual/WhereGreaterThan/WhereGreaterThanOrEqual) explicitly. Note: this analyzer only fires on Select-family methods today; coverage of.Where/.Any/.Firstetc. is tracked as a follow-up. - No code fix.
CombineLimitAnalyzer — TRLS014
- Flags the outermost
.Combine(...)or.CombineAsync(...)chain when the resulting tuple would exceed 9 elements. - Counts tuple width semantically, so chains continued through intermediate variables are still measured correctly.
- No code fix.
Error, EF Core, and value-object rules
UseSaveChangesResultAnalyzer — TRLS015
- Activates only when the compilation references
Trellis.EntityFrameworkCore.DbContextExtensions. - Flags direct
DbContext.SaveChangesAsync(...)andDbContext.SaveChanges(...)calls, including unqualified calls inside aDbContextsubclass. - Recommends (non-UoW contexts):
SaveChangesResultAsyncwhen the return value is usedSaveChangesResultUnitAsyncwhen the value is discarded
- Under
AddTrellisUnitOfWork<TContext>theTransactionalCommandBehaviorowns commit; repositories should stage changes via DbContext APIs (Add/Update/Remove) and not invokeSaveChanges/SaveChangesAsyncat all. - Code fix:
UseSaveChangesResultCodeFixProvider.
HasIndexMaybePropertyAnalyzer — TRLS016
- Activates only when the compilation references
Trellis.EntityFrameworkCore.MaybeConvention. - Flags
EntityTypeBuilder.HasIndex(...)lambda members that referenceMaybe<T>properties. - Reports both the CLR property name and the generated storage-member fallback name (for example
_submittedAt). - No code fix.
WrongAttributeNamespaceAnalyzer — TRLS017
- Flags
System.ComponentModel.DataAnnotations.StringLengthAttributeandSystem.ComponentModel.DataAnnotations.RangeAttributeapplied to types that inherit from Trellis value-object base types:ScalarValueObjectRequiredStringRequiredIntRequiredDecimalRequiredLongRequiredGuidRequiredBoolRequiredDateTimeRequiredEnum
- No code fix.
UnsafeResultDeconstructionAnalyzer — TRLS018
- Flags reads of the value position of a
Result<T>deconstruction (var (success, value, error) = result;) when the read is not guarded by:- an
if/while/conditional on the success bool, - an early-return on failure (
if (!success) return ...), - a check that the error is
null, or - the value being assigned to
_(discard).
- an
- Skips deconstructions where the value identifier is never read.
- No code fix.
DefaultResultOrMaybeAnalyzer — TRLS019
- Flags explicit
default(Result),default(Result<T>), anddefault(Maybe<T>)expressions at use sites. - Uses
IDefaultValueOperation(operation-based, not syntax-based) so it covers all surface forms equivalently:default(T)typeof-style:return default(Result<int>);- Target-typed
default:return default;in aResult<T>-returning method, parameter defaults, etc. - Null-suppressed
default!:return default!;is treated identically — the null-suppressing operator does not change the underlying value.
default(Result)/default(Result<T>)represent typed failures carrying the sharednew Error.Unexpected("default_initialized")sentinel — never silent successes.default(Maybe<T>)equalsMaybe<T>.None(semantically correct) but the explicit literal obscures intent.- Suggested replacements:
Result→Result.Ok()orResult.Fail(error)Result<T>→Result.Ok(value)orResult.Fail<T>(error)Maybe<T>→Maybe<T>.NoneorMaybe.From(value)
- For sanctioned sentinel/test-helper sites, suppress with
[SuppressMessage("Trellis", "TRLS019", Justification = "...")]on the enclosing member or#pragma warning disable TRLS019around the offending span. - No code fix (the appropriate replacement depends on intent — success vs. failure for
Result, value vs. None forMaybe).
CompositeValueObjectDtoConverterAnalyzer — TRLS020
- Flags ASP.NET controller request/response DTOs, Minimal API handler request DTOs, and Mediator
IRequest<T>/ICommand<T>/IQuery<T>message DTOs with properties whose type is an[OwnedEntity]TrellisValueObjectmissing[JsonConverter(typeof(CompositeValueObjectJsonConverter<T>))]. - This catches the silent JSON-binding failure where System.Text.Json can default-construct the composite value object and bypass
TryCreatevalidation. - Does not flag domain model properties that are not exposed through DTO surfaces.
- Does not flag composite value-object types that carry the matching
CompositeValueObjectJsonConverter<T>attribute. - No code fix.
RedundantEfConfigurationAnalyzer — TRLS021
- Activates only when the compilation references
Trellis.EntityFrameworkCore.MaybeConvention. - Reports only when the source also wires Trellis conventions via
ApplyTrellisConventions(...)or generatedApplyTrellisConventionsFor<TContext>(). - Flags manual EF configuration for convention-owned properties:
builder.Property(e => e.MaybeProperty).HasConversion(...)builder.OwnsOne(e => e.OwnedEntityValueObject)builder.Ignore(e => e.MaybeOrOwnedEntityProperty)
- Targets
Maybe<T>and types annotated withTrellis.EntityFrameworkCore.OwnedEntityAttribute. - No code fix.
OwnedEntityInitOnlyPropertyAnalyzer — TRLS022
- Activates only when the compilation references
Trellis.EntityFrameworkCore.OwnedEntityAttribute. - Flags property declarations with an
initaccessor whose containing class is annotated with[OwnedEntity]. - Diagnostic anchors at the
initkeyword and includes the property name and class name. - Recommends
{ get; private set; }, the supported and tested shape for owned-entity properties materialized through the generator-emitted parameterless constructor. - No code fix.
Code fix providers
| Code fix provider | Fixes | Behavior |
|---|---|---|
AddResultGuardCodeFixProvider |
TRLS003 |
Wraps the current statement block in if (maybe.HasValue) and tracks consecutive statements that keep using the guarded value. |
UseBindInsteadOfMapCodeFixProvider |
TRLS002 |
Replaces Map with Bind and MapAsync with BindAsync. |
UseAsyncMethodVariantCodeFixProvider |
TRLS009 |
Replaces sync method names with async variants: MapAsync, BindAsync, TapAsync, EnsureAsync, TapOnFailureAsync. |
UseSaveChangesResultCodeFixProvider |
TRLS015 |
Replaces SaveChangesAsync / SaveChanges with SaveChangesResultAsync or SaveChangesResultUnitAsync, adds await/async for sync SaveChanges, and adds using Trellis.EntityFrameworkCore; when needed. |
Compilable examples
using System.Threading.Tasks;
using Trellis;
public static class AnalyzerExamples
{
public static Result<int> Parse(string text) => Result.Ok(text.Length);
public static Result<int> Valid()
{
var result = Parse("abc");
return result.Map(length => Result.Ok(length + 1)); // TRLS002
}
public static async Task<Result<int>> ValidAsync()
{
Task<Result<int>> task = Task.FromResult(Result.Ok(42));
var result = await task; // preferred over task.Result / task.Wait() / task.GetAwaiter().GetResult()
return result;
}
}
using System.Threading.Tasks;
using Microsoft.EntityFrameworkCore;
public sealed class AppDbContext : DbContext
{
}
public static class EfExample
{
public static async Task SaveAsync(AppDbContext dbContext)
{
await dbContext.SaveChangesAsync(); // TRLS015
}
}
Cross-references
- trellis-api-core.md —
Result<T>,Maybe<T>,Bind,Map,Match,Combine - trellis-api-efcore.md —
SaveChangesResultAsync,SaveChangesResultUnitAsync,HasTrellisIndex - trellis-api-primitives.md — Trellis
[StringLength]and[Range] - trellis-api-testing-reference.md — testing helpers that intentionally work with analyzer rules