AutoMap.Generator 1.10.0

There is a newer version of this package available.
See the version list below for details.
dotnet add package AutoMap.Generator --version 1.10.0
                    
NuGet\Install-Package AutoMap.Generator -Version 1.10.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="AutoMap.Generator" Version="1.10.0" />
                    
For projects that support PackageReference, copy this XML node into the project file to reference the package.
<PackageVersion Include="AutoMap.Generator" Version="1.10.0" />
                    
Directory.Packages.props
<PackageReference Include="AutoMap.Generator" />
                    
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 AutoMap.Generator --version 1.10.0
                    
#r "nuget: AutoMap.Generator, 1.10.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 AutoMap.Generator@1.10.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=AutoMap.Generator&version=1.10.0
                    
Install as a Cake Addin
#tool nuget:?package=AutoMap.Generator&version=1.10.0
                    
Install as a Cake Tool

AutoMap.Generator

NuGet NuGet Downloads CI License: MIT

๐Ÿ“– Documentation site  ยท  NuGet  ยท  Changelog  ยท  Migrate from AutoMapper

Compile-time object mapping for .NET via Roslyn source generators.

Add [Map(typeof(OrderDto))] to your class โ€” AutoMap generates a strongly-typed ToOrderDto() extension method at build time. No reflection. No runtime overhead. AOT-safe.

[Map(typeof(OrderDto))]
public class Order
{
    public int Id { get; set; }
    public string Customer { get; set; } = "";
    public decimal Total { get; set; }
}

// Generated automatically:
public static partial class AutoMapExtensions
{
    public static OrderDto ToOrderDto(this Order src)
    {
        if (src is null) throw new ArgumentNullException(nameof(src));
        return new OrderDto
        {
            Id       = src.Id,
            Customer = src.Customer,
            Total    = src.Total,
        };
    }
}

// Usage:
var dto = order.ToOrderDto();

Installation

dotnet add package AutoMap.Generator

Targets netstandard2.0 โ€” works with .NET 6, 7, 8, 9, and MAUI.


Quick start

1. [Map] โ€” attribute on the source type

Place [Map(typeof(Destination))] on the class you want to map from:

using AutoMap;

[Map(typeof(UserDto))]
public class User
{
    public int Id { get; set; }
    public string Email { get; set; } = "";
    public string PasswordHash { get; set; } = "";  // no matching dest โ†’ silently omitted
}

public class UserDto
{
    public int Id { get; set; }
    public string Email { get; set; } = "";
}

Generated: user.ToUserDto()


2. [MapFrom] โ€” attribute on the destination type

Place [MapFrom(typeof(Source))] on the DTO when you want to keep source types clean:

using AutoMap;

public class Order { public int Id { get; set; } public string Customer { get; set; } = ""; }

[MapFrom(typeof(Order))]
public class OrderDto
{
    public int Id { get; set; }
    public string Customer { get; set; } = "";
}

Generated: order.ToOrderDto() โ€” extension method is on Order, returning OrderDto.


3. Multiple mappings on one class

Both directions work. Stack [Map] for multiple destinations:

[Map(typeof(OrderDto))]
[Map(typeof(OrderSummary))]
public class Order { ... }

4. Override the method name

[Map(typeof(OrderDto), MethodName = "AsDto")]
public class Order { ... }

// Generated:
order.AsDto()

5. Reverse mapping in one line

[Map(typeof(OrderDto), Reverse = true)]
public class Order { ... }

// Generated both:
order.ToOrderDto()
dto.ToOrder()

Controlling properties

[MapIgnore] โ€” exclude a destination property

[MapFrom(typeof(Order))]
public class OrderDto
{
    public int Id { get; set; }
    [MapIgnore] public string InternalNote { get; set; } = ""; // โ† never mapped
}

[MapProperty("SourceName")] โ€” map from a differently-named source property

public class Order { public string CustomerName { get; set; } = ""; }

[MapFrom(typeof(Order))]
public class OrderDto
{
    [MapProperty("CustomerName")]
    public string Client { get; set; } = "";
    // Generated: Client = src.CustomerName
}

Nested object mapping

When a destination property type differs from the source, AutoMap.Generator checks whether a [Map] relationship exists between the two types and emits a null-safe chained call automatically:

