Skip to content

Commit 1f59ec9

Browse files
Mapping improvements (#3)
* Inline mapping initial implementation * Merged PR 136785: Conditional mapping * MapStatic better reflects method functionality * Mapper bug fixed * Switch API reworked * API improvements * SC comments fix
1 parent cfd23f6 commit 1f59ec9

File tree

17 files changed

+465
-70
lines changed

17 files changed

+465
-70
lines changed

samples/Rql.Sample.Api/Mapping/ProductMapper.cs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,10 +8,10 @@ internal class ProductMapper : IRqlMapper<Product, ProductView>
88
{
99
public void MapEntity(IRqlMapperContext<Product, ProductView> context)
1010
{
11-
context.Map(t => t.Id, t => t.ProductId)
11+
context.MapStatic(t => t.Id, t => t.ProductId)
1212
.MapDynamic(t => t.Model, t => t.ProductModel)
1313
.MapDynamic(t => t.SaleDetails, t => t.SalesOrderDetails)
14-
.Map(t => t.SaleDetailIds, t => t.SalesOrderDetails.Select(s => s.SalesOrderDetailId));
14+
.MapStatic(t => t.SaleDetailIds, t => t.SalesOrderDetails.Select(s => s.SalesOrderDetailId));
1515
}
1616
}
1717
}

samples/Rql.Sample.Api/Mapping/ProductModelMapper.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ internal class ProductModelMapper : IRqlMapper<ProductModel, ProductModelView>
88
{
99
public void MapEntity(IRqlMapperContext<ProductModel, ProductModelView> context)
1010
{
11-
context.Map(t => t.Name, t => t.Name);
11+
context.MapStatic(t => t.Name, t => t.Name);
1212
}
1313
}
1414
}

samples/Rql.Sample.Api/Mapping/ProductSaleOrderMapper.cs

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -8,11 +8,11 @@ public class ProductSaleOrderMapper : IRqlMapper<SalesOrderDetail, ProductSaleOr
88
{
99
public void MapEntity(IRqlMapperContext<SalesOrderDetail, ProductSaleOrder> context)
1010
{
11-
context.Map(t => t.OrderQty, t => t.OrderQty)
12-
.Map(t => t.SalesOrderDetailId, t => t.SalesOrderDetailId)
13-
.Map(t => t.SalesOrderId, t => t.SalesOrderId)
14-
.Map(t => t.AddressLine1, t => t.SalesOrder.BillToAddress!.AddressLine1)
15-
.Map(t => t.City, t => t.SalesOrder.BillToAddress!.City)
11+
context.MapStatic(t => t.OrderQty, t => t.OrderQty)
12+
.MapStatic(t => t.SalesOrderDetailId, t => t.SalesOrderDetailId)
13+
.MapStatic(t => t.SalesOrderId, t => t.SalesOrderId)
14+
.MapStatic(t => t.AddressLine1, t => t.SalesOrder.BillToAddress!.AddressLine1)
15+
.MapStatic(t => t.City, t => t.SalesOrder.BillToAddress!.City)
1616
;
1717
}
1818
}

samples/Rql.Sample.Api/Mapping/SampleEntityMapper.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ internal class SampleEntityMapper : IRqlMapper<SampleEntity, SampleEntityView>
88
{
99
public void MapEntity(IRqlMapperContext<SampleEntity, SampleEntityView> context)
1010
{
11-
context.Map(t => t.Id, t => t.Id);
11+
context.MapStatic(t => t.Id, t => t.Id);
1212
}
1313
}
1414
}

src/SoftwareOne.Rql.Linq/Services/Mapping/IRqlMapAccessor.cs

Lines changed: 12 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -5,16 +5,22 @@ namespace SoftwareOne.Rql;
55

66
public interface IRqlMapAccessor
77
{
8-
RqlMapDescriptor Get<TFrom, TTo>();
8+
Dictionary<string, RqlMapEntry> GetMap<TFrom, TTo>()
9+
=> GetMap(typeof(TFrom), typeof(TTo));
910

10-
RqlMapDescriptor Get(Type typeFrom, Type typeTo);
11+
Dictionary<string, RqlMapEntry> GetMap(Type typeFrom, Type typeTo);
12+
13+
public IEnumerable<RqlMapEntry> GetEntries<TFrom, TTo>()
14+
=> GetEntries(typeof(TFrom), typeof(TTo));
15+
16+
public IEnumerable<RqlMapEntry> GetEntries(Type typeFrom, Type typeTo);
1117
}
1218

