Reputation: 223
After much reading and trial/error, I decided to post here to get a larger audience for input. This is a simple use case, a DTO being mapped to an Entity class. The DTO has seriously odd property names because it is decoded from an Avro message body from the Salesforce PubSub CDC API and Salesforce has a nuanced way of suffixing custom fields.
Because of this, I'm using the [SourceMember] attribute to decorate target member properties, so AutoMapper will know how to source the value for the respectively decorated target. In addition to this, I'm using an AutoMapper Profile class to set up a Condition for all members in all maps to ignore null source fields. This is because the majority of the DTO's member properties will likely be null most of the time, since this is a CDC event.
Here's the problem- there seems to be some sort of configuration/setup collision when using a Profile (to specify the Conditions for a map) and also use the [AutoMap] class attribute on the target class, to support the [SourceMember] decorators. Looking at the AutoMapper docs here https://docs.automapper.org/en/stable/Attribute-mapping.html it says "Attribute maps can supplement or replace fluent mapping configuration." I can only assume the "replace" action is happening and not "supplement."
The most curious thing here is this, the second problem- if I comment out
c.AddProfile(new AutoMapperProfile())
then AutoMapper will respect the [SourceMember] decorators, but not honor the ignore null Conditions in the Profile.
BUT if I comment out c.AddMaps(typeof(TargetClass))
AutoMapper will respect the ignore null Conditions in the Profile, but it will not honor the [SourceMember] decorators.
The end result is I can either get good property values on the TargetEntityClass mapped over from the SourceDtoClass (using the [SourceMember] decorator), -OR- I end up overwriting values on TargetEntityClass with nulls from the SourceDtoClass which I want to avoid (e.g. by using the Conditions)
An example of source DTO, target class and AutoMapper config is below.
Environment is .NET 7, EF Core 7.0.9, AutoMapper and AutoMapper extensions 12.0.1, Visual Studio for Mac v17.6.1.
Config:
var autoMapperConfig = new MapperConfiguration(c =>
{
c.AddProfile(new AutoMapperProfile());
c.AddMaps(typeof(TargetClass));
});
IMapper mapper = autoMapperConfig.CreateMapper();
services.AddSingleton(mapper);
Profile:
public class AutoMapperProfile : Profile {
public AutoMapperProfile() {
CreateMap<SourceDtoClass, TargetEntityClass>()
.ForAllMembers(option => option.Condition((src, dst, srcMember) => srcMember != null));
}
}
SourceDtoClass:
public class SourceDtoClass {
public string? Name { get; set; } = "";
public string? Phone_number_1__c { get; set; } = "";
}
TargetEntityClass:
[DataContract]
[Serializable]
[AutoMap(typeof(SourceDtoClass))]
public class TargetEntityClass {
public string Name { get; set; } = "";
[DataMember]
[SourceMember("Phone_number_1__c")]
public string? Phone { get; set; } = "";
}
Usage:
(assume mapper has been given via IoC, sourceDtoClass has been instantiated and targetEntityClass is a fully hydrated EF Entity class instance with values from an EF SQL Select interaction)
mapper.map(sourceDtoClass,targetEntityClass)
Upvotes: 0
Views: 1015
Reputation: 223
Thanks @LucianBargaoanu for the comment, using ForAllPropertyMaps in the AutoMapper InternalAPI worked!
While I'm not sure why the ignored null source values on a string data type in the source class yield an assignment value of zero when targeting an int data type on the destination (when the destination already had a value present, e.g. an overwrite occurred) I found a workaround, just not providing a [SourceMember] decoration on that property excludes it. I found I didn't need that property mapped after all, but I still wish I knew more about AutoMapper's behavior in this regard.
Configuration solution below:
var autoMapperConfig = new MapperConfiguration(config =>
{
config.AddMaps(typeof(TargetEntityClass));
InternalApi.Internal(config).ForAllPropertyMaps(
propertymaps => true, (propertymap, condition) =>
{
condition.Condition((source, destination, srcval) => srcval !=null);
});
});
Upvotes: 0