[Map(typeof(AddressDto))]
public class Address { public string City { get; set; } = ""; }

[Map(typeof(OrderDto))]
public class Order { public int Id { get; set; } public Address? Address { get; set; } }

public class OrderDto { public int Id { get; set; } public AddressDto? Address { get; set; } }

// Generated:
return new OrderDto
{
    Id      = src.Id,
    Address = src.Address?.ToAddressDto(),  // โ† resolved automatically
};

No configuration needed โ€” as long as the [Map] for the nested type exists anywhere in the compilation, AutoMap.Generator wires it up.


Collection mapping

List<T>, T[], IEnumerable<T>, ICollection<T>, and other standard collection types are mapped automatically when the element type has a registered [Map]:

[Map(typeof(ItemDto))]
public class Item { public int Id { get; set; } }

[Map(typeof(OrderDto))]
public class Order { public int Id { get; set; } public List<Item> Items { get; set; } = new(); }

public class OrderDto { public int Id { get; set; } public List<ItemDto> Items { get; set; } = new(); }

// Generated (using System.Linq added automatically):
return new OrderDto
{
    Id    = src.Id,
    Items = src.Items?.Select(x => x.ToItemDto()).ToList(),
};
Source collection Destination collection Emitted expression
List<T> / IEnumerable<T> / ICollection<T> List<TDto> .Select(x => x.To...()).ToList()
T[] TDto[] .Select(x => x.To...()).ToArray()

Reverse mapping

Set Reverse = true on [Map] or [MapFrom] to generate both directions at once:

[Map(typeof(OrderDto), Reverse = true)]
public class Order
{
    public int Id { get; set; }
    public string Customer { get; set; } = "";
}

public class OrderDto
{
    public int Id { get; set; }
    public string Customer { get; set; } = "";
}

// Generated:
order.ToOrderDto()    // Order โ†’ OrderDto (forward)
dto.ToOrder()         // OrderDto โ†’ Order (reverse)

Both directions are registered in the mapping registry, so nested and collection resolution works bidirectionally too.


IAutoMapper<TSource, TResult> interface

AutoMap.Generator emits an IAutoMapper<in TSource, out TResult> interface into your compilation alongside a concrete sealed mapper class for every registered mapping:

// Interface (emitted into your compilation automatically):
public interface IAutoMapper<in TSource, out TResult>
{
    TResult Map(TSource source);
}

// For [Map(typeof(OrderDto))] on Order, the following is generated:
public sealed class OrderToOrderDtoMapper : IAutoMapper<Order, OrderDto>
{
    public static readonly OrderToOrderDtoMapper Instance = new OrderToOrderDtoMapper();
    public OrderDto Map(Order source) => source.ToOrderDto();
}

Use Instance to avoid allocations, or inject IAutoMapper<Order, OrderDto> into your services for testability:

// DI registration:
services.AddSingleton<IAutoMapper<Order, OrderDto>>(AutoMapExtensions.OrderToOrderDtoMapper.Instance);

// Service:
public class OrderService(IAutoMapper<Order, OrderDto> mapper) { ... }

[MapWith("expression")] โ€” custom expression

Use [MapWith] when you need a computed or transformed value rather than a direct property copy. Write any valid C# expression using src to reference the source object:

public class Order
{
    public int Id { get; set; }
    public decimal Price { get; set; }
    public List<string> Tags { get; set; } = new();
}

[MapFrom(typeof(Order))]
public class OrderDto
{
    public int Id { get; set; }

    [MapWith("src.Price.ToString(\"C2\")")]
    public string PriceFormatted { get; set; } = "";

    [MapWith("src.Tags.Count")]
    public int TagCount { get; set; }

    [MapWith("src.Id > 1000 ? \"Premium\" : \"Standard\"")]
    public string Tier { get; set; } = "";
}

Generated:

return new OrderDto
{
    Id             = src.Id,
    PriceFormatted = src.Price.ToString("C2"),
    TagCount       = src.Tags.Count,
    Tier           = src.Id > 1000 ? "Premium" : "Standard",
};

