Facet Analyzer Rules

July 28, 2026 · View on GitHub

Facet includes comprehensive Roslyn analyzers that provide real-time feedback in your IDE. These analyzers catch common mistakes and configuration issues at design-time, before you even compile your code.

Quick Reference

Rule IDSeverityCategoryDescription
FAC001ErrorUsageType must be annotated with [Facet]
FAC002InfoPerformanceConsider using two-generic variant
FAC003ErrorDeclarationMissing partial keyword on [Facet] type
FAC004ErrorUsageInvalid property name in Exclude/Include
FAC005ErrorUsageInvalid source type
FAC006ErrorUsageInvalid Configuration type
FAC007WarningUsageInvalid NestedFacets type
FAC008WarningPerformanceCircular reference risk
FAC009ErrorUsageBoth Include and Exclude specified
FAC010WarningPerformanceUnusual MaxDepth value
FAC011ErrorUsage[GenerateDtos] on non-class type
FAC012WarningUsageInvalid ExcludeProperties
FAC013WarningUsageNo DTO types selected
FAC014ErrorDeclarationMissing partial keyword on [Flatten] type
FAC015ErrorUsageInvalid source type in [Flatten]
FAC016WarningPerformanceUnusual MaxDepth in [Flatten]
FAC017InfoUsageLeafOnly naming collision risk
FAC022WarningSourceTrackingSource entity structure changed
FAC101ErrorGeneratorOutputType combines multiple concrete kinds
FAC102ErrorGeneratorOutputType sets Partial without a kind
FAC103ErrorGeneratorEF model manifest could not be read
FAC104ErrorGeneratorEF model manifest version not supported
FAC105ErrorGeneratorSource type not in the EF model manifest
FAC106WarningGeneratorProperty unknown to the EF model manifest

Extension Method Analyzers

FAC001

Type must be annotated with [Facet]

  • Severity: Error
  • Category: Usage

Description

When using extension methods like ToFacet<T>(), ToSource<T>(), SelectFacet<T>(), etc., the target type must be annotated with the [Facet] attribute.

Bad Code

// UserDto does NOT have [Facet] attribute
public class UserDto
{
    public int Id { get; set; }
    public string Name { get; set; }
}

var dto = user.ToFacet<User, UserDto>(); // ❌ FAC001

Good Code

[Facet(typeof(User))]
public partial class UserDto { }

var dto = user.ToFacet<User, UserDto>(); // ✅ OK

FAC002

Consider using the two-generic variant for better performance

  • Severity: Info
  • Category: Performance

Description

When using single-generic extension methods like ToFacet<TTarget>(), the library uses reflection to discover the source type. For better performance, use the two-generic variant ToFacet<TSource, TTarget>().

Code Triggering Warning

var dto = user.ToFacet<UserDto>(); // ℹ️ FAC002: Consider ToFacet<User, UserDto>()
var dto = user.ToFacet<User, UserDto>(); // ✅ Better performance

Impact

The performance difference is minimal (a few nanoseconds) but can add up in tight loops or high-throughput scenarios.


[Facet] Attribute Analyzers

FAC003

Type with [Facet] attribute must be declared as partial

  • Severity: Error
  • Category: Declaration

Description

Source generators require types to be partial so they can add generated members. Any type marked with [Facet] must be declared as partial.

Bad Code

[Facet(typeof(User))]
public class UserDto { } // ❌ FAC003: Missing 'partial' keyword

Good Code

[Facet(typeof(User))]
public partial class UserDto { } // ✅ OK

FAC004

Property name does not exist in source type

  • Severity: Error
  • Category: Usage

Description

Property names specified in Exclude or Include parameters must exist in the source type.

Bad Code

public class User
{
    public int Id { get; set; }
    public string Name { get; set; }
}

[Facet(typeof(User), "PasswordHash")] // ❌ FAC004: User doesn't have PasswordHash
public partial class UserDto { }

Good Code

public class User
{
    public int Id { get; set; }
    public string Name { get; set; }
    public string PasswordHash { get; set; }
}

[Facet(typeof(User), "PasswordHash")] // ✅ OK
public partial class UserDto { }

FAC005

Source type is not accessible or does not exist

  • Severity: Error
  • Category: Usage

Description

The source type specified in the [Facet] attribute must be a valid, accessible type.

Bad Code

[Facet(typeof(NonExistentType))] // ❌ FAC005
public partial class UserDto { }

Good Code

[Facet(typeof(User))] // ✅ OK
public partial class UserDto { }

FAC006

Configuration type does not implement required interface

  • Severity: Error
  • Category: Usage

Description

