Skip to content

Repository files navigation

AutoMap.Generator

NuGet NuGet Downloads CI License: MIT .NET 10 Ready

📖 Documentation site  ·  NuGet  ·  Changelog  ·  Migrate from AutoMapper  ·  Benchmarks

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.

Related Swevo packages


Table of Contents


[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();

Performance

AutoMap.Generator generates the same code a developer would write by hand — there is no runtime overhead beyond the property assignments themselves.

Method Mean Ratio Alloc
Hand-written 7.23 ns 1.00 64 B
AutoMap.Generator 6.64 ns 0.93 64 B
Mapperly 6.68 ns 0.93 64 B
AutoMapper 53.40 ns 7.45x 64 B

Flat 5-property mapping, BenchmarkDotNet on Windows 11 / AMD Ryzen 9 5900X / .NET 9.0.18. A nested-object + 10-item collection scenario shows the same pattern (AutoMap 105.8 ns vs AutoMapper 234.2 ns, a 2.07x gap) since AutoMapper's reflection overhead scales with mapping complexity. Run dotnet run -c Release -- --filter '*' in benchmarks/ to reproduce both scenarios.


Why AutoMap.Generator over Mapperly?

Both are Roslyn source generators with identical runtime performance. The key differences are in the developer experience:

AutoMap.Generator Mapperly
Configuration style Attribute on the class ([Map]) Separate mapper class ([Mapper] partial class)
Setup needed None — extension methods, no setup One mapper class per mapping group
AOT / MAUI ✅ ✅
Reverse mapping Reverse = true in the attribute [MapperIgnoreSource] + manual reverse method
Custom expressions [MapWith("src.Price.ToString(\"C2\")")] [MapProperty(Use = nameof(...))]
Conditional mapping [MapWhen("src.IsActive")] Manual partial method
Build-time diagnostics AM001–AM012 Yes
Migration guide AutoMapper → AutoMap —

AutoMap.Generator is the better fit when you want zero setup — just annotate your domain class and use the generated extension method. No extra mapper classes, no DI registration needed.


Installation

dotnet add package AutoMap.Generator

Targets netstandard2.0 — works with .NET 6, 7, 8, 9, and MAUI.


Try it in 30 seconds

Option A — single file, zero setup (requires .NET 10 SDK's file-based apps):

// Save as automap-try.cs, then run: dotnet run automap-try.cs
#:package AutoMap.Generator@1.*

using AutoMap;

[Map(typeof(OrderDto))]
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; } = ""; }

var order = new Order { Id = 1, Customer = "Ada" };
Console.WriteLine(order.ToOrderDto().Customer); // "Ada"

Option B — full runnable demo project exercising every attribute (flattening, enums, reverse mapping, projections, and more):

dotnet new install AutoMap.Generator.Templates
dotnet new automap-demo -o MyAutoMapDemo
cd MyAutoMapDemo
dotnet run

See templates/ for the template source, or run it straight from a clone without installing anything:

git clone https://github.com/Swevo/AutoMap.Generator.git
cd AutoMap.Generator/templates/content/AutoMap.Demo
dotnet run

Migrating from AutoMapper

AutoMap.Generator now ships an AM009 analyzer + code fix to remove the most repetitive part of AutoMapper migrations.

When the analyzer sees an AutoMapper-style CreateMap<TSource, TDest>() call, it reports an informational suggestion at the call site:

CreateMap<Order, OrderDto>();
// ℹ AM009: 'CreateMap<Order, OrderDto>()' can be migrated to AutoMap —
//          add [Map(typeof(OrderDto))] to 'Order' instead.

Apply the lightbulb and AutoMap.Generator will add the attribute to the source type for you — even when the source type lives in a different file in the same project:

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

Current scope:

  • Detects CreateMap<TSource, TDest>() calls from AutoMapper-style APIs
  • Adds [Map(typeof(TDest))] to the source type when that type is available in source
  • Leaves the original AutoMapper configuration in place for manual cleanup, so the fix does not silently remove custom profile logic
  • Does not yet translate fluent member configuration such as .ForMember(...), .Ignore(), .MapFrom(...), .Condition(...), or .ReverseMap() — use the existing migration guide in MIGRATION.md for those patterns

This analyzer works from method/type names and does not require a reference to the real AutoMapper NuGet package in order to function in tests or custom tooling scenarios.


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()

