AutoMapper.Collection - Skip mapping entirely if source collection property is null - automapper

Is there any way to get AutoMapper.Collection to skip the mapping of a collection if the source collection property is null?
In my case, the client may want to signal to the API that there is no need to update a given collection. A logical way do this would be to set the collection property to null in the dto sent from the client.
Basically:
If the source collection property is null: Do not touch the destination collection at all but leave it as-is. Do not clear it
If the source collection is not null: Do the collection mapping. If the source collection is empty this means clearing the destination collection
Is there any way to achieve this in AutoMapper.Collection? What I am looking for is this:
// Sample Classes
public class Entity
{
public ICollection<EntityChild> Children { get; set; }
}
public class EntityChild
{
public int Id { get; set; }
public string Value { get; set; }
}
public class Dto
{
public ICollection<DtoChild> Children { get; set; }
}
public class DtoChild
{
public int Id { get; set; }
public string Value { get; set; }
}
// AutoMapper setup including equality for children
CreateMap<Dto, Entity>();
CreateMap<DtoChild, EntityChild>()
.EqualityComparison((src, dst) => src.Id == dst.Id)
.ReverseMap();
// Sample 1, null source collection
var entity = new Entity
{
Children = new List<EntityChild>
{
new() { Id = 1, Value = "Value 1" },
new() { Id = 2, Value = "Value 2" }
}
};
var dtoSkipChildren = new Dto
{
Children = null
};
// Since the source Children property is null, do not update the destination collection
mapper.Map(dtoSkipChildren, entity);
// Sample 2, empty source collection
entity = new Entity
{
Children = new List<EntityChild>
{
new() { Id = 1, Value = "Value 1" },
new() { Id = 2, Value = "Value 2" }
}
};
var dtoClearChildren = new Dto
{
Children = new List<DtoChild>()
};
// Now the source children is not null (but empty) so the destination collection should
// be updated (in this case cleared since the source collection is empty)
mapper.Map(dtoClearChildren, entity);
AutoMapper.Collection treats the null source Children property the same as the source Children property containing an empty collection. In both cases the destination Children collection is cleared.
I have tried to tell AutoMapper to skip the source.Children property if null:
CreateMap<Dto, Entity>()
.ForMember(dst => dst.Children, opt => opt.Condition(src => null != src.Children));
This does not change things. Also in this case, the source collection property is null when AutoMapper.Collection sets to work and the destination collection is cleared.
This is not a real solution either:
CreateMap<Dto, Entity>()
.ForMember(
src => src.Children,
opt => opt.MapFrom((src, dst, _, ctx) => src.Children ?? ctx.Mapper.Map<ICollection<DtoChild>>(dst.Children)));
This means reverse mapping the destination collection to the (null) source collection, so it can be mapped back. An ugly hack:
It's crossing the river twice to end up where you started
Wasted effort which is silly on large collections
Risky as the reverse map for other reasons may not be 100% thus introducing pretty hidden bugs
Does anyone have an advice on how to achieve this - or why my use case is not a good idea?

I had the same problem and I figured it out this way
CreateMap<Dto, Entity>()
.ForMember(
dest=> dest.Children,
opt => {
opt.PreCondition(src =>src.Children!= null);
opt.MapFrom(src =>src.Children);
});
Then in the place, you want to use mapping write code like this:
entity = mapper.Map<Dto, Entity>(dto , entity);
opt.PreCondition(src =>src.Children!= null) basically says to AutoMapper to proceed to map if the source field is not empty else do not map.
to read more look at this https://docs.automapper.org/en/stable/Conditional-mapping.html#preconditions

Related

"Traditional" one-to-many Query with RavenDB