1319
internal class RqlMapAccessor(IEntityMapCache mapCache) : IRqlMapAccessor
1420
{
15-
public RqlMapDescriptor Get<TFrom, TTo>()
16-
=> Get(typeof(TFrom), typeof(TTo));
21+
public IEnumerable<RqlMapEntry> GetEntries(Type typeFrom, Type typeTo)
22+
=> GetMap(typeFrom, typeTo).Values;
1723

18-
public RqlMapDescriptor Get(Type typeFrom, Type typeTo)
19-
=> new(mapCache.Get(typeFrom, typeTo));
24+
public Dictionary<string, RqlMapEntry> GetMap(Type typeFrom, Type typeTo)
25+
=> mapCache.Get(typeFrom, typeTo);
2026
}

src/SoftwareOne.Rql.Linq/Services/Mapping/IRqlMapperContext.cs

Lines changed: 17 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,9 +5,23 @@ namespace SoftwareOne.Rql;
55

66
public interface IRqlMapperContext<TStorage, TView>
77
{
8-
IRqlMapperContext<TStorage, TView> Map<TFrom, TTo>(Expression<Func<TView, TTo?>> to, Expression<Func<TStorage, TFrom?>> from) where TTo : TFrom;
8+
IRqlMapperContext<TStorage, TView> MapStatic<TFrom, TTo>(Expression<Func<TView, TTo?>> to, Expression<Func<TStorage, TFrom?>> from) where TTo : TFrom;
9+
10+
IRqlMapperContext<TStorage, TView> MapDynamic<TFrom, TTo>(Expression<Func<TView, TTo?>> to, Expression<Func<TStorage, TFrom?>> from, Action<IRqlMapperContext<TFrom, TTo>>? configureInline = null);
11+
12+
IRqlMapperContext<TStorage, TView> MapDynamic<TFrom, TTo>(Expression<Func<TView, IEnumerable<TTo>?>> to, Expression<Func<TStorage, IEnumerable<TFrom>?>> from, Action<IRqlMapperContext<TFrom, TTo>>? configureInline = null);
13+
14+
IRqlMapperSwitchContext<TStorage> Switch<TTo>(Expression<Func<TView, TTo?>> to);
915

10-
IRqlMapperContext<TStorage, TView> MapDynamic<TFrom, TTo>(Expression<Func<TView, TTo?>> to, Expression<Func<TStorage, TFrom?>> from);
11-
1216
IRqlMapperContext<TStorage, TView> Ignore<TTo>(Expression<Func<TView, TTo?>> toIgnore);
17+
}
18+
19+
public interface IRqlMapperSwitchContext<TFromOwner>
20+
{
21+
IRqlMapperSwitchContextFinalizer<TFromOwner> Case<TFrom>(Expression<Func<TFromOwner, bool>> condition, Expression<Func<TFromOwner, TFrom?>> from, bool mapStatic = false);
22+
}
23+
24+
public interface IRqlMapperSwitchContextFinalizer<TFromOwner> : IRqlMapperSwitchContext<TFromOwner>
25+
{
26+
void Default<TFrom>(Expression<Func<TFromOwner, TFrom?>> from, bool mapStatic = false);
1327
}

src/SoftwareOne.Rql.Linq/Services/Mapping/MappingService.cs

Lines changed: 43 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -28,33 +28,58 @@ public IQueryable<TView> Apply(IQueryable<TStorage> query)
2828
private MemberInitExpression MakeInitExpression(Expression param, RqlNode rqlNode, Type typeFrom, Type typeTo)
2929
{
3030
var typeMap = _mapCache.Get(typeFrom, typeTo);
31+
return MakeInitExpression(param, rqlNode, typeTo, typeMap);
32+
}
3133

34+
private MemberInitExpression MakeInitExpression(Expression param, RqlNode rqlNode, Type typeTo, IReadOnlyDictionary<string, RqlMapEntry> typeMap)
35+
{
3236
var bindings = new List<MemberBinding>(_queryContext.Graph.Count);
3337

3438
foreach (var node in rqlNode.Children.Where(t => t.IsIncluded))
3539
{
3640
if (typeMap.TryGetValue(node.Property.Property.Name, out var map))
3741
{
38-
var fromExpression = MakeBindExpression(param, node, map.SourceExpression, map.IsDynamic);
42+
var fromExpression = MakeBindExpression(param, node, map);
43+
fromExpression = TryMakeConditionalBindExpression(param, node, fromExpression, map);
3944
bindings.Add(Expression.Bind(node.Property.Property, fromExpression));
4045
}
4146
}
4247

4348
return Expression.MemberInit(Expression.New(typeTo.GetConstructor(Type.EmptyTypes)!), bindings);
4449
}
4550