Dictionary<TKey, TValue>, IDictionary<TKey, TValue>, and IReadOnlyDictionary<TKey, TValue> are also mapped automatically when the key types match — keys are copied as-is, and values are converted via .ToXxx() when the value type has a registered [Map]:

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

[Map(typeof(OrderDto))]
public class Order { public Dictionary<string, Item> Items { get; set; } = new(); }

public class OrderDto { public Dictionary<string, ItemDto> Items { get; set; } = new(); }

// Generated:
Items = src.Items?.ToDictionary(kv => kv.Key, kv => kv.Value.ToItemDto()),

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

Enum mapping

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)

IQueryable projection — GenerateProjection

Add GenerateProjection = true to [Map]/[MapFrom] to also generate a static Expression<Func<TSource, TDest>> plus an IQueryable<TDest> extension method — the equivalent of AutoMapper's ProjectTo<T>(). EF Core (or any IQueryable provider) can translate the expression directly into the query, so only the columns you actually map are selected from the database:

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

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

// Generated:
public static readonly Expression<Func<Order, OrderDto>> ToOrderDtoExpression = src => new OrderDto
{
    Id       = src.Id,
    Customer = src.Customer,
};

public static IQueryable<OrderDto> ProjectToOrderDto(this IQueryable<Order> source)
    => source.Select(ToOrderDtoExpression);

// Usage — EF Core only selects Id and Customer from the database, never InternalNotes:
var dtos = await dbContext.Orders.ProjectToOrderDto().ToListAsync();

Limitations

C# expression trees cannot contain the null-conditional operator (?.) or a switch expression (compiler restriction — CS8072/CS8829). Constructs that would need either of these are not eligible for projection:

  • Nested object mapping and collection mapping (both use ?. internally)
  • [TrimStrings] (uses src.Prop?.Trim())
  • Automatic flattening through a nullable reference path (e.g. CustomerName → src.Customer?.Name)
  • Enum mapping (uses a switch expression)

When a mapping requests GenerateProjection = true but contains one of these constructs, AM008 is reported and no projection expression is emitted — the regular instance ToXxx() extension method is unaffected either way. [MapWith], [MapDefault] (??), [MapWhen] (?:), [MapFormat] (when not nullable), constructor mapping, and plain property-to-property copies are all fully supported.


In-place patching — GenerateUpdate / UpdateFrom

Set GenerateUpdate = true on [Map], [MapFrom], or [MapExternal] to also generate an UpdateFrom(TSource src) instance-extension method that patches an existing destination instance's settable properties in place, instead of allocating a new one. This is the equivalent of AutoMapper's mapper.Map(source, existingDestination) — useful for PUT/PATCH endpoints and EF Core Update scenarios where you already have a tracked entity and only want to overwrite its properties.

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

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

// Generated (alongside the regular ToOrderDto()):
public static OrderDto UpdateFrom(this OrderDto dest, Order src)
{
    if (dest is null) throw new ArgumentNullException(nameof(dest));
    if (src is null) throw new ArgumentNullException(nameof(src));
    dest.Id = src.Id;
    dest.Name = src.Name;
    return dest;
}

// Usage — patch a tracked entity's DTO in place instead of replacing it:
var existing = await db.OrderDtos.FindAsync(id);
existing.UpdateFrom(updatedOrder);
await db.SaveChangesAsync();

Only properties with a public setter are patched — constructor-only properties (e.g. positional records with no other settable properties) are left untouched, and no UpdateFrom method is emitted at all when there is nothing to patch. Respects the same [MapIgnore], [MapProperty], [MapWith], [MapDefault], [MapWhen], [MapFormat], [MapConverter], [MapNamingConvention], and nested/collection/dictionary resolution rules as the regular ToXxx() method.


[MapNamingConvention] — flexible name matching

Place on the source or destination type (either placement works, like [TrimStrings]) to enable separator- and case-insensitive property matching. Useful when mapping from snake_case or kebab-case sources (e.g. deserialized JSON/DB rows) to PascalCase C# properties, without adding a [MapProperty("X")] to every property:

[MapNamingConvention]
public class OrderRow
{
    public string customer_name { get; set; } = "";
    public string ShippingCity { get; set; } = "";   // already matches directly — unaffected
}

