C
C#2y ago
Anton

❔ Using automapper to update EF Core entities recursively

I'm trying the neat idea to try and make a generic create/update method which supports includes. The first branch takes care of inserts. It takes requests of this form (currently doesn't check the nested ids, which it should):
{
"name": "John",
"projects": [
{
"projectName": "A"
},
{
"projectName": "B"
}
]
}
{
"name": "John",
"projects": [
{
"projectName": "A"
},
{
"projectName": "B"
}
]
}
It then maps this dto to a recursively tracked entity. The second branch works with requests of this form, taking care of updates:
{
"id": 1,
"projects": [
{
"id": 7,
"projectName": "Changed"
},
{
// Should not update anything, but try and find the project with this id.
"id": 8
},
{
"projectName": "Q"
}
]
}
{
"id": 1,
"projects": [
{
"id": 7,
"projectName": "Changed"
},
{
// Should not update anything, but try and find the project with this id.
"id": 8
},
{
"projectName": "Q"
}
]
}
Currently I'm doing a bunch of things that are related to nested entities manually. I'm manually sorting the projects in reverse order, so that the projects with null key (projects to be inserted), end up at the end of the list. I'm then querying the entity from the db, checking if all of the projects meant for updating were found, and then I do a recursive copy of the dto into that entity. This maps all properties of the person and of each nested project. At least it should, what actually happens is it copies the null values over as well (see the mapper profile below). Then the tracker picks up on all of the newly mapped projects. My question is, why does automapper map null properties even though it's set to ignore them, and is there a way to generalize this thing? The way I came up with doesn't seem overly complex, it's just not fully generic. I want the both the include filtering and the mappings to work independent of the level of depth of the objects.
12 Replies
Anton
AntonOP2y ago
app.MapPost("/people", async(
[FromServices] ApplicationDbContext data,
[FromBody] PersonRequestDto person,
[FromServices] IMapper mapper) =>
{
Person entity;
if (person.Id == default)
{
entity = mapper.Map<Person>(person);
data.ChangeTracker.TrackGraph(entity, e =>
{
e.Entry.State = EntityState.Added;
});
}
else
{
person.Projects.Sort((a, b) =>
{
var v0 = a.Id ?? default;
var v1 = b.Id ?? default;
return v1.CompareTo(v0);
});

var projectIds = person.Projects
.Select(p => p.Id)
.SelectNonZeroValue()
.ToHashSet();

entity = await data.Set<Person>()
.Include(p => p.Projects
.OrderBy(proj => -proj.Id)
.Where(proj => projectIds.Contains(proj.Id)))
.FirstAsync(p => p.Id == person.Id);

if (entity.Projects.Count != projectIds.Count)
throw new Exception("Invalid project ids present in request.");

mapper.Map(person, entity);
data.ChangeTracker.TrackGraph(entity, e =>
{
if (e.Entry.IsKeySet)
e.Entry.State = EntityState.Modified;
else
e.Entry.State = EntityState.Added;
});
}

await data.SaveChangesAsync();
return mapper.Map<PersonResponseDto>(entity);
});
app.MapPost("/people", async(
[FromServices] ApplicationDbContext data,
[FromBody] PersonRequestDto person,
[FromServices] IMapper mapper) =>
{
Person entity;
if (person.Id == default)
{
entity = mapper.Map<Person>(person);
data.ChangeTracker.TrackGraph(entity, e =>
{
e.Entry.State = EntityState.Added;
});
}
else
{
person.Projects.Sort((a, b) =>
{
var v0 = a.Id ?? default;
var v1 = b.Id ?? default;
return v1.CompareTo(v0);
});

var projectIds = person.Projects
.Select(p => p.Id)
.SelectNonZeroValue()
.ToHashSet();

entity = await data.Set<Person>()
.Include(p => p.Projects
.OrderBy(proj => -proj.Id)
.Where(proj => projectIds.Contains(proj.Id)))
.FirstAsync(p => p.Id == person.Id);

if (entity.Projects.Count != projectIds.Count)
throw new Exception("Invalid project ids present in request.");

mapper.Map(person, entity);
data.ChangeTracker.TrackGraph(entity, e =>
{
if (e.Entry.IsKeySet)
e.Entry.State = EntityState.Modified;
else
e.Entry.State = EntityState.Added;
});
}

await data.SaveChangesAsync();
return mapper.Map<PersonResponseDto>(entity);
});
public static class AutoMapperExtensions
{
public static IMappingExpression<TSource, TDestination> UnmapAllNulls<TSource, TDestination>(this IMappingExpression<TSource, TDestination> expression)
{
expression.ForAllMembers(opts =>
opts.Condition((_, _, srcMember) => srcMember is not null));
return expression;
}
}

