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
2 changes: 1 addition & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ jobs:
run: dotnet nuget add source https://www.myget.org/F/automapperdev/api/v3/index.json -n automappermyget

- name: Test
run: dotnet test --configuration Release --verbosity normal
run: dotnet test --configuration Release --verbosity normal /p:CollectCoverage=true /p:Threshold=94 /p:ThresholdType=line /p:ThresholdStat=Average /p:CoverletOutputFormat=opencover /p:CoverletOutput=./TestResults/ /p:ExcludeByAttribute="GeneratedCodeAttribute"

- name: Pack and push
env:
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ jobs:
fetch-depth: 0

- name: Test
run: dotnet test --configuration Release --verbosity normal
run: dotnet test --configuration Release --verbosity normal /p:CollectCoverage=true /p:Threshold=94 /p:ThresholdType=line /p:ThresholdStat=Average /p:CoverletOutputFormat=opencover /p:CoverletOutput=./TestResults/ /p:ExcludeByAttribute="GeneratedCodeAttribute"

- name: Pack and push
env:
Expand Down
69 changes: 67 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -69,7 +69,7 @@ The methods below map the DTO query expresions to the equivalent data query expr
return mapper.Map<TDataResult, TModelResult>(mappedQueryFunc(query));
}

//This version compiles the queryable expression.
//This example compiles the queryable expression.
internal static IQueryable<TModel> GetQuery1<TModel, TData>(this IQueryable<TData> query,
IMapper mapper,
Expression<Func<TModel, bool>> filter = null,
Expand All @@ -87,7 +87,7 @@ The methods below map the DTO query expresions to the equivalent data query expr
Expression<Func<TModel, object>>[] GetExpansions() => expansions?.ToArray() ?? [];
}