[MapFrom(typeof(OrderRow))]
public class OrderDto
{
    public string CustomerName { get; set; } = "";   // → src.customer_name
    public string ShippingCity { get; set; } = "";   // → src.ShippingCity (direct match, unaffected)
}

Exact/case-insensitive matches and [MapProperty] overrides always take priority; normalized matching is only used as a fallback, before automatic flattening is attempted.


[MapConverter] — reusable value converters

Place on a destination property to call a static conversion method instead of duplicating the same [MapWith] expression across multiple mappings:

public static class MoneyConverter
{
    public static string ToDisplayString(decimal value) => value.ToString("C2");
}

[MapFrom(typeof(Order))]
public class OrderDto
{
    [MapConverter(typeof(MoneyConverter), "ToDisplayString")]
    public string Price { get; set; } = "";
}

public class Order { public decimal Price { get; set; } }

// Generated: Price = global::MyApp.MoneyConverter.ToDisplayString(src.Price),

Like [MapFormat], [MapConverter] bypasses the normal type-compatibility check, so the converter method's parameter type does not need to match the destination property's type. Composes with [MapWhen].


Mapping external / third-party types — [MapExternal]

[Map] and [MapFrom] require adding an attribute to a type you own. For types from a NuGet package, another assembly, or generated code you can't modify, use [MapExternal] on any accessible placeholder type instead — a static class works well:

[MapExternal(typeof(SomeNuGetPackage.Order), typeof(OrderDto))]
public static class ExternalMaps { }

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

// Generated exactly as if [Map(typeof(OrderDto))] had been placed on SomeNuGetPackage.Order:
// order.ToOrderDto()

[MapExternal] supports the same MethodName, Reverse, and Strict options as [Map]/[MapFrom], and can be repeated (AllowMultiple = true) to register several external mappings from the same placeholder type.


AutoMapGraph — compile-time mapping dependency graph

AutoMap.Generator always emits a static AutoMap.AutoMapGraph class summarizing every [Map]/[MapFrom]/[MapExternal] mapping registered in the compilation — baked in at build time, not reconstructed via reflection over a runtime configuration the way AutoMapper's MapperConfiguration would need to be:

public static class AutoMapGraph
{
    public const string Mermaid = @"graph LR
    Order -->|ToOrderDto| OrderDto
    Order -->|ToOrderSummary| OrderSummary
";

    public static readonly (string Source, string Destination, string MethodName)[] Edges = new (string, string, string)[]
    {
        ("Order", "OrderDto", "ToOrderDto"),
        ("Order", "OrderSummary", "ToOrderSummary"),
    };
}

Paste AutoMapGraph.Mermaid into mermaid.live or a Markdown code fence to render a diagram of your project's entire mapping surface — handy for onboarding, architecture reviews, or a CI step that fails when the graph diverges from a checked-in snapshot. Edges gives the same information as a plain array for programmatic use (e.g. a test asserting no mapping was accidentally removed).


IDE generated-code preview — AM011 code fix

Hovering a [Map]/[MapFrom] attribute already surfaces a one-line AM011 preview (hidden severity, visible via lightbulb/Error List) summarizing the flattened/defaulted/ignored/custom categories for that mapping. Invoking the AM011 code fix ("Insert generated code preview as a comment") goes further: it inserts the full generated method body as a comment block directly above the decorated type, so you can review exactly what will be emitted without ever opening AutoMapExtensions.g.cs:

// ── AutoMap.Generator preview: ToOrderDto ──
// public static OrderDto ToOrderDto(this Order src)
// {
//     if (src is null) throw new ArgumentNullException(nameof(src));
//     var result = new OrderDto
//     {
//         Id = src.Id,
//         CustomerName = src.CustomerName,
//     };
//     OnToOrderDto(src, result);
//     return result;
// }
// ── end preview ──
[AutoMap.MapFrom(typeof(Order))]
public sealed class OrderDto
{
    public int Id { get; set; }
    public string CustomerName { get; set; } = "";
}