I know the include-feature of RavenDB. It allows me to fetch a referenced document right away in one roundtrip to the database. But my problem is: The document i fetch in the first place is not including a reference to the "other" documents. But the "other" documents have references to the current document.
Imagine a setup where we have sites across the world. Each site may trigger various alarms. Each alarm has a reference to the site via siteId.
Now i would like to get a list of all the sites including all alarms. But it looks like, this is not possible with RavenDB? Since include only accepts a "path" in the site-Document which holds an id (or an array of ids) to the referenced document.
This could be solved by providing an array of alarmIds within the site'-document and referencing this array in include. But in contrast to a lot of examples featuring stuff like an orderwithlineItemswhere the order is a self contained thing, mysite` will be running for years, collecting alarms anywhere between 0 and a million. Which seems to be a bad idea to me.
Of course i could go the other way round: Query all alarms and include the sites via sitesId. But this would not return a site that has zero alarms.
So is this just a design error on my side? To i misunderstand something? Or is it just not possible to do this in one query and prevent a "n+1 query"?
public class A
{
public string Id { get; set; }
}
public class B
{
public string Id { get; set; }
public string A { get; set; }
}
public class MultiMapIndex : AbstractMultiMapIndexCreationTask<MultiMapIndex.Result>
{
public class Result
{
public string Id { get; set; }
public IEnumerable<string> Bs { get; set; }
}
public MultiMapIndex()
{
AddMap<A>(items => from a in items
select new Result {Id = a.Id, Bs = new string[0]});
AddMap<B>(items => from b in items
select new Result {Id = b.A, Bs = new[] {b.Id}});
Reduce = results => from result in results
group result by result.Id
into g
select new Result {Id = g.Key, Bs = g.SelectMany(r => r.Bs)};
}
}
[Fact]
public async Task TestCase()
{
using var store = GetDocumentStore();
await new MultiMapIndex().ExecuteAsync(store);
using (var session = store.OpenAsyncSession())
{
await session.StoreAsync(new B {A = "a/1"}, "b/0");
await session.StoreAsync(new A(), "a/1");
await session.StoreAsync(new A(), "a/2");
await session.SaveChangesAsync();
}
WaitForIndexing(store);
using (var session = store.OpenAsyncSession())
{
var results = await session.Query<MultiMapIndex.Result, MultiMapIndex>()
.Include(r => r.Bs)
.ToArrayAsync();
var before = session.Advanced.NumberOfRequests;
var bs = session.LoadAsync<B>(results[0].Bs);
Assert.Equal(before, session.Advanced.NumberOfRequests);
}
}
If you do choose to query all Alarms, as you mention,
then you can create a Map-Reduce index on the Alarms collection which will group-by the Sites.
Then you can query this Map-Reduce index and know per Site the count of Alarms it has or doesn't have...
https://demo.ravendb.net/demos/csharp/static-indexes/map-reduce-index

Where to place calculated property of HasChildren for Domain Model?

We have an Department model (domain-driven design). Each department has its child departments, so domain model looks like
public class Department
{
int Id { get; set; }
...
ICollection<Department> Children { get; set; }
}
At the API domain models of the same hierarchy path, coming from repository, it will transforms to DTO trough AutoMapper and does not include children by default.
public class DepartmentDto
{
int Id { get; set; }
...
ICollection<DepartmentDto> Children { get; set; } // Empty set.
}
Does it a good way to add [NotMapped] bool HasChildren property to the Department domain model to show or hide expand arrows at the client? For lazy load.
This field smells strange: can be filled or can be not (depends on query).
Repository returns a collection of departments, belongs to parent Id (may become Null to root nodes):
ICollection<Department> GetDepartments(int? parentId = null);
So, based on Lucian Bargaoanu comments, I've found the solution:
IDepartmentRepository.cs
IQueryable<Department> GetDepartmentsQuery(int? parentId = null);
DepartmentsController.cs (API):
[HttpGet]
public async Task<ActionResult<ICollection<DepartmentDto>>> GetRootDepartments()
{
var dtoItems = await _repository.GetDepartmentsQuery()
.ProjectTo<DepartmentDto>(_mapper.ConfigurationProvider)
.ToListAsync();
return Ok(dtoItems);
}
AutoMapper configuration:
CreateMap<Department, DepartmentDto>()
.ForMember(x => x.HasChildren,
opts => opts.MapFrom(x => x.Children.Any()))
.ForMember(x => x.Children,
opts => opts.Ignore());

Automapper UseDestinationValue() or Ignore() both setting integer destination value to 0