//This version updates IQueryable<TData>.Expression with the mapped queryable expression parameter.
//This example updates IQueryable<TData>.Expression with the mapped queryable expression argument.
internal static IQueryable<TModel> GetQuery2<TModel, TData>(this IQueryable<TData> query,
IMapper mapper,
Expression<Func<TModel, bool>> filter = null,
Expand Down Expand Up @@ -128,3 +128,68 @@ The methods below map the DTO query expresions to the equivalent data query expr
}
}
```

## Known Issues
Mapping a single type in the source expression to multiple types in the destination expression is not supported e.g.
```c#
```
[Fact]
public void Can_map_if_source_type_targets_multiple_destination_types_in_the_same_expression()
{
var mapper = ConfigurationHelper.GetMapperConfiguration(cfg =>
{
cfg.CreateMap<SourceType, TargetType>().ReverseMap();
cfg.CreateMap<SourceChildType, TargetChildType>().ReverseMap();

// Same source type can map to different target types. This seems unsupported currently.
cfg.CreateMap<SourceListItemType, TargetListItemType>().ReverseMap();
cfg.CreateMap<SourceListItemType, TargetChildListItemType>().ReverseMap();

}).CreateMapper();

Expression<Func<SourceType, bool>> sourcesWithListItemsExpr = src => src.Id != 0 && src.ItemList.Any() && src.Child.ItemList.Any(); // Sources with non-empty ItemList
Expression<Func<TargetType, bool>> target1sWithListItemsExpr = mapper.MapExpression<Expression<Func<TargetType, bool>>>(sourcesWithListItemsExpr);
}

private class SourceChildType
{
public int Id { get; set; }
public IEnumerable<SourceListItemType> ItemList { get; set; } // Uses same type (SourceListItemType) for its itemlist as SourceType
}

private class SourceType
{
public int Id { get; set; }
public SourceChildType Child { set; get; }
public IEnumerable<SourceListItemType> ItemList { get; set; }
}

private class SourceListItemType
{
public int Id { get; set; }
}

private class TargetChildType
{
public virtual int Id { get; set; }
public virtual ICollection<TargetChildListItemType> ItemList { get; set; } = [];
}

private class TargetChildListItemType
{
public virtual int Id { get; set; }
}

private class TargetType
{
public virtual int Id { get; set; }

public virtual TargetChildType Child { get; set; }

public virtual ICollection<TargetListItemType> ItemList { get; set; } = [];
}

private class TargetListItemType
{
public virtual int Id { get; set; }
}
Original file line number Diff line number Diff line change
Expand Up @@ -183,7 +183,8 @@ protected override Expression VisitLambda<T>(Expression<T> node)

protected override Expression VisitNew(NewExpression node)
{
if (this.TypeMappings.TryGetValue(node.Type, out Type newType))
Type newType = this.TypeMappingsManager.ReplaceType(node.Type);
if (newType != node.Type && !IsAnonymousType(node.Type))
{
return Expression.New(newType);
}
Expand Down Expand Up @@ -217,7 +218,8 @@ private static bool IsAnonymousType(Type type)

protected override Expression VisitMemberInit(MemberInitExpression node)
{
if (this.TypeMappings.TryGetValue(node.Type, out Type newType))
Type newType = this.TypeMappingsManager.ReplaceType(node.Type);
if (newType != node.Type && !IsAnonymousType(node.Type))
{
var typeMap = ConfigurationProvider.CheckIfTypeMapExists(sourceType: newType, destinationType: node.Type);
//The destination becomes the source because to map a source expression to a destination expression,
Expand Down Expand Up @@ -474,7 +476,8 @@ Expression DoVisitConditional(Expression test, Expression ifTrue, Expression ifF

protected override Expression VisitTypeBinary(TypeBinaryExpression node)
{
if (this.TypeMappings.TryGetValue(node.TypeOperand, out Type mappedType))
Type mappedType = this.TypeMappingsManager.ReplaceType(node.TypeOperand);
if (mappedType != node.TypeOperand)
return MapTypeBinary(this.Visit(node.Expression));

return base.VisitTypeBinary(node);
Expand All @@ -498,7 +501,8 @@ protected override Expression VisitUnary(UnaryExpression node)

Expression DoVisitUnary(Expression updated)
{
if (this.TypeMappings.TryGetValue(node.Type, out Type mappedType))
Type mappedType = this.TypeMappingsManager.ReplaceType(node.Type);
if (mappedType != node.Type)
return Expression.MakeUnary
(
node.NodeType,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Linq.Expressions;
using Xunit;

namespace AutoMapper.Extensions.ExpressionMapping.UnitTests
{
public class CanMapIfASourceTypeTargetsMultipleDestinationTypesInTheSameExpression
{
#pragma warning disable xUnit1004 // Test methods should not be skipped
[Fact(Skip = "This test is currently skipped due to unsupported scenario.")]
#pragma warning restore xUnit1004 // Test methods should not be skipped
public void Can_map_if_source_type_targets_multiple_destination_types_in_the_same_expression()
{
// Arrange
var mapper = ConfigurationHelper.GetMapperConfiguration(cfg =>
{
cfg.CreateMap<SourceType, TargetType>().ReverseMap();
cfg.CreateMap<SourceChildType, TargetChildType>().ReverseMap();

// Same source type can map to different target types. This seems unsupported currently.
cfg.CreateMap<SourceListItemType, TargetListItemType>().ReverseMap();
cfg.CreateMap<SourceListItemType, TargetChildListItemType>().ReverseMap();

}).CreateMapper();
Expression<Func<SourceType, bool>> sourcesWithListItemsExpr = src => src.Id != 0 && src.ItemList.Any() && src.Child.ItemList.Any(); // Sources with non-empty ItemList

// Act
Expression<Func<TargetType, bool>> target1sWithListItemsExpr = mapper.MapExpression<Expression<Func<TargetType, bool>>>(sourcesWithListItemsExpr);

// Assert
Assert.NotNull(target1sWithListItemsExpr);
}

#pragma warning disable xUnit1004 // Test methods should not be skipped
[Fact(Skip = "This test is currently skipped due to unsupported scenario.")]
#pragma warning restore xUnit1004 // Test methods should not be skipped
public void Can_map_if_source_type_targets_multiple_destination_types_in_the_same_expression_including_nested_parameters()
{
// Arrange
var mapper = ConfigurationHelper.GetMapperConfiguration(cfg =>
{
cfg.CreateMap<SourceType, TargetType>().ReverseMap();
cfg.CreateMap<SourceChildType, TargetChildType>().ReverseMap();

// Same source type can map to different target types. This seems unsupported currently.
cfg.CreateMap<SourceListItemType, TargetListItemType>().ReverseMap();
cfg.CreateMap<SourceListItemType, TargetChildListItemType>().ReverseMap();

}).CreateMapper();
Expression<Func<SourceType, bool>> sourcesWithListItemsExpr = src => src.Id != 0 && src.ItemList.FirstOrDefault(i => i.Id == 1) == null && src.Child.ItemList.FirstOrDefault(i => i.Id == 1) == null; // Sources with non-empty ItemList

// Act
Expression<Func<TargetType, bool>> target1sWithListItemsExpr = mapper.MapExpression<Expression<Func<TargetType, bool>>>(sourcesWithListItemsExpr);

//Assert
Assert.NotNull(target1sWithListItemsExpr);
}

private class SourceChildType
{
public int Id { get; set; }
public IEnumerable<SourceListItemType> ItemList { get; set; } // Uses same type (SourceListItemType) for its itemlist as SourceType
}

private class SourceType
{
public int Id { get; set; }
public SourceChildType Child { set; get; }
public IEnumerable<SourceListItemType> ItemList { get; set; }
}

private class SourceListItemType
{
public int Id { get; set; }
}

private class TargetChildType
{
public virtual int Id { get; set; }
public virtual ICollection<TargetChildListItemType> ItemList { get; set; } = [];
}

private class TargetChildListItemType
{
public virtual int Id { get; set; }
}

private class TargetType
{
public virtual int Id { get; set; }

public virtual TargetChildType Child { get; set; }

public virtual ICollection<TargetListItemType> ItemList { get; set; } = [];
}

private class TargetListItemType
{
public virtual int Id { get; set; }
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,36 @@ public XpressionMapperTests()

#region Tests

[Fact]
public void Map_expression_list()
{
//Arrange
ICollection<Expression<Func<UserModel, object>>> selections = [s => s.AccountModel.Bal, s => s.AccountName];

//Act
List<Expression<Func<User, object>>> selectionsMapped = [.. mapper.MapExpressionList<Expression<Func<User, object>>>(selections)];
List<object> accounts = [.. Users.Select(selectionsMapped[0])];
List<object> branches = [.. Users.Select(selectionsMapped[1])];

//Assert
Assert.True(accounts.Count == 2 && branches.Count == 2);
}

[Fact]
public void Map_expression_list_using_two_generic_arguments_override()
{
//Arrange
ICollection<Expression<Func<UserModel, object>>> selections = [s => s.AccountModel.Bal, s => s.AccountName];

//Act
List<Expression<Func<User, object>>> selectionsMapped = [.. mapper.MapExpressionList<Expression < Func<UserModel, object>>, Expression <Func<User, object>>>(selections)];
List<object> accounts = [.. Users.Select(selectionsMapped[0])];
List<object> branches = [.. Users.Select(selectionsMapped[1])];

//Assert
Assert.True(accounts.Count == 2 && branches.Count == 2);
}

[Fact]
public void Map_object_type_change()
{
Expand Down Expand Up @@ -895,6 +925,16 @@ public void Can_map_expression_with_condittional_logic_while_deflattening()
Assert.NotNull(mappedExpression);
}

[Fact]
public void Returns_null_when_soure_is_null()
{
Expression<Func<TestDTO, bool>> expr = null;

var mappedExpression = mapper.MapExpression<Expression<Func<TestEntity, bool>>>(expr);

Assert.Null(mappedExpression);
}

[Fact]
public void Can_map_expression_with_multiple_destination_parameters_of_the_same_type()
{
Expand Down