The fix is idempotent (it won't be offered again once a preview comment is present) and is purely informational — deleting the comment has no effect on the generated code. Nested/collection/dictionary properties resolved via other [Map] attributes elsewhere in the compilation are summarized with a // + N nested/collection properties resolved only in the generated .g.cs file line instead of being inlined, since resolving them requires the generator's full cross-compilation pass.


CI verification — dotnet automap verify

The AutoMap.Cli global tool closes the gap AutoMapper leaves wide open: nothing stops a mapping from silently changing or disappearing as a codebase evolves, since CreateMap<> profiles are only checked at runtime (and only for the mappings that actually get exercised in a test). dotnet automap verify reflects into a built assembly's always-emitted AutoMap.AutoMapGraph.Edges (see AutoMapGraph above) and diffs it against a checked-in snapshot file, failing the build the moment a mapping is added, removed, or renamed without the snapshot being intentionally updated.

# install once (per machine or per repo via a local tool manifest)
dotnet tool install --global AutoMap.Cli

# first run: create the snapshot from the current build output
automap verify --assembly bin/Release/net9.0/MyApp.dll --snapshot automap.snapshot.txt --update

# every subsequent CI run: fail if the mapping surface diverged
automap verify --assembly bin/Release/net9.0/MyApp.dll --snapshot automap.snapshot.txt

A typical GitHub Actions step:

- name: Verify AutoMap mapping surface
  run: |
    dotnet tool install --global AutoMap.Cli
    automap verify --assembly bin/Release/net9.0/MyApp.dll --snapshot automap.snapshot.txt

On divergence, automap verify exits non-zero and prints a +/- diff of the affected mappings (Source -> Destination : MethodName) — commit the updated snapshot (via --update) once the change is confirmed intentional.


AOT & trimming certification

verify/AutoMap.AotVerification is a small console project configured with PublishAot=true / PublishTrimmed=true / TrimMode=full that exercises a representative cross-section of generated code — direct properties, nested objects, enums, collections, and dictionaries — and asserts the mapped output is correct at runtime. Because AutoMap.Generator never uses reflection (unlike AutoMapper, which relies on runtime-compiled expression trees and reflection-based member discovery), its generated code needs no [DynamicallyAccessedMembers]/RequiresUnreferencedCode annotations to trim or AOT-compile cleanly:

dotnet publish verify/AutoMap.AotVerification -c Release -r win-x64 --self-contained

In this repository's CI/dev sandbox (no "Desktop development with C++" workload installed), the project builds, restores, and passes the IL-trimming analysis cleanly; a full self-contained trimmed (non-native) publish succeeds and the resulting executable runs and passes its assertions. The final native AOT link step additionally requires a platform C/C++ linker (prerequisites) — install that workload locally or in CI to certify the native-executable path too; nothing in the generated C# itself is AOT-incompatible.


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
Strict bool Unmapped/incompatible properties become build errors instead of warnings. Default: false
GenerateProjection bool Also generate an Expression<Func<TSource,TDest>> + IQueryable<TDest> projection helper for EF Core. Default: false
GenerateUpdate bool Also generate an UpdateFrom(TSource src) instance method that patches an existing destination instance's settable properties in place. 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.

[MapNamingConvention]

No properties — applies to the source or destination type to enable separator/case-insensitive property matching (e.g. customer_name ↔ CustomerName).

[MapConverter]

Property Type Description
(constructor) Type, string Converter type and static method name; called as {ConverterType}.{MethodName}(src.Prop). Bypasses type-compatibility checks

[MapExternal]

Property Type Description
(constructor) Type, Type Source type, then destination type — both given explicitly since the placeholder type owns neither
MethodName string? Override the generated method name. Default: To{DestinationTypeName}
Reverse bool Also generate the opposite-direction mapping. Default: false
Strict bool Unmapped/incompatible properties become build errors instead of warnings. Default: false
GenerateUpdate bool Also generate an UpdateFrom(TSource src) instance method that patches an existing destination instance's settable properties in place. Default: false

Diagnostics

AutoMap.Generator ships twelve 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. IDE code fix: add [property: MapProperty("X")] to the closest-matching source property (when one can be found)
AM006 ⚠ Warning A source enum member has no matching destination enum member — _ => default fallback used. IDE code fix: add [MapEnum("X")], offered once per destination member
AM007 ⚠ Warning Reverse = true was requested, but no reverse properties could be generated
AM008 ⚠ Warning GenerateProjection = true requested, but the mapping needs ?. or a switch expression — not supported in Expression<Func<,>>. No projection is emitted for this mapping. IDE code fix: remove GenerateProjection = true
AM009 ℹ Info AutoMapper CreateMap<TSource, TDest>() can be migrated to AutoMap with a [Map(typeof(TDest))] attribute
AM010 ℹ Info The generated mapping method for this [Map]/[MapFrom] attribute does not appear to be referenced anywhere in the project (as a call, nameof(...), or DI registration) — diagnostic only, no automatic fix, since it may be used via reflection or another project
AM011 🔕 Hidden A one-line IDE-only preview of the generated method's signature and its flattened/defaulted/ignored/custom property categories — visible via hover/lightbulb without opening the .g.cs file
AM012 ⚠ Warning [MapNamingConvention] found multiple source properties that normalize to the same destination property name; mapping is ambiguous. IDE code fix: add explicit [MapProperty("X")] and choose one candidate

All diagnostic messages include a concrete, ready-to-paste fix snippet (e.g. [MapIgnore], [MapProperty("X")], [MapEnum("X")]) rather than just describing the problem. AM004, AM005, AM006, AM008 and AM012 are also re-reported by AutoMapAnalyzer with real source locations (on the property, constructor parameter, enum member, or [Map]/[MapFrom] attribute respectively) so IDE lightbulb code fixes are available for all five. AM010 and AM011 run as standalone analyzers (AutoMapUnusedMappingAnalyzer and AutoMapPreviewAnalyzer) that always report on the [Map]/[MapFrom] attribute itself.

AM012 example

[MapNamingConvention]
public class Source
{
    public string Customer_Name_Value { get; set; } = "";
    public string CustomerNameValue_ { get; set; } = "";
}

[MapFrom(typeof(Source))]
public class Destination
{
    public string CustomerNameValue { get; set; } = "";
}

// ⚠ AM012: Property 'CustomerNameValue' matched multiple source properties:
//          'Customer_Name_Value', 'CustomerNameValue_'.
// ✅ Fix: choose explicitly with [MapProperty("...")] on the destination member.
[MapFrom(typeof(Source))]
public class DestinationFixed
{
    [MapProperty("Customer_Name_Value")]
    public string CustomerNameValue { get; set; } = "";
}

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)? Yes — List<T>, T[], IEnumerable<T>, and ICollection<T> are mapped automatically when the element type has a [Map] relationship. See the Collection mapping section.

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.

