CrossTypeExpressionConverter 0.2.2
dotnet add package CrossTypeExpressionConverter --version 0.2.2
NuGet\Install-Package CrossTypeExpressionConverter -Version 0.2.2
<PackageReference Include="CrossTypeExpressionConverter" Version="0.2.2" />
<PackageVersion Include="CrossTypeExpressionConverter" Version="0.2.2" />
<PackageReference Include="CrossTypeExpressionConverter" />
paket add CrossTypeExpressionConverter --version 0.2.2
#r "nuget: CrossTypeExpressionConverter, 0.2.2"
#addin nuget:?package=CrossTypeExpressionConverter&version=0.2.2
#tool nuget:?package=CrossTypeExpressionConverter&version=0.2.2
CrossTypeExpressionConverter
CrossTypeExpressionConverter is a .NET library designed to seamlessly translate LINQ predicate expressions (Expression<Func<TSource, bool>>
) from a source type (TSource
) to an equivalent expression for a destination type (TDestination
). This is particularly powerful when working with different layers in your application, such as mapping query logic between domain entities and Data Transfer Objects (DTOs), while ensuring full compatibility with IQueryable
providers like Entity Framework Core for efficient server-side query execution.
Stop rewriting similar filter logic for different types! CrossTypeExpressionConverter
allows you to define your logic once and reuse it across various type representations.
🌟 Key Features
- Type-Safe Conversion: Translates strongly-typed LINQ expressions, reducing the risk of runtime errors.
IQueryable
Compatible: Generated expressions are fully translatable by LINQ providers (e.g., Entity Framework Core), ensuring filters are applied at the database level for optimal performance.- Flexible Member Mapping:
- Automatic Name Matching: By default, matches properties with the same name.
- Mapping Utility: Includes
MappingUtils.BuildMemberMap
to conveniently generate the member map dictionary from a type-safe LINQ projection expression. - Explicit Dictionary Mapping: Provide an
IDictionary<string, string>
to map properties with different names. - Custom Delegate Mapping: Supply a
Func<MemberExpression, ParameterExpression, Expression?>
for complex, custom translation logic for specific members.
- Nested Property Support: Correctly handles expressions involving nested properties (e.g.,
customer => customer.Address.Street == "Main St"
) by translating member access chains. - Captured Variable Support: Correctly processes predicates that compare against properties of captured (closed-over) variables (e.g.,
s => s.Id == localOrder.Id
). - Reduced Boilerplate: Eliminates the need to manually reconstruct expression trees or write repetitive mapping logic.
💾 Installation
You can install CrossTypeExpressionConverter
via NuGet Package Manager:
Install-Package CrossTypeExpressionConverter -Version 0.2.1
Or using the .NET CLI:
dotnet add package CrossTypeExpressionConverter --version 0.2.1
🚀 Quick Start: Basic Usage
Let's say you have a User
domain model and a UserEntity
for your database context, and some property names differ.
1. Define Your Types:
// Domain Model
public class User
{
public int Id { get; set; }
public string? Name { get; set; }
public bool IsActive { get; set; }
public DateTime BirthDate { get; set; }
}
// Database Entity
public class UserEntity
{
public int UserId { get; set; } // Different name for ID
public string? UserName { get; set; } // Different name for Name
public bool Enabled { get; set; } // Different name for IsActive
public DateTime DateOfBirth { get; set; } // Different name for BirthDate
}
2. Define a Reusable Domain Filter:
using System.Linq.Expressions;
using CrossTypeExpressionConverter; // Your package's namespace
// Filter for active adult users
Expression<Func<User, bool>> isActiveAdultUserDomainFilter =
user => user.IsActive && user.BirthDate <= DateTime.Today.AddYears(-18);
3. Configure Mapping (if names differ) using MappingUtils:
var userToEntityMap = MappingUtils.BuildMemberMap<User, UserEntity>(user =>
new UserEntity
{
UserId = user.Id,
UserName = user.Name,
Enabled = user.IsActive,
DateOfBirth = user.BirthDate
// Properties with the same name are automatically handled by BuildMemberMap if assigned
// (though ExpressionConverter itself would match them by name if not in a map).
});
4. Convert the Expression:
// Convert using the memberMap generated by MappingUtils
Expression<Func<UserEntity, bool>> entityPredicate =
ExpressionConverter.Convert<User, UserEntity>(isActiveAdultUserDomainFilter, userToEntityMap);
5. Use it in Your IQueryable
Query (e.g., EF Core):
// Assuming 'dbContext' is your EF Core DbContext instance
// IQueryable<UserEntity> usersQuery = dbContext.Set<UserEntity>();
// var adultEntities = usersQuery.Where(entityPredicate).ToList();
// foreach (var entity in adultEntities)
// {
// Console.WriteLine($"Found adult user: {entity.UserName}");
// }
🛠️ Advanced Usage: memberMap
and customMap
The ExpressionConverter.Convert
method takes two optional parameters for controlling the mapping:
IDictionary<string, string>? memberMap = null
:- This dictionary allows you to explicitly define mappings between source member names (keys) and destination member names (values) for direct members of
TSource
. - The
MappingUtils.BuildMemberMap
helper is the recommended way to create this dictionary. - If a source member name is found in this dictionary, its value will be used as the target member name on the destination type.
- This takes precedence over automatic name matching for members of
TSource
. - If a member of
TSource
is not in the map, automatic name matching is attempted. - Example (manual creation):
new Dictionary<string, string> { { "SourcePropertyName", "DestinationPropertyName" } }
- This dictionary allows you to explicitly define mappings between source member names (keys) and destination member names (values) for direct members of
Func<MemberExpression, ParameterExpression, Expression?>? customMap = null
:- This delegate provides the ultimate control for complex mapping scenarios where direct name or dictionary mapping is insufficient.
- The function is called for each
MemberExpression
in the source predicate (e.g.,user.IsActive
, or evenuser.Address.Street
). - Parameters:
sourceMemberExpr
: The originalMemberExpression
from the source predicate (e.g.,user.IsActive
oruser.Address.Street
).destParamExpr
: TheParameterExpression
for the destination type (e.g.,entity
of typeUserEntity
).
- Return Value:
- If you return a non-null
Expression
, that expression will be used directly as the replacement in the new expression tree for that specificsourceMemberExpr
. This allows you to, for example, call a method on the destination, combine multiple destination properties, perform transformations, or handle complex path remapping (e.g.,s.Child.Name
tod.ChildName
). - If you return
null
, the converter will fall back to its default logic for that specific member (which involves usingmemberMap
for direct members ofTSource
, or direct name matching for nested members or members of captured variables).
- If you return a non-null
- This
customMap
has the highest precedence in the mapping logic. ThecustomMap
has the highest precedence in the mapping logic and can handle complex scenarios like nested properties or captured variables.
Custom Mapping Example:
Map SourceType.Data
to DestinationType.ProcessedData
.
public class SourceType { public string Data { get; set; } }
public class DestinationType { public string ProcessedData { get; set; } }
Expression<Func<SourceType, bool>> sourceFilter = s => s.Data == "value";
Func<MemberExpression, ParameterExpression, Expression?> myCustomMap = (srcMember, destParam) =>
{
if (srcMember.Member.Name == nameof(SourceType.Data))
{
// Replace 's.Data' with 'd.ProcessedData'
PropertyInfo destProp = typeof(DestinationType).GetProperty(nameof(DestinationType.ProcessedData));
return Expression.Property(destParam, destProp);
}
return null; // Fallback for other members
};
Expression<Func<DestinationType, bool>> destFilter =
ExpressionConverter.Convert<SourceType, DestinationType>(sourceFilter, customMap: myCustomMap);
A more complex customMap
might transform the value itself or map to a method, e.g.:
Expression<Func<SourceType, bool>> complexFilter = s => s.NumericValue > 10;
Func<MemberExpression, ParameterExpression, Expression?> complexCustomMap = (srcMember, destParam) =>
{
if (srcMember.Member.Name == "NumericValue")
{
// Map s.NumericValue to d.CalculationResult (which might be int)
// This custom map returns d.CalculationResult. The "> 10" is applied afterwards.
PropertyInfo destProp = typeof(DestinationTypeWithCalc).GetProperty("CalculationResult");
return Expression.Property(destParam, destProp);
}
return null;
};
This would convert complexFilter
to d => d.CalculationResult > 10
.
🛣️ Roadmap & Future Enhancements (Post v0.2.1)
While CrossTypeExpressionConverter v0.2.1
focuses on robust predicate conversion with flexible mapping, future versions may include:
- ExpressionConverterOptions Object: Introduce a dedicated options class to simplify the Convert method signature and allow for more configuration points (e.g., ThrowOnFailedMemberMapping toggle, case-sensitivity options for name matching).
- Selector Conversion: Support for converting projection expressions (e.g., Expression<Func<TSource, TResult>> to Expression<Func<TDestination, TDestResult>>).
- Order By Conversion: Support for key selector expressions for ordering.
- Fluent Configuration API: A fluent interface for defining mappings.
- Performance Caching: Internal caching of MemberInfo and type mapping details.
- Attribute-Based Mapping: Define mappings via attributes on type members.
- Roslyn Analyzer: For compile-time diagnostics of potential mapping issues.
🤝 Contributing
Contributions are welcome! If you have an idea for a new feature, an improvement, or a bug fix, please:
- Check the Issues to see if your idea or bug has already been discussed.
- If not, open a new issue to discuss the change.
- Fork the repository, make your changes in a feature branch, and submit a pull request with a clear description of your changes.
Please ensure that any new code includes appropriate unit tests.
❓ FAQ
Q: Does this work with complex nested objects?
- A: Yes, for member access chains like
source.Order.Details.ProductName
. The converter will attempt to map each segment based on its default logic (name matching for nested parts) or what yourcustomMap
provides. For instance, ifcustomMap
isn't used forsource.Order
and it's mapped todest.CustomerOrder
(viamemberMap
or name), the converter will then try to resolveDetails
onCustomerOrder
's type, and so on. Complex re-structuring of nested paths (e.g., flatteningsource.Order.Details.ProductName
todest.ProductName
) would typically requirecustomMap
to handle the fullsource.Order.Details.ProductName
expression.
- A: Yes, for member access chains like
Q: What happens if a property doesn't exist on the destination type and isn't mapped?
- A: The converter will throw an
InvalidOperationException
detailing which member could not be mapped. Currently, this behavior is not configurable.
- A: The converter will throw an
Q: Is this similar to AutoMapper's
ProjectTo
?- A: It shares the goal of translating expressions for ORM querying. However,
CrossTypeExpressionConverter
is a more focused utility for converting individualExpression<Func<TSource, bool>>
. It doesn't perform full object-to-object mapping or offerIQueryable
extension methods likeProjectTo
out-of-the-box, but it can be a powerful component in building such systems or for more direct expression manipulation.
- A: It shares the goal of translating expressions for ORM querying. However,
⚖️ License
This project is licensed under the MIT License. See the LICENSE file for details.
Copyright (c) 2025 scherenhaenden
Product | Versions Compatible and additional computed target framework versions. |
---|---|
.NET | net8.0 is compatible. net8.0-android was computed. net8.0-browser was computed. net8.0-ios was computed. net8.0-maccatalyst was computed. net8.0-macos was computed. net8.0-tvos was computed. net8.0-windows was computed. net9.0 was computed. net9.0-android was computed. net9.0-browser was computed. net9.0-ios was computed. net9.0-maccatalyst was computed. net9.0-macos was computed. net9.0-tvos was computed. net9.0-windows was computed. net10.0 was computed. net10.0-android was computed. net10.0-browser was computed. net10.0-ios was computed. net10.0-maccatalyst was computed. net10.0-macos was computed. net10.0-tvos was computed. net10.0-windows was computed. |
-
net8.0
- No dependencies.
NuGet packages
This package is not used by any NuGet packages.
GitHub repositories
This package is not used by any popular GitHub repositories.