Configuration types must implement IFacetMapConfiguration<TSource, TTarget>, IFacetMapConfigurationAsync<TSource, TTarget>, IFacetProjectionMapConfiguration<TSource, TTarget>, or provide a static Map method.

Bad Code

public class UserMapper // ❌ No interface, no Map method
{
    public void DoSomething(User source, UserDto target) { }
}

[Facet(typeof(User), Configuration = typeof(UserMapper))] // ❌ FAC006
public partial class UserDto { }

Good Code

// Option 1: Implement IFacetMapConfiguration
public class UserMapper : IFacetMapConfiguration<User, UserDto>
{
    public static void Map(User source, UserDto target)
    {
        target.FullName = $"{source.FirstName} {source.LastName}";
    }
}

// Option 2: Implement IFacetProjectionMapConfiguration (expression-only, reused in constructors)
public class UserMapper : IFacetProjectionMapConfiguration<User, UserDto>
{
    public static void ConfigureProjection(IFacetProjectionBuilder<User, UserDto> builder)
    {
        builder.Map(d => d.FullName, s => s.FirstName + " " + s.LastName);
    }
}

// Option 3: Provide static Map method
public class UserMapper
{
    public static void Map(User source, UserDto target)
    {
        target.FullName = $"{source.FirstName} {source.LastName}";
    }
}

[Facet(typeof(User), Configuration = typeof(UserMapper))] // ✅ OK
public partial class UserDto { }

FAC007

Nested facet type is not marked with [Facet] attribute

  • Severity: Warning
  • Category: Usage

Description

All types specified in the NestedFacets array must be marked with the [Facet] attribute.

Bad Code

public class AddressDto { } // ❌ Missing [Facet] attribute

[Facet(typeof(User), NestedFacets = [typeof(AddressDto)])] // ⚠️ FAC007
public partial class UserDto { }

Good Code

[Facet(typeof(Address))]
public partial class AddressDto { }

[Facet(typeof(User), NestedFacets = [typeof(AddressDto)])] // ✅ OK
public partial class UserDto { }

FAC008

Potential stack overflow with circular references

  • Severity: Warning
  • Category: Performance

Description

When MaxDepth is set to 0 (unlimited) and PreserveReferences is false, circular references in object graphs can cause stack overflow exceptions.

Bad Code

[Facet(typeof(User),
    MaxDepth = 0,
    PreserveReferences = false,
    NestedFacets = [typeof(CompanyDto)])] // ⚠️ FAC008
public partial class UserDto { }

Good Code

// Option 1: Enable PreserveReferences (default)
[Facet(typeof(User),
    NestedFacets = [typeof(CompanyDto)])] // ✅ OK (PreserveReferences defaults to true)

// Option 2: Set MaxDepth limit
[Facet(typeof(User),
    MaxDepth = 5,
    NestedFacets = [typeof(CompanyDto)])] // ✅ OK

// Option 3: Both
[Facet(typeof(User),
    MaxDepth = 10,
    PreserveReferences = true,
    NestedFacets = [typeof(CompanyDto)])] // ✅ OK (safest)

FAC009

Cannot specify both Include and Exclude

  • Severity: Error
  • Category: Usage

Description

The Include and Exclude parameters are mutually exclusive. Use either Include to whitelist properties or Exclude to blacklist properties, but not both.

Bad Code

[Facet(typeof(User),
    nameof(User.PasswordHash),  // Exclude parameter
    Include = [nameof(User.Id), nameof(User.Name)])] // ❌ FAC009: Can't use both
public partial class UserDto { }

Good Code

// Option 1: Exclude approach
[Facet(typeof(User), nameof(User.PasswordHash), nameof(User.SecretKey))] // ✅ OK
public partial class UserDto { }

// Option 2: Include approach
[Facet(typeof(User), Include = [nameof(User.Id), nameof(User.Name), nameof(User.Email)])] // ✅ OK
public partial class UserDto { }

FAC010

MaxDepth value is unusual

  • Severity: Warning
  • Category: Performance

Description

MaxDepth values should typically be between 1 and 10 for most scenarios. Negative values are invalid, and values above 100 may indicate a configuration error.

Code Triggering Warning

[Facet(typeof(User), MaxDepth = -1)] // ⚠️ FAC010: Negative
[Facet(typeof(User), MaxDepth = 500)] // ⚠️ FAC010: Too large

Good Code

[Facet(typeof(User), MaxDepth = 5)] // ✅ OK
[Facet(typeof(User), MaxDepth = 10)] // ✅ OK (default)

[GenerateDtos] Attribute Analyzers

FAC011