Q: Why not just use Mapperly? Mapperly is excellent and shares the same zero-overhead goal. AutoMap.Generator takes a different ergonomic approach: annotate the class directly ([Map(typeof(Dto))]) rather than creating a separate mapper class. This means no boilerplate mapper files, no DI setup, and a one-liner migration path from AutoMapper. See the full comparison table above.


Also by the same author

🌐 Full suite overview: swevo.github.io

Package Description
AutoWire Compile-time DI auto-registration — [Scoped]/[Singleton]/[Transient] generates IServiceCollection code. Zero reflection.
AutoValidate.Generator Compile-time FluentValidation wiring — discovers AbstractValidator<T> subclasses and generates AddValidators().
AutoResult.Generator Compile-time Result<T> monad — [TryWrap] generates Try*() wrappers for sync, async and void methods.
AutoQuery.Generator Compile-time LINQ query specs — [QuerySpec(typeof(T))] generates Apply(IQueryable<T>).
AutoDispatch.Generator Compile-time CQRS dispatcher — [Handler] generates a strongly-typed IDispatcher. No IRequest<T>, no reflection.
AutoLog.Generator Compile-time high-performance logging — [Log(Level, Message)] on a partial method generates LoggerMessage.Define. AOT-safe.
AutoHttpClient.Generator Compile-time typed HTTP client — [HttpClient] on an interface generates a strongly-typed client. AOT-safe Refit alternative.

Contributing

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

Related Packages

Package Downloads Description
AutoWire Downloads Compile-time dependency injection auto-registration for
AutoQuery.Generator Downloads Compile-time query composition for IQueryable using Roslyn incremental source generators
AutoArchitecture Downloads Compile-time architecture/dependency-rule enforcement for
AutoHttpClient.Generator Downloads Compile-time typed HTTP client generation for
AutoDispatch.Generator Downloads Compile-time CQRS dispatcher for
AutoLog.Generator Downloads Compile-time high-performance logging for
AutoValidate.Generator Downloads Compile-time FluentValidation wiring for

License

MIT

Releases

Packages

Contributors

Languages