[MapWith] does not require a source property with a matching name โ€” it is injected verbatim. If both [MapWith] and [MapIgnore] are on the same property, [MapIgnore] wins.


[MapWhen] โ€” conditional mapping

Place [MapWhen("condition")] on a destination property to wrap the assignment in a compile-time ternary. The property is mapped when condition is true; otherwise Fallback (default: default) is used.

public class Order { public bool IsPremium { get; set; } public string Tag { get; set; } = ""; public decimal Price { get; set; } }

[MapFrom(typeof(Order))]
public class OrderDto
{
    // Map only when active, fall back to default
    [MapWhen("src.IsPremium")]
    public string Tag { get; set; } = "";
    // Generated: Tag = src.IsPremium ? src.Tag : default,

    // Custom fallback value
    [MapWhen("src.IsPremium", Fallback = "\"Standard\"")]
    public string Tier { get; set; } = "";
    // Generated: Tier = src.IsPremium ? src.Tier : "Standard",

    // Combine with [MapWith] โ€” the custom expression becomes the true branch
    [MapWhen("src.IsPremium")]
    [MapWith("src.Price.ToString(\"C2\")")]
    public string PriceLabel { get; set; } = "";
    // Generated: PriceLabel = src.IsPremium ? src.Price.ToString("C2") : default,

    // Also works with flattening
    [MapWhen("src.IsPremium", Fallback = "\"Guest\"")]
    public string CustomerName { get; set; } = "";
    // Generated: CustomerName = src.IsPremium ? src.Customer?.Name : "Guest",
}

[MapIgnore] takes precedence when both attributes are on the same property.


[TrimStrings] โ€” string sanitisation

Place [TrimStrings] on the class decorated with [Map] or [MapFrom] to automatically wrap every mapped string property with ?.Trim(). Ideal for user input, CSV imports, or data coming from external APIs.

[Map(typeof(OrderDto))]
[TrimStrings]
public class Order
{
    public string Name { get; set; } = "";
    public string Tag  { get; set; } = "";
    public int    Id   { get; set; }
}

// Generated:
return new OrderDto
{
    Name = src.Name?.Trim(),   // โ† trimmed
    Tag  = src.Tag?.Trim(),    // โ† trimmed
    Id   = src.Id,             // โ† non-string: unchanged
};

[TrimStrings] can be placed on either the source or the destination type. [MapWith] still takes per-property precedence.


[MapFormat("format")] โ€” formatting shorthand

Use [MapFormat] when you want to format a source value as a string. It generates .ToString("format") (or ?.ToString("format") for reference/nullable types) without needing a [MapWith] expression. Works across type boundaries (e.g. decimal โ†’ string).

public class Order { public decimal Price { get; set; } public DateTime? ShippedAt { get; set; } }

[MapFrom(typeof(Order))]
public class OrderDto
{
    [MapFormat("C2")]
    public string Price { get; set; } = "";          // โ†’ src.Price.ToString("C2")

    [MapFormat("yyyy-MM-dd")]
    public string ShippedAt { get; set; } = "";      // โ†’ src.ShippedAt?.ToString("yyyy-MM-dd")
}

Composes with [MapWhen]:

[MapFormat("yyyy-MM-dd")]
[MapWhen("src.IsShipped", Fallback = "\"N/A\"")]
public string ShippedAt { get; set; } = "";
// Generated: ShippedAt = src.IsShipped ? src.ShippedAt.ToString("yyyy-MM-dd") : "N/A",

IMapFrom<T> โ€” convention-based mapping

Implement AutoMap.IMapFrom<TSource> on a DTO to register the mapping without any attribute. Equivalent to [MapFrom(typeof(TSource))]. Deduplicates automatically if both are present.

public class OrderDto : IMapFrom<Order>
{
    public int Id { get; set; }
    public string Name { get; set; } = "";
}

// Automatically generates: order.ToOrderDto()
// No attribute needed on Order or OrderDto.

Partial method hooks โ€” On{MethodName}

Every generated mapping method stores the mapped object in a local variable and then calls a static partial void On{MethodName}(TSource src, TDest result) before returning. Implement the partial method in your own companion file for post-mapping logic. The call is compiled away at zero cost if you don't implement it.

