-
-
Notifications
You must be signed in to change notification settings - Fork 51
15_MapFromAttribute
The [MapFrom] attribute provides declarative property mapping, allowing you to rename properties without implementing a full custom mapping configuration. This is perfect for simple property renames, API response shaping, and maintaining clean separation between domain and DTO property names.
Use [MapFrom] on properties in your Facet class to specify which source property to map from. Use nameof() for type-safe property references:
public class User
{
public int Id { get; set; }
public string FirstName { get; set; }
public string LastName { get; set; }
public string Email { get; set; }
public int Age { get; set; }
}
[Facet(typeof(User), GenerateToSource = true)]
public partial class UserDto
{
[MapFrom(nameof(User.FirstName), Reversible = true)]
public string Name { get; set; } = string.Empty;
[MapFrom(nameof(User.LastName), Reversible = true)]
public string FamilyName { get; set; } = string.Empty;
}This generates:
-
Constructor: Maps
source.FirstNametoNameandsource.LastNametoFamilyName - Projection: Uses the same mapping for EF Core queries
- ToSource(): Reverses the mapping automatically
When you use [MapFrom]:
- The source property is not auto-generated - You declare the target property with its new name
- Mapping is automatic - Constructor, Projection, and ToSource all use the mapping
-
Other properties remain unchanged - Properties without
[MapFrom]work normally
var user = new User
{
Id = 1,
FirstName = "John",
LastName = "Doe",
Email = "john@example.com",
Age = 30
};
var dto = new UserDto(user);
// dto.Id = 1 (auto-mapped)
// dto.Name = "John" (mapped from FirstName)
// dto.FamilyName = "Doe" (mapped from LastName)
// dto.Email = "john@example.com" (auto-mapped)
// dto.Age = 30 (auto-mapped)
// Reverse mapping
var entity = dto.ToSource();
// entity.FirstName = "John" (mapped from Name)
// entity.LastName = "Doe" (mapped from FamilyName)The source property name or expression to map from. Use nameof() for type-safe references:
// Type-safe property reference (recommended)
[MapFrom(nameof(User.FirstName))]
public string Name { get; set; }
// Expression with multiple properties (string required)
[MapFrom(nameof(User.FirstName) + " + \" \" + " + nameof(User.LastName))]
public string FullName { get; set; }
// Or simply use a string for expressions
[MapFrom("FirstName + \" \" + LastName")]
public string FullName { get; set; }Controls whether the mapping is included in ToSource(). Default is false (opt-in).
// This property WILL be mapped back to the source
[MapFrom(nameof(User.FirstName), Reversible = true)]
public string Name { get; set; } = string.Empty;
// This property will NOT be mapped back (default)
[MapFrom(nameof(User.LastName))]
public string DisplayName { get; set; } = string.Empty;Use Reversible = true when:
- You need the mapping to work both ways (source ↔ DTO)
- The property should be included in
ToSource()output
Keep Reversible = false (default) for:
- One-way mappings (source → DTO only)
- Read-only DTOs that don't need reverse mapping
- Properties that shouldn't modify the source entity
Controls whether the mapping is included in the static Projection expression. Default is true.
// This property will NOT be included in EF Core projections
[MapFrom("ComplexField", IncludeInProjection = false)]
public string Computed { get; set; } = string.Empty;Use IncludeInProjection = false for:
- Mappings that cannot be translated to SQL
- Properties requiring client-side evaluation
- Complex expressions that EF Core doesn't support
Overrides the collection type for a single mapped collection property. Accepts an open generic type such as typeof(List<>). This is useful when the source uses Collection<T> (common in EF Core entities) but you want a List<T> in your DTO, without applying the override globally to all collection properties.
public class UnitEntity
{
public int Id { get; set; }
public Collection<UnitItemEntity> Items { get; set; } = new();
public Collection<UnitItemEntity> ArchivedItems { get; set; } = new();
}
[Facet(typeof(UnitEntity), NestedFacets = [typeof(UnitItemDto)])]
public partial class UnitDto
{
// Only Items is remapped to List<>; ArchivedItems keeps its source type
[MapFrom(nameof(UnitEntity.Items), AsCollection = typeof(List<>))]
public List<UnitItemDto> Items { get; set; } = new();
}For a facet-wide override of all collection properties, use CollectionTargetType on the [Facet] attribute instead. See Collection Type Mapping.
[Facet(typeof(Customer), GenerateToSource = true)]
public partial class CustomerDto
{
[MapFrom(nameof(Customer.CompanyName), Reversible = true)]
public string Company { get; set; } = string.Empty;
[MapFrom(nameof(Customer.ContactName), Reversible = true)]
public string Contact { get; set; } = string.Empty;
}[Facet(typeof(Product))]
public partial class ProductDto
{
// Display-only property, default is not reversible
[MapFrom(nameof(Product.Name))]
public string ProductTitle { get; set; } = string.Empty;
}Use expressions to combine or transform properties:
[Facet(typeof(User))]
public partial class UserDto
{
// Concatenate first and last name
[MapFrom("FirstName + \" \" + LastName")]
public string FullName { get; set; } = string.Empty;
// Mathematical expressions
[MapFrom("Price * Quantity")]
public decimal Total { get; set; }
// Method calls (works in constructor, may not translate to SQL)
[MapFrom("Name.ToUpper()")]
public string UpperName { get; set; } = string.Empty;
}Note: Complex expressions may not translate to SQL in EF Core projections. Use IncludeInProjection = false for expressions that require client-side evaluation.
MapFrom works with nested facets too:
[Facet(typeof(Company), GenerateToSource = true)]
public partial class CompanyDto
{
[MapFrom(nameof(Company.CompanyName), Reversible = true)]
public string Name { get; set; } = string.Empty;
}
[Facet(typeof(Employee),
NestedFacets = [typeof(CompanyDto)],
GenerateToSource = true)]
public partial class EmployeeDto;MapFrom mappings are applied first, then your custom mapper runs:
public class UserMapper : IFacetMapConfiguration<User, UserDto>
{
public static void Map(User source, UserDto target)
{
// This runs AFTER MapFrom mappings are applied
target.FullName = $"{target.Name} {target.FamilyName}";
}
}
[Facet(typeof(User), Configuration = typeof(UserMapper), GenerateToSource = true)]
public partial class UserDto
{
[MapFrom(nameof(User.FirstName), Reversible = true)]
public string Name { get; set; } = string.Empty;
[MapFrom(nameof(User.LastName), Reversible = true)]
public string FamilyName { get; set; } = string.Empty;
public string FullName { get; set; } = string.Empty;
}| Scenario | MapFrom | Custom Config |
|---|---|---|
| Simple property rename | ✅ Best choice | Overkill |
| Multiple renames | ✅ Best choice | Overkill |
| Computed values (e.g., concatenation) | ✅ Supported | Alternative |
| Mathematical expressions | ✅ Supported | Alternative |
| Async operations | ❌ | ✅ Required |
| Complex transformations | ❌ | ✅ Required |
| Type conversions | ❌ | ✅ Required |
| Conditional logic | ❌ | ✅ Required |
-
Use
nameof()for type safety - Prevents typos and enables refactoring support - Set Reversible = false for computed properties - Expressions can't be reversed
-
Consider projection compatibility - Set
IncludeInProjection = falsefor expressions that can't translate to SQL - Combine with custom mappers when needed - MapFrom handles the basics, custom mapper handles the rest
You can use MapFrom to flatten nested object structures by specifying property paths:
public class Order
{
public int Id { get; set; }
public Customer Customer { get; set; }
}
public class Customer
{
public string Name { get; set; }
public Address Address { get; set; }
}
public class Address
{
public string City { get; set; }
public string Country { get; set; }
}
[Facet(typeof(Order), exclude: [nameof(Order.Customer)])]
public partial class OrderDto
{
// Single-level nested path
[MapFrom("Customer.Name")]
public string CustomerName { get; set; }
// Multi-level nested path
[MapFrom("Customer.Address.City")]
public string City { get; set; }
[MapFrom("Customer.Address.Country")]
public string Country { get; set; }
}Important: Nested property paths do not include null-checking. If any intermediate property is null, a NullReferenceException will be thrown. Ensure intermediate properties are non-nullable or handle null cases in your application logic.
- Same type required - Source and target property types must match
- Expressions are not reversible - Computed expressions are one-way (source → DTO only)
-
No null-checking in nested paths -
"Company.Address.City"will throw if Company or Address is null
For complex scenarios like async operations or conditional logic, use Custom Mapping instead.