[GenerateDtos] can only be applied to classes

  • Severity: Error
  • Category: Usage

Description

The [GenerateDtos] and [GenerateAuditableDtos] attributes are designed for class types and cannot be applied to structs, interfaces, or other type kinds.

Bad Code

[GenerateDtos(DtoTypes.All)]
public struct Product { } // ❌ FAC011: Can't use on struct

Good Code

[GenerateDtos(DtoTypes.All)]
public class Product { } // ✅ OK

FAC012

Excluded property does not exist

  • Severity: Warning
  • Category: Usage

Description

Properties specified in ExcludeProperties should exist in the source type.

Bad Code

public class Product
{
    public int Id { get; set; }
    public string Name { get; set; }
}

[GenerateDtos(DtoTypes.All,
    ExcludeProperties = ["InternalNotes"])] // ⚠️ FAC012: Doesn't exist
public class Product { }

Good Code

public class Product
{
    public int Id { get; set; }
    public string Name { get; set; }
    public string InternalNotes { get; set; }
}

[GenerateDtos(DtoTypes.All,
    ExcludeProperties = ["InternalNotes"])] // ✅ OK
public class Product { }

FAC013

No DTO types selected for generation

  • Severity: Warning
  • Category: Usage

Description

Setting Types to DtoTypes.None will not generate any DTOs.

Bad Code

[GenerateDtos(Types = DtoTypes.None)] // ⚠️ FAC013: No DTOs will be generated
public class Product { }

Good Code

[GenerateDtos(Types = DtoTypes.All)] // ✅ OK
public class Product { }

// Or specify specific types
[GenerateDtos(Types = DtoTypes.Create | DtoTypes.Update | DtoTypes.Response)]
public class Product { }

FAC101

OutputType combines multiple concrete output kinds

  • Severity: Error
  • Category: Generator

OutputType is a flags value, but concrete kinds (Class, Record, Struct, RecordStruct) all generate identically-named types and would collide. Combine at most one concrete kind with OutputType.Interface and/or the Partial modifier.

[GenerateDtos(OutputType = OutputType.Class | OutputType.Record)]     // ❌ FAC101
[GenerateDtos(OutputType = OutputType.Interface | OutputType.Record)] // ✅ OK

FAC102

OutputType sets the Partial modifier without an output kind

  • Severity: Error
  • Category: Generator

Partial only modifies how requested kinds are emitted; on its own there is nothing to generate.

[GenerateDtos(OutputType = OutputType.Partial)]                      // ❌ FAC102
[GenerateDtos(OutputType = OutputType.Record | OutputType.Partial)]  // ✅ OK

FAC103

EF model manifest could not be read

  • Severity: Error
  • Category: Generator

A *.facetmodel.json file wired up as an AdditionalFile is not readable as a manifest (malformed JSON, wrong shape). The file is ignored in full — never half-applied — and reported here rather than silently skipped. A rejected file also does not count as a wired manifest, so it does not flip the ExcludeNavigationProperties default: this error stands alone instead of being buried under a FAC105 per source type. Regenerate the manifest (dotnet ef migrations add/remove) or remove it from AdditionalFiles.


FAC104

EF model manifest version is not supported

  • Severity: Error
  • Category: Generator

The manifest was written by a Facet.Extensions.EFCore version whose format this generator does not read — a package version mismatch. Align the Facet and Facet.Extensions.EFCore versions and regenerate the manifest.


FAC105

GenerateDtos source type is not in the EF model manifest

  • Severity: Error
  • Category: Generator

ExcludeNavigationProperties is driven entirely by the EF model manifest — there is no heuristic fallback — and it defaults to true for every [GenerateDtos] attribute in a project that wires a manifest into <AdditionalFiles>. A shaped source type with no manifest entry cannot be generated safely, so this is a hard error: the type is absent from the accepted manifests (regenerate with dotnet ef migrations add), or it is not an EF entity — set ExcludeNavigationProperties = false on it; an explicit value always wins over the wired-manifest default. For explicitly shaped types this error also catches a mistyped <AdditionalFiles> path, since a glob that matches nothing supplies no manifests at all. Under the wired-manifest default, a mistyped glob instead means no wired manifest, no shaping, and no diagnostic — pin ExcludeNavigationProperties = true on one representative entity if you want that failure loud. The message states whether the requirement came from an explicit ExcludeNavigationProperties = true or from the wired-manifest default.


FAC106

Property is unknown to the EF model manifest

  • Severity: Warning
  • Category: Generator