// AutoMap generates:
public static OrderDto ToOrderDto(this Order src)
{
    if (src is null) throw new ArgumentNullException(nameof(src));
    var result = new OrderDto { Id = src.Id, Name = src.Name };
    OnToOrderDto(src, result);   // โ† you implement this (optional)
    return result;
}
static partial void OnToOrderDto(global::MyApp.Order src, global::MyApp.OrderDto result);

// Your code (in your own partial class):
namespace AutoMap
{
    public static partial class AutoMapExtensions
    {
        static partial void OnToOrderDto(Order src, OrderDto result)
        {
            result.MappedAt = DateTime.UtcNow;
        }
    }
}

Strict = true โ€” compile-time enforcement

Add Strict = true to [Map] or [MapFrom] to turn mapping warnings into errors. AM001 (no properties mapped) and AM004 (type incompatibility) are promoted from warnings to errors:

[Map(typeof(OrderDto), Strict = true)]
public class Order { /* ... */ }
// Any unresolvable property โ†’ build error, not warning

When source and destination properties are different enum types, AutoMap.Generator generates a compile-time switch expression mapping values by name automatically:

public enum OrderStatus    { Pending, Active, Cancelled }
public enum OrderStatusDto { Pending, Active, Cancelled }

[Map(typeof(OrderDto))]
public class Order { public OrderStatus Status { get; set; } }
public class OrderDto { public OrderStatusDto Status { get; set; } }

// Generated:
Status = src.Status switch
{
    global::MyApp.OrderStatus.Pending   => global::MyApp.OrderStatusDto.Pending,
    global::MyApp.OrderStatus.Active    => global::MyApp.OrderStatusDto.Active,
    global::MyApp.OrderStatus.Cancelled => global::MyApp.OrderStatusDto.Cancelled,
    _ => default
},

Same-type enum properties are mapped directly (Status = src.Status) โ€” no switch needed.

[MapEnum("DestValueName")] โ€” rename a value

Place [MapEnum] on a source enum member to redirect it to a differently-named destination member:

public enum SrcStatus
{
    [MapEnum("Running")]   // โ† maps to DstStatus.Running
    Active,
    Done
}
public enum DstStatus { Running, Done }

AM006 โ€” unmatched enum member

When a source enum member has no matching destination member and no [MapEnum] redirect, AM006 is reported and the _ => default fallback is used so the build succeeds:

// โš  AM006: Source enum member 'Unknown' on 'SrcStatus' has no matching member in 'DstStatus'.
public enum SrcStatus { Active, Unknown }
public enum DstStatus { Active }         // โ† no Unknown
// Generated: _ => default (covers Unknown at runtime)

Flattening

When a destination property has no direct source match, AutoMap.Generator automatically tries to resolve it by splitting the name at PascalCase boundaries and walking the source type tree โ€” up to 3 levels deep.

public class Address  { public string City  { get; set; } = ""; }
public class Customer { public Address? Address { get; set; } public string Name { get; set; } = ""; }
public class Order    { public int Id { get; set; } public Customer? Customer { get; set; } }

[MapFrom(typeof(Order))]
public class OrderDto
{
    public int Id                  { get; set; }  // direct match
    public string CustomerName     { get; set; } = "";  // โ†’ src.Customer?.Name
    public string CustomerAddressCity { get; set; } = "";  // โ†’ src.Customer?.Address?.City
}

Generated:

return new OrderDto
{
    Id                  = src.Id,
    CustomerName        = src.Customer?.Name,
    CustomerAddressCity = src.Customer?.Address?.City,
};

