Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
40 changes: 40 additions & 0 deletions Mapify.NET.Tests.EFCore/MapifyEfCoreProjectionTests.UseMap.cs
Original file line number Diff line number Diff line change
Expand Up @@ -338,4 +338,44 @@ public void InstanceMapify_Map_ShouldFallbackToBaseMap_ForProxyLikeVirtualListsL
Assert.All(items, item => Assert.NotNull(item));
Assert.All(items, item => Assert.True(item.Price > 0));
}

[Fact]
public void InstanceMapify_Map_ShouldHandleLazyLoadedProxyPolymorphicChildren_WithSiblingCastProfileShape() {
var options = new DbContextOptionsBuilder<EfCoreMapifyContext>()
.UseLazyLoadingProxies()
.UseInMemoryDatabase(Guid.NewGuid().ToString("N"))
.Options;

using (var seed = new EfCoreMapifyContext(options)) {
seed.BillsWithVirtualListBlocks.Add(new EfCoreBillWithVirtualListBlocks {
Blocks = [
new EfCoreVirtualListBlock {
CostItems = [
new EfCoreVirtualListCostItemType1 { Price = 10m },
new EfCoreVirtualListCostItemType2 { TotalPrice = 25m }
]
}
]
});

seed.SaveChanges();
}

using var db = new EfCoreMapifyContext(options);

var loaded = db.BillsWithVirtualListBlocks.Single();

var mapify = new Mapify([
new EfCoreVirtualListCostItemProfile(),
new EfCoreVirtualListBlockProfile(),
new EfCoreVirtualListBillProfile()
]);

var mapped = mapify.Map<EfCoreBillWithVirtualListBlocks, EfCoreBillWithVirtualListBlocksDto>(loaded);

var items = mapped.Blocks.Single().CostItems.ToArray();
Assert.Equal(2, items.Length);
Assert.All(items, item => Assert.NotNull(item));
Assert.Equal(new[] { 10m, 25m }, items.Select(x => x.Price).OrderBy(x => x).ToArray());
}
}
209 changes: 207 additions & 2 deletions Mapify.NET.Tests/MapifyInstanceTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -579,14 +579,20 @@ private class NullSafeEnumerableBranchTarget {
private abstract class PolymorphicCostItemSource {
}

private sealed class PolymorphicCostItemType1Source : PolymorphicCostItemSource {
private class PolymorphicCostItemType1Source : PolymorphicCostItemSource {
public decimal Price { get; set; }
}

private sealed class PolymorphicCostItemType2Source : PolymorphicCostItemSource {
private class PolymorphicCostItemType2Source : PolymorphicCostItemSource {
public decimal TotalPrice { get; set; }
}

private sealed class PolymorphicCostItemType1ProxySource : PolymorphicCostItemType1Source {
}

private sealed class PolymorphicCostItemType2ProxySource : PolymorphicCostItemType2Source {
}

private class PolymorphicBillSource {
public IEnumerable<PolymorphicCostItemSource>? CostItems { get; set; }
}
Expand Down Expand Up @@ -1335,6 +1341,133 @@ protected override void Configure() {
}
}

private class PolymorphicSiblingCastProfile : MapifyProfile {
protected override void Configure() {
CreateMap<PolymorphicCostItemSource, PolymorphicCostItemTarget>(x => new PolymorphicCostItemTarget {
Price = x is PolymorphicCostItemType1Source
? ((PolymorphicCostItemType1Source)x).Price
: x is PolymorphicCostItemType2Source
? ((PolymorphicCostItemType2Source)x).TotalPrice
: 0m
});
}
}

private class PolymorphicNestedName {
public string Value { get; set; } = string.Empty;
}

private abstract class PolymorphicNestedItemSource {
}

private class PolymorphicNestedType1Source : PolymorphicNestedItemSource {
public PolymorphicNestedName? Name { get; set; }
}

private class PolymorphicNestedType2Source : PolymorphicNestedItemSource {
public PolymorphicNestedName? OtherName { get; set; }
}

private sealed class PolymorphicNestedType1ProxySource : PolymorphicNestedType1Source {
}

private sealed class PolymorphicNestedType2ProxySource : PolymorphicNestedType2Source {
}

private class PolymorphicNestedTarget {
public string Name { get; set; } = string.Empty;
}

private enum PolymorphicIndicator {
Default = 0,
Custom = 1
}

private class PolymorphicIndicatorDetail {
public PolymorphicIndicator Indicator { get; set; }
}

private abstract class PolymorphicIndicatorItemSource {
}

private class PolymorphicIndicatorType1Source : PolymorphicIndicatorItemSource {
}

private class PolymorphicIndicatorType2Source : PolymorphicIndicatorItemSource {
public PolymorphicIndicatorDetail? Detail { get; set; }
}

private sealed class PolymorphicIndicatorType1ProxySource : PolymorphicIndicatorType1Source {
}

private sealed class PolymorphicIndicatorType2ProxySource : PolymorphicIndicatorType2Source {
}

private class PolymorphicIndicatorTarget {
public PolymorphicIndicator? Indicator { get; set; }
}

private abstract class PolymorphicDiscriminatorItemSource {
public string Kind { get; set; } = string.Empty;
}

private class PolymorphicDiscriminatorType1Source : PolymorphicDiscriminatorItemSource {
public PolymorphicNestedName? Name { get; set; }
}

private class PolymorphicDiscriminatorType2Source : PolymorphicDiscriminatorItemSource {
public PolymorphicNestedName? OtherName { get; set; }
public PolymorphicIndicatorDetail? Detail { get; set; }
}

private sealed class PolymorphicDiscriminatorType1ProxySource : PolymorphicDiscriminatorType1Source {
}

private sealed class PolymorphicDiscriminatorType2ProxySource : PolymorphicDiscriminatorType2Source {
}

private class PolymorphicDiscriminatorTarget {
public string Name { get; set; } = string.Empty;
public PolymorphicIndicator? Indicator { get; set; }
}

private class PolymorphicNestedSiblingCastProfile : MapifyProfile {
protected override void Configure() {
CreateMap<PolymorphicNestedItemSource, PolymorphicNestedTarget>(x => new PolymorphicNestedTarget {
Name = x is PolymorphicNestedType1Source
? ((PolymorphicNestedType1Source)x).Name!.Value
: x is PolymorphicNestedType2Source
? ((PolymorphicNestedType2Source)x).OtherName!.Value
: string.Empty
});
}
}

private class PolymorphicIndicatorSiblingCastProfile : MapifyProfile {
protected override void Configure() {
CreateMap<PolymorphicIndicatorItemSource, PolymorphicIndicatorTarget>(x => new PolymorphicIndicatorTarget {
Indicator = x is PolymorphicIndicatorType2Source
? ((PolymorphicIndicatorType2Source)x).Detail!.Indicator
: PolymorphicIndicator.Default
});
}
}

private class PolymorphicDiscriminatorSiblingCastProfile : MapifyProfile {
protected override void Configure() {
CreateMap<PolymorphicDiscriminatorItemSource, PolymorphicDiscriminatorTarget>(x => new PolymorphicDiscriminatorTarget {
Name = x.Kind == "Type1"
? ((PolymorphicDiscriminatorType1Source)x).Name!.Value
: x.Kind == "Type2"
? ((PolymorphicDiscriminatorType2Source)x).OtherName!.Value
: string.Empty,
Indicator = x.Kind == "Type2"
? ((PolymorphicDiscriminatorType2Source)x).Detail!.Indicator
: PolymorphicIndicator.Default
});
}
}

[Fact]
public void Map_ToNewObject_ShouldWorkLikeStaticMapper() {
var mapify = new Mapify();
Expand Down Expand Up @@ -2042,6 +2175,78 @@ public void Map_ProjectToShouldMapPolymorphicItems_WhenElementMapUsesTypeChecks(
Assert.Equal([10m, 25m], prices);
}

[Fact]
public void Map_ShouldHandleProxyLikePolymorphicSiblings_WithGuardedSiblingCasts() {
var mapify = new Mapify(new PolymorphicSiblingCastProfile());

var mapped1 = mapify.Map<PolymorphicCostItemSource, PolymorphicCostItemTarget>(
new PolymorphicCostItemType1ProxySource { Price = 10m }
);

var mapped2 = mapify.Map<PolymorphicCostItemSource, PolymorphicCostItemTarget>(
new PolymorphicCostItemType2ProxySource { TotalPrice = 25m }
);

Assert.Equal(10m, mapped1.Price);
Assert.Equal(25m, mapped2.Price);
}

[Fact]
public void Map_ShouldHandleProxyLikePolymorphicSiblings_WithNestedGuardedSiblingCasts() {
var mapify = new Mapify(new PolymorphicNestedSiblingCastProfile());

var mapped1 = mapify.Map<PolymorphicNestedItemSource, PolymorphicNestedTarget>(
new PolymorphicNestedType1ProxySource {
Name = new PolymorphicNestedName { Value = "A" }
}
);

var mapped2 = mapify.Map<PolymorphicNestedItemSource, PolymorphicNestedTarget>(
new PolymorphicNestedType2ProxySource {
OtherName = new PolymorphicNestedName { Value = "B" }
}
);

Assert.Equal("A", mapped1.Name);
Assert.Equal("B", mapped2.Name);
}

[Fact]
public void Map_ShouldNotEvaluateSiblingCast_ForConvertWrappedConditional_OnProxyType() {
var mapify = new Mapify(new PolymorphicIndicatorSiblingCastProfile());

var mapped = mapify.Map<PolymorphicIndicatorItemSource, PolymorphicIndicatorTarget>(
new PolymorphicIndicatorType1ProxySource()
);

Assert.Equal(PolymorphicIndicator.Default, mapped.Indicator);
}

[Fact]
public void Map_ShouldHandleProxyLikePolymorphicSiblings_WithStringDiscriminatorGuards() {
var mapify = new Mapify(new PolymorphicDiscriminatorSiblingCastProfile());

var mapped1 = mapify.Map<PolymorphicDiscriminatorItemSource, PolymorphicDiscriminatorTarget>(
new PolymorphicDiscriminatorType1ProxySource {
Kind = "Type1",
Name = new PolymorphicNestedName { Value = "A" }
}
);

var mapped2 = mapify.Map<PolymorphicDiscriminatorItemSource, PolymorphicDiscriminatorTarget>(
new PolymorphicDiscriminatorType2ProxySource {
Kind = "Type2",
OtherName = new PolymorphicNestedName { Value = "B" },
Detail = new PolymorphicIndicatorDetail { Indicator = PolymorphicIndicator.Custom }
}
);

Assert.Equal("A", mapped1.Name);
Assert.Equal(PolymorphicIndicator.Default, mapped1.Indicator);
Assert.Equal("B", mapped2.Name);
Assert.Equal(PolymorphicIndicator.Custom, mapped2.Indicator);
}

[Fact]
public void Map_ShouldBuildTransitiveDependencies_WhenProfilesAreUnordered() {
// Registration order is reverse dependency order: root -> middle -> leaf.
Expand Down
13 changes: 13 additions & 0 deletions Mapify/Mapify.Engine.NullSafety.cs
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,19 @@ private static Expression ApplyNestedNullSafetyCore(Expression expression, Expre
return ApplyNestedNullSafetyCore(normalized, fallback);
}

if (expression is UnaryExpression unaryExpression
&& (unaryExpression.NodeType == ExpressionType.Convert || unaryExpression.NodeType == ExpressionType.ConvertChecked)
&& unaryExpression.Operand is ConditionalExpression) {
var operandFallback = AdaptFallbackToType(fallback, unaryExpression.Operand.Type);
var guardedOperand = ApplyNestedNullSafetyCore(unaryExpression.Operand, operandFallback);

if (guardedOperand.Type == unaryExpression.Type) {
return guardedOperand;
}

return Expression.MakeUnary(unaryExpression.NodeType, guardedOperand, unaryExpression.Type, unaryExpression.Method);
}

if (expression is ConditionalExpression conditionalExpression) {
var guardedTest = ApplyNestedNullSafetyToBooleanExpression(conditionalExpression.Test);
var guardedIfTrue = ApplyNestedNullSafetyCore(
Expand Down
28 changes: 28 additions & 0 deletions Mapify/Mapify.Runtime.Diagnostics.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
using System.Linq.Expressions;

namespace Mapify.NET;

public partial class Mapify {
private const string _runtimeExpressionDebugEnvVar = "MAPIFY_DEBUG_RUNTIME_EXPRESSION";

private static bool IsRuntimeExpressionDebugEnabled() {
var value = Environment.GetEnvironmentVariable(_runtimeExpressionDebugEnvVar);
return string.Equals(value, "1", StringComparison.Ordinal)
|| string.Equals(value, "true", StringComparison.OrdinalIgnoreCase)
|| string.Equals(value, "yes", StringComparison.OrdinalIgnoreCase);
}

private static void LogRuntimeExpression<TSource, TTarget>(
string? name,
Expression<Func<TSource, TTarget>> expression,
bool fromCache
) {
if (!IsRuntimeExpressionDebugEnabled()) {
return;
}

var mapName = name ?? "<default>";
Console.WriteLine($"[Mapify Runtime Expression] fromCache={fromCache} source={typeof(TSource).FullName} target={typeof(TTarget).FullName} name={mapName}");
Console.WriteLine(expression.ToString());
}
}
14 changes: 14 additions & 0 deletions Mapify/Mapify.Runtime.Execution.cs
Original file line number Diff line number Diff line change
Expand Up @@ -129,10 +129,16 @@ public TTarget Map<TSource, TTarget>(TSource source, string name) {

var key = new MapKey(typeof(TSource), typeof(TTarget), name);
if (_compiledMapToNewCache.TryGetValue(key, out var map)) {
if (IsRuntimeExpressionDebugEnabled()) {
var debugExpression = GetRequiredRuntimeMap<TSource, TTarget>(name, _emptyParameters);
LogRuntimeExpression(name, debugExpression, fromCache: true);
}

return ((Func<TSource, TTarget>)map).Invoke(source);
}

var expression = GetRequiredRuntimeMap<TSource, TTarget>(name, _emptyParameters);
LogRuntimeExpression(name, expression, fromCache: false);
var compiled = expression.Compile();
_compiledMapToNewCache[key] = compiled;
return compiled.Invoke(source);
Expand All @@ -156,6 +162,7 @@ public TTarget Map<TSource, TTarget>(TSource source, string name, IReadOnlyDicti
ValidateRuntimeParameters(parameters);

var expression = GetRequiredRuntimeMap<TSource, TTarget>(name, parameters);
LogRuntimeExpression(name, expression, fromCache: false);
var compiled = expression.Compile();
return compiled.Invoke(source);
}
Expand All @@ -171,10 +178,16 @@ public TTarget Map<TSource, TTarget>(TSource source, string name, IReadOnlyDicti
public TTarget Map<TSource, TTarget>(TSource source) {
var key = new MapKey(typeof(TSource), typeof(TTarget), null);
if (_compiledMapToNewCache.TryGetValue(key, out var map)) {
if (IsRuntimeExpressionDebugEnabled()) {
var debugExpression = GetRequiredRuntimeMap<TSource, TTarget>(null, _emptyParameters);
LogRuntimeExpression(null, debugExpression, fromCache: true);
}

return ((Func<TSource, TTarget>)map).Invoke(source);
}

var expression = GetRequiredRuntimeMap<TSource, TTarget>(null, _emptyParameters);
LogRuntimeExpression(null, expression, fromCache: false);
var compiled = expression.Compile();
_compiledMapToNewCache[key] = compiled;
return compiled.Invoke(source);
Expand All @@ -193,6 +206,7 @@ public TTarget Map<TSource, TTarget>(TSource source, IReadOnlyDictionary<string,
ValidateRuntimeParameters(parameters);

var expression = GetRequiredRuntimeMap<TSource, TTarget>(null, parameters);
LogRuntimeExpression(null, expression, fromCache: false);
var compiled = expression.Compile();
return compiled.Invoke(source);
}
Expand Down
Loading