Skip to content

Common Mistakes

These are the most common issues developers encounter when using Pragmatic.Mapping. Each section shows the wrong approach, the correct approach, and explains why.


Wrong:

[MapFrom<Guest>]
public class GuestDto
{
public Guid Id { get; init; }
public string FirstName { get; init; } = "";
public string LastName { get; init; } = "";
}

Compile result: PRAG0300 error — “Type ‘GuestDto’ must be declared as partial to use [MapFrom]”.

Right:

[MapFrom<Guest>]
public partial class GuestDto
{
public Guid Id { get; init; }
public string FirstName { get; init; } = "";
public string LastName { get; init; } = "";
}

Why: The source generator emits FromEntity(), Selector, hooks, and the MappingContext struct into a partial class. Without partial, the compiler cannot merge the generated code with your class. This applies to class, record, struct, and record struct.


2. Property Name Mismatch Without Explicit Mapping

Section titled “2. Property Name Mismatch Without Explicit Mapping”

Wrong:

public class Guest
{
public string FirstName { get; set; } = "";
}
[MapFrom<Guest>]
public partial class GuestDto
{
public string Name { get; init; } = ""; // Does not match FirstName!
}

Compile result: PRAG0303 warning — “Property ‘Name’ has no matching source property on ‘Guest’”. The DTO compiles, but Name receives default("") at runtime.

Right:

[MapFrom<Guest>]
public partial class GuestDto
{
// Option 1: Match the name exactly
public string FirstName { get; init; } = "";
// Option 2: Use [MapProperty] for explicit mapping
[MapProperty(nameof(Guest.FirstName))]
public string Name { get; init; } = "";
}

Why: Property matching is case-insensitive, but the names must match. Name does not match FirstName. The generator emits PRAG0303 as a warning (not error) so you can build and iterate, but the property silently gets default. Always check build warnings.


3. Using Both MapFrom and MapTo When You Only Need One

Section titled “3. Using Both MapFrom and MapTo When You Only Need One”

Wrong:

// Using [MapTo] for a read-only DTO
[MapFrom<Guest>]
[MapTo<Guest>]
public partial class GuestSummaryDto
{
public Guid Id { get; init; }
public string FirstName { get; init; } = "";
public string Email { get; init; } = "";
}

Result: Works, but generates unnecessary ToEntity() that you never call. The generated code includes ID exclusion logic (PRAG0324 info) and potentially triggers PRAG0307 warnings for required properties on the entity that are not present on the DTO.

Right:

// Read-only DTO: only [MapFrom]
[MapFrom<Guest>]
public partial class GuestSummaryDto
{
public Guid Id { get; init; }
public string FirstName { get; init; } = "";
public string Email { get; init; } = "";
}
// Write DTO: only [MapTo]
[MapTo<Guest>]
public partial record CreateGuestDto
{
public string FirstName { get; init; } = "";
public string LastName { get; init; } = "";
public string Email { get; init; } = "";
}

Why: Bidirectional mapping ([MapFrom] + [MapTo]) is valid but should be intentional. Use it when the same DTO genuinely serves both read and write paths. For most applications, separate read DTOs and write DTOs are cleaner and avoid unnecessary generated code and diagnostics.


4. Missing Custom Converter for Complex Types

Section titled “4. Missing Custom Converter for Complex Types”

Wrong:

public class Invoice
{
public Money TotalAmount { get; set; } // Custom domain type
}
[MapFrom<Invoice>]
public partial class InvoiceDto
{
public string TotalAmount { get; init; } = ""; // Money -> string ???
}

Compile result: PRAG0304 error — “Cannot map from ‘Money’ to ‘string’: incompatible types”. The generator does not know how to convert Money to string.

Right:

// 1. Implement a converter
public sealed class MoneyToStringConverter : IValueConverter<Money, string>
{
public string Convert(Money source) => source.ToString("N2");
public Money ConvertBack(string target) => Money.Parse(target);
}
// 2. Apply it
[MapFrom<Invoice>]
public partial class InvoiceDto
{
[MapProperty(nameof(Invoice.TotalAmount))]
[MapConverter<MoneyToStringConverter>]
public string TotalAmount { get; init; } = "";
}

Why: The generator handles built-in type conversions automatically (int to string, DateTime to DateOnly, etc.), but it cannot guess how to convert custom domain types. Implement IValueConverter<TSource, TTarget> with a parameterless constructor, and apply [MapConverter<T>] to the property.


5. Expecting Converters to Work in Projections

Section titled “5. Expecting Converters to Work in Projections”

Wrong:

[MapFrom<Invoice>]
[GenerateProjection]
public partial class InvoiceDto
{
[MapProperty(nameof(Invoice.TotalAmount))]
[MapConverter<MoneyToStringConverter>]
public string TotalFormatted { get; init; } = ""; // Excluded from Projection!
}
// Usage:
var dtos = await db.Invoices
.Select(InvoiceDto.Projection)
.ToListAsync();
// TotalFormatted is always "" (default) in the projection result!

Compile result: PRAG0320 warning — “[MapConverter] on property ‘TotalFormatted’ is not supported in Projection, property excluded”.

Right:

[MapFrom<Invoice>]
[GenerateProjection]
public partial class InvoiceDto
{
// Keep the raw value for projection (SQL-translatable)
public decimal TotalAmount { get; init; }
// Converter-based property for in-memory mapping
[MapProperty(nameof(Invoice.TotalAmount))]
[MapConverter<MoneyToStringConverter>]
[MapIgnore] // Not needed? Use a separate DTO
public string TotalFormatted { get; init; } = "";
}
// Or better: use separate DTOs for query vs. detail
[MapFrom<Invoice>]
[GenerateProjection]
public partial class InvoiceListDto // For queries: projection-safe
{
public Guid Id { get; init; }
public decimal TotalAmount { get; init; }
}
[MapFrom<Invoice>]
public partial class InvoiceDetailDto // For single item: full features
{
public Guid Id { get; init; }
[MapProperty(nameof(Invoice.TotalAmount))]
[MapConverter<MoneyToStringConverter>]
public string TotalFormatted { get; init; } = "";
}

Why: Projections generate Expression<Func<TEntity, TDto>> that EF Core translates to SQL. Arbitrary C# method calls (like converter methods) cannot be translated to SQL. The generator excludes converter properties from projections and they receive default. Design your DTOs accordingly — use projection-safe DTOs for list queries and richer DTOs for single-entity display.


Wrong:

[MapFrom<Order>]
public partial class OrderDto
{
public Guid Id { get; init; }
public AddressDto ShippingAddress { get; init; } // AddressDto has no [MapFrom]!
}
public class AddressDto // Missing [MapFrom<Address>] and partial
{
public string Street { get; init; } = "";
public string City { get; init; } = "";
}

Compile result: PRAG0309 error — “Nested type ‘AddressDto’ must have [MapFrom

] for mapping”.

Right:

[MapFrom<Order>]
public partial class OrderDto
{
public Guid Id { get; init; }
public AddressDto ShippingAddress { get; init; }
}
[MapFrom<Address>]
public partial class AddressDto
{
public string Street { get; init; } = "";
public string City { get; init; } = "";
}

Why: When the generator encounters a DTO-typed property, it needs to call FromEntity() on that nested DTO. The nested type must have [MapFrom<T>] so the generator knows the source type and can produce the recursive mapping. If you do not want the property mapped, use [MapIgnore].


7. Non-Translatable Expressions in Projection DTOs

Section titled “7. Non-Translatable Expressions in Projection DTOs”

Wrong:

[MapFrom<Guest>]
[GenerateProjection]
public partial class GuestDto
{
public string FirstName { get; init; } = "";
[MapProperty(nameof(Guest.CreatedAt), Format = "yyyy-MM-dd")]
public string CreatedDate { get; init; } = ""; // Format not SQL-translatable!
}