First of all there are several similar questions and I have tried looking at them every where but none of them worked .
I have this mapping code
var updateEntities = await _dbContext.AnswerAnchors.Where(x => x.SurveyResultId == surveyResultId).ToListAsync();
updateEntities = _mapper.Map<IEnumerable<AnswerAnchorsDomainModel>, List<AnswerAnchors>>(anchors, updateEntities);
_dbContext.UpdateRange(updateEntities);
And here is my destination Object structure .
public class AnswerAnchors : BaseEntity<long>
{
public long SurveyResultId { get; set; }
public DateTimeOffset AnchorDate { get; set; }
public string AnchorDescription { get; set; }
}
public class BaseEntity<T> : IBaseEntity
where T : struct
{
[Key]
public T Id { get; set; }
}
What is happening is the existing Id needs to be mapped and not overwritten by the destination under any circumstances because entity framework needs to update the entity. However whenever the mapping happens the Id property gets overwritten to zero(0). I tried using opt.Ignore() and opt.UseDestinationValue() but none of them retains the destination Id value.
Here is the profile -
CreateMap<AnswerAnchorsDomainModel, AnswerAnchors>()
.ForMember(anchor => anchor.Id, opt => opt.UseDestinationValue())
.AfterMap((src, dest) =>
{
dest.CreatedBy = "";
dest.LastUpdateBy = "";
dest.CreateOn = DateTimeOffset.Now;
dest.UpdateOn = DateTimeOffset.Now;
});
My work around to this, fetching all data from repo, do a deep clone, then _context.Entry(entity here).State = EntityState.Detached; mapping all retaliated object _mapper.Map(source, destination) assigning the id's back using the clone object .... witch change the state back to modified, then await _repo.SaveAllAsync() ....I do this for each related object. Not the best of ways, but it works for now.
Another workaround and better way is the get updateEntities from repo witch contains the id properties in detached state ...
then foreach loop the AnswerAnchorsDomainModel assighn the id's with query in updateEntities to AnswerAnchorsDomainModel
_mapper.Map, List>(anchors,
updateEntities);
Change the updateEntities state back to modified.
Save Changes...
Done..
This way you map the correct id's to the update enity's ...
now EF and in my case EFcore, know's what to update and what to add.

Automapper mapping IList<> to Iesi.Collections.Generic.ISet<>

I am having some issues in the mapping mentioned in the title. Here are the details:
class MyDomain
{
public Iesi.Collections.Generic.ISet<SomeType> MySomeTypes{ get; set; }
....
}
class MyDTO
{
public IList<SomeTypeDTO> MySomeTypes{ get; set; }
...
}
The mapping:
Mapper.CreateMap<MyDomain, MyDTO>().ForMember(dto=>dto.MySomeTypes, opt.ResolveUsing<DomaintoDTOMySomeTypesResolver>());
Mapper.CreateMap<MyDTO, MyDomain>().ForMember(domain=>domain.MySomeTypes, opt.ResolveUsing<DTOtoDomainMySomeTypesResolver>());
The Resolvers:
class DomaintoDTOMySomeTypesResolver: ValueResolver<MyDomain, IList<SomeTypeDTO>>
{
protected override IList<SomeTypeDTO> ResolveCore(MyDomain source)
{
IList<SomeTypeDTO> abc = new List<DemandClassConfigurationDTO>();
//Do custom mapping
return abc;
}
}
class DTOtoDomainMySomeTypesResolver: ValueResolver<MyDTO, Iesi.Collections.Generic.ISet<SomeType>>
{
protected override Iesi.Collections.Generic.ISet<SomeType> ResolveCore(SystemParameterDTO source)
{
Iesi.Collections.Generic.ISet<SomeType> abc = new HashedSet<SomeType>();
//Do custom mapping
return abc;
}
}
Mapping from Domain to DTO works ok and as expected I get a MyDTO object with IList of "SomeTypeDTO" objects.
However mapping of the DTO to Domain throws the following error:
Exception of type 'AutoMapper.AutoMapperMappingException' was thrown.
----> AutoMapper.AutoMapperMappingException : Trying to map Iesi.Collections.Generic.HashedSet`1[SomeType, MyAssembly...] to Iesi.Collections.Generic.ISet`1[SomeType, MyAssembly...]
Exception of type 'AutoMapper.AutoMapperMappingException' was thrown.
----> System.InvalidCastException : Unable to cast object of type 'System.Collections.Generic.List`1[SomeType]' to type 'Iesi.Collections.Generic.ISet`1[SomeType]
What might I be doing wrong and what do the error messages imply? It almost seems that automapper is having some issues in mapping the ISet ( together with its concrete implementation HashedSet). My understanding is that in the above described scenario automapper should just use the ISet reference returned by "DTOtoDomainMySomeTypesResolver". I also don't see why I am getting the "cast from List to ISet error".
This is because AutoMapper currently doesn't support ISet<> collection properties. It works when the destination property of ISet<> is already instantiated (is not null), because the ISet<> actually inherits from ICollection<>, thus Automapper can understand that and will do the collection mapping properly.
It doesn't work when the destination property is null and is interface type. You get this error, because automapper actually found out it can be assigned from ICollection<> so it instantiates the property using generic List<>, which is default collection when automapper must create new collection property, but then when it tries to actually assign it, it will fail, because obviously List<> cannot be cast to ISet<>
There are three solution to this:
Create a feature request to support ISet<> collections and hope they will add it
Make sure the property is not null. Eg. instantiate it in constructor to empty HashSet<>. This might cause some troubles for ORM layers, but is doable
The best solution that I went with is to create custom value resolver, which you already have and instantiate the property yourself if it is null. You need to implement the IValueResolver, because the provided base ValueResolver will not let you instantiate the property. Here is the code snippet that I used:
public class EntityCollectionMerge : IValueResolver
where TDest : IEntityWithId
where TSource : IDtoWithId
{
public ResolutionResult Resolve(ResolutionResult source)
{
//if source collection is not enumerable return
var sourceCollection = source.Value as IEnumerable;
if (sourceCollection == null) return source.New(null, typeof(IEnumerable));
//if the destination collection is ISet
if (typeof(ISet).IsAssignableFrom(source.Context.DestinationType))
{
//get the destination ISet
var destSet = source.Context.PropertyMap.GetDestinationValue(source.Context.DestinationValue) as ISet;
//if destination set is null, instantiate it
if (destSet == null)
{
destSet = new HashSet();
source.Context.PropertyMap.DestinationProperty.SetValue(source.Context.DestinationValue, destSet);
}
Merge(sourceCollection, destSet);
return source.New(destSet);
}
if (typeof(ICollection).IsAssignableFrom(source.Context.DestinationType))
{
//get the destination collection
var destCollection = source.Context.PropertyMap.GetDestinationValue(source.Context.DestinationValue) as ICollection;
//if destination collection is null, instantiate it
if (destCollection == null)
{
destCollection = new List();
source.Context.PropertyMap.DestinationProperty.SetValue(source.Context.DestinationValue, destCollection);
}
Merge(sourceCollection, destCollection);
return source.New(destCollection);
}
throw new ArgumentException("Only ISet and ICollection are supported at the moment.");
}
public static void Merge(IEnumerable source, ICollection destination)
{
if (source == null) return;
var destinationIds = destination.Select(x => x.Id).ToHashSet();
var sourceDtos = source.ToDictionary(x => x.Id);
//add new or update
foreach (var sourceDto in sourceDtos)
{
//if the source doesnt exist in destionation add it
if (sourceDto.Key (sourceDto.Value));
continue;
}
//update exisiting one
Mapper.Map(sourceDto.Value, destination.First(x => x.Id == sourceDto.Key));
}
//delete entity in destination which were removed from source dto
foreach (var entityToDelete in destination.Where(entity => !sourceDtos.ContainsKey(entity.Id)).ToList())
{
destination.Remove(entityToDelete);
}
}
}
Then on your mapping use opt => opt.ResolveUsing(new EntitCollectionMerge<Entity,Dto>()).FromMember(x => x.ISetMember) or if you have lots of collection like this you can add them automatically to all of them via typeMaps.

