Repository navigation
Configuration Based Mapping
Map types you don't own -- third-party libraries, external DTOs, generated code -- without modifying the source types. Configuration-based mapping lets you define mappings in a separate mapper class or via assembly-level attributes, while the generator produces the same efficient extension methods as attribute-based [MapTo].
π― Key Benefits:
- π¦ Map types you don't own - No need to modify source or target types
- β
Same generated output - Produces identical extension methods as
[MapTo] - π Multiple API styles - Mapper class pattern, assembly-level shorthand, class-level
[MapTypes], or fluent MappingBuilder - π€ Mix with attributes - Use both
[MapTo]and configuration in the same project - π Native AOT ready - Pure compile-time generation with zero reflection
β‘ Quick Example:
// No attributes on the source type β it's from an external library
[MappingConfiguration]
public static partial class ExternalMappings
{
public static partial TargetDto MapToTargetDto(this ExternalLib.Source source);
}
// Generated: Extension method (same output as [MapTo])
public static TargetDto MapToTargetDto(this ExternalLib.Source source) =>
new TargetDto { Id = source.Id, Name = source.Name };- πΊοΈ Working with Object Mapping - Attribute-based mapping reference
- π Working with Enum Mapping - Standalone enum mapping reference
- π» Sample Projects - Working code examples
- Overview and When to Use
- Quick Start (5-Minute Guide)
- Comparison: All Mapping Approaches
- Mapper Class Pattern (Primary API)
- Assembly-Level Pattern (Shorthand API)
- Enum Auto-Detection
- Mixed Mode: Attributes + Configuration
- Diagnostics Reference
- Constructor and Struct Support
- Built-in Type Conversions
- Auto-Generated Nested Type Mappings
- Private Setter and Metadata Type Support
- Best Practices
- Phase 9: Class-Level MapTypes and MappingBuilder
- Related Documentation
The standard [MapTo] attribute requires you to decorate the source type directly. This works perfectly when you own the source code, but falls short in several common scenarios:
| Scenario | Problem with [MapTo]
|
Solution |
|---|---|---|
| π¦ Third-party library types | Can't add attributes to types you don't own | Configuration-based mapping |
| π€ Generated code (gRPC, EF scaffolding) | Attributes get overwritten on regeneration | Configuration-based mapping |
| π€ Shared packages | Don't want mapping concerns in shared contracts | Configuration-based mapping |
| π Cross-cutting mappings | Mapping logic scattered across many files | Centralized mapper class |
Configuration-based mapping solves all of these by letting you define mappings externally in a dedicated mapper class or via assembly-level attributes β without touching the source or target types.
The generated output is identical to [MapTo]: efficient extension methods with null checks, constructor mapping, collection support, enum conversion, and all other mapping features.
dotnet add package Atc.SourceGeneratorsπ‘ Optional (recommended for better IntelliSense):
dotnet add package Atc.SourceGenerators.AnnotationsOr in your .csproj:
<ItemGroup>
<!-- Required: Source generator -->
<PackageReference Include="Atc.SourceGenerators" Version="1.0.0" />
<!-- Optional: Better IntelliSense for attributes -->
<PackageReference Include="Atc.SourceGenerators.Annotations" Version="1.0.0" />
</ItemGroup>Create a static partial class decorated with [MappingConfiguration]:
using Atc.SourceGenerators.Annotations;
[MappingConfiguration]
public static partial class ExternalMappings
{
}Add partial extension methods for each mapping you need. The generator fills in the implementation:
using Atc.SourceGenerators.Annotations;
using ThirdParty.Models;
using MyApp.Dtos;
[MappingConfiguration]
public static partial class ExternalMappings
{
// Properties are matched by name automatically
public static partial CustomerDto MapToCustomerDto(this ExternalCustomer source);
}The generated extension methods work exactly like [MapTo] mappings:
using Atc.Mapping;
var externalCustomer = thirdPartyApi.GetCustomer(id);
CustomerDto dto = externalCustomer.MapToCustomerDto();That's it! π The generator produces the same efficient mapping code as attribute-based mappings, but without modifying the source type.
Use this decision matrix to choose the right approach:
| Criteria |
[MapTo] Attribute |
Mapper Class [MappingConfiguration]
|
Assembly-Level [MapTypes]
|
Class-Level [MapTypes]
|
MappingBuilder Fluent API |
|---|---|---|---|---|---|
| You own the source type | Preferred | Works | Works | Works | Works |
| Third-party / external types | Can't modify | Preferred | Works | Works | Works |
| Generated types (gRPC, EF) | Gets overwritten | Preferred | Works | Works | Works |
| Mapping definition location | On the source type | Separate mapper class | Any file (typically AssemblyInfo.cs) | Dedicated static class | Program.cs / Startup.cs |
| Discoverability | High (on the type) | Medium (separate file) | Low (assembly-level) | Medium (grouped in class) | High (in composition root) |
| Centralized control | Scattered | One location | Scattered | One location | One location |
| Property ignore | [MapIgnore] |
[MapConfigIgnore] |
IgnoreSourceProperties |
IgnoreSourceProperties |
ignoreSourceProperties |
| Property rename | [MapProperty] |
[MapConfigProperty] |
PropertyMap |
PropertyMap |
propertyMap |
| Bidirectional | Bidirectional = true |
[MapConfigOptions] |
Bidirectional = true |
Bidirectional = true |
bidirectional: true |
| IDE navigation | Go to definition | Go to definition | No | No | No |
| Generated output | Extension methods | Identical extension methods | Identical extension methods | Identical extension methods | Identical extension methods |
| Performance | Zero overhead | Zero overhead | Zero overhead | Zero overhead | Zero overhead |
π‘ Rule of thumb: Use [MapTo] when you own the type. Use configuration-based mapping when you don't -- or when you want all mappings in one place. For DI-integrated projects, MappingBuilder provides the most natural registration experience.
The mapper class pattern is the primary way to define configuration-based mappings. You create a static partial class, decorate it with [MappingConfiguration], and define partial extension methods for each mapping.
Define a partial extension method with the source type as the this parameter and the target type as the return type. The generator matches properties by name (case-insensitive):
using Atc.SourceGenerators.Annotations;
[MappingConfiguration]
public static partial class ExternalMappings
{
// Simple mapping - properties matched by name
public static partial TargetDto MapToTargetDto(this ExternalLib.Source source);
}What gets generated:
public static TargetDto MapToTargetDto(this ExternalLib.Source source)
{
if (source is null)
{
return default!;
}
return new TargetDto
{
Id = source.Id,
Name = source.Name,
Email = source.Email
};
}You can define multiple mappings in the same class:
[MappingConfiguration]
public static partial class ExternalMappings
{
public static partial CustomerDto MapToCustomerDto(this ExternalLib.Customer source);
public static partial OrderDto MapToOrderDto(this ExternalLib.Order source);
public static partial ProductDto MapToProductDto(this ExternalLib.Product source);
}Use [MapConfigIgnore] on a method to exclude specific properties from the mapping. This is the configuration-based equivalent of [MapIgnore] on a property.
[MappingConfiguration]
public static partial class ExternalMappings
{
// Exclude InternalId and AuditTrail from the mapping
[MapConfigIgnore("InternalId")]
[MapConfigIgnore("AuditTrail")]
public static partial UserDto MapToUserDto(this ThirdParty.User source);
}What gets generated:
public static UserDto MapToUserDto(this ThirdParty.User source)
{
if (source is null)
{
return default!;
}
return new UserDto
{
Id = source.Id,
Name = source.Name,
Email = source.Email
// InternalId and AuditTrail are excluded
};
}The property name passed to [MapConfigIgnore] is matched against source properties. If the property is not found on the source type, diagnostic ATCMCF006 is emitted.
Use [MapConfigProperty] to map properties with different names between source and target types. This is the configuration-based equivalent of [MapProperty] on a property.
The first argument is the source property name and the second is the target property name:
[MappingConfiguration]
public static partial class ExternalMappings
{
[MapConfigProperty("FullName", "DisplayName")]
[MapConfigProperty("EmailAddress", "Email")]
public static partial CustomerDto MapToCustomerDto(this ThirdParty.Contact source);
}What gets generated:
public static CustomerDto MapToCustomerDto(this ThirdParty.Contact source)
{
if (source is null)
{
return default!;
}
return new CustomerDto
{
DisplayName = source.FullName, // Renamed: FullName -> DisplayName
Email = source.EmailAddress, // Renamed: EmailAddress -> Email
Phone = source.Phone // Auto-matched by name
};
}If the source property is not found, diagnostic ATCMCF006 is emitted. If the target property is not found, diagnostic ATCMCF007 is emitted.
Use [MapConfigOptions] to enable advanced mapping features like bidirectional mapping:
[MappingConfiguration]
public static partial class ExternalMappings
{
[MapConfigOptions(Bidirectional = true)]
public static partial OrderDto MapToOrderDto(this ExternalLib.Order source);
}With Bidirectional = true, the generator produces two extension methods:
// Forward mapping
public static OrderDto MapToOrderDto(this ExternalLib.Order source) { ... }
// Reverse mapping (auto-generated)
public static ExternalLib.Order MapToOrder(this OrderDto source) { ... }You can combine [MapConfigOptions] with [MapConfigIgnore] and [MapConfigProperty]:
[MappingConfiguration]
public static partial class ExternalMappings
{
[MapConfigOptions(Bidirectional = true)]
[MapConfigIgnore("InternalId")]
[MapConfigProperty("FullName", "DisplayName")]
public static partial CustomerDto MapToCustomerDto(this ExternalLib.Customer source);
}For simple mappings where you don't need the full mapper class pattern, you can use [assembly: MapTypes(...)] attributes. This is a concise shorthand that works well for straightforward type-to-type mappings.
Apply [assembly: MapTypes] in any file (typically AssemblyInfo.cs or a dedicated Mappings.cs):
using Atc.SourceGenerators.Annotations;
// Simple mapping - properties matched by name
[assembly: MapTypes(typeof(ExternalLib.Contact), typeof(MyApp.Customer))]
// With options
[assembly: MapTypes(
typeof(ExternalLib.Order),
typeof(MyApp.OrderDto),
Bidirectional = true)]
// With property ignores
[assembly: MapTypes(
typeof(ExternalLib.User),
typeof(MyApp.UserDto),
IgnoreSourceProperties = new[] { "InternalId", "AuditTrail" })]Use the PropertyMap parameter to rename properties. Each entry uses "SourceProperty:TargetProperty" syntax:
[assembly: MapTypes(
typeof(ExternalLib.Contact),
typeof(MyApp.Customer),
PropertyMap = new[] { "EmailAddress:Email", "PhoneNumber:Phone" },
IgnoreSourceProperties = new[] { "InternalId" },
Bidirectional = true)]This generates the same output as the equivalent mapper class definition:
[MappingConfiguration]
public static partial class ContactMappings
{
[MapConfigOptions(Bidirectional = true)]
[MapConfigIgnore("InternalId")]
[MapConfigProperty("EmailAddress", "Email")]
[MapConfigProperty("PhoneNumber", "Phone")]
public static partial Customer MapToCustomer(this ExternalLib.Contact source);
}π€ When to prefer assembly-level vs mapper class:
Assembly-Level [MapTypes]
|
Mapper Class [MappingConfiguration]
|
|---|---|
| Simple, straightforward mappings | Complex mappings with many renames |
| Quick one-liners | Multiple related mappings grouped together |
| No IDE navigation to method | Navigate-to-definition support |
| Less ceremony | More explicit and discoverable |
When the source and target types have properties with different enum types, the generator automatically detects the mismatch and generates type-safe switch expression mappings -- no explicit enum mapping configuration required.
- π The generator compares source and target property types during mapping analysis
- π When it finds two properties with the same name but different enum types, it generates an inline switch expression
- π€ Enum values are matched by name (case-insensitive), with automatic special case handling
// Source (third-party library)
public class ExternalOrder
{
public int Id { get; set; }
public ExternalOrderStatus Status { get; set; } // Different enum type
}
public enum ExternalOrderStatus { None, Pending, Shipped, Delivered }
// Target (your app)
public class OrderDto
{
public int Id { get; set; }
public OrderStatus Status { get; set; } // Different enum type
}
public enum OrderStatus { Unknown, Pending, Shipped, Delivered }
// Configuration
[MappingConfiguration]
public static partial class OrderMappings
{
public static partial OrderDto MapToOrderDto(this ExternalOrder source);
}What gets generated:
public static OrderDto MapToOrderDto(this ExternalOrder source)
{
if (source is null)
{
return default!;
}
return new OrderDto
{
Id = source.Id,
Status = source.Status switch
{
ExternalOrderStatus.None => OrderStatus.Unknown, // Special case auto-detected
ExternalOrderStatus.Pending => OrderStatus.Pending,
ExternalOrderStatus.Shipped => OrderStatus.Shipped,
ExternalOrderStatus.Delivered => OrderStatus.Delivered,
_ => throw new global::System.ArgumentOutOfRangeException(
nameof(source.Status), source.Status, "Unmapped enum value"),
}
};
}The generator uses EnumMappingHelper to automatically detect common naming patterns:
| Source Value | Auto-Detected Target Values |
|---|---|
None |
Unknown, Default, NotSet
|
Active |
Enabled, On, Running
|
Inactive |
Disabled, Off, Stopped
|
Deleted |
Removed, Archived
|
Pending |
InProgress, Processing
|
Completed |
Done, Finished
|
These special cases work in both directions (e.g., Unknown also maps to None).
If you need different enum mapping behavior than the auto-detected defaults, you have two options:
Option 1: Define an explicit [MapTo] attribute on the enum (if you own it):
[MapTo(typeof(OrderStatus))]
public enum ExternalOrderStatus
{
None, // Will use [MapTo] mapping rules instead of auto-detection
Pending,
Shipped,
Delivered
}When a [MapTo] attribute exists on the enum, the generator uses the attribute-based enum mapping extension method instead of generating an inline switch.
Option 2: If you don't own the enum either, use a manual conversion in a BeforeMap / AfterMap hook (see Working with Object Mapping for hook details).
You can use both [MapTo] attributes and configuration-based mapping in the same project. This is useful when migrating gradually or when some types are yours (use [MapTo]) and others are external (use configuration).
When both an attribute-based [MapTo] and a configuration-based mapping exist for the same source-to-target pair, the following rules apply:
- π Attribute-based
[MapTo]always wins -- the configuration-based mapping is skipped β οΈ WarningATCMAP005is emitted -- so you know about the conflict- β No duplicate extension methods -- only one mapping is generated
// Attribute-based (on the source type)
[MapTo(typeof(CustomerDto))]
public partial class Customer
{
public string Name { get; set; } = string.Empty;
}
// Configuration-based (in mapper class) β this will be SKIPPED
[MappingConfiguration]
public static partial class Mappings
{
// ATCMAP005: Mapping from Customer to CustomerDto already defined via [MapTo]
public static partial CustomerDto MapToCustomerDto(this Customer source);
}Severity: Warning
Message: Mapping from 'Customer' to 'CustomerDto' is already defined via [MapTo] attribute. The configuration-based mapping will be skipped.
Why it exists: Having two mapping definitions for the same type pair would produce duplicate extension methods. The generator resolves this by giving precedence to [MapTo] and warning you about the redundant configuration.
π§ How to fix:
- β
Remove the configuration-based mapping if the
[MapTo]attribute is correct - β
Remove the
[MapTo]attribute if you want the configuration-based mapping to take effect - π‘ Use
#pragma warning disable ATCMAP005if both exist intentionally (e.g., during migration)
The configuration-based mapping generator provides the following diagnostics during compilation.
Severity: Error
The class decorated with [MappingConfiguration] is not static.
Example:
[MappingConfiguration]
public partial class ExternalMappings // Missing 'static' keyword
{
public static partial TargetDto MapToTargetDto(this Source source);
}Fix:
[MappingConfiguration]
public static partial class ExternalMappings // Added 'static'
{
public static partial TargetDto MapToTargetDto(this Source source);
}Why: Mapping configuration classes contain only static extension methods and must be declared static.
Severity: Error
The class decorated with [MappingConfiguration] is not partial.
Example:
[MappingConfiguration]
public static class ExternalMappings // Missing 'partial' keyword
{
public static partial TargetDto MapToTargetDto(this Source source);
}Fix:
[MappingConfiguration]
public static partial class ExternalMappings // Added 'partial'
{
public static partial TargetDto MapToTargetDto(this Source source);
}Why: The generator needs to emit the method implementations in a separate file, which requires the class to be partial.
Severity: Error
A method in a [MappingConfiguration] class is not an extension method (missing this keyword on the first parameter).
Example:
[MappingConfiguration]
public static partial class ExternalMappings
{
public static partial TargetDto MapToTargetDto(Source source); // Missing 'this'
}Fix:
[MappingConfiguration]
public static partial class ExternalMappings
{
public static partial TargetDto MapToTargetDto(this Source source); // Added 'this'
}Why: The generator produces extension methods that are called as source.MapToTargetDto(). The first parameter must use the this modifier.
Severity: Error
The return type of a mapping method is not a concrete type.
Example:
[MappingConfiguration]
public static partial class ExternalMappings
{
public static partial ITargetDto MapToTargetDto(this Source source); // Interface return type
}Fix:
[MappingConfiguration]
public static partial class ExternalMappings
{
public static partial TargetDto MapToTargetDto(this Source source); // Concrete class
}Why: The generator needs to instantiate the return type using new. Interfaces and abstract classes cannot be instantiated.
Severity: Error
A property name referenced in [MapConfigIgnore] or [MapConfigProperty] does not exist on the source type.
Example:
[MappingConfiguration]
public static partial class ExternalMappings
{
[MapConfigIgnore("NonExistent")] // Source type doesn't have this property
public static partial TargetDto MapToTargetDto(this Source source);
}Fix: Check the source type for the correct property name. Property names are case-sensitive in [MapConfigIgnore] and [MapConfigProperty].
Severity: Error
A target property name referenced in [MapConfigProperty] does not exist on the target type.
Example:
[MappingConfiguration]
public static partial class ExternalMappings
{
[MapConfigProperty("Name", "NonExistent")] // Target type doesn't have this property
public static partial TargetDto MapToTargetDto(this Source source);
}Fix: Check the target type for the correct property name.
Severity: Info
Lists source or target properties that could not be automatically matched by name.
This is an informational diagnostic to help you identify properties that are not being mapped. It does not prevent compilation.
π§ How to resolve:
- π·οΈ Use
[MapConfigProperty]to map properties with different names - π« Use
[MapConfigIgnore]to explicitly ignore properties you don't want mapped - β No action needed if unmapped properties are intentional
Severity: Error
The source type specified in [assembly: MapTypes] could not be resolved.
Example:
[assembly: MapTypes(typeof(NonExistent.Type), typeof(MyApp.TargetDto))] // Type not foundFix: Ensure the source type is accessible from the current project. You may need to add a project or package reference.
Severity: Warning
Message: Mapping from 'SourceType' to 'TargetType' is already defined via [MapTo] attribute. The configuration-based mapping will be skipped.
This occurs when both a [MapTo] attribute and a configuration-based mapping target the same source-to-target pair. See Mixed Mode: Attributes + Configuration for details.
Severity: Warning
Two configuration-based mappings define the same source-to-target pair.
Example:
[MappingConfiguration]
public static partial class Mappings
{
public static partial TargetDto MapToTargetDto(this Source source);
public static partial TargetDto ConvertToTarget(this Source source); // Duplicate pair
}Fix: Remove one of the duplicate mappings. Only one mapping per source-to-target pair is supported.
Severity: Error
A Map() call in a MappingBuilder lambda does not have exactly two type arguments.
Example:
builder.Services.AddMappings(map =>
{
map.Map<AddressDto>(); // Missing second type argument
});Fix: Provide both source and target type arguments: map.Map<AddressDto, AddressEntity>().
Severity: Error
The argument to AddMappings() or MappingBuilder.Configure() is not a lambda expression.
Example:
Action<MappingBuilder> config = GetMappingConfig();
builder.Services.AddMappings(config); // Must be an inline lambdaFix: Pass an inline lambda expression:
builder.Services.AddMappings(map =>
{
map.Map<AddressDto, AddressEntity>();
});Why: The generator parses the lambda at compile time. Delegates, method groups, and variables cannot be analyzed statically.
Severity: Warning
A type referenced in a mapping method could not be resolved. This typically occurs when the type is produced by another source generator (sibling generator) running in the same compilation.
Example:
[MappingConfiguration]
public static partial class Mappings
{
// ATCMCF014: If 'GeneratedDto' is produced by another source generator
public static partial GeneratedDto MapToDto(this MyDomain.Entity source);
}Fix: Move the generated type to a separate project (compiled assembly reference). See Integration with Atc.Rest.Api.SourceGenerator for the recommended multi-project setup.
Why: Source generators cannot reference types produced by sibling generators in the same compilation. This is a fundamental Roslyn limitation, not a bug in your code.
The configuration-based mapping generator supports constructor mapping, struct source types, and several advanced type-matching scenarios.
The generator automatically detects constructors on target types (records, classes with primary constructors) and uses constructor call syntax when all parameters can be matched from source properties:
// Target record with positional parameters
public record TargetDto(string Name, int Width, int Height);
// Source type (third-party)
public class Widget { public string Name { get; set; } public int Width { get; set; } public int Height { get; set; } }
[MappingConfiguration]
public static partial class Mappings
{
public static partial TargetDto MapToTargetDto(this Widget source);
}What gets generated:
public static TargetDto MapToTargetDto(this Widget source)
{
if (source is null)
{
return default!;
}
return new TargetDto(
source.Name,
source.Width,
source.Height);
}If not all constructor parameters match source properties, unmatched parameters use defaults (null for reference types, default for value types).
When [MapConfigProperty] renames map to constructor parameter names, the generator uses named arguments to ensure correct parameter binding:
public record GridPositionDto(int Row, int Column);
[MappingConfiguration]
public static partial class Mappings
{
[MapConfigProperty("X", "Column")]
[MapConfigProperty("Y", "Row")]
public static partial GridPositionDto MapToGridPositionDto(this GridPosition source);
}What gets generated:
return new GridPositionDto(Row: source.Y, Column: source.X);When the source type is a value type (struct), the generator omits the null guard. Structs cannot be null, so the if (source is null) check would cause CS0037:
public struct Point { public int X { get; set; } public int Y { get; set; } }
[MappingConfiguration]
public static partial class Mappings
{
public static partial PointDto MapToPointDto(this Point source);
}What gets generated:
public static PointDto MapToPointDto(this Point source)
{
// No null guard β structs cannot be null
return new PointDto
{
X = source.X,
Y = source.Y
};
}When mapping a reference-type property to a value-type (struct) target, the generator uses a ternary pattern instead of ?. to avoid producing Nullable<T>:
// source.Location?.MapToPosition()! would produce Position? (nullable struct)
// Instead, the generator emits:
source.Location is not null ? source.Location.MapToPosition() : defaultConstructor parameter matching accepts numeric type pairs (e.g., uint to int, ushort to int). The generator inserts explicit casts:
// Source has uint Width, target constructor takes int width
return new TargetDto((int)source.Width, (int)source.Height);Constructor matching accepts collection parameters with compatible element types. For example, IList<SourceItem> matches a List<TargetItem> constructor parameter when SourceItem and TargetItem are mappable custom types.
The generator automatically handles common type conversions between source and target properties.
// string β Uri
new global::System.Uri(source.Link)
// Uri β string
source.Link.ToString()
// Nullable variants use null-safe patterns
// string? β Uri?
source.Link is not null ? new global::System.Uri(source.Link) : null
// Uri? β string?
source.Link?.ToString()// string β enum
global::System.Enum.TryParse<global::TargetNs.StatusType>(source.Status, out var __parsed)
? __parsed
: default(global::TargetNs.StatusType)
// enum β string
source.Status.ToString()
// Nullable enum β string
source.Status?.ToString()Widening and narrowing conversions between numeric types generate explicit casts:
// int β double
(double)source.Value
// long β int
(int)source.Count
// Nullable variants
(double?)source.ValueThe generator unwraps Nullable<T> for type analysis:
-
int?toint?β mapped as numeric conversion -
MyEnum?toMyEnum?β mapped with null-safe pattern:
source.Status is { } __status ? __status.MapToTargetEnum() : nullWhen a mapping references nested types (via properties or collection elements) that don't have their own explicit mapping declaration, the generator automatically creates private mapping sub-methods. This is recursive β if the nested type itself has nested properties, those are also generated.
// Only declare the top-level mapping
[MappingConfiguration]
public static partial class Mappings
{
public static partial TargetNs.Order MapToOrder(this SourceNs.Order source);
// If Order has a Detail property of type OrderDetail,
// and OrderDetail has a Price property of type Money,
// the generator auto-creates private MapToOrderDetail and MapToMoney sub-methods
}Key behaviors:
- π Works for both nested object properties AND collection element types
- π« Avoids duplicate generation β if a top-level mapping already exists for the type pair, no sub-method is generated
- π Sub-methods are emitted as
private staticmethods within the mapping class - π Recursion is unbounded β deeply nested structures are fully supported
When the target type comes from a compiled assembly reference (DLL), properties with { get; private set; } are still included in property matching.
Roslyn hides private setters on metadata-loaded types (SetMethod returns null), but the generator no longer requires a public setter for target property matching. This is critical for positional records that re-declare properties with private setters for controlled mutability.
// Target from an external assembly (DLL reference)
public record ExternalDto
{
public string Name { get; private set; } // Private setter β still matched
public int Value { get; private set; } // Private setter β still matched
}Properties are matched by name and included in constructor mapping or object initializer as appropriate, regardless of setter accessibility on metadata-loaded types.
-
π Group related mappings together. Create one
[MappingConfiguration]class per external library or bounded context:// PaymentGatewayMappings.cs [MappingConfiguration] public static partial class PaymentGatewayMappings { public static partial PaymentDto MapToPaymentDto(this Stripe.Payment source); public static partial RefundDto MapToRefundDto(this Stripe.Refund source); }
-
π― Prefer the mapper class pattern over assembly-level
[MapTypes]for mappings with property renames or ignores. The mapper class provides better discoverability and IDE navigation. -
π·οΈ Use
[MapTo]when you own the type. Don't reach for configuration-based mapping if you can simply decorate the source type. Attribute-based mapping is more discoverable and colocated with the type. -
π Name your mapper methods consistently. Follow the
MapTo{TargetType}convention to match the pattern used by[MapTo]-generated methods:// Consistent naming public static partial CustomerDto MapToCustomerDto(this Contact source); // Inconsistent naming (avoid) public static partial CustomerDto ConvertContact(this Contact source);
-
π Handle enum mismatches explicitly when auto-detection doesn't match your requirements. Review the generated switch expressions to ensure special case mappings (e.g.,
NonetoUnknown) are correct for your domain. -
π Use
[MapConfigIgnore]for sensitive data. When mapping external types that contain internal identifiers, tokens, or other sensitive properties, explicitly ignore them:[MapConfigIgnore("ApiSecret")] [MapConfigIgnore("InternalTrackingId")] public static partial PublicUserDto MapToPublicUserDto(this ExternalUser source);
-
β οΈ Don't mix both approaches for the same pair. If a source-to-target pair already has a[MapTo]attribute, don't also define it in a configuration class. TheATCMAP005warning exists to catch this.
Phase 9 introduces two additional ways to define configuration-based mappings: class-level [MapTypes] for better organization and a fluent MappingBuilder API for registration in Program.cs/Startup.cs.
In addition to applying [MapTypes] at the assembly level, you can apply it on a static class. This provides the same functionality but scopes the mappings to a class for better organization and discoverability.
[MapTypes(typeof(DeliveryReport), typeof(DeliveryStatus))]
[MapTypes(typeof(AddressDto), typeof(AddressEntity), Bidirectional = true)]
static class Mappings;This is semantically equivalent to assembly-level [MapTypes]:
[assembly: MapTypes(typeof(DeliveryReport), typeof(DeliveryStatus))]
[assembly: MapTypes(typeof(AddressDto), typeof(AddressEntity), Bidirectional = true)]β Benefits over assembly-level:
- π Mappings are grouped in a named class, making them easier to find
- ποΈ Multiple mapping classes can organize mappings by domain or external library
- π Better code navigation in IDEs (find the class, see all its mappings)
Example: Organizing by external library:
[MapTypes(typeof(Stripe.Payment), typeof(PaymentDto))]
[MapTypes(typeof(Stripe.Refund), typeof(RefundDto))]
static class StripeMappings;
[MapTypes(typeof(SendGrid.Message), typeof(EmailDto))]
[MapTypes(typeof(SendGrid.Contact), typeof(RecipientDto), Bidirectional = true)]
static class SendGridMappings;All [MapTypes] parameters work identically in both assembly-level and class-level contexts: PropertyMap, IgnoreSourceProperties, IgnoreTargetProperties, Bidirectional, and PropertyNameStrategy.
Register mappings in Program.cs or Startup.cs using a fluent API. This approach integrates naturally with the standard .NET service registration pattern.
With DI (generates AddMappings extension on IServiceCollection):
// In Program.cs
builder.Services.AddMappings(map =>
{
map.Map<AddressDto, AddressEntity>(bidirectional: true);
map.Map(typeof(OrderDto), typeof(OrderEntity));
map.Map<CustomerDto, CustomerEntity>(
propertyMap: new[] { "FullName:Name" },
ignoreSourceProperties: new[] { "InternalId" });
});Without DI:
MappingBuilder.Configure(map =>
{
map.Map<AddressDto, AddressEntity>(bidirectional: true);
});Both forms are no-ops at runtime β the generator parses the lambda expression at compile time to produce the same extension methods as all other mapping approaches. The AddMappings() call and MappingBuilder.Configure() call are completely eliminated from the compiled output.
Map() Parameters:
| Parameter | Type | Description |
|---|---|---|
bidirectional |
bool |
Generate both forward and reverse mappings |
propertyMap |
string[] |
Rename properties with "Source:Target" syntax |
ignoreSourceProperties |
string[] |
Exclude source properties from mapping |
ignoreTargetProperties |
string[] |
Exclude target properties from mapping |
propertyNameStrategy |
PropertyNameStrategy |
PascalCase/camelCase/snake_case conversion |
π€ When to prefer MappingBuilder:
- π§ Your project already uses
builder.Servicesfor all registration - ποΈ You want mapping configuration visible in the composition root alongside DI and options
- π§ You prefer fluent APIs over attributes
- π You want all configuration in one place (Program.cs)
- The argument to
AddMappings()andMappingBuilder.Configure()must be an inline lambda expression. Variables, method groups, and delegates cannot be analyzed at compile time (see diagnostic ATCMCF013). - Each
Map()call must have exactly two type arguments (see diagnostic ATCMCF011).
- πΊοΈ Working with Object Mapping - Attribute-based mapping (
[MapTo]) reference - π Working with Enum Mapping - Standalone enum mapping reference
- π» Sample Projects - Working code examples
- π Getting Started - Installation and setup guide
- πΊοΈ Object-Mapping-Feature-Roadmap - Feature roadmap for mapping generators
- π Diagnostics Reference - All diagnostic codes and fixes
- π§ Troubleshooting - Common issues and solutions
For more information and examples, visit the Atc.SourceGenerators GitHub repository.
π Home
- π Getting Started
- π Migration Guide
- π Working with Dependency Registration
- βοΈ Working with Options Binding
- πΊοΈ Working with Object Mapping
- π Working with Enum Mapping
- π Working with Annotation Constants
- π¦ Sample Projects
- πΎ PetStore API Example
- π Attribute API Reference
- π©Ί Diagnostics Reference
β οΈ Common Patterns and Anti-Patterns- π Native AOT Compatibility
- π§ Troubleshooting
- π Dependency Registration Feature Roadmap
- βοΈ Options Binding Feature Roadmap
- πΊοΈ Object Mapping Feature Roadmap
- π Annotation Constants Feature Roadmap
- π¦ GitHub Repository
- π₯ NuGet Package
- π Report Issues