# Expression Mapping with Facet.Mapping.Expressions Transform business logic expressions between entities and DTOs with `Facet.Mapping.Expressions`. This library enables you to define business rules, filters, and selectors once for your entities and seamlessly use them with your Facet DTOs. ## Overview Expression mapping solves the common problem of duplicating business logic when working with both entities and their corresponding DTOs. Instead of rewriting predicates and selectors for each type, you can transform existing expressions to work with different but compatible types. ## Installation ```bash dotnet add package Facet.Mapping.Expressions ``` ## Core Concepts ### Predicate Mapping Transform filter expressions from entity types to DTO types: ```csharp using Facet.Mapping.Expressions; // Business rule defined for entities Expression> activeUsers = u => u.IsActive && !u.IsDeleted; // Transform to work with DTOs Expression> activeDtoUsers = activeUsers.MapToFacet(); // Use with collections var filteredDtos = dtoCollection.Where(activeDtoUsers.Compile()).ToList(); ``` ### Selector Mapping Transform sorting and selection expressions: ```csharp // Original selector for entity sorting Expression> sortByLastName = u => u.LastName; // Transform to work with DTO Expression> dtoSortByLastName = sortByLastName.MapToFacet(); // Use for sorting DTOs var sortedDtos = dtoCollection.OrderBy(dtoSortByLastName.Compile()).ToList(); ``` ### Generic Expression Transformation Handle complex expressions with anonymous objects and method calls: ```csharp // Complex projection expression Expression> complexProjection = u => new { FullName = u.FirstName + " " + u.LastName, IsEligible = u.Age > 21 && u.Email.Contains("@company.com"), Status = u.IsActive ? "Active" : "Inactive" }; // Transform to DTO context var dtoProjection = complexProjection.MapToFacetGeneric(); ``` ## Expression Composition ### Combining Predicates Combine multiple conditions with logical operators: ```csharp var isAdult = (Expression>)(u => u.Age >= 18); var isActive = (Expression>)(u => u.IsActive); var hasValidEmail = (Expression>)(u => !string.IsNullOrEmpty(u.Email)); // Combine with AND var validUserFilter = FacetExpressionExtensions.CombineWithAnd(isAdult, isActive, hasValidEmail); // Combine with OR var flexibleFilter = FacetExpressionExtensions.CombineWithOr(isAdult, hasValidEmail); // Transform combined expressions var dtoFilter = validUserFilter.MapToFacet(); ``` ### Negating Conditions Create opposite conditions easily: ```csharp var activeUsers = (Expression>)(u => u.IsActive); var inactiveUsers = activeUsers.Negate(); // Transform negated expression var inactiveDtoUsers = inactiveUsers.MapToFacet(); ``` ## Real-World Examples ### Repository Pattern with Shared Business Logic ```csharp public class UserService { // Define business rules once private readonly Expression> _activeUsersFilter = u => u.IsActive && !u.IsDeleted && u.EmailVerified; private readonly Expression> _displayNameSelector = u => u.FirstName + " " + u.LastName; public IQueryable GetActiveUsers(IQueryable query) { return query.Where(_activeUsersFilter); } public IEnumerable FilterActiveDtos(IEnumerable dtos) { var dtoFilter = _activeUsersFilter.MapToFacet(); return dtos.Where(dtoFilter.Compile()); } public IEnumerable SortDtosByName(IEnumerable dtos) { var dtoSelector = _displayNameSelector.MapToFacet(); return dtos.OrderBy(dtoSelector.Compile()); } } ``` ### Dynamic Query Building Build complex filters dynamically and apply to both entities and DTOs: ```csharp public static class UserFilters { public static Expression> ByAgeRange(int minAge, int maxAge) => u => u.Age >= minAge && u.Age <= maxAge; public static Expression> ByStatus(bool isActive) => u => u.IsActive == isActive; public static Expression> ByEmailDomain(string domain) => u => u.Email.EndsWith("@" + domain); } public class UserQueryBuilder { public (Expression>, Expression>) BuildFilters( int? minAge = null, int? maxAge = null, bool? activeOnly = null, string emailDomain = null) { var filters = new List>>(); if (minAge.HasValue && maxAge.HasValue) filters.Add(UserFilters.ByAgeRange(minAge.Value, maxAge.Value)); if (activeOnly.HasValue) filters.Add(UserFilters.ByStatus(activeOnly.Value)); if (!string.IsNullOrEmpty(emailDomain)) filters.Add(UserFilters.ByEmailDomain(emailDomain)); var entityFilter = FacetExpressionExtensions.CombineWithAnd(filters.ToArray()); var dtoFilter = entityFilter.MapToFacet(); return (entityFilter, dtoFilter); } } ``` ### API Controller with Consistent Filtering ```csharp [ApiController] [Route("api/[controller]")] public class UsersController : ControllerBase { private readonly UserService _userService; // Shared business logic private readonly Expression> _publicUsersFilter = u => u.IsActive && u.IsPublic && !u.IsDeleted; [HttpGet("entities")] public async Task>> GetEntities() { var users = await _userService.GetUsers() .Where(_publicUsersFilter) .ToListAsync(); return Ok(users); } [HttpGet("dtos")] public async Task>> GetDtos() { var users = await _userService.GetUsers().ToListAsync(); var dtos = users.Select(u => u.ToFacet()).ToList(); var dtoFilter = _publicUsersFilter.MapToFacet(); var filteredDtos = dtos.Where(dtoFilter.Compile()).ToList(); return Ok(filteredDtos); } } ``` ## Advanced Scenarios ### Nested Property Access Handle complex object hierarchies: ```csharp // Works with nested properties Expression> recentOrders = o => o.Customer.IsActive && o.OrderDate > DateTime.Now.AddMonths(-1) && o.Items.Any(i => i.Price > 100); // Transform to DTO (assuming OrderDto has matching nested structure) var dtoFilter = recentOrders.MapToFacet(); ``` ### Method Call Transformations Support common string and numeric operations: ```csharp // Method calls are preserved in transformation Expression> emailDomainFilter = u => u.Email.ToLower().EndsWith("@company.com") && u.FirstName.StartsWith("John") && u.Age.ToString().Length == 2; var dtoFilter = emailDomainFilter.MapToFacet(); ``` ## Performance Considerations - **Caching**: Property mappings are cached per type pair for optimal performance - **Reflection Optimization**: Reflection results are cached and reused - **Lazy Compilation**: Expression compilation happens only when needed - **Thread Safety**: All caching mechanisms are thread-safe ## Best Practices ### 1. Define Business Rules Centrally ```csharp public static class BusinessRules { public static Expression> ActiveUser => u => u.IsActive && !u.IsDeleted; public static Expression> EligibleForPromotion => u => u.IsActive && u.LastLoginDate > DateTime.Now.AddMonths(-1); } ``` ### 2. Use Composition for Complex Logic ```csharp var eligibleActiveUser = FacetExpressionExtensions.CombineWithAnd( BusinessRules.ActiveUser, BusinessRules.EligibleForPromotion ); ``` ### 3. Cache Transformed Expressions ```csharp public class UserFilterCache { private static readonly Lazy>> _activeDtoFilter = new(() => BusinessRules.ActiveUser.MapToFacet()); public static Expression> ActiveDtoFilter => _activeDtoFilter.Value; } ``` ## Supported Expression Types The expression mapping library supports: - **Binary expressions**: Comparisons (`==`, `!=`, `>`, `<`, `>=`, `<=`), logical operations (`&&`, `||`) - **Unary expressions**: Negation (`!`), conversions, increment/decrement - **Member access**: Property and field access (`u.Name`, `u.Profile.Email`) - **Method calls**: Instance methods (`u.Name.StartsWith("A")`), static methods - **Constants and literals**: Preserved as-is during transformation - **Lambda expressions**: Parameter replacement and body transformation - **New expressions**: Object creation and anonymous types - **Conditional expressions**: Ternary operators (`condition ? true : false`) ## Troubleshooting ### Property Not Found Errors Ensure that properties exist in both source and target types with compatible types: ```csharp // This will fail if UserDto doesn't have an Age property Expression> ageFilter = u => u.Age > 18; var dtoFilter = ageFilter.MapToFacet(); // May throw if Age doesn't exist ``` ### Type Compatibility Issues Property types must be compatible between source and target: ```csharp // Works: both User.Id and UserDto.Id are int Expression> idFilter = u => u.Id > 0; // May fail: if types don't match between User.CreatedDate and UserDto.CreatedDate Expression> dateFilter = u => u.CreatedDate > DateTime.Now; ``` ## Integration with Other Facet Libraries Expression mapping works seamlessly with: - **Facet**: All Facet-generated types are supported - **Facet.Extensions**: Use with LINQ extension methods - **Facet.Mapping**: Combine with custom mapping configurations - **Facet.Extensions.EFCore**: Transform expressions before EF Core queries ```csharp // Combined usage example var entityFilter = BusinessRules.ActiveUser; var users = await dbContext.Users .Where(entityFilter) .ToFacetsAsync(); var dtoFilter = entityFilter.MapToFacet(); var additionalFiltering = users.Where(dtoFilter.Compile()).ToList(); ```