-
-
Notifications
You must be signed in to change notification settings - Fork 53
19_MappingHooks
github-actions[bot] edited this page Jan 2, 2026
·
1 revision
Facet provides hooks to run custom logic before and after the automatic property mapping. This is useful for validation, setting defaults, computing derived values, or applying transformations.
| Interface | When Called | Use Case |
|---|---|---|
IFacetBeforeMapConfiguration<TSource, TTarget> |
Before properties are copied | Validation, defaults, state preparation |
IFacetAfterMapConfiguration<TSource, TTarget> |
After properties are copied | Computed values, transformations, post-processing |
IFacetMapHooksConfiguration<TSource, TTarget> |
Both before and after | Combined scenarios |
- Validating input before mapping starts
- Setting default/timestamp values on the target
- Preparing state that affects the mapping
- Throwing validation errors before mapping completes
- Computing derived values from mapped properties
- Transforming values after they're copied
- Applying business rules to the result
- Validating the final mapped result
- You only need to add computed properties
- Simple transformations that don't need "before" logic
- Legacy code or simpler scenarios
using Facet.Mapping;
public class UserBeforeMapConfig : IFacetBeforeMapConfiguration<User, UserDto>
{
public static void BeforeMap(User source, UserDto target)
{
// Validate source
if (source == null)
throw new ArgumentNullException(nameof(source));
if (string.IsNullOrEmpty(source.Email))
throw new ValidationException("Email is required");
// Set default values on target
target.MappedAt = DateTime.UtcNow;
target.MappingVersion = "1.0";
}
}
[Facet(typeof(User), BeforeMapConfiguration = typeof(UserBeforeMapConfig))]
public partial class UserDto
{
public DateTime MappedAt { get; set; }
public string MappingVersion { get; set; } = string.Empty;
}using Facet.Mapping;
public class UserAfterMapConfig : IFacetAfterMapConfiguration<User, UserDto>
{
public static void AfterMap(User source, UserDto target)
{
// Compute derived values after properties are mapped
target.FullName = $"{target.FirstName} {target.LastName}";
target.Age = CalculateAge(source.DateOfBirth);
target.IsAdult = target.Age >= 18;
// Apply transformations
target.Email = target.Email?.ToLowerInvariant();
}
private static int CalculateAge(DateTime birthDate)
{
var today = DateTime.Today;
var age = today.Year - birthDate.Year;
if (birthDate.Date > today.AddYears(-age)) age--;
return age;
}
}
[Facet(typeof(User), AfterMapConfiguration = typeof(UserAfterMapConfig))]
public partial class UserDto
{
public string FullName { get; set; } = string.Empty;
public int Age { get; set; }
public bool IsAdult { get; set; }
}Use IFacetMapHooksConfiguration for both before and after logic in one class:
using Facet.Mapping;
public class UserMappingHooks : IFacetMapHooksConfiguration<User, UserDto>
{
public static void BeforeMap(User source, UserDto target)
{
target.MappedAt = DateTime.UtcNow;
}
public static void AfterMap(User source, UserDto target)
{
target.FullName = $"{target.FirstName} {target.LastName}";
}
}
// Reference the same class for both hooks
[Facet(typeof(User),
BeforeMapConfiguration = typeof(UserMappingHooks),
AfterMapConfiguration = typeof(UserMappingHooks))]
public partial class UserDto
{
public DateTime MappedAt { get; set; }
public string FullName { get; set; } = string.Empty;
}For scenarios requiring injected services:
using Facet.Mapping;
public class UserValidationHook : IFacetBeforeMapConfigurationInstance<User, UserDto>
{
private readonly IUserValidator _validator;
public UserValidationHook(IUserValidator validator)
{
_validator = validator;
}
public void BeforeMap(User source, UserDto target)
{
var result = _validator.Validate(source);
if (!result.IsValid)
{
throw new ValidationException(result.Errors);
}
target.MappedAt = DateTime.UtcNow;
}
}using Facet.Mapping;
public class UserEnrichmentHook : IFacetAfterMapConfigurationInstance<User, UserDto>
{
private readonly IProfileService _profileService;
private readonly IAgeCalculator _ageCalculator;
public UserEnrichmentHook(IProfileService profileService, IAgeCalculator ageCalculator)
{
_profileService = profileService;
_ageCalculator = ageCalculator;
}
public void AfterMap(User source, UserDto target)
{
target.FullName = $"{target.FirstName} {target.LastName}";
target.Age = _ageCalculator.Calculate(source.DateOfBirth);
target.ProfileUrl = _profileService.GetProfileUrl(source.Id);
}
}For operations requiring async I/O:
using Facet.Mapping;
public class UserAsyncBeforeHook : IFacetBeforeMapConfigurationAsync<User, UserDto>
{
public static async Task BeforeMapAsync(User source, UserDto target, CancellationToken ct = default)
{
// Async validation
var isValid = await ValidateUserAsync(source, ct);
if (!isValid)
{
throw new ValidationException("User validation failed");
}
target.MappedAt = DateTime.UtcNow;
}
}using Facet.Mapping;
public class UserAsyncAfterHook : IFacetAfterMapConfigurationAsync<User, UserDto>
{
public static async Task AfterMapAsync(User source, UserDto target, CancellationToken ct = default)
{
// Async enrichment
target.ProfilePictureUrl = await GetProfilePictureAsync(source.Id, ct);
target.ReputationScore = await CalculateReputationAsync(source.Email, ct);
}
}using Facet.Mapping;
public class UserCompleteAsyncHook : IFacetMapHooksConfigurationAsyncInstance<User, UserDto>
{
private readonly IUserValidator _validator;
private readonly IProfileService _profileService;
public UserCompleteAsyncHook(IUserValidator validator, IProfileService profileService)
{
_validator = validator;
_profileService = profileService;
}
public async Task BeforeMapAsync(User source, UserDto target, CancellationToken ct = default)
{
await _validator.ValidateAsync(source, ct);
target.MappedAt = DateTime.UtcNow;
}
public async Task AfterMapAsync(User source, UserDto target, CancellationToken ct = default)
{
target.ProfileUrl = await _profileService.GetProfileUrlAsync(source.Id, ct);
}
}When using both Configuration and Hooks, the execution order is:
-
BeforeMap (from
BeforeMapConfiguration) - Property Mapping (automatic)
-
Configuration.Map (from
Configuration) -
AfterMap (from
AfterMapConfiguration)
// Execution timeline:
// 1. BeforeMapConfig.BeforeMap(source, target) <- target is empty
// 2. target.Id = source.Id <- automatic mapping
// 3. target.Name = source.Name <- automatic mapping
// 4. CustomConfig.Map(source, target) <- Configuration (if specified)
// 5. AfterMapConfig.AfterMap(source, target) <- target is fully populatedpublic class OrderValidationHook : IFacetBeforeMapConfiguration<Order, OrderDto>
{
public static void BeforeMap(Order source, OrderDto target)
{
if (source.Items == null || !source.Items.Any())
throw new BusinessRuleException("Order must have at least one item");
if (source.Total <= 0)
throw new BusinessRuleException("Order total must be positive");
}
}public class AuditBeforeHook<TSource, TDto> : IFacetBeforeMapConfiguration<TSource, TDto>
where TDto : IAuditable
{
public static void BeforeMap(TSource source, TDto target)
{
target.MappedAt = DateTime.UtcNow;
target.MappedBy = Thread.CurrentPrincipal?.Identity?.Name ?? "System";
}
}public class UserSanitizationHook : IFacetAfterMapConfiguration<User, UserDto>
{
public static void AfterMap(User source, UserDto target)
{
// Sanitize PII for logging/display
target.Email = MaskEmail(target.Email);
target.Phone = MaskPhone(target.Phone);
}
private static string MaskEmail(string email)
{
if (string.IsNullOrEmpty(email)) return email;
var atIndex = email.IndexOf('@');
if (atIndex < 2) return "***" + email.Substring(atIndex);
return email.Substring(0, 2) + "***" + email.Substring(atIndex);
}
}public class ProductEnrichmentHook : IFacetAfterMapConfigurationAsyncInstance<Product, ProductDto>
{
private readonly IPricingService _pricing;
private readonly IInventoryService _inventory;
public ProductEnrichmentHook(IPricingService pricing, IInventoryService inventory)
{
_pricing = pricing;
_inventory = inventory;
}
public async Task AfterMapAsync(Product source, ProductDto target, CancellationToken ct = default)
{
// Enrich with external data
target.CurrentPrice = await _pricing.GetCurrentPriceAsync(source.Id, ct);
target.StockLevel = await _inventory.GetStockLevelAsync(source.Id, ct);
target.IsAvailable = target.StockLevel > 0;
}
}| Interface | Method Signature |
|---|---|
IFacetBeforeMapConfiguration<TSource, TTarget> |
static void BeforeMap(TSource source, TTarget target) |
IFacetAfterMapConfiguration<TSource, TTarget> |
static void AfterMap(TSource source, TTarget target) |
IFacetMapHooksConfiguration<TSource, TTarget> |
Both methods above |
IFacetBeforeMapConfigurationAsync<TSource, TTarget> |
static Task BeforeMapAsync(TSource source, TTarget target, CancellationToken ct) |
IFacetAfterMapConfigurationAsync<TSource, TTarget> |
static Task AfterMapAsync(TSource source, TTarget target, CancellationToken ct) |
IFacetMapHooksConfigurationAsync<TSource, TTarget> |
Both async methods above |
| Interface | Method Signature |
|---|---|
IFacetBeforeMapConfigurationInstance<TSource, TTarget> |
void BeforeMap(TSource source, TTarget target) |
IFacetAfterMapConfigurationInstance<TSource, TTarget> |
void AfterMap(TSource source, TTarget target) |
IFacetMapHooksConfigurationInstance<TSource, TTarget> |
Both methods above |
IFacetBeforeMapConfigurationAsyncInstance<TSource, TTarget> |
Task BeforeMapAsync(TSource source, TTarget target, CancellationToken ct) |
IFacetAfterMapConfigurationAsyncInstance<TSource, TTarget> |
Task AfterMapAsync(TSource source, TTarget target, CancellationToken ct) |
IFacetMapHooksConfigurationAsyncInstance<TSource, TTarget> |
Both async methods above |
- Keep hooks focused: Each hook should have a single responsibility
- Avoid side effects in BeforeMap: Only validate and set defaults
- Use AfterMap for derived values: Computed properties should be set in AfterMap
- Prefer async for I/O: Use async hooks when calling external services
- Handle nulls: Check for null references, especially in AfterMap
- Consider performance: Hooks run on every mapping operation
- Test hooks independently: Write unit tests for your hook implementations
-
Projection expressions: Hooks are not called when using
Projection(LINQ/EF queries) - Circular references: Be careful not to create infinite loops in hooks
- Thread safety: Static hooks must be thread-safe