Compile result: PRAG0321 info — “Format string on property ‘CreatedDate’ not translatable to SQL, property excluded from Projection”.

Right:

[MapFrom<Guest>]
[GenerateProjection]
public partial class GuestDto
{
public string FirstName { get; init; } = "";
// Option 1: Use the raw type (SQL-translatable)
public DateTime CreatedAt { get; init; }
// Format in the UI layer: createdAt.ToString("yyyy-MM-dd")
// Option 2: Use DateOnly (SQL-translatable conversion)
public DateOnly CreatedDate { get; init; }
// The generator uses DateOnly.FromDateTime() which EF Core can translate
}

Why: .ToString(format) is a C# runtime call that EF Core cannot translate to SQL. In projections, format-string properties are excluded and receive default. Keep projection DTOs simple — use raw types and format in the UI or API layer. The same applies to CustomizeMapping() (PRAG0319) and [MapConverter<T>] (PRAG0320).


8. Wrong Namespace or Type Name for Source Type

Section titled “8. Wrong Namespace or Type Name for Source Type”

Wrong:

using Pragmatic.Mapping.Attributes;
[MapFrom<Guest>] // Which Guest? Using directive might resolve to wrong type
public partial class GuestDto
{
public Guid Id { get; init; }
public string Name { get; init; } = "";
}

Possible result: If multiple types named Guest exist in different namespaces, the generator maps from whichever the compiler resolves. If the resolved type does not have the expected properties, you get PRAG0303 warnings for every property.

Right:

using Pragmatic.Mapping.Attributes;
using Showcase.Booking.Entities; // Be explicit about the namespace
[MapFrom<Guest>]
public partial class GuestDto
{
public Guid Id { get; init; }
public string Name { get; init; } = "";
}
// Or use fully qualified name:
[MapFrom<Showcase.Booking.Entities.Guest>]
public partial class GuestDto { /* ... */ }

Why: The generic type parameter in [MapFrom<T>] is resolved by the C# compiler using standard name resolution rules. If you have multiple types with the same name (common in large solutions), the wrong one may be selected. Use explicit using directives or fully qualified names to avoid ambiguity.


9. Conflicting [MapIgnore] and [MapProperty] on the Same Property

Section titled “9. Conflicting [MapIgnore] and [MapProperty] on the Same Property”

Wrong:

[MapFrom<Guest>]
public partial class GuestDto
{
[MapIgnore]
[MapProperty(nameof(Guest.Email))]
public string Email { get; init; } = "";
}

Compile result: PRAG0314 error — “Property ‘Email’ has conflicting attributes [MapIgnore] and [MapProperty]”.

Right:

[MapFrom<Guest>]
public partial class GuestDto
{
// Either ignore it:
[MapIgnore]
public string Email { get; init; } = "";
// Or map it explicitly:
[MapProperty(nameof(Guest.Email))]
public string Email { get; init; } = "";
}

Why: [MapIgnore] says “do not map this property.” [MapProperty] says “map this property from a specific source.” These are contradictory instructions. The generator reports an error rather than guessing which one you intended. Remove one of the two attributes.


10. Manual Mapping When the Source Generator Handles It

Section titled “10. Manual Mapping When the Source Generator Handles It”

Wrong:

public static class GuestMapper
{
public static GuestDto ToDto(Guest entity)
{
return new GuestDto
{
Id = entity.Id,
FirstName = entity.FirstName,
LastName = entity.LastName,
Email = entity.Email,
Phone = entity.Phone ?? "",
};
}
}
// Usage:
var dto = GuestMapper.ToDto(guest);

Result: Works, but you are maintaining mapping code that the generator produces automatically. When you add a property to Guest and GuestDto, you must also update GuestMapper — a third place to change.

Right:

[MapFrom<Guest>]
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; }
}
// Usage:
var dto = GuestDto.FromEntity(guest);
var dto = guest.ToGuestDto();

