Skip to content

Getting Started with Pragmatic.Mapping

This guide walks you through creating your first entity-to-DTO mapping with Pragmatic.Mapping.

  • .NET 10.0+
  • Pragmatic.Mapping package
  • Pragmatic.SourceGenerator referenced as an analyzer

Entities are your domain objects. They can be classes, records, or structs.

public class Guest
{
public Guid Id { get; set; }
public string FirstName { get; set; } = "";
public string LastName { get; set; } = "";
public string Email { get; set; } = "";
public string? Phone { get; set; }
public string? Nationality { get; set; }
public string PreferredLanguage { get; set; } = "";
}

Decorate a partial type with [MapFrom<T>]. The type must be partial (PRAG0300 error otherwise).

using Pragmatic.Mapping.Attributes;
[MapFrom<Guest>]
[GenerateProjection]
public partial class GuestDto
{
public Guid Id { get; init; }
public string FirstName { get; init; } = "";
public string LastName { get; init; } = "";
public string Email { get; init; } = "";
public string? Phone { get; init; }
public string? Nationality { get; init; }
public string PreferredLanguage { get; init; } = "";
// Computed property -- excluded from mapping
[MapIgnore]
public string FullName => $"{FirstName} {LastName}";
}

Properties are matched by name (case-insensitive). Properties that exist on both the entity and DTO are mapped automatically.

After building, the source generator creates several ways to map:

// Option 1: Static factory method
var dto = GuestDto.FromEntity(guest);
// Option 2: Extension method on a single entity
var dto = guest.ToGuestDto();
// Option 3: Extension method on a collection
IEnumerable<GuestDto> dtos = guests.ToGuestDto();
List<GuestDto> dtoList = guestList.ToGuestDto();
GuestDto[] dtoArray = guestArray.ToGuestDto();
// Option 4: In-memory selector for LINQ
var dtos = guests.Select(GuestDto.Selector).ToList();
// Option 5: EF Core projection (requires [GenerateProjection])
var dtos = await db.Guests
.Where(g => g.Email != null)
.Select(GuestDto.Projection)
.ToListAsync();

For a [MapFrom<Guest>] DTO, the generator produces two files:

1. Main partial class (GuestDto.Mapping.g.cs)

Section titled “1. Main partial class (GuestDto.Mapping.g.cs)”
public partial class GuestDto
{
public static GuestDto FromEntity(Guest entity) { ... }
public static Func<Guest, GuestDto> Selector { get; } = FromEntity;
public static Expression<Func<Guest, GuestDto>> Projection { get; } = ...;
static partial void BeforeMapping(Guest source, ref GuestDto? result);
static partial void CustomizeMapping(Guest source, ref GuestDtoMappingContext ctx);
public struct GuestDtoMappingContext { ... }
}

2. Extension methods (GuestDto.Extensions.g.cs)

Section titled “2. Extension methods (GuestDto.Extensions.g.cs)”
public static class GuestDtoMappingExtensions
{
public static GuestDto ToGuestDto(this Guest entity) => GuestDto.FromEntity(entity);
public static IEnumerable<GuestDto> ToGuestDto(this IEnumerable<Guest> entities) => ...;
public static List<GuestDto> ToGuestDto(this List<Guest> entities) => ...;
public static GuestDto[] ToGuestDto(this Guest[] entities) => ...;
}

When a DTO property name follows the pattern {NavigationName}{PropertyName}, the generator flattens automatically:

// Entity
public class RoomType
{
public Property Property { get; set; } // Navigation
}
// DTO -- AddressCity auto-resolved? No, use [MapProperty] for navigation paths
[MapFrom<RoomType>]
public partial class RoomTypeSummaryDto
{
[MapProperty("Property.Name")]
public string PropertyName { get; init; } = "";
}

Use [MapIgnore] for computed properties or properties populated by other means:

[MapFrom<Property>]
public partial class PropertyDetailDto
{
public string Name { get; init; } = "";
public string City { get; init; } = "";
public int StarRating { get; init; }
[MapIgnore]
public string DisplayLabel => $"{Name} ({City}, {StarRating})";
[MapIgnore]
public IReadOnlyDictionary<string, string>? Descriptions { get; init; }
}

Use [MapProperty(Format = "...")] to format values via .ToString(format):

[MapFrom<Property>]
public partial class PropertyDetailDto
{
[MapProperty("CreatedAt", Format = "yyyy-MM-dd")]
public string CreatedDate { get; init; } = "";
}

Note: Format strings are not supported in projections (PRAG0321 warning) because .ToString(format) cannot be translated to SQL.

A single DTO can have both [MapFrom<T>] and [MapTo<T>]:

[MapFrom<User>]
[MapTo<User>]
public partial class UserDto
{
public Guid Id { get; init; }
public string Name { get; init; } = "";
}
// Read path
var dto = UserDto.FromEntity(user);
// Write path
var entity = dto.ToEntity();
dto.ApplyTo(existingEntity);

Use [MapTo<T>] for create operations:

[MapTo<Guest>]
public partial record CreateGuestDto
{
public string FirstName { get; init; } = "";
public string LastName { get; init; } = "";
public string Email { get; init; } = "";
}
// Creates a new Guest with mapped properties
Guest guest = dto.ToEntity();

ID properties are excluded from ToEntity() by default. Add [MapProperty] (with no arguments) to the ID property to force inclusion.

The generated extension class matches the DTO’s accessibility:

DTO AccessibilityExtensions Class
publicpublic static
internalinternal static
DTO Type[MapFrom][MapTo][GenerateProjection]
classYesYesYes
recordYesYesYes
structYesPlannedPlanned
record structYesPlannedPlanned