Rules:

  • Direct name matches always take priority over flattening
  • Value-type intermediates use . instead of ?. (structs can't be null)
  • Flattening is attempted before AM004 is reported

[MapDefault] โ€” null substitution

Place [MapDefault("expression")] on any destination property to substitute the provided expression when the source value is null. The expression is appended as ?? expr and works with both direct and flattened paths:

public class Order { public string? Region { get; set; } public Customer? Customer { get; set; } }

[MapFrom(typeof(Order))]
public class OrderDto
{
    [MapDefault("\"Global\"")]
    public string Region { get; set; } = "";          // โ†’ src.Region ?? "Global"

    [MapDefault("\"Guest\"")]
    public string CustomerName { get; set; } = "";    // โ†’ src.Customer?.Name ?? "Guest"

    [MapDefault("0")]
    public int CustomerOrderCount { get; set; }       // โ†’ src.Customer?.OrderCount ?? 0
}

[MapIgnore] takes precedence when both are on the same property. [MapDefault] has no effect on [MapWith] โ€” write the full expression there instead.


Constructor mapping

AutoMap.Generator automatically detects when the destination type has no public parameterless constructor and switches to constructor-call syntax โ€” no configuration needed.

Positional records (automatic)

// Destination: positional record (no parameterless ctor)
public record OrderDto(int Id, string Customer);

[Map(typeof(OrderDto))]
public class Order { public int Id { get; set; } public string Customer { get; set; } = ""; }

// Generated:
return new global::MyApp.OrderDto(src.Id, src.Customer);

Parameter names are matched to source properties case-insensitively.

[MapConstructor] โ€” explicit opt-in

Use [MapConstructor] on the destination type to force constructor mapping even when a parameterless constructor exists, or to select the primary constructor among several:

[MapConstructor]       // โ† force ctor mapping
public class OrderDto
{
    public int Id { get; }
    public string Name { get; }

    public OrderDto() { }                              // parameterless exists, but ignored
    public OrderDto(int id, string name) { ... }       // โ† selected (longest)
}

[Map(typeof(OrderDto))]
public class Order { public int Id { get; set; } public string Name { get; set; } = ""; }

// Generated:
return new global::MyApp.OrderDto(src.Id, src.Name);

Mixed: ctor params + init properties

When the selected constructor covers only some properties, remaining writable properties are mapped in an object-initializer block:

public class OrderDto
{
    public int Id { get; }
    public string Tag { get; set; } = "";
    public OrderDto(int id) { Id = id; }
}

// Generated:
return new global::MyApp.OrderDto(src.Id)
{
    Tag = src.Tag,
};

AM005 โ€” unmatched constructor parameter

If a constructor parameter has no matching source property, AM005 is reported and default is emitted so the build still succeeds:

// โš  AM005: Constructor parameter 'Missing' on 'OrderDto' has no matching property on 'Order'.
public record OrderDto(int Id, string Missing);

[Map(typeof(OrderDto))]
public class Order { public int Id { get; set; } }
// Generated: new OrderDto(src.Id, default)

Property matching rules

Rule Behaviour
Name match Case-insensitive name comparison
Type match Source type must be identical or implicitly convertible to destination type
[MapIgnore] on dest Property is skipped
[MapProperty("X")] on dest Looks up X on the source instead
Readonly dest Properties with no public setter/init are skipped
Static/indexer Always skipped
Inherited properties Source and destination inheritance chains are walked

Nested object mapping and collection mapping are resolved automatically when the related [Map] exists anywhere in the compilation.


Attribute reference

[Map] / [MapFrom]

Property Type Description
(constructor) Type Destination type ([Map]) or source type ([MapFrom])
MethodName string? Override the generated method name. Default: To{TypeName}
Reverse bool Also generate the opposite-direction mapping. Default: false

[MapProperty]

Property Type Description
(constructor) string Name of the source property to read from

[MapWith]

Property Type Description
(constructor) string C# expression using src as the source variable; emitted verbatim as the property assignment RHS

[MapWhen]

Property Type Description
(constructor) string C# boolean expression; when true the property maps normally, when false Fallback is used
Fallback string? C# expression for the false branch. Default: default

[MapDefault]

Property Type Description
(constructor) string C# expression appended as ?? expr after the source value; applied to direct and flattened paths

[MapIgnore]

No properties โ€” applies to any destination property to exclude it from all mappings.


Diagnostics

AutoMap.Generator ships six built-in diagnostics that surface problems at build time.

ID Severity Meaning
AM001 โš  Warning No properties matched between source and destination โ€” the mapping would be empty
AM002 โŒ Error [MapProperty("X")] references a source property that does not exist
AM003 โŒ Error The type passed to [Map] or [MapFrom] could not be resolved
AM004 โš  Warning A destination property with a matching name was skipped โ€” incompatible types with no registered mapping
AM005 โš  Warning A required constructor parameter has no matching source property โ€” default is emitted
AM006 โš  Warning A source enum member has no matching destination enum member โ€” _ => default fallback used

AM001 example

// โš  AM001: Mapping from 'Order' to 'ProductDto' produced no property matches.
[Map(typeof(ProductDto))]
public class Order { public int Id { get; set; } }
public class ProductDto { public string Sku { get; set; } = ""; }  // โ† no common names

// โœ… Fix: ensure source and destination share property names, or use [MapProperty].

AM002 example

// โŒ AM002: [MapProperty("Foo")] on 'OrderDto.Name' references a property
//           that does not exist on source type 'Order'.
public class Order { public string CustomerName { get; set; } = ""; }

[MapFrom(typeof(Order))]
public class OrderDto
{
    [MapProperty("Foo")]   // โ† typo!
    public string Name { get; set; } = "";
}

// โœ… Fix:
[MapProperty("CustomerName")]
public string Name { get; set; } = "";

Records and structs

Structs โ€” the null-guard is omitted since value types can't be null:

[Map(typeof(PointDto))]
public struct Point { public int X { get; set; } public int Y { get; set; } }
// Generated: return new PointDto { X = src.X, Y = src.Y };   โ† no null check

Records โ€” init-only properties work out of the box with object initialiser syntax. Positional records (primary constructor parameters) require the destination type to have either a parameterless constructor or explicit init properties:

// โœ… Works โ€” standard record with init properties
public record OrderDto { public int Id { get; init; } public string Name { get; init; } = ""; }

[Map(typeof(OrderDto))]
public class Order { public int Id { get; set; } public string Name { get; set; } = ""; }

FAQ

Q: Does AutoMap support collection properties (List<T>, arrays)? Not in v1.0.0 โ€” collection properties are skipped if the types don't match exactly. Map them manually with [MapIgnore] + a custom post-processing step.

Q: Can I map to a type in a different assembly? Yes. The destination type just needs to be accessible (public, or internal with InternalsVisibleTo).

Q: Does it work with nullable reference types? Yes. string โ†’ string? (and vice versa) maps correctly since the underlying type is the same.

Q: Is it AOT-safe? Yes. All code is generated at build time โ€” zero reflection at runtime.

Q: Why not just use AutoMapper? AutoMapper is powerful but relies on runtime reflection, is not AOT-safe, and requires a MapperConfiguration setup. AutoMap is a build-time generator: if it compiles, it maps correctly.


Also by the same author

Package Description
AutoWire Compile-time DI auto-registration โ€” [Scoped]/[Singleton]/[Transient] generates IServiceCollection code. Zero reflection.

Contributing

Issues and PRs welcome at github.com/Swevo/AutoMap.Generator.

License

MIT

There are no supported framework assets in this package.

Learn more about Target Frameworks and .NET Standard.

  • .NETStandard 2.0

    • No dependencies.

NuGet packages

This package is not used by any NuGet packages.

GitHub repositories

This package is not used by any popular GitHub repositories.

Version Downloads Last Updated
1.11.1 41 6/26/2026
1.11.0 47 6/25/2026
1.10.0 46 6/25/2026
1.9.0 47 6/25/2026
1.8.0 47 6/25/2026
1.7.0 41 6/25/2026
1.6.0 47 6/25/2026
1.5.0 49 6/25/2026
1.4.0 50 6/25/2026
1.3.0 47 6/25/2026
1.2.0 52 6/25/2026
1.1.0 44 6/25/2026
1.0.0 47 6/25/2026

1.10.0: AM004 code-fix provider (Add [MapIgnore] lightbulb). 1.9.0: Collection-level extension methods (orders.ToOrderDtos()). 1.8.0: [TrimStrings], [MapFormat], IMapFrom<T> convention, partial On{Method} hooks, Strict mode. 1.7.0: [MapWhen] conditional mapping. 1.6.0: Cross-enum mapping + [MapEnum]. 1.5.0: Flattening + [MapDefault]. 1.4.0: [MapWith]. 1.3.0: Constructor mapping. 1.2.0: Reverse + IAutoMapper. 1.1.0: Nested + collections. 1.0.0: Initial.