The manifest records every member the model has an opinion on (mapped, navigation, owned, skip navigation, ignored, service). A settable property outside that set is unknown to the model — almost always a property added after the manifest was last generated, which would otherwise silently vanish from generated DTOs. Regenerate the manifest, or mark the property [NotMapped]/Ignore() if the model genuinely does not map it (which also documents the intent). This stays a warning so mid-development edits (property added, migration not yet run) don't block the build; escalate for CI with <WarningsAsErrors>$(WarningsAsErrors);FAC106</WarningsAsErrors>.


[Flatten] Attribute Analyzers

FAC014

Type with [Flatten] attribute must be declared as partial

  • Severity: Error
  • Category: Declaration

Description

Similar to [Facet], types marked with [Flatten] must be partial.

Bad Code

[Flatten(typeof(Person))]
public class PersonFlat { } // ❌ FAC014

Good Code

[Flatten(typeof(Person))]
public partial class PersonFlat { } // ✅ OK

FAC015

Source type is not accessible or does not exist

  • Severity: Error
  • Category: Usage

Description

The source type specified in the [Flatten] attribute must be valid and accessible.

Bad Code

[Flatten(typeof(NonExistentType))] // ❌ FAC015
public partial class PersonFlat { }

Good Code

[Flatten(typeof(Person))] // ✅ OK
public partial class PersonFlat { }

FAC016

MaxDepth value is unusual

  • Severity: Warning
  • Category: Performance

Description

For flatten scenarios, MaxDepth values should typically be between 1 and 5. Values above 10 may cause excessive property generation.

Code Triggering Warning

[Flatten(typeof(Person), MaxDepth = -1)] // ⚠️ FAC016: Negative
[Flatten(typeof(Person), MaxDepth = 50)] // ⚠️ FAC016: Too large

Good Code

[Flatten(typeof(Person), MaxDepth = 3)] // ✅ OK (default)
[Flatten(typeof(Person), MaxDepth = 5)] // ✅ OK

FAC017

LeafOnly naming strategy may cause property name collisions

  • Severity: Info
  • Category: Usage

Description

Using FlattenNamingStrategy.LeafOnly can cause name collisions when multiple nested objects have properties with the same name. Consider using the Prefix strategy instead.

Code Triggering Warning

[Flatten(typeof(Person),
    NamingStrategy = FlattenNamingStrategy.LeafOnly)] // ℹ️ FAC017
public partial class PersonFlat { }

Potential Issue

public class Person
{
    public Address HomeAddress { get; set; }
    public Address WorkAddress { get; set; }
}

public class Address
{
    public string Street { get; set; }
    public string City { get; set; }
}

// With LeafOnly, both addresses map to "Street" and "City" → collision!
[Flatten(typeof(Person),
    NamingStrategy = FlattenNamingStrategy.Prefix)] // ✅ Better
public partial class PersonFlat { }

// Generates: HomeAddressStreet, HomeAddressCity, WorkAddressStreet, WorkAddressCity

Source Signature Analyzers

FAC022

Source entity structure changed

  • Severity: Warning
  • Category: SourceTracking

Description

When you set SourceSignature on a [Facet] attribute, the analyzer computes a hash of the source type's properties and compares it to the stored signature. This warning is raised when the source entity's structure changes.

Code Triggering Warning

public class User
{
    public int Id { get; set; }
    public string Name { get; set; }
    public string Email { get; set; }  // New property added
}

[Facet(typeof(User), SourceSignature = "oldvalue")]  // ⚠️ FAC022
public partial class UserDto { }

Resolution

Use the provided code fix to update the signature, or manually update it:

[Facet(typeof(User), SourceSignature = "newvalue")]  // ✅ OK
public partial class UserDto { }

Notes

  • The signature is an 8-character hash computed from property names and types
  • Respects Include/Exclude filters when computing the signature
  • A code fix provider automatically offers to update the signature
  • See Source Signature Change Tracking for details

Suppressing Analyzer Rules

If you need to suppress a specific analyzer rule, you can use:

In Code

#pragma warning disable FAC002
var dto = user.ToFacet<UserDto>();
#pragma warning restore FAC002

In .editorconfig

[*.cs]
dotnet_diagnostic.FAC002.severity = none

For Entire Project

<PropertyGroup>
  <NoWarn>$(NoWarn);FAC002</NoWarn>
</PropertyGroup>

Configuration

All analyzers are enabled by default. You can configure their severity in your .editorconfig file:

[*.cs]

# Set a rule to error
dotnet_diagnostic.FAC007.severity = error

# Set a rule to warning
dotnet_diagnostic.FAC002.severity = warning

# Disable a rule
dotnet_diagnostic.FAC017.severity = none

See Also