public class MapperProfile : Profile
{
void CreateNonNullableToDefaultMap<T>() where T : struct
{
CreateMap<T?, T>().ConvertUsing((src, dest) => src ?? dest);
}

public MapperProfile()
{
CreateNonNullableToDefaultMap<int>();
CreateNonNullableToDefaultMap<long>();

CreateMap<PersonRequestDto, Person>(MemberList.Source)
.UnmapAllNulls();
CreateMap<PersonProjectRequestDto, Project>(MemberList.Source)
.UnmapAllNulls();

CreateMap<Person, PersonResponseDto>(MemberList.Destination);
CreateMap<Project, PersonProjectResponseDto>(MemberList.Destination);

CreateMap<ProjectRequestDto, Project>(MemberList.Source)
.UnmapAllNulls();
CreateMap<Project, ProjectResponseDto>(MemberList.Destination);
}
}
public static class AutoMapperExtensions
{
public static IMappingExpression<TSource, TDestination> UnmapAllNulls<TSource, TDestination>(this IMappingExpression<TSource, TDestination> expression)
{
expression.ForAllMembers(opts =>
opts.Condition((_, _, srcMember) => srcMember is not null));
return expression;
}
}

public class MapperProfile : Profile
{
void CreateNonNullableToDefaultMap<T>() where T : struct
{
CreateMap<T?, T>().ConvertUsing((src, dest) => src ?? dest);
}

public MapperProfile()
{
CreateNonNullableToDefaultMap<int>();
CreateNonNullableToDefaultMap<long>();

CreateMap<PersonRequestDto, Person>(MemberList.Source)
.UnmapAllNulls();
CreateMap<PersonProjectRequestDto, Project>(MemberList.Source)
.UnmapAllNulls();

CreateMap<Person, PersonResponseDto>(MemberList.Destination);
CreateMap<Project, PersonProjectResponseDto>(MemberList.Destination);

CreateMap<ProjectRequestDto, Project>(MemberList.Source)
.UnmapAllNulls();
CreateMap<Project, ProjectResponseDto>(MemberList.Destination);
}
}
public static class EnumerableExtensions
{
public static IEnumerable<T> SelectNonZeroValue<T>(this IEnumerable<T?> source) where T : struct, INumber<T>
{
return source
.Where(x => x.HasValue)
.Select(x => x!.Value)
.Where(x => x != default);
}
}
public static class EnumerableExtensions
{
public static IEnumerable<T> SelectNonZeroValue<T>(this IEnumerable<T?> source) where T : struct, INumber<T>
{
return source
.Where(x => x.HasValue)
.Select(x => x!.Value)
.Where(x => x != default);
}
}
(the check != default should disallowed by validation, but I'm just trying things rn) The request dtos have nullable ids, the entities have normal ids The automapper behavior is weird: - It doesn't track new elements with the same id. So it seems to be keeping the elements already present in the list; - It does reset the projectName to null prior to checking the condition in UnmapAllNulls, while ending up checking it afterward. Since it doesn't create new elements if they're already present in the collection, it must be resetting the instances. But I'm gonna make extra sure; ah yeah it does recreate the elements
Anton
AntonOP2y ago
GitHub
GitHub - AutoMapper/AutoMapper.Collection: AutoMapper support for u...
AutoMapper support for updating existing collections by equivalency - GitHub - AutoMapper/AutoMapper.Collection: AutoMapper support for updating existing collections by equivalency
Anton
AntonOP2y ago
it's weird why ef core doesn't yell at me that there are tracked entities with the same ids
Jayy
Jayy2y ago
No part of this idea seems neat, it's seems awful
Anton
AntonOP2y ago
well, if that can be made generic, it could both do complex, nested updates in a single query to the server, and for all types that exist for the sake of CRUD at once. I'm hoping something like this can be achieved with graphql, reading the docs rn
Jayy
Jayy2y ago
ya its still an awful idea, you have somehow made automapper (by itself an awful idea) and made it worse lol
Anton
AntonOP2y ago
well thousands of lines of boilerplate for 100 types vs one manageable method might be a good tradeoff
Jayy
Jayy2y ago
no it wont be, you lose all type safety i have spent the past year ripping automapper out of our api for that very reason
Anton
AntonOP2y ago
do you mean like if the mapping has not been defined?
Jayy
Jayy2y ago
no, i mean the mapping is not type safe lol
Anton
AntonOP2y ago
to me, checking if all maps resolve with a test is good enough but that's to me
Accord
Accord2y ago
Was this issue resolved? If so, run /close - otherwise I will mark this as stale and this post will be archived until there is new activity.
Want results from more Discord servers?
Add your server