46-
private Expression MakeBindExpression(Expression param, RqlNode node, LambdaExpression sourceExpression, bool isDynamic)
51+
private Expression TryMakeConditionalBindExpression(Expression param, RqlNode rqlNode, Expression defaultExpression, RqlMapEntry parentEntry)
52+
{
53+
if (parentEntry.Conditions == null || parentEntry.Conditions.Count == 0)
54+
return defaultExpression;
55+
56+
Expression conditionalExpr = defaultExpression;
57+
58+
for (int i = parentEntry.Conditions.Count - 1; i >= 0; i--)
59+
{
60+
var condition = parentEntry.Conditions[i];
61+
62+
var replaceParamVisitor = new ReplaceParameterVisitor(condition.If.Parameters[0], param);
63+
var ifExpression = replaceParamVisitor.Visit(condition.If.Body);
64+
65+
conditionalExpr = Expression.Condition(ifExpression, MakeBindExpression(param, rqlNode, condition.Entry), conditionalExpr);
66+
}
67+
68+
return conditionalExpr;
69+
}
70+
71+
private Expression MakeBindExpression(Expression param, RqlNode node, RqlMapEntry map)
4772
{
4873
var targetType = node.Property.Property.PropertyType;
49-
var replaceParamVisitor = new ReplaceParameterVisitor(sourceExpression.Parameters[0], param);
50-
var fromExpression = replaceParamVisitor.Visit(sourceExpression.Body);
74+
var replaceParamVisitor = new ReplaceParameterVisitor(map.SourceExpression.Parameters[0], param);
75+
var fromExpression = replaceParamVisitor.Visit(map.SourceExpression.Body);
5176

52-
if (isDynamic)
77+
if (map.IsDynamic)
5378
{
5479
fromExpression = node.Property.Type switch
5580
{
56-
RqlPropertyType.Reference => MakeReferenceInit(fromExpression, node, targetType),
57-
RqlPropertyType.Collection => MakeCollectionInit(fromExpression, node, node.Property.ElementType!),
81+
RqlPropertyType.Reference => MakeReferenceInit(fromExpression, node, map),
82+
RqlPropertyType.Collection => MakeCollectionInit(fromExpression, node, map),
5883
_ => fromExpression
5984
};
6085
}
@@ -68,9 +93,11 @@ private Expression MakeBindExpression(Expression param, RqlNode node, LambdaExpr
6893
return fromExpression;
6994
}
7095

71-
private Expression MakeReferenceInit(Expression fromExpression, RqlNode node, Type targetType)
96+
private Expression MakeReferenceInit(Expression fromExpression, RqlNode node, RqlMapEntry map)
7297
{
73-
var subInit = MakeInitExpression(fromExpression, node, fromExpression.Type, targetType);
98+
var innerMap = GetInnerMapFromEntry(fromExpression.Type, map);
99+
100+
var subInit = MakeInitExpression(fromExpression, node, map.TargetType, innerMap);
74101

75102
if (node.Property.IsNullable)
76103
{
@@ -85,7 +112,7 @@ private Expression MakeReferenceInit(Expression fromExpression, RqlNode node, Ty
85112
return fromExpression;
86113
}
87114

88-
private Expression MakeCollectionInit(Expression fromExpression, RqlNode node, Type targetItemType)
115+
private Expression MakeCollectionInit(Expression fromExpression, RqlNode node, RqlMapEntry map)
89116
{
90117
// Temporarily only support List
91118
if (!typeof(IList).IsAssignableFrom(node.Property.Property.PropertyType))
@@ -96,11 +123,15 @@ private Expression MakeCollectionInit(Expression fromExpression, RqlNode node, T
96123
if (!TypeHelper.IsUserComplexType(srcItemType))
97124
return fromExpression;
98125

126+
var innerMap = GetInnerMapFromEntry(srcItemType, map);
99127
var innerParam = Expression.Parameter(srcItemType);
100-
var subInit = MakeInitExpression(innerParam, node, srcItemType, targetItemType);
128+
var subInit = MakeInitExpression(innerParam, node, map.TargetType, innerMap);
101129
var selectLambda = Expression.Lambda(subInit, innerParam);
102-
var functions = (IProjectionFunctions)Activator.CreateInstance(typeof(ProjectionFunctions<,>).MakeGenericType(srcItemType, targetItemType))!;
130+
var functions = (IProjectionFunctions)Activator.CreateInstance(typeof(ProjectionFunctions<,>).MakeGenericType(srcItemType, map.TargetType))!;
103131
var selectCall = Expression.Call(null, functions.GetSelect(), fromExpression, selectLambda);
104132
return Expression.Call(null, functions.GetToList(), selectCall);
105133
}
134+
135+
private IReadOnlyDictionary<string, RqlMapEntry> GetInnerMapFromEntry(Type typeFrom, RqlMapEntry map)
136+
=> map.InlineMap ?? _mapCache.Get(typeFrom, map.TargetType);
106137
}

src/SoftwareOne.Rql.Linq/Services/Mapping/RqlMapDescriptor.cs

Lines changed: 0 additions & 14 deletions
This file was deleted.

src/SoftwareOne.Rql.Linq/Services/Mapping/RqlMapEntry.cs

Lines changed: 16 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -6,9 +6,22 @@ namespace SoftwareOne.Rql;
66

77
public class RqlMapEntry
88
{
9-
public IRqlPropertyInfo TargetProperty { get; init; } = null!;
9+
public IRqlPropertyInfo TargetProperty { get; internal set; } = null!;
1010

11-
public LambdaExpression SourceExpression { get; init; } = null!;
11+
public LambdaExpression SourceExpression { get; internal set; } = null!;
1212

13-
public bool IsDynamic { get; init; }
13+
public IReadOnlyDictionary<string, RqlMapEntry>? InlineMap { get; internal set; }
14+
15+
public List<RqlMapEntryCondition>? Conditions { get; internal set; }
16+
17+
public bool IsDynamic { get; internal set; }
18+
19+
public Type TargetType => TargetProperty.ElementType ?? TargetProperty.Property.PropertyType;
20+
}
21+
22+
public class RqlMapEntryCondition
23+
{
24+
public LambdaExpression If { get; init; } = null!;
25+
26+
public RqlMapEntry Entry { get; init; } = null!;
1427
}

src/SoftwareOne.Rql.Linq/Services/Mapping/RqlMapperContext.cs

Lines changed: 64 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -16,21 +16,46 @@ internal class RqlMapperContext<TStorage, TView> : RqlMapperContext, IRqlMapperC
1616
private readonly Dictionary<string, IRqlPropertyInfo> _targetProperties;
1717
private readonly Dictionary<string, RqlMapEntry> _mapping;
1818
private readonly HashSet<string> _ignored;
19+
private readonly HashSet<RqlMapEntry> _switch;
1920

2021
public RqlMapperContext(IRqlMetadataProvider rqlMetadataProvider)
2122
{
2223
_mapping = [];
2324
_ignored = [];
25+
_switch = [];
2426
_rqlMetadataProvider = rqlMetadataProvider;
2527

2628
_targetProperties = _rqlMetadataProvider.GetPropertiesByDeclaringType(typeof(TView)).ToDictionary(k => k.Property.Name);
2729
}
2830

29-
public IRqlMapperContext<TStorage, TView> Map<TFrom, TTo>(Expression<Func<TView, TTo?>> to, Expression<Func<TStorage, TFrom?>> from) where TTo : TFrom
30-
=> MapInternal(to, from, false);
31+
public IRqlMapperContext<TStorage, TView> MapStatic<TFrom, TTo>(Expression<Func<TView, TTo?>> to, Expression<Func<TStorage, TFrom?>> from) where TTo : TFrom
32+
=> MapInternal(new RqlMapEntry
33+
{
34+
TargetProperty = GetTargetProperty(to),
35+
SourceExpression = from,
36+
IsDynamic = false,
37+
InlineMap = null,
38+
Conditions = null
39+
});
40+
41+
public IRqlMapperContext<TStorage, TView> MapDynamic<TFrom, TTo>(Expression<Func<TView, TTo?>> to, Expression<Func<TStorage, TFrom?>> from, Action<IRqlMapperContext<TFrom, TTo>>? configureInline = null)
42+
=> MapInternal(GetTargetProperty(to), from, true, configureInline);
43+
44+
public IRqlMapperContext<TStorage, TView> MapDynamic<TFrom, TTo>(Expression<Func<TView, IEnumerable<TTo>?>> to, Expression<Func<TStorage, IEnumerable<TFrom>?>> from, Action<IRqlMapperContext<TFrom, TTo>>? configureInline = null)
45+
=> MapInternal(GetTargetProperty(to), from, true, configureInline);
46+
47+
public IRqlMapperSwitchContext<TStorage> Switch<TTo>(Expression<Func<TView, TTo?>> to)
48+
{
49+
var entry = new RqlMapEntry
50+
{
51+
TargetProperty = GetTargetProperty(to),
52+
SourceExpression = null!,
53+
IsDynamic = true,
54+
};
3155

32-
public IRqlMapperContext<TStorage, TView> MapDynamic<TFrom, TTo>(Expression<Func<TView, TTo?>> to, Expression<Func<TStorage, TFrom?>> from)
33-
=> MapInternal(to, from, true);
56+
_switch.Add(entry);
57+
return new RqlMapperSwitchContext<TStorage>(entry);
58+
}
3459

3560
public IRqlMapperContext<TStorage, TView> Ignore<TTo>(Expression<Func<TView, TTo?>> toIgnore)
3661
{
@@ -42,6 +67,14 @@ public IRqlMapperContext<TStorage, TView> Ignore<TTo>(Expression<Func<TView, TTo
4267

4368
public override void AddMissing()
4469
{
70+
foreach (var switchEntry in _switch)
71+
{
72+
if (switchEntry.SourceExpression == null)
73+
throw new RqlMappingException($"Switch mapping for property '{switchEntry.TargetProperty.Name}' must have default case.");
74+
75+
MapInternal(switchEntry);
76+
}
77+
4578
var fromProps = _rqlMetadataProvider.GetPropertiesByDeclaringType(typeof(TStorage)).ToDictionary(k => k.Property.Name);
4679

4780
foreach (var targetProp in _targetProperties.Values)
@@ -61,24 +94,42 @@ public override void AddMissing()
6194
{
6295
var param = Expression.Parameter(typeof(TStorage));
6396
var sourceExpression = Expression.Lambda(Expression.MakeMemberAccess(param, srcProp.Property), param);
64-
MapInternal(targetProp, sourceExpression, true);
97+
MapInternal(new RqlMapEntry
98+
{
99+
TargetProperty = targetProp,
100+
SourceExpression = sourceExpression,
101+
IsDynamic = true,
102+
InlineMap = null,
103+
Conditions = null
104+
});
65105
}
66106
}
67107
}
68108

69-
private RqlMapperContext<TStorage, TView> MapInternal<TFrom, TTo>(Expression<Func<TView, TTo?>> to, Expression<Func<TStorage, TFrom?>> from, bool isDynamic)
70-
=> MapInternal(GetTargetProperty(to), from, isDynamic);
71-
72-
private RqlMapperContext<TStorage, TView> MapInternal(IRqlPropertyInfo target, LambdaExpression source, bool isDynamic)
109+
private RqlMapperContext<TStorage, TView> MapInternal<TFrom, TTo>(IRqlPropertyInfo target, LambdaExpression source, bool isDynamic, Action<IRqlMapperContext<TFrom, TTo>>? configureInline)
73110
{
74-
var mapProperty = new RqlMapEntry
111+
Dictionary<string, RqlMapEntry>? inline = null;
112+
if (configureInline != null)
113+
{
114+
var mapperContext = new RqlMapperContext<TFrom, TTo>(_rqlMetadataProvider);
115+
configureInline(mapperContext);
116+
mapperContext.AddMissing();
117+
inline = mapperContext.Mapping;
118+
}
119+
120+
return MapInternal(new RqlMapEntry
75121
{
76122
TargetProperty = target,
77123
SourceExpression = source,
78-
IsDynamic = isDynamic
79-
};
124+
IsDynamic = isDynamic,
125+
InlineMap = inline,
126+
Conditions = null
127+
});
128+
}
80129

81-
_mapping.Add(target.Property.Name, mapProperty);
130+
private RqlMapperContext<TStorage, TView> MapInternal(RqlMapEntry mapEntry)
131+
{
132+
_mapping.Add(mapEntry.TargetProperty.Property.Name, mapEntry);
82133
return this;
83134
}
84135

0 commit comments

Comments
 (0)