Automapper ignoring ignore for properties

I would like to ignore certain properties when mapping deep (ie levels > 1) object models.
The following test works fine:
class Foo
{
public string Text { get; set; }
}
class Bar
{
public string Text { get; set; }
}
Mapper.CreateMap<Foo, Bar>()
.ForMember(x => x.Text, opts => opts.Ignore());
var foo = new Foo { Text = "foo" };
var bar = new Bar { Text = "bar" };
Mapper.Map(foo, bar);
Assert.AreEqual("bar", bar.Text);
However when I try to do the same mapping but have Foo and Bar as properties on a parent class the following test fails:
class ParentFoo
{
public Foo Child { get; set; }
}
class ParentBar
{
public Bar Child { get; set; }
}
Mapper.CreateMap<ParentFoo, ParentBar>();
Mapper.CreateMap<Foo, Bar>()
.ForMember(x => x.Text, opts => opts.Ignore());
var parentFoo = new ParentFoo
{
Child = new Foo { Text = "foo" }
};
var parentBar = new ParentBar
{
Child = new Bar { Text = "bar" }
};
Mapper.Map(parentFoo, parentBar);
Assert.AreEqual("bar", parentBar.Child.Text);
Instead of ignoring the text of the Child class (ie left it as "bar") automapper sets the value to null. What am I doing wrong with my mapping configuration?
There are two ways Automapper can perform the mapping. The first way is to simply give Automapper your source object and it will create a new destination object and populate everything for you. This is the way most apps use Automapper. However, the second way is to give it both a source and an existing destination and Automapper will update the existing destination with your mappings.
In the first example, you're giving it an existing destination value so Automapper will use that. In the second example, Automapper is going to do the mapping for ParentFoo and ParentBar, but when it gets to the Child, it's going to create a new Child and then do the mapping (this is the default behavior). This results in the Text property being null. If you want to use the existing Child object, you'll need to configure Automapper to do that with UseDestinationValue:
Mapper.CreateMap<ParentFoo, ParentBar>()
.ForMember(b => b.Child, o => o.UseDestinationValue());
This makes your test pass (as long as you get rid of the first space when setting the parentBar.Child.Text!).

Resources