Migrating from AutoMapper
This guide shows how to replace AutoMapper with ForgeMap in your .NET project. Each section maps an AutoMapper concept to its ForgeMap equivalent with before/after code examples.
Core Concepts
| AutoMapper | ForgeMap | Notes |
|---|---|---|
Profile class |
[ForgeMap] partial class |
One partial class per forger group |
CreateMap<S,D>() |
partial D Forge(S source); |
Each mapping is a partial method |
IMapper (injected) |
Forger class (injected) | Register via services.AddForgeMaps() |
mapper.Map<D>(src) |
forger.Forge(src) |
Direct method call |
mapper.Map(src, dest) |
forger.ForgeInto(src, dest) |
Void method with [UseExistingValue] parameter |
Step-by-Step Migration
1. Install ForgeMap
dotnet add package ForgeMap
2. Create a Forger Class
Replace each AutoMapper Profile with a [ForgeMap] partial class:
// BEFORE (AutoMapper)
public class UserProfile : Profile
{
public UserProfile()
{
CreateMap<User, UserDto>();
}
}
// AFTER (ForgeMap)
[ForgeMap]
public partial class UserForger
{
public partial UserDto Forge(User source);
}
3. Replace DI Registration
// BEFORE
services.AddAutoMapper(typeof(Program).Assembly);
// AFTER
services.AddForgeMaps();
4. Update Injection Sites
// BEFORE
public class UserService(IMapper mapper) { ... }
// AFTER
public class UserService(UserForger forger) { ... }
Property Mapping
| AutoMapper | ForgeMap |
|---|---|
| Auto by name | Auto by name (default) |
.ForMember(d => d.X, o => o.MapFrom(s => s.Y)) |
[ForgeProperty(nameof(S.Y), nameof(D.X))] |
.ForMember(d => d.X, o => o.Ignore()) |
[Ignore(nameof(D.X))] |
.ForMember(d => d.X, o => o.MapFrom(s => s.A.B)) |
[ForgeProperty("A.B", nameof(D.X))] |
Flattening (CustomerName from Customer.Name) |
Auto-flattened by convention |
Example: Custom property mapping with ignore
// BEFORE
CreateMap<User, UserDto>()
.ForMember(d => d.FullName, o => o.MapFrom(s => $"{s.First} {s.Last}"))
.ForMember(d => d.Password, o => o.Ignore());
// AFTER
[ForgeFrom(nameof(UserDto.FullName), nameof(ResolveFullName))]
[Ignore(nameof(UserDto.Password))]
public partial UserDto Forge(User source);
private static string ResolveFullName(User source)
=> $"{source.First} {source.Last}";
Custom Resolution
| AutoMapper | ForgeMap |
|---|---|
IValueResolver<S,D,TVal> |
[ForgeFrom(nameof(D.Prop), nameof(Method))] |
.MapFrom(s => expr) |
[ForgeFrom(...)] with resolver method |
ITypeConverter<S,D> |
[ConvertWith(typeof(Converter))] |
Resolver methods live on the forger class:
// Full source access
private static string ResolveFullName(UserEntity source)
=> $"{source.FirstName} {source.LastName}";
// Single property access
private static decimal ConvertPrice(int priceInCents)
=> priceInCents / 100m;
Nested Objects
ForgeMap v1.3+ auto-wires nested mappings when a matching forge method exists in the same forger:
// BEFORE
CreateMap<Order, OrderDto>();
CreateMap<Address, AddressDto>();
// AFTER — auto-wired, no [ForgeWith] needed
[ForgeMap]
public partial class OrderForger
{
public partial OrderDto Forge(Order source);
public partial AddressDto Forge(Address source);
}
Use [ForgeWith] to disambiguate when multiple forge methods match (FM0025).
Reverse Mapping
// BEFORE
CreateMap<Order, OrderDto>().ReverseMap();
// AFTER
[ReverseForge]
public partial OrderDto Forge(Order source);
Note
[ForgeFrom] resolvers cannot be auto-reversed (FM0012). Add manual reverse mappings for those properties.
Lifecycle Hooks
| AutoMapper | ForgeMap |
|---|---|
.BeforeMap((s,d) => ...) |
[BeforeForge(nameof(Method))] — void Method(S source) |
.AfterMap((s,d) => ...) |
[AfterForge(nameof(Method))] — void Method(S source, D dest) |
// BEFORE
CreateMap<Order, OrderDto>()
.BeforeMap((s, d) => ValidateOrder(s))
.AfterMap((s, d) => d.Total = d.Items.Sum(i => i.Price));
// AFTER
[BeforeForge(nameof(ValidateOrder))]
[AfterForge(nameof(CalculateTotal))]
public partial OrderDto Forge(Order source);
private void ValidateOrder(Order source) { /* ... */ }
private void CalculateTotal(Order source, OrderDto dest)
=> dest.Total = dest.Items.Sum(i => i.Price);
Inheritance & Polymorphic Dispatch
// BEFORE
CreateMap<BaseEntity, BaseDto>()
.ForMember(d => d.AuditTrail, o => o.Ignore())
.IncludeAllDerived();
CreateMap<ChildEntity, ChildDto>()
.IncludeBase<BaseEntity, BaseDto>();
// AFTER
[ForgeAllDerived]
[Ignore(nameof(BaseDto.AuditTrail))]
public partial BaseDto Forge(BaseEntity source);
[IncludeBaseForge(typeof(BaseEntity), typeof(BaseDto))]
public partial ChildDto Forge(ChildEntity source);
[IncludeBaseForge] inherits [Ignore], [ForgeProperty], [ForgeFrom], and [ForgeWith] from the base method. Explicit attributes on the derived method override inherited ones.
Map Into Existing Object
// BEFORE
mapper.Map(source, existingDest);
// AFTER
[ForgeMap]
public partial class AppForger
{
public partial void ForgeInto(Source source, [UseExistingValue] Dest destination);
}
forger.ForgeInto(source, existingDest);
For nested in-place updates with EF Core change tracking:
[ForgeProperty("Items", "Items", ExistingTarget = true,
CollectionUpdate = CollectionUpdateStrategy.Sync, KeyProperty = "Id")]
public partial void ForgeInto(OrderUpdateDto source, [UseExistingValue] Order target);
Null Handling
| AutoMapper | ForgeMap |
|---|---|
| Default (assigns null through) | NullPropertyHandling.NullForgiving (default) |
AllowNullCollections = false |
NullPropertyHandling.CoalesceToDefault |
| Null → new T() via AfterMap | NullPropertyHandling.CoalesceToNew (v1.5+) |
| No equivalent | NullPropertyHandling.SkipNull |
| No equivalent | NullPropertyHandling.ThrowException |
Three-tier configuration: per-property > per-forger > assembly default:
[assembly: ForgeMapDefaults(NullPropertyHandling = NullPropertyHandling.CoalesceToDefault)]
[ForgeMap(NullPropertyHandling = NullPropertyHandling.SkipNull)]
public partial class AppForger { ... }
[ForgeProperty("Tags", "Tags", NullPropertyHandling = NullPropertyHandling.ThrowException)]
Collections
| AutoMapper | ForgeMap |
|---|---|
| Auto collection mapping | Auto-generated (default on) |
mapper.Map<List<D>>(list) |
Standalone collection method (v1.5+) |
.ProjectTo<D>(config) |
Not supported (compile-time only) |
// Standalone collection mapping (v1.5+)
public partial UserDto Forge(User source);
public partial IReadOnlyList<UserDto> ForgeAll(IEnumerable<User> source);
String-to-Enum Conversion
// BEFORE
CreateMap<Ticket, TicketDto>()
.ForMember(d => d.Priority, o => o.MapFrom(s => Enum.Parse<Priority>(s.Priority, true)));
// AFTER — auto-converted, no configuration needed (v1.4+)
public partial TicketDto Forge(TicketEntity source);
Configure behavior via StringToEnum on [ForgeMap] or [ForgeMapDefaults].
[ConvertWith] Type Converters
// BEFORE
CreateMap<Request, StorageModel>()
.ConvertUsing<RequestConverter>();
// AFTER — type-based
[ConvertWith(typeof(RequestConverter))]
public partial StorageModel Forge(Request source);
// AFTER — member-based (DI-injected)
[ConvertWith(nameof(_converter))]
public partial StorageModel Forge(Request source);
Features Not in ForgeMap
| AutoMapper Feature | Workaround |
|---|---|
ProjectTo<T>() (IQueryable) |
Map in-memory after materializing |
ConstructUsing() |
Adjust constructors or create manually before ForgeInto |
Conditional mapping (.PreCondition()) |
Use [BeforeForge] or [ForgeFrom] with conditional logic |
| Dynamic/runtime mapping | Not supported — ForgeMap is compile-time only |
Validation
AutoMapper's AssertConfigurationIsValid() is replaced by compile-time diagnostics. All mapping errors are caught during build, not at runtime. See the Diagnostics Reference for the full list.