Skip to content
Merged
Show file tree
Hide file tree
Changes from 2 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,94 @@
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()
{
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);
}

#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()
{
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
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 @@ -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