Open Closed

AutoMapper to Mapperly #10637


User avatar
0
DominaTang created

When Migrate from Auto Mapper to Mapperly, we have current existing code: used in AutoMapper profile, how to migrate this code:

CreateMap<UploadedSurveyData, SurveyData>()
.IgnoreNotInListMembers (...);

public static class AutoMapperProfileExtension
{
    public static IMappingExpression<TSource, TDestination> IgnoreNotInListMembers<TSource, TDestination>(this IMappingExpression<TSource, TDestination> expr, List<string> requiredProperties) 
            where TDestination : class
    {
        var destinationType = typeof(TDestination);
        if (requiredProperties == null || requiredProperties.Count == 0)
        {
            return expr;
        }

        foreach (var property in from property in destinationType.GetProperties()
                                 where !requiredProperties.Contains(property.Name)
                                 select property)
        {
            expr.ForMember(property.Name, opt => opt.Ignore());
        }

        return expr;
    }
}
Markdown supported.
Copy, paste, or drag & drop images and files (max 100 MB per file, 100 MB total per post)

17 Answer(s)
  • User Avatar
    0
    AI-Bot created
    Support Team ABP Agent
    • You can’t migrate this IgnoreNotInListMembers(List<string>) concept 1:1 to Mapperly.
      AutoMapper can decide at runtime which destination members to ignore using reflection. Mapperly generates mapping code at compile time, so “ignore members by a runtime string list” is not something it can express via attributes.

    • Recommended migration approach (Mapperly + manual “patch” mapping):
      Treat this as a partial update (“patch”) use-case and apply the requiredProperties list yourself after (or instead of) mapping.

      Example:

    using Riok.Mapperly.Abstractions;
    
      public class UploadedSurveyData
      {
          // source props...
      }
    
      public class SurveyData
      {
          // destination props...
          public string? Q1 { get; set; }
          public string? Q2 { get; set; }
          public string? Q3 { get; set; }
      }
    
      [Mapper]
      public partial class SurveyDataMapper
      {
          // Normal full mapping (Mapperly generates this)
          public partial SurveyData Map(UploadedSurveyData source);
    
          // Update existing destination (Mapperly generates this)
          public partial void Map(UploadedSurveyData source, SurveyData destination);
      }
    
      public static class SurveyDataPatchMapping
      {
          // Runtime selective update (replacement for IgnoreNotInListMembers)
          public static void MapOnly(
              this SurveyDataMapper mapper,
              UploadedSurveyData source,
              SurveyData destination,
              List<string> requiredProperties)
          {
              // If requiredProperties is empty => do nothing (same behavior as your extension)
              if (requiredProperties == null || requiredProperties.Count == 0)
              {
                  return;
              }
    
              // Update only the fields you allow.
              // (This is explicit; you can generate this switch via a source generator/T4 if the list is large.)
              foreach (var p in requiredProperties)
              {
                  switch (p)
                  {
                      case nameof(SurveyData.Q1):
                          destination.Q1 = mapper.Map(source).Q1; // or map from source directly
                          break;
    
                      case nameof(SurveyData.Q2):
                          destination.Q2 = mapper.Map(source).Q2;
                          break;
    
                      case nameof(SurveyData.Q3):
                          destination.Q3 = mapper.Map(source).Q3;
                          break;
                  }
              }
          }
      }
    

    Notes:

    • If you want to avoid calling mapper.Map(source) repeatedly, map once into a temp object and copy selected props.

    • If you have many properties, the most maintainable pattern is to model the patch DTO to contain only patchable fields (preferred) or generate the switch/copy code.

    • ABP-specific note: Mapperly integration is attribute-based; “ignore” behavior is typically expressed via Mapperly attributes (static configuration), not runtime reflection lists.

      Links (validated):

      • https://abp.io/docs/latest/framework/infrastructure/object-to-object-mapping
      • https://abp.io/docs/10.2/release-info/migration-guides/AutoMapper-To-Mapperly
      • https://mapperly.riok.app/docs/intro/

    This response was generated by AI. If it didn’t solve your problem, feel free to post a new answer — our support team will jump in to help you soon.

    Markdown supported.
    Copy, paste, or drag & drop images and files (max 100 MB per file, 100 MB total per post)
  • User Avatar
    0
    DominaTang created

    Then it's hard code a lot of property names in above code, what about there 100 properties that should map? and this extension method is used by many classes' mapping.

    Markdown supported.
    Copy, paste, or drag & drop images and files (max 100 MB per file, 100 MB total per post)
  • User Avatar
    0
    AI-Bot created
    Support Team ABP Agent
    • Mapperly (and ABP’s Mapperly integration) is compile-time / static mapping. Your AutoMapper extension is runtime / reflection-based (“ignore everything not in this string list”). So if you truly need a List<string> decided at runtime, you will not get an equivalent without either:

      1. doing a runtime reflection-based copy yourself (outside Mapperly), or
      2. redesigning the API to avoid “string list of fields”.
    • If you have 100+ properties and this pattern is used in many mappings, the two scalable options are:

    1) Keep Mapperly for normal mapping, but implement a reusable runtime “patch” helper (reflection) once

    This keeps your “only map these fields” behavior without hardcoding 100 property names and works for all DTO/entity pairs.

    using System.Reflection;
    
    public static class PatchMappingExtensions
    {
        /// <summary>
        /// Copies values from <paramref name="source"/> to <paramref name="destination"/>
        /// only for destination properties whose names are in <paramref name="requiredProperties"/>.
        /// </summary>
        public static void PatchByPropertyList<TSource, TDestination>(
            TSource source,
            TDestination destination,
            IReadOnlyCollection<string> requiredProperties)
            where TDestination : class
        {
            if (requiredProperties == null || requiredProperties.Count == 0)
            {
                return; // same semantics as your current extension
            }
    
            var srcType = typeof(TSource);
            var destType = typeof(TDestination);
    
            // Build lookup for source properties by name (public instance)
            var srcProps = srcType
                .GetProperties(BindingFlags.Instance | BindingFlags.Public)
                .Where(p => p.CanRead)
                .ToDictionary(p => p.Name, p => p);
    
            foreach (var destProp in destType.GetProperties(BindingFlags.Instance | BindingFlags.Public))
            {
                if (!destProp.CanWrite)
                {
                    continue;
                }
    
                if (!requiredProperties.Contains(destProp.Name))
                {
                    continue;
                }
    
                if (!srcProps.TryGetValue(destProp.Name, out var srcProp))
                {
                    continue;
                }
    
                // Only assign when compatible (basic safety)
                if (!destProp.PropertyType.IsAssignableFrom(srcProp.PropertyType))
                {
                    continue;
                }
    
                var value = srcProp.GetValue(source);
                destProp.SetValue(destination, value);
            }
        }
    }
    

    Usage with Mapperly in ABP:

    • Use Mapperly for the normal case (full mapping).
    • Use PatchByPropertyList for this special dynamic-field-list case:
    // Full map (generated, fast)
    mapper.Map(uploaded, surveyData);
    
    // Dynamic partial update (runtime)
    PatchMappingExtensions.PatchByPropertyList(uploaded, surveyData, requiredProperties);
    

    This is essentially what AutoMapper was doing for you (runtime reflection), but now it’s explicit, reusable, and you write it once.

    2) Change the contract: use a “patch DTO” (only contains allowed fields) instead of a string list

    This is the most Mapperly-friendly approach: define a DTO that contains only the fields that can be updated, and map that DTO to the entity. No reflection, no list of strings, no 100-case switch.

    Example idea:

    • UploadedSurveyDataPatchDto includes only patchable properties (maybe nullable)
    • mapper maps patch DTO → entity (to existing)
    • you decide which fields are present by model binding / JSON payload rather than a separate list.

    This approach aligns with Mapperly’s compile-time nature and is typically cleaner long-term, but it requires changing your API shape.


    Validated references:

    • https://abp.io/docs/latest/framework/infrastructure/object-to-object-mapping
    • https://abp.io/docs/10.2/release-info/migration-guides/AutoMapper-To-Mapperly
    • https://mapperly.riok.app/docs/intro/

    This response was generated by AI. If it didn’t solve your problem, feel free to post a new answer — our support team will jump in to help you soon.

    Markdown supported.
    Copy, paste, or drag & drop images and files (max 100 MB per file, 100 MB total per post)
  • User Avatar
    0
    DominaTang created

    What about this converter: public class CurrencyObjectTypeConverter : ITypeConverter<double?, CurrencyObject[]?> { public CurrencyObject[]? Convert(double? source, CurrencyObject[]? destination, ResolutionContext context) { if (!source.HasValue) return null; var arr = new CurrencyObject[1] { new CurrencyObject { CurrencyValue = source.Value, Currency = "USD" } };

        return arr;
    }
    

    }

    Markdown supported.
    Copy, paste, or drag & drop images and files (max 100 MB per file, 100 MB total per post)
  • User Avatar
    0
    AI-Bot created
    Support Team ABP Agent
    • In Mapperly, the equivalent of an AutoMapper ITypeConverter<TSource, TDestination> is to add a custom mapping method (a normal C# method) that Mapperly will use when it needs to map between those types.

    1) Solution

    Create a Mapperly mapper (ABP style) and add a method which maps double? → CurrencyObject[]?:

    using Riok.Mapperly.Abstractions;
    using Volo.Abp.DependencyInjection;
    using Volo.Abp.Mapperly;
    
    public class CurrencyObject
    {
        public double CurrencyValue { get; set; }
        public string? Currency { get; set; }
    }
    
    [Mapper]
    public partial class SurveyCurrencyMapper
        : MapperBase<double?, CurrencyObject[]?>, ITransientDependency
    {
        // ABP expects these overrides for ObjectMapper.Map<double?, CurrencyObject[]?>()
        public override partial CurrencyObject[]? Map(double? source);
        public override partial void Map(double? source, CurrencyObject[]? destination);
    
        // Mapperly custom conversion (this replaces AutoMapper ITypeConverter)
        private static CurrencyObject[]? MapToCurrencyObjectArray(double? source)
            => source.HasValue
                ? new[]
                  {
                      new CurrencyObject
                      {
                          CurrencyValue = source.Value,
                          Currency = "USD"
                      }
                  }
                : null;
    }
    

    How it works:

    • When Mapperly sees it must map double? to CurrencyObject[]?, it can use your custom method.
    • In ABP, inheriting from MapperBase<TSource, TDestination> + ITransientDependency allows ObjectMapper.Map<,> to resolve it via the Mapperly provider.

    If you don’t need ObjectMapper.Map<double?, CurrencyObject[]?>() directly and only need it as part of a larger mapping (e.g., UploadedSurveyData -> SurveyData), you can also just put the MapToCurrencyObjectArray(double?) method inside that mapper class; Mapperly will reuse it for that conversion.

    2) Sources

    • https://abp.io/docs/latest/framework/infrastructure/object-to-object-mapping
    • https://abp.io/docs/10.2/release-info/migration-guides/AutoMapper-To-Mapperly
    • https://mapperly.riok.app/docs/intro/

    This response was generated by AI. If it didn’t solve your problem, feel free to post a new answer — our support team will jump in to help you soon.

    Markdown supported.
    Copy, paste, or drag & drop images and files (max 100 MB per file, 100 MB total per post)
  • User Avatar
    0
    maliming created
    Support Team Fullstack Developer

    Hi,

    Mapperly is a compile-time source generator, not a runtime mapper. All of its ignore configuration is expressed as attributes that take a hard-coded property name as a string literal — [MapperIgnoreTarget(nameof(SurveyData.Foo))], [MapperIgnoreSource(...)], [MapperIgnoreObsoleteMembers], etc. The decision of "which fields to map" is baked into the generated code at build time and cannot be driven by a runtime List<string>. So IgnoreNotInListMembers(List<string>) has no direct equivalent in Mapperly.

    The Mapperly-idiomatic way to model "only update some fields" is to change the API contract: introduce a small patch DTO that contains only the patchable fields (typically as nullable properties), and let Mapperly generate a partial update mapping for it at compile time. No reflection, no string list, fully type-checked at build time. If you can refactor the API to that shape, this is the recommended path long-term.

    If keeping the existing requiredProperties shape is important (for example, the field list comes from the client and you don't want to change the contract), you can keep using Mapperly for the actual mapping and use a small reusable helper to commit only the listed fields. Mapperly does the real work (including type conversions, nested mappings and any user-defined converters); reflection is only used to pick which properties to copy over, and PropertyInfo lookup is cached per destination type so the per-call cost stays low.

    using System.Collections.Concurrent;
    using System.Reflection;
    
    public static class PatchExtensions
    {
        private static readonly ConcurrentDictionary<Type, Dictionary<string, PropertyInfo>> WritablePropertyCache = new();
    
        public static void PatchFromMapped<TSource, TDestination>(
            TSource source,
            TDestination destination,
            Func<TSource, TDestination> mapFunc,
            IReadOnlyCollection<string>? requiredProperties)
            where TSource : class
            where TDestination : class
        {
            if (source is null || destination is null || requiredProperties is null || requiredProperties.Count == 0)
            {
                return;
            }
    
            var mapped = mapFunc(source);
    
            var writableProps = WritablePropertyCache.GetOrAdd(
                typeof(TDestination),
                static t => t.GetProperties(BindingFlags.Instance | BindingFlags.Public)
                    .Where(p => p.CanWrite)
                    .ToDictionary(p => p.Name));
    
            foreach (var name in requiredProperties)
            {
                if (writableProps.TryGetValue(name, out var prop))
                {
                    prop.SetValue(destination, prop.GetValue(mapped));
                }
            }
        }
    }
    

    Usage (replaces mapper.Map(uploaded, surveyData) + IgnoreNotInListMembers):

    PatchExtensions.PatchFromMapped(uploaded, surveyData, mapper.Map, requiredProperties);
    

    The same helper covers all your TSource/TDestination pairs (including the 100+ property classes) with no per-class boilerplate. There is one fully-mapped intermediate allocated per call plus reflection GetValue/SetValue, which is fine for typical request paths (sub-microsecond per call in a quick benchmark). For genuinely hot paths, prefer the patch DTO approach above.


    For your ITypeConverter<double?, CurrencyObject[]?>, Mapperly's design is different from AutoMapper. Instead of registering a converter type, Mapperly looks for user-implemented mapping methods directly on the mapper class — any normal method whose signature matches a type pair Mapperly needs to map is picked up automatically (AutoUserMappings is true by default). No attribute, no registration, just a method.

    using Riok.Mapperly.Abstractions;
    
    [Mapper]
    public partial class SurveyDataMapper
    {
        public partial SurveyData Map(UploadedSurveyData source);
        public partial void Map(UploadedSurveyData source, SurveyData destination);
    
        private static CurrencyObject[]? ToCurrencyObjects(double? source)
            => source.HasValue
                ? new[] { new CurrencyObject { CurrencyValue = source.Value, Currency = "USD" } }
                : null;
    }
    

    Verified locally with Mapperly 4.3.1; the generated code calls your method directly:

    // generated SurveyDataMapper.g.cs
    target.Salary = ToCurrencyObjects(source.Salary);
    

    If you want a single converter shared across multiple mappers (the AutoMapper "register once" behavior), put it in a static class and reference it from each mapper via [UseStaticMapper]:

    public static class SurveyConverters
    {
        public static CurrencyObject[]? ToCurrencyObjects(double? source)
            => source.HasValue
                ? new[] { new CurrencyObject { CurrencyValue = source.Value, Currency = "USD" } }
                : null;
    }
    
    [Mapper]
    [UseStaticMapper(typeof(SurveyConverters))]
    public partial class AnotherMapper
    {
        public partial AnotherDto Map(AnotherEntity source);
    }
    

    Mapperly docs:

    • https://mapperly.riok.app/docs/configuration/user-implemented-methods/
    • https://mapperly.riok.app/docs/configuration/static-mappers/

    Thanks

    Markdown supported.
    Copy, paste, or drag & drop images and files (max 100 MB per file, 100 MB total per post)
  • User Avatar
    0
    DominaTang created

    where should put this line: PatchExtensions.PatchFromMapped(uploaded, surveyData, mapper.Map, requiredProperties)?

    Markdown supported.
    Copy, paste, or drag & drop images and files (max 100 MB per file, 100 MB total per post)
  • User Avatar
    0
    AI-Bot created
    Support Team ABP Agent
    • Put this call exactly at the point where you would otherwise do the mapping / update (i.e., the “write path”), typically in your Application Service method (or Domain Service if that’s where you apply updates), right before you persist changes.

    Typical ABP placement (Application Service)

    Example: you receive UploadedSurveyData + requiredProperties, you load/update an existing SurveyData entity, then patch:

    public class SurveyAppService : ApplicationService
    {
        private readonly SurveyDataMapper _mapper; // your Mapperly mapper
        private readonly IRepository<SurveyData, Guid> _repo;
    
        public SurveyAppService(SurveyDataMapper mapper, IRepository<SurveyData, Guid> repo)
        {
            _mapper = mapper;
            _repo = repo;
        }
    
        public async Task UpdateFromUploadAsync(Guid id, UploadedSurveyData uploaded, List<string> requiredProperties)
        {
            var surveyData = await _repo.GetAsync(id);
    
            // Only apply the selected fields:
            PatchExtensions.PatchFromMapped(uploaded, surveyData, _mapper.Map, requiredProperties);
    
            await _repo.UpdateAsync(surveyData, autoSave: true);
        }
    }
    

    Where to put the PatchExtensions class

    • Put PatchExtensions in a shared place where all mappings can use it, e.g.:
      • *.Application project (common for DTO→Entity update helpers), or
      • a small *.Domain.Shared / *.Shared project if multiple modules need it.

    This is consistent with ABP’s object-mapping usage pattern (mapping is typically executed inside application service methods when creating/updating entities).
    Reference (validated): https://abp.io/docs/latest/framework/infrastructure/object-to-object-mapping


    This response was generated by AI. If it didn’t solve your problem, feel free to post a new answer — our support team will jump in to help you soon.

    Markdown supported.
    Copy, paste, or drag & drop images and files (max 100 MB per file, 100 MB total per post)
  • User Avatar
    0
    DominaTang created

    Say the SurveyDataMapper has default Map method that map every property, and then call this extension method, it's too late, since SurveyDataMapper already mapped the properties that should ignore.

    Markdown supported.
    Copy, paste, or drag & drop images and files (max 100 MB per file, 100 MB total per post)
  • User Avatar
    0
    AI-Bot created
    Support Team ABP Agent

    You’re right: calling the Mapperly Map(source, destination) first and then “patching” is too late if the unwanted members have already been written into destination.

    The fix is: never map directly into the real destination when you need runtime-selected members.

    1) Solution

    Use a two-step approach:

    1. Map into a temporary mapped object (new instance) using Mapperly (so you still get all Mapperly conversions/nested mappings).
    2. Copy only the allowed properties from that temporary object into the real destination.

    That way, the “ignored” members are never touched on the real entity.

    // GOOD for "only update these fields"
    PatchExtensions.PatchFromMapped(uploaded, surveyData, mapper.Map, requiredProperties);
    
    // BAD for "only update these fields" (too late)
    // mapper.Map(uploaded, surveyData);
    // PatchExtensions.PatchFromExisting(surveyData, requiredProperties);
    

    A complete reusable helper (same concept as the other user posted) looks like this:

    using System.Collections.Concurrent;
    using System.Reflection;
    
    public static class PatchExtensions
    {
        private static readonly ConcurrentDictionary<Type, Dictionary<string, PropertyInfo>> WritablePropertyCache = new();
    
        public static void PatchFromMapped<TSource, TDestination>(
            TSource source,
            TDestination destination,
            Func<TSource, TDestination> mapFunc,
            IReadOnlyCollection<string>? requiredProperties)
            where TSource : class
            where TDestination : class
        {
            if (requiredProperties == null || requiredProperties.Count == 0)
            {
                return;
            }
    
            // 1) Map into a NEW object (destination not touched yet)
            var mapped = mapFunc(source);
    
            // 2) Copy only selected properties into the REAL destination
            var writableProps = WritablePropertyCache.GetOrAdd(
                typeof(TDestination),
                static t => t.GetProperties(BindingFlags.Instance | BindingFlags.Public)
                    .Where(p => p.CanWrite)
                    .ToDictionary(p => p.Name));
    
            foreach (var name in requiredProperties)
            {
                if (writableProps.TryGetValue(name, out var prop))
                {
                    prop.SetValue(destination, prop.GetValue(mapped));
                }
            }
        }
    }
    

    Where to put / call it in ABP: inside the method where you update the entity (usually an Application Service method), after you load the entity and before UpdateAsync / SaveChanges.

    Important note

    If your existing workflow depends on “requiredProperties empty means map everything”, you can implement that behavior too:

    if (requiredProperties == null || requiredProperties.Count == 0)
    {
        // full update
        mapper.Map(source, destination);
        return;
    }
    

    2) Sources

    • https://abp.io/docs/latest/framework/infrastructure/object-to-object-mapping
    • https://abp.io/docs/10.2/release-info/migration-guides/AutoMapper-To-Mapperly
    • https://mapperly.riok.app/docs/intro/
    • https://mapperly.riok.app/docs/configuration/user-implemented-methods/

    This response was generated by AI. If it didn’t solve your problem, feel free to post a new answer — our support team will jump in to help you soon.

    Markdown supported.
    Copy, paste, or drag & drop images and files (max 100 MB per file, 100 MB total per post)
  • User Avatar
    0
    DominaTang created

    It is not feasible.. Say the target class has big object graph, Auto Mapper map whole graph but ignore a list of properties from parameter. If only copy should copy properties by reflection which contain child entities, the list would be long and code would be complex.

    Markdown supported.
    Copy, paste, or drag & drop images and files (max 100 MB per file, 100 MB total per post)
  • User Avatar
    0
    AI-Bot created
    Support Team ABP Agent
    • With Mapperly, a runtime “ignore everything except this List<string>” cannot be applied during the generated deep object-graph mapping. That’s not an ABP limitation; it’s inherent to Mapperly being a compile-time source generator. So for your exact AutoMapper behavior (dynamic, runtime-selected member set applied across a big graph), there is no Mapperly-native equivalent.

    Given your constraints (large graph, shared extension used everywhere, long property list, and you don’t want reflection-copy of the entire graph), you have only these practical options:

    1) Keep AutoMapper for these dynamic mappings (recommended if you must keep the contract)

    ABP v10+ can use Mapperly as default while you still keep AutoMapper for specific cases. Use Mapperly for the majority of mappings, but keep AutoMapper profiles for the “patch by property list” scenarios where runtime configuration is essential.

    This is the only option that preserves:

    • deep graph mapping
    • runtime “fields list” semantics
    • no custom reflection patch logic for nested objects

    (ABP explicitly allows keeping AutoMapper in your app even after v10.)

    2) Change the API contract to a typed “patch DTO” (Mapperly-idiomatic)

    If you can change the shape, this is the clean Mapperly solution:

    • Replace List<string> requiredProperties with a DTO which contains only patchable fields (often nullable) and/or nested patch DTOs.
    • Mapperly generates compile-time partial update methods for that DTO and object graph.

    This avoids reflection and avoids string lists completely, but it’s a refactor.

    3) If you must keep the string list and must use Mapperly: reflection patch is unavoidable somewhere

    To drive a runtime list, something must interpret that list at runtime. If you don’t want to reflect-copy the whole graph, the best you can do is:

    • still map to a fully-mapped temporary graph using Mapperly (so conversions/nested mappings are handled),
    • then apply the list to copy only selected root properties (or a path-based list like Address.Street, which gets complex quickly),
    • OR redesign the list to be structured (e.g., JSON Patch / RFC6902 style paths) and write a generic patch applier.

    But fundamentally: without changing the contract, complexity has to live somewhere, and Mapperly won’t generate “dynamic graph patch” code for you.

    Sources (validated)

    • https://abp.io/docs/latest/framework/infrastructure/object-to-object-mapping
    • https://abp.io/docs/10.2/release-info/migration-guides/AutoMapper-To-Mapperly
    • https://mapperly.riok.app/docs/intro/
    • https://mapperly.riok.app/docs/configuration/user-implemented-methods/

    This response was generated by AI. If it didn’t solve your problem, feel free to post a new answer — our support team will jump in to help you soon.

    Markdown supported.
    Copy, paste, or drag & drop images and files (max 100 MB per file, 100 MB total per post)
  • User Avatar
    0
    DominaTang created

    I think the simple way is, don't use ObjectMapper.Map method, just using the relection copy a set of properties values to target class. Though need specially handling if properties list contains CurrencyObject

    Markdown supported.
    Copy, paste, or drag & drop images and files (max 100 MB per file, 100 MB total per post)
  • User Avatar
    0
    AI-Bot created
    Support Team ABP Agent
    • If you decide to skip ObjectMapper.Map / Mapperly entirely for this “map only these fields” scenario, then yes: a reusable reflection-based patch is the simplest way to preserve your existing requiredProperties contract.

    • For “special handling” like double? -> CurrencyObject[]?, don’t hardcode it in every mapping. Instead, add a converter registry (dictionary) keyed by (sourceType, destType) and let the patcher use it when the property types don’t match.

    1) Solution

    Create one generic patcher which:

    1. copies only the properties listed in requiredProperties
    2. if types match, assigns directly
    3. if not, tries a registered converter (e.g., double? -> CurrencyObject[]?)
    using System.Collections.Concurrent;
    using System.Reflection;
    
    public static class ReflectionPatchMapper
    {
        private static readonly ConcurrentDictionary<(Type Src, Type Dest), (PropertyInfo[] SrcProps, Dictionary<string, PropertyInfo> DestProps)>
            Cache = new();
    
        // Register custom converters here (extend as needed)
        private static readonly ConcurrentDictionary<(Type Src, Type Dest), Func<object?, object?>> Converters = new();
    
        static ReflectionPatchMapper()
        {
            // Example: double? -> CurrencyObject[]?
            Converters[(typeof(double?), typeof(CurrencyObject[]))] = src =>
            {
                var v = (double?)src;
                return v.HasValue
                    ? new[]
                      {
                          new CurrencyObject { CurrencyValue = v.Value, Currency = "USD" }
                      }
                    : null;
            };
    
            // If your destination property is CurrencyObject[]? (nullable annotation),
            // reflection sees it as CurrencyObject[] (same runtime type), so this works.
        }
    
        public static void Patch<TSource, TDestination>(
            TSource source,
            TDestination destination,
            IReadOnlyCollection<string> requiredProperties)
            where TSource : class
            where TDestination : class
        {
            if (requiredProperties == null || requiredProperties.Count == 0)
            {
                return;
            }
    
            var key = (typeof(TSource), typeof(TDestination));
            var meta = Cache.GetOrAdd(key, static k =>
            {
                var srcProps = k.Src.GetProperties(BindingFlags.Instance | BindingFlags.Public)
                    .Where(p => p.CanRead)
                    .ToArray();
    
                var destProps = k.Dest.GetProperties(BindingFlags.Instance | BindingFlags.Public)
                    .Where(p => p.CanWrite)
                    .ToDictionary(p => p.Name, p => p);
    
                return (srcProps, destProps);
            });
    
            // Build a quick lookup for source properties by name
            // (optional: cache too; kept simple here)
            var srcByName = meta.SrcProps.ToDictionary(p => p.Name, p => p);
    
            foreach (var name in requiredProperties)
            {
                if (!meta.DestProps.TryGetValue(name, out var destProp))
                    continue;
    
                if (!srcByName.TryGetValue(name, out var srcProp))
                    continue;
    
                var srcValue = srcProp.GetValue(source);
    
                // 1) direct assignment if compatible
                if (destProp.PropertyType.IsAssignableFrom(srcProp.PropertyType))
                {
                    destProp.SetValue(destination, srcValue);
                    continue;
                }
    
                // 2) try registered converter
                if (Converters.TryGetValue((srcProp.PropertyType, destProp.PropertyType), out var convert))
                {
                    var converted = convert(srcValue);
                    destProp.SetValue(destination, converted);
                    continue;
                }
    
                // else: ignore or throw (your choice). I’d recommend ignore to match your old behavior.
            }
        }
    
        // Optional: allow registering converters from modules
        public static void RegisterConverter<TSrc, TDest>(Func<TSrc, TDest> converter)
            => Converters[(typeof(TSrc), typeof(TDest))] = o => converter((TSrc)o!);
    }
    

    Usage:

    ReflectionPatchMapper.Patch(uploaded, surveyData, requiredProperties);
    

    This gives you:

    • no ObjectMapper.Map
    • no Mapperly dependency for this path
    • one shared mechanism for all your mappings
    • special conversions centralized (CurrencyObject, etc.)

    2) Sources

    • https://abp.io/docs/latest/framework/infrastructure/object-to-object-mapping
    • https://abp.io/docs/10.2/release-info/migration-guides/AutoMapper-To-Mapperly

    This response was generated by AI. If it didn’t solve your problem, feel free to post a new answer — our support team will jump in to help you soon.

    Markdown supported.
    Copy, paste, or drag & drop images and files (max 100 MB per file, 100 MB total per post)
  • User Avatar
    0
    maliming created
    Support Team Fullstack Developer

    Hi,

    A few clarifications based on your follow-up questions, because the helper does not have the two problems you were worried about.

    1. mapper.Map does not overwrite the existing destination first

    The third argument of PatchFromMapped is Func<TSource, TDestination>. When you write mapper.Map, C# resolves the method group against this delegate signature. The mapper has two Map overloads:

    public partial SurveyData Map(UploadedSurveyData source);                  // returns a new instance
    public partial void       Map(UploadedSurveyData source, SurveyData dst);  // updates in place
    

    Only the first one matches Func<UploadedSurveyData, SurveyData>, so that's the one the helper calls. It builds a brand-new SurveyData, fully mapped, and the helper then copies only the listed properties from that temporary object onto your existing destination. The existing destination is never overwritten in full — there is no "too late" problem.

    You can verify this on the delegate itself:

    Func<UploadedSurveyData, SurveyData> mapFunc = mapper.Map;
    // mapFunc.Method.ReturnType  == typeof(SurveyData)
    // mapFunc.Method.GetParameters().Length == 1   (no destination parameter)
    

    2. The helper does not need to recurse into the object graph

    The whole subgraph — nested entities, collections, your double? -> CurrencyObject[]? user-defined converter, anything Mapperly knows how to map — is built by Mapperly's compile-time generated code inside the temporary mapped object. The reflection step only assigns the top-level property reference from mapped to your destination. There is no reflection recursion, no per-class code, no special handling for CurrencyObject or any child entity.

    Concretely, with a domain like:

    public class SurveyData
    {
        public string? Q1 { get; set; }
        public CurrencyObject[]? Salary { get; set; }    // custom converter from double?
        public AddressEntity? Address { get; set; }      // nested entity, has its own Zip
        public List<ItemEntity>? Items { get; set; }     // collection
    }
    

    Calling:

    PatchExtensions.PatchFromMapped(uploaded, surveyData, mapper.Map, new[] { "Address" });
    

    surveyData.Address is replaced with a fully-mapped AddressEntity (including the nested Zip). All other properties on surveyData (Q1, Salary, Items) are untouched, including object identity — they keep pointing at the same instances they did before the call. So "the list would be long and code would be complex" doesn't apply: Mapperly handles the graph, the helper just commits the listed top-level fields.

    So you don't need to drop ObjectMapper.Map and write a pure-reflection copy with special cases for CurrencyObject. The helper above already keeps Mapperly responsible for all type conversions and object-graph mapping.

    3. Where to call the helper

    Wherever you used to call ObjectMapper.Map<UploadedSurveyData, SurveyData>(uploaded, existingSurvey) together with IgnoreNotInListMembers. Typical Application Service shape:

    public async Task UpdateAsync(Guid id, UploadedSurveyData input, List<string> requiredProperties)
    {
        var existing = await _repository.GetAsync(id);
        PatchExtensions.PatchFromMapped(input, existing, _mapper.Map, requiredProperties);
        await _repository.UpdateAsync(existing);
    }
    

    I have these scenarios covered by xUnit tests locally (Mapperly 4.3.1, all passing): top-level property selection, double? → CurrencyObject[]? conversion through the listed property, full nested subgraph (Address → Zip), collection mapping (List<ItemDto> → List<ItemEntity>), and reference-identity preservation for non-listed properties. Happy to share more snippets if helpful.

    Thanks

    Markdown supported.
    Copy, paste, or drag & drop images and files (max 100 MB per file, 100 MB total per post)
  • User Avatar
    0
    DominaTang created

    thanks

    Markdown supported.
    Copy, paste, or drag & drop images and files (max 100 MB per file, 100 MB total per post)
  • User Avatar
    0
    maliming created
    Support Team Fullstack Developer

    👍

    Markdown supported.
    Copy, paste, or drag & drop images and files (max 100 MB per file, 100 MB total per post)
Boost Your Development
ABP Live Training
Packages
See Trainings
Mastering ABP Framework Book
The Official Guide
Mastering
ABP Framework
Learn More
Mastering ABP Framework Book
Made with ❤️ on ABP v10.8.0-preview. Updated on September 28, 2026, 11:44
1
ABP Assistant
🔐 You need to be logged in to use the chatbot. Please log in first.