Is this possible in AutoMapper?
Select list of type X and filter child entity of type Y (return single value of Y)
ProjectTo to a flat DTO contains props from X and Y.
If it is not, then what is the best way to populate the DTO in this scenario, the tables are just for example, in the real scenario the tables have a lot of columns and I want to avoid reading the whole row just to get one or two props.
Below is a quick console App code in .Net 5.0
Project.csproj
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net5.0</TargetFramework>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="AutoMapper.Extensions.Microsoft.DependencyInjection" Version="11.0.0" />
<PackageReference Include="Microsoft.EntityFrameworkCore" Version="5.0.0" />
<PackageReference Include="Microsoft.EntityFrameworkCore.InMemory" Version="5.0.0" />
</ItemGroup>
</Project>
Console App Testing Code
using System;
using System.Linq;
using AutoMapper;
using AutoMapper.QueryableExtensions;
using Microsoft.EntityFrameworkCore;
using System.Collections.Generic;
namespace MappingTest
{
public class Parent
{
public int Id { get; set; }
public string ParentName { get; set; }
public List<Child> Children { get; set; } = new List<Child>();
}
public class Child
{
public int Id { get; set; }
public int Age { get; set; }
public string ChildName { get; set; }
public Parent Parent { get; set; }
}
public class ParentDto
{
public int Id { get; set; }
public string ParentName { get; set; }
public string ChildName { get; set; }
}
public class DataContext : DbContext
{
public DbSet<Parent> Parents { get; set; }
public DbSet<Child> Children { get; set; }
protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
{
optionsBuilder.UseInMemoryDatabase("MyDb");
}
}
internal class Program
{
public static void Seed(DataContext context)
{
context.Parents.Add(new Parent
{
Id = 1,
ParentName = "John",
Children = new List<Child>()
{
new Child { Id = 1, ChildName = "Peter", Age = 10 },
new Child { Id = 2, ChildName = "Smith", Age = 20 }
}
});
context.Parents.Add(new Parent
{
Id = 2,
ParentName = "Micheal",
Children = new List<Child>()
{
new Child { Id = 3, ChildName = "Wale", Age = 10 },
new Child { Id = 4, ChildName = "Robert", Age = 25 }
}
});
context.SaveChanges();
}
static void Main(string[] args)
{
var config = new MapperConfiguration((cfg) =>
{
cfg.CreateMap<Parent, ParentDto>();
cfg.CreateMap<Child, ParentDto>();
});
var mapper = config.CreateMapper();
using (var context = new DataContext())
{
Seed(context);
var parent = context.Parents
// Filter children and get only the 10 years old (Peter and Wale)
// Project to the Dto and have the ChildName mapped to the Dto
// Note: Parent should have only one 10 years old child
.ProjectTo<ParentDto>(mapper.ConfigurationProvider)
.ToList();
foreach(var p in parent)
{
Console.WriteLine(string.Format("{0} - {1} - {2}",p.Id, p.ParentName, p.ChildName));
}
}
}
}
}
Could not find a similar scenario with a solution
Update #1
I'm really considering Dapper No Mapper, #Prolog your answer helped a lot, I managed to solve it correctly by using the other way around
var config = new MapperConfiguration((cfg) =>
{
cfg.CreateMap<Parent, ParentDto>();
cfg.CreateMap<Child, ParentDto>()
.IncludeMembers(e => e.Parent);
});
And then ProjectTo like this
var parents = context.Parents
.SelectMany(e => e.Children.Where(a => a.Age == 10))
.ProjectTo<ParentDto>(mapper.ConfigurationProvider)
.ToList();
But the generated SQL is funny
SELECT [t].[ChildName], [t].[Id], [p0].[ParentName]
FROM [Parents] AS [p]
INNER JOIN (
SELECT [c].[Id], [c].[ChildName], [c].[ParentId]
FROM [Children] AS [c]
WHERE [c].[Age] = 10
) AS [t] ON [p].[Id] = [t].[ParentId]
LEFT JOIN [Parents] AS [p0] ON [t].[ParentId] = [p0].[Id]
Where the required SQL is very simple
SELECT p.Id,
p.ParentName,
c.ChildName
FROM dbo.Parents p
LEFT JOIN dbo.Children c ON p.Id = c.Id
-- or INNER JOIN
WHERE c.Age = 10;
You can use IncludeMembers() to tell AutoMapper to try to fill properties of ParentDto will values from Child after it is done with mapping from Parent. Read more about this feature in AutoMapper documentation.
var config = new MapperConfiguration((cfg) =>
{
cfg.CreateMap<Parent, ParentDto>()
.IncludeMembers(src => src.Children.First());
cfg.CreateMap<Child, ParentDto>();
});
Also, don't test your database-oriented projections with an In-Memory database, as it will hide all kind of query execution errors until you switch to a real database.
So if you want to filter out only to parents with a child that's 10 years old:
var parents = context.Parents
.Where(x => x.Children.Any(x => x.Age == 10))
.ProjectTo<ParentDto>(mapper.ConfigurationProvider)
.ToList();
with Microsoft SQL-Server, such query will be produced:
SELECT [p].[id],
[p].[parentname],
(SELECT TOP(1) [c0].[childname]
FROM [children] AS [c0]
WHERE [p].[id] = [c0].[parentid]) AS [ChildName]
FROM [parents] AS [p]
WHERE EXISTS (SELECT 1
FROM [children] AS [c]
WHERE ( [p].[id] = [c].[parentid] )
AND ( [c].[age] = 10 ))
Related
I have 3 types :
type [<CLIMutable>] status = { id : Guid; name : string }
type [<CLIMutable>] container = { id : Guid; name : string; status : status}
type [<CLIMutable>] scontainer = { id : Guid; name : string; status : string}
and next configuration
let c =
MapperConfiguration(
fun config ->
config.CreateMap<container, scontainer>()
.ForMemberFs((fun sc -> sc.name), (fun opts -> opts.MapFrom(fun m _ -> m.status.name))) |> ignore
)
I'm trying to map with next code
let con = { id = Guid.NewGuid(); name = "Template 1"; container.status = { id = Guid.NewGuid(); name = "Draft" } }
let mapper = c.CreateMapper()
let sc = mapper.Map<scontainer>(con)
But member mapping isn't called and sc.status contains a string representation of status object(id and name together).
When I add new map:
config.CreateMap<status, string>().ConvertUsing(fun s -> s.name) |> ignore
then sc.status contains correct value.
Does anyone know how to make it work without additional mappings?
Next lines solves my problem:
let c =
AutoMapper.MapperConfiguration(
fun config ->
config.ShouldMapProperty <- fun _ -> false
config.ShouldMapField <- fun _ -> true
)
Metadata produced for constructor is (id, name, status) in both scontainer and container.
Metadata produced for properties is: id, name, status in both scontainer and container.
Metadata produced for fields is: id#, name#, status# in both scontainer and container.
If I don't disable properties usage then automapper will find a match between constructor parameters and properties and will use properties as resolver, which means constructor resolver will be used.
If I disable properties usage, then there will be no match between constructor parameters and fields and constructor will no be used.
I might be wrong but bug is in the next few lines in method: private void MapDestinationCtorToSource(TypeMap typeMap, List ctorParamConfigurations)
in next code:
var resolvers = new LinkedList();
var canResolve = typeMap.Profile.MapDestinationPropertyToSource(typeMap.SourceTypeDetails, destCtor.DeclaringType, parameter.GetType(), parameter.Name, resolvers, IsReverseMap);
if ((!canResolve && parameter.IsOptional) || IsConfigured(parameter))
{
canResolve = true;
}
Here is a test that reproduces a bug
using AutoMapper;
using System;
using Xunit;
namespace ConstructorBug
{
public class Status
{
public Guid Id { get; set; }
public string Name { get; set; }
}
public class Container
{
public Guid Id { get; set; }
public string Name { get; set; }
public Status Status { get; set; }
}
public class SContainer
{
public SContainer()
{
}
public SContainer(string id, string name, string status)
{
Id = id;
Name = name;
Status = status;
}
public string Id { get; set; }
public string Name { get; set; }
public string Status { get; set; }
}
public class ConstructorBug
{
[Fact]
public void ConstructorMapBug()
{
var mapperConfiguration = new MapperConfiguration(
config =>
{
config.CreateMap<Container, SContainer>()
.ForMember(dest => dest.Status, opts => opts.MapFrom(x => x.Status.Name));
}
);
var mapper = mapperConfiguration.CreateMapper();
var con = new Container
{
Id = Guid.NewGuid(),
Name = "Container 1",
Status = new Status { Id = Guid.NewGuid(), Name = "Draft" }
};
var scon = mapper.Map<SContainer>(con);
Assert.Equal(scon.Id, con.Id.ToString());
Assert.Equal(scon.Name, con.Name);
Assert.Equal(scon.Status, con.Status.Name);
}
}
}
I found this post describing how to conditionally copy values over a destination object if they are not null.
Works great except for list members, it always overwrites them with an empty list. I'm not sure if I've just not configured the mapper correctly or if this is a bug. The following program demonstrates the issue.
namespace automapper_test
{
using AutoMapper;
using System;
using System.Collections.Generic;
class Program
{
class Test
{
public int? A { get; set; }
public string B { get; set; }
public Guid? C { get; set; }
public List<Guid> D { get; set; }
}
static void Main(string[] args)
{
var config = new MapperConfiguration(cfg =>
{
cfg.AllowNullCollections = true;
cfg.CreateMap<Test, Test>().ForAllMembers(opt => opt.Condition((src, dest, member) => member != null));
});
var mapper = config.CreateMapper();
var source = new Test { A = 2, C = Guid.Empty };
var target = new Test { A = 1, B = "hello", C = Guid.NewGuid(), D = new List<Guid> { Guid.NewGuid() } };
mapper.Map(source, target);
System.Diagnostics.Debug.Assert(target.D.Count == 1);
}
}
}
I need some help to map an anonymous object using Automapper. The goal is combine Product and Unity in a ProductDto (in which unity is a product's property).
Autommaper CreateMissingTypeMaps configuration is set to true.
My Classes:
public class Product
{
public int Id { get; set; }
}
public class Unity
{
public int Id { get; set; }
}
public class ProductDto
{
public int Id { get; set; }
public UnityDto Unity{ get; set; }
}
public class UnityDto
{
public int Id { get; set; }
}
Test Code
Product p = new Product() { Id = 1 };
Unity u = new Unity() { Id = 999 };
var a = new { Product = p, Unity = u };
var t1 = Mapper.Map<ProductDto>(a.Product);
var t2 = Mapper.Map<UnityDto>(a.Unity);
var t3 = Mapper.Map<ProductDto>(a);
Console.WriteLine(string.Format("ProductId: {0}", t1.Id)); // Print 1
Console.WriteLine(string.Format("UnityId: {0}", t2.Id)); // Print 999
Console.WriteLine(string.Format("Anonymous ProductId: {0}", t3.Id)); // Print 0 <<< ERROR: It should be 1 >>>
Console.WriteLine(string.Format("Anonymous UnityId: {0}", t3.Unity.Id)); // Print 999
There are two maps added to the profile:
CreateMap<Product, ProductDto>();
CreateMap<Unity, UnityDto>();
The problem is how Automapper map anonymous objects. I haven't time to check out Automapper source code but I got the desired behaviour with minor changes on my anonymous object:
var a = new { Id = p.Id, Unity = u };
By doing this, I might even delete previous mappings because now it is using only CreateMissingTypeMaps.
Note: As matter of fact I'm not sure if it is really an issue or I it was just my unreal expectations.
Consider the following situation:
public class EntityA
{
public int Id { get; set; }
}
public class EntityB
{
public int Id { get; set; }
public virtual EntityA EntityA { get; set; }
}
public class Context : DbContext
{
public Context()
: base("EF_SPIKE")
{
}
public IDbSet<EntityA> EntityAs { get; set; }
public IDbSet<EntityB> EntityBs { get; set; }
}
static void Main(string[] args)
{
using (var context = new Context())
{
var a = context.EntityAs.Create();
context.EntityAs.Add(a);
var b = context.EntityBs.Create();
b.EntityA = a;
context.EntityBs.Add(b);
context.SaveChanges();
using (var transaction = new TransactionScope())
{
context.EntityBs.Remove(b);
context.SaveChanges();
}
Trace.Assert(b.EntityA == null);
Trace.Assert(context.EntityBs.Local.All(x => x.Id != b.Id));
Trace.Assert(context.EntityBs.Any(x => x.Id == b.Id));
}
}
So although the transaction is rolled back and the entity still exists in the database, the entity framework entity b no longer exists in the local cache, loses all of its foreign key references and b can no longer be worked with.
This behaviour seems odd to me since although I've rolled back, my entity is still dead.
Is there any standard workaround to this so that:
Trace.Assert(context.EntityBs.Local.Any(x => x.Id == b.Id));
passes?
I have used automapper for mapping lists in the past, for for some reason it won't work in this case.
public class MyType1 {
public int Id { get; set; }
public string Description { get; set; }
}
public class MyType2 {
public int Id { get; set; }
public string Description { get; set; }
}
public void DoTheMap() {
Mapper.CreateMap<MyType2, MyType1>();
Mapper.AssertConfigurationIsValid();
var theDto1 = new MyType2() { Id = 1, Description = "desc" };
var theDto2 = new MyType2() { Id = 2, Description = "desc2" };
List<MyType2> type2List = new List<MyType2> { theDto1, theDto2 };
List<MyType1> type1List = Mapper.DynamicMap<List<MyType1>>(type2List);
//FAILURE. NO EXCEPTION, BUT ZERO VALUES
List<MyType1> type1List2 =type2List.Select(Mapper.DynamicMap<MyType1>).ToList();
//SUCCESS, WITH LINQ SELECT
}
Change this:
Mapper.DynamicMap<List<MyType1>>(type2List)
To this:
Mapper.Map<List<MyType1>, List<MyType2>>(type2List);
DynamicMap is only if you don't know the type at compile time - for things like anonymous types.