Why: The generator keeps the mapping in sync with both the entity and DTO. Add a property to both types and the mapping updates automatically at the next build. The generated code handles null checks, nullable-to-non-nullable defaults, navigation flattening, and collection mapping. Manual mapping should only be used for scenarios the generator cannot handle.


11. Forgetting to Add [GenerateProjection] for EF Core Queries

Section titled “11. Forgetting to Add [GenerateProjection] for EF Core Queries”

Wrong:

[MapFrom<Property>]
public partial class PropertySummaryDto
{
public Guid Id { get; init; }
public string Name { get; init; } = "";
}
// Using Selector instead of Projection:
var dtos = await db.Properties
.Select(PropertySummaryDto.Selector) // This is a Func, not an Expression!
.ToListAsync();

Runtime result: EF Core cannot translate a Func<T, R> to SQL. It loads all properties from the database into memory and then applies the mapping in-memory. For large tables, this is a performance disaster.

Right:

[MapFrom<Property>]
[GenerateProjection] // Generates Expression<Func<Property, PropertySummaryDto>>
public partial class PropertySummaryDto
{
public Guid Id { get; init; }
public string Name { get; init; } = "";
}
// Now using Projection:
var dtos = await db.Properties
.Select(PropertySummaryDto.Projection) // Expression -- translates to SQL
.ToListAsync();

Why: Selector is a compiled Func<TSource, TDto> delegate for in-memory LINQ. Projection is an Expression<Func<TSource, TDto>> that EF Core can inspect and translate to SQL. Without [GenerateProjection], the Projection property is not generated. If you use Selector in an EF Core query, EF Core evaluates it client-side, defeating the purpose of projection.


12. Using [MapProperty(Default)] When Auto-Default Handles It

Section titled “12. Using [MapProperty(Default)] When Auto-Default Handles It”

Wrong:

[MapFrom<Guest>]
public partial class GuestDto
{
[MapProperty(nameof(Guest.Phone), Default = "")]
public string Phone { get; init; } = "";
[MapProperty(nameof(Guest.Age), Default = "0")]
public int Age { get; init; }
[MapProperty(nameof(Guest.Score), Default = "0")]
public decimal Score { get; init; }
}

Result: Works, but the explicit Default is unnecessary. The generator already handles nullable-to-non-nullable conversions with sensible defaults: string? to string uses ?? "", int? to int uses .GetValueOrDefault(), etc.

Right:

[MapFrom<Guest>]
public partial class GuestDto
{
// Auto-default handles these automatically:
public string Phone { get; init; } = ""; // string? -> string: ?? ""
public int Age { get; init; } // int? -> int: .GetValueOrDefault()
public decimal Score { get; init; } // decimal? -> decimal: .GetValueOrDefault()
// Use Default only for non-standard fallbacks:
[MapProperty(nameof(Guest.Nationality), Default = "Unknown")]
public string Nationality { get; init; } = ""; // "Unknown" instead of ""
}

Why: The generator’s auto-default system handles the common nullable-to-non-nullable patterns. Reserve [MapProperty(Default = "...")] for cases where you need a specific fallback value different from the type’s natural default.


MistakeDiagnostic / Symptom
Missing partialPRAG0300 compile error
Property name mismatchPRAG0303 warning, property gets default
Unnecessary bidirectional mappingExtra generated code, PRAG0307/PRAG0324 noise
Missing converter for complex typePRAG0304 compile error
Converter in projection DTOPRAG0320 warning, property gets default
Nested DTO without [MapFrom]PRAG0309 compile error
Format string in projectionPRAG0321 info, property excluded
Wrong source type resolvedPRAG0303 warnings on all properties
Conflicting [MapIgnore] + [MapProperty]PRAG0314 compile error
Manual mapping instead of SGWorks but triple maintenance burden
Missing [GenerateProjection]No Projection property, client-side evaluation
Redundant [MapProperty(Default)]Harmless but unnecessary code