C
C#16mo 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
Anton16mo 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
Anton16mo 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
Anton16mo ago
it's weird why ef core doesn't yell at me that there are tracked entities with the same ids
Jayy
Jayy16mo ago
No part of this idea seems neat, it's seems awful
Anton
Anton16mo 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
Jayy16mo ago
ya its still an awful idea, you have somehow made automapper (by itself an awful idea) and made it worse lol
Anton
Anton16mo ago
well thousands of lines of boilerplate for 100 types vs one manageable method might be a good tradeoff
Jayy
Jayy16mo 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
Anton16mo ago
do you mean like if the mapping has not been defined?
Jayy
Jayy16mo ago
no, i mean the mapping is not type safe lol
Anton
Anton16mo ago
to me, checking if all maps resolve with a test is good enough but that's to me
Accord
Accord16mo 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
More Posts
Mvvm problemWhy is the text block not updating dynamically when I enter data. It will only update when I click o❔ Serilog filteringWe’re getting a lot of service health check spam in our logs, and we’re only really interested in wh❔ Understanding project structure - Visual Studio ASP.NET CoreI was assigned a task, to split a big Web Service into smaller independent Web Services. The thing i❔ CRUD(REST) + DDDHi, I’m developing a simple CRUD messenger and trying to also have it DDD’ed. The problem is that th✅ ERROR: An exception was thrown while attempting to evaluate a LINQ query parameter expressionwhen a user clicks in this button. He will be go to the AddToCart() method in my ShoppingCartControl✅ If else statement problemEven or odd number I got a problem to write an app to specify every digit from a number of 4 digits❔ Razor Pages: Temporary Sites like Doodle Surveys with an Share Link?Hey everyone, is it Possible in Razor Pages to create "temporary" Pages?` I would like to create som✅ nuget naming nuances - Microsoft.Extensions.Configuration.Yaml doesn't belong to MicrosoftCan package names be whatever the uploader desires and there's no indication of hierarchy, as far as❔ Creating a base abstract class to ensure a set of derived classes receive similar dependencies?Hi, working on creating a class library and I have a set of classes, let's call them PlainPizza, Pep❔ Using razor pages to generate a list and send to code fileSo I have a table called GenericEvents, and I have various activities that use this generic event to