Skip to content

Repository files navigation

AutoMap.Generator

NuGetNuGet DownloadsCILicense: 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.


Table of Contents


[Map(typeof(OrderDto))]publicclassOrder{publicintId{get;set;}publicstringCustomer{get;set;}="";publicdecimalTotal{get;set;}}// Generated automatically:publicstaticpartialclassAutoMapExtensions{publicstaticOrderDtoToOrderDto(thisOrdersrc){if(srcisnull)thrownewArgumentNullException(nameof(src));returnnewOrderDto{Id=src.Id,Customer=src.Customer,Total=src.Total,};}}// Usage:vardto=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.

MethodMeanRatioAlloc
Hand-written7.23 ns1.0064 B
AutoMap.Generator6.64 ns0.9364 B
Mapperly6.68 ns0.9364 B
AutoMapper53.40 ns7.45x64 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.GeneratorMapperly
Configuration styleAttribute on the class ([Map])Separate mapper class ([Mapper] partial class)
Setup neededNone — extension methods, no setupOne mapper class per mapping group
AOT / MAUI
Reverse mappingReverse = 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 diagnosticsAM001–AM011Yes
Migration guideAutoMapper → 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
#:packageAutoMap.Generator@1.*usingAutoMap;[Map(typeof(OrderDto))]publicclassOrder{publicintId{get;set;}publicstringCustomer{get;set;}="";}publicclassOrderDto{publicintId{get;set;}publicstringCustomer{get;set;}="";}varorder=newOrder{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))]publicclassOrder{publicintId{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:

usingAutoMap;[Map(typeof(UserDto))]publicclassUser{publicintId{get;set;}publicstringEmail{get;set;}="";publicstringPasswordHash{get;set;}="";// no matching dest → silently omitted}publicclassUserDto{publicintId{get;set;}publicstringEmail{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:

usingAutoMap;publicclassOrder{publicintId{get;set;}publicstringCustomer{get;set;}="";}[MapFrom(typeof(Order))]publicclassOrderDto{publicintId{get;set;}publicstringCustomer{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))]publicclassOrder{ ...}

4. Override the method name

[Map(typeof(OrderDto),MethodName="AsDto")]publicclassOrder{ ...}// Generated:order.AsDto()

5. Reverse mapping in one line

[Map(typeof(OrderDto),Reverse=true)]publicclassOrder{ ...}// Generated both:order.ToOrderDto()
dto.ToOrder()

Controlling properties

[MapIgnore] — exclude a destination property

[MapFrom(typeof(Order))]publicclassOrderDto{publicintId{get;set;}[MapIgnore]publicstringInternalNote{get;set;}="";// ← never mapped}

[MapProperty("SourceName")] — map from a differently-named source property

publicclassOrder{publicstringCustomerName{get;set;}="";}[MapFrom(typeof(Order))]publicclassOrderDto{[MapProperty("CustomerName")]publicstringClient{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))]publicclassAddress{publicstringCity{get;set;}="";}[Map(typeof(OrderDto))]publicclassOrder{publicintId{get;set;}publicAddress?Address{get;set;}}publicclassOrderDto{publicintId{get;set;}publicAddressDto?Address{get;set;}}// Generated:returnnewOrderDto{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))]publicclassItem{publicintId{get;set;}}[Map(typeof(OrderDto))]publicclassOrder{publicintId{get;set;}publicList<Item>Items{get;set;}=new();}publicclassOrderDto{publicintId{get;set;}publicList<ItemDto>Items{get;set;}=new();}// Generated (using System.Linq added automatically):returnnewOrderDto{Id=src.Id,Items=src.Items?.Select(x =>x.ToItemDto()).ToList(),};
Source collectionDestination collectionEmitted 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)]publicclassOrder{publicintId{get;set;}publicstringCustomer{get;set;}="";}publicclassOrderDto{publicintId{get;set;}publicstringCustomer{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):publicinterfaceIAutoMapper<inTSource,outTResult>{TResultMap(TSourcesource);}// For [Map(typeof(OrderDto))] on Order, the following is generated:publicsealedclassOrderToOrderDtoMapper:IAutoMapper<Order,OrderDto>{publicstaticreadonlyOrderToOrderDtoMapperInstance=newOrderToOrderDtoMapper();publicOrderDtoMap(Ordersource)=>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:publicclassOrderService(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:

publicclassOrder{publicintId{get;set;}publicdecimalPrice{get;set;}publicList<string>Tags{get;set;}=new();}[MapFrom(typeof(Order))]publicclassOrderDto{publicintId{get;set;}[MapWith("src.Price.ToString(\"C2\")")]publicstringPriceFormatted{get;set;}="";[MapWith("src.Tags.Count")]publicintTagCount{get;set;}[MapWith("src.Id > 1000 ? \"Premium\" : \"Standard\"")]publicstringTier{get;set;}="";}

Generated:

returnnewOrderDto{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.

publicclassOrder{publicboolIsPremium{get;set;}publicstringTag{get;set;}="";publicdecimalPrice{get;set;}}[MapFrom(typeof(Order))]publicclassOrderDto{// Map only when active, fall back to default[MapWhen("src.IsPremium")]publicstringTag{get;set;}="";// Generated: Tag = src.IsPremium ? src.Tag : default,// Custom fallback value[MapWhen("src.IsPremium",Fallback="\"Standard\"")]publicstringTier{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\")")]publicstringPriceLabel{get;set;}="";// Generated: PriceLabel = src.IsPremium ? src.Price.ToString("C2") : default,// Also works with flattening[MapWhen("src.IsPremium",Fallback="\"Guest\"")]publicstringCustomerName{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]publicclassOrder{publicstringName{get;set;}="";publicstringTag{get;set;}="";publicintId{get;set;}}// Generated:returnnewOrderDto{Name=src.Name?.Trim(),// ← trimmedTag=src.Tag?.Trim(),// ← trimmedId=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).

publicclassOrder{publicdecimalPrice{get;set;}publicDateTime?ShippedAt{get;set;}}[MapFrom(typeof(Order))]publicclassOrderDto{[MapFormat("C2")]publicstringPrice{get;set;}="";// → src.Price.ToString("C2")[MapFormat("yyyy-MM-dd")]publicstringShippedAt{get;set;}="";// → src.ShippedAt?.ToString("yyyy-MM-dd")}

Composes with [MapWhen]:

[MapFormat("yyyy-MM-dd")][MapWhen("src.IsShipped",Fallback="\"N/A\"")]publicstringShippedAt{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.

publicclassOrderDto:IMapFrom<Order>{publicintId{get;set;}publicstringName{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:publicstaticOrderDtoToOrderDto(thisOrdersrc){if(srcisnull)thrownewArgumentNullException(nameof(src));varresult=newOrderDto{Id=src.Id,Name=src.Name};OnToOrderDto(src,result);// ← you implement this (optional)returnresult;}staticpartialvoidOnToOrderDto(global::MyApp.Ordersrc,global::MyApp.OrderDtoresult);// Your code (in your own partial class):namespaceAutoMap{publicstaticpartialclassAutoMapExtensions{staticpartialvoidOnToOrderDto(Ordersrc,OrderDtoresult){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)]publicclassOrder{/* ... */}// 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:

publicenumOrderStatus{Pending,Active,Cancelled}publicenumOrderStatusDto{Pending,Active,Cancelled}[Map(typeof(OrderDto))]publicclassOrder{publicOrderStatusStatus{get;set;}}publicclassOrderDto{publicOrderStatusDtoStatus{get;set;}}// Generated:Status=src.Statusswitch{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:

publicenumSrcStatus{[MapEnum("Running")]// ← maps to DstStatus.RunningActive,Done}publicenumDstStatus{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'.publicenumSrcStatus{Active,Unknown}publicenumDstStatus{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.

publicclassAddress{publicstringCity{get;set;}="";}publicclassCustomer{publicAddress?Address{get;set;}publicstringName{get;set;}="";}publicclassOrder{publicintId{get;set;}publicCustomer?Customer{get;set;}}[MapFrom(typeof(Order))]publicclassOrderDto{publicintId{get;set;}// direct matchpublicstringCustomerName{get;set;}="";// → src.Customer?.NamepublicstringCustomerAddressCity{get;set;}="";// → src.Customer?.Address?.City}

Generated:

returnnewOrderDto{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:

publicclassOrder{publicstring?Region{get;set;}publicCustomer?Customer{get;set;}}[MapFrom(typeof(Order))]publicclassOrderDto{[MapDefault("\"Global\"")]publicstringRegion{get;set;}="";// → src.Region ?? "Global"[MapDefault("\"Guest\"")]publicstringCustomerName{get;set;}="";// → src.Customer?.Name ?? "Guest"[MapDefault("0")]publicintCustomerOrderCount{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)publicrecordOrderDto(intId,stringCustomer);[Map(typeof(OrderDto))]publicclassOrder{publicintId{get;set;}publicstringCustomer{get;set;}="";}// Generated:returnnewglobal::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 mappingpublicclassOrderDto{publicintId{get;}publicstringName{get;}publicOrderDto(){}// parameterless exists, but ignoredpublicOrderDto(intid,stringname){ ...}// ← selected (longest)}[Map(typeof(OrderDto))]publicclassOrder{publicintId{get;set;}publicstringName{get;set;}="";}// Generated:returnnewglobal::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:

publicclassOrderDto{publicintId{get;}publicstringTag{get;set;}="";publicOrderDto(intid){Id=id;}}// Generated:returnnewglobal::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'.publicrecordOrderDto(intId,stringMissing);[Map(typeof(OrderDto))]publicclassOrder{publicintId{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:

publicclassOrderDto{publicintId{get;set;}publicstringCustomer{get;set;}="";}[Map(typeof(OrderDto),GenerateProjection=true)]publicclassOrder{publicintId{get;set;}publicstringCustomer{get;set;}="";publicstringInternalNotes{get;set;}="";}// Generated:publicstaticreadonlyExpression<Func<Order,OrderDto>>ToOrderDtoExpression= src =>newOrderDto{Id=src.Id,Customer=src.Customer,};publicstaticIQueryable<OrderDto>ProjectToOrderDto(thisIQueryable<Order>source)=>source.Select(ToOrderDtoExpression);// Usage — EF Core only selects Id and Customer from the database, never InternalNotes:vardtos=awaitdbContext.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. CustomerNamesrc.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.


Property matching rules

RuleBehaviour
Name matchCase-insensitive name comparison
Type matchSource type must be identical or implicitly convertible to destination type
[MapIgnore] on destProperty is skipped
[MapProperty("X")] on destLooks up X on the source instead
Readonly destProperties with no public setter/init are skipped
Static/indexerAlways skipped
Inherited propertiesSource 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]

PropertyTypeDescription
(constructor)TypeDestination type ([Map]) or source type ([MapFrom])
MethodNamestring?Override the generated method name. Default: To{TypeName}
ReverseboolAlso generate the opposite-direction mapping. Default: false
StrictboolUnmapped/incompatible properties become build errors instead of warnings. Default: false
GenerateProjectionboolAlso generate an Expression<Func<TSource,TDest>> + IQueryable<TDest> projection helper for EF Core. Default: false

[MapProperty]

PropertyTypeDescription
(constructor)stringName of the source property to read from

[MapWith]

PropertyTypeDescription
(constructor)stringC# expression using src as the source variable; emitted verbatim as the property assignment RHS

[MapWhen]

PropertyTypeDescription
(constructor)stringC# boolean expression; when true the property maps normally, when false Fallback is used
Fallbackstring?C# expression for the false branch. Default: default

[MapDefault]

PropertyTypeDescription
(constructor)stringC# 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 eleven built-in diagnostics that surface problems at build time.

IDSeverityMeaning
AM001⚠ WarningNo properties matched between source and destination — the mapping would be empty
AM002❌ Error[MapProperty("X")] references a source property that does not exist
AM003❌ ErrorThe type passed to [Map] or [MapFrom] could not be resolved
AM004⚠ WarningA destination property with a matching name was skipped — incompatible types with no registered mapping
AM005⚠ WarningA 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⚠ WarningA source enum member has no matching destination enum member — _ => default fallback used. IDE code fix: add [MapEnum("X")], offered once per destination member
AM007⚠ WarningReverse = true was requested, but no reverse properties could be generated
AM008⚠ WarningGenerateProjection = 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ℹ InfoAutoMapper CreateMap<TSource, TDest>() can be migrated to AutoMap with a [Map(typeof(TDest))] attribute
AM010ℹ InfoThe 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🔕 HiddenA 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

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 and AM008 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 four. AM010 and AM011 run as standalone analyzers (AutoMapUnusedMappingAnalyzer and AutoMapPreviewAnalyzer) that always report on the [Map]/[MapFrom] attribute itself.

AM001 example

// ⚠ AM001: Mapping from 'Order' to 'ProductDto' produced no property matches.[Map(typeof(ProductDto))]publicclassOrder{publicintId{get;set;}}publicclassProductDto{publicstringSku{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'.publicclassOrder{publicstringCustomerName{get;set;}="";}[MapFrom(typeof(Order))]publicclassOrderDto{[MapProperty("Foo")]// ← typo!publicstringName{get;set;}="";}// ✅ Fix:[MapProperty("CustomerName")]publicstringName{get;set;}= "";

Records and structs

Structs — the null-guard is omitted since value types can't be null:

[Map(typeof(PointDto))]publicstructPoint{publicintX{get;set;}publicintY{get;set;}}// Generated: return new PointDto { X = src.X, Y = src.Y }; ← no null check

Recordsinit-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 propertiespublicrecordOrderDto{publicintId{get;init;}publicstringName{get;init;}="";}[Map(typeof(OrderDto))]publicclassOrder{publicintId{get;set;}publicstringName{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. stringstring? (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

PackageDescription
AutoWireCompile-time DI auto-registration — [Scoped]/[Singleton]/[Transient] generates IServiceCollection code. Zero reflection.
AutoValidate.GeneratorCompile-time FluentValidation wiring — discovers AbstractValidator<T> subclasses and generates AddValidators().
AutoResult.GeneratorCompile-time Result<T> monad — [TryWrap] generates Try*() wrappers for sync, async and void methods.
AutoQuery.GeneratorCompile-time LINQ query specs — [QuerySpec(typeof(T))] generates Apply(IQueryable<T>).
AutoDispatch.GeneratorCompile-time CQRS dispatcher — [Handler] generates a strongly-typed IDispatcher. No IRequest<T>, no reflection.
AutoLog.GeneratorCompile-time high-performance logging — [Log(Level, Message)] on a partial method generates LoggerMessage.Define. AOT-safe.
AutoHttpClient.GeneratorCompile-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

PackageDownloadsDescription
AutoWireDownloadsCompile-time dependency injection auto-registration for
AutoQuery.GeneratorDownloadsCompile-time query composition for IQueryable using Roslyn incremental source generators
AutoArchitectureDownloadsCompile-time architecture/dependency-rule enforcement for
AutoHttpClient.GeneratorDownloadsCompile-time typed HTTP client generation for
AutoDispatch.GeneratorDownloadsCompile-time CQRS dispatcher for
AutoLog.GeneratorDownloadsCompile-time high-performance logging for
AutoValidate.GeneratorDownloadsCompile-time FluentValidation wiring for

License

MIT

About

Compile-time object mapping for .NET via Roslyn source generator. Zero reflection, AOT-safe.

Topics

Resources

Contributing

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages