0

I have a class DocumentObject that extends DynamicObject to allow dynamic membership attributes.

public class DocumentObject : DynamicObject
    {
        /// <summary>
        /// Inner dictionary that holds the dynamic members of the object
        /// </summary>
        Dictionary<string, object> dictionary = new Dictionary<string, object>();

        /// <summary>
        /// Try to get the member that is not defined in the class (additional dynamic members) from inner dictionary
        /// </summary>
        /// <param name="binder"></param>
        /// <param name="result"></param>
        /// <returns></returns>
        public override bool TryGetMember(GetMemberBinder binder, out object result)
        {
            // Converting the property name to lowercase
            // so that property names become case-insensitive.
            string name = binder.Name.ToLower();

            // If the property name is found in a dictionary,
            // set the result parameter to the property value and return true.
            // Otherwise, return false.
            return dictionary.TryGetValue(name, out result);
        }

        /// <summary>
        /// Try to set the member that is not defined in the class (additional dynamic members) to inner dictionary
        /// </summary>
        /// <param name="binder"></param>
        /// <param name="value"></param>
        /// <returns></returns>
        public override bool TrySetMember(SetMemberBinder binder, object value)
        {
            // Converting the property name to lowercase
            // so that property names become case-insensitive.
            dictionary[binder.Name.ToLower()] = value;

            // You can always add a value to a dictionary,
            // so this method always returns true.
            return true;
        }

        /// <summary>
        /// Get the names of all the dynamic members
        /// </summary>
        /// <returns></returns>
        public override IEnumerable<string> GetDynamicMemberNames()
        {
            return dictionary.Keys;
        }

    }

I have a base Person class that inherits DocumentObject

public class PersonDto : DocumentObject
    {
        [JsonProperty("id")]
        public string Id { get; set; }
    }

Another child OfficePersonDto class that inherits PersonDto

public class OfficePersonDto : PersonDto 
    {
        [JsonProperty("name")]
        public string Name { get; set; }
    }

In my function, I am receiving JSON object that has to be at least PersonDto object, but if its an OfficePersonDto type, I wish to be able to cast PersonDto into OfficePersonDto. I.e. JSON = {"Id":1, "Name": "Orchard"}, in PersonDto, name attribute will be saved using DocumentObject's dictionary, while casting to OfficePersonDto, both Id and name are attributes of the class.

How can I cast from PersonDto to a child class e.g. OfficePersonDto?

PersonDto personDto = ...
OfficePersonDto off = personDto as OfficePersonDto  // results in null or Name is null
chiaDev
  • 389
  • 3
  • 17

1 Answers1

0

Automapper is very helpful when the issue is about these kind of conversions. I'll show you a quick working example. But it will be still open for more refactoring of course.

PersonDto personDto = ...
// Do not use the following conversion, instead get help from automapper
// OfficePersonDto off = personDto as OfficePersonDto
    
var config = new MapperConfiguration(cfg =>
{
    cfg.CreateMap<PersonDto, OfficePersonDto>()
        .ForMember(d => d.Name, opt => opt.MapFrom(new OfficePersonNameResolver()));
});
    
var mapper = config.CreateMapper();
var off = mapper.Map<OfficePersonDto>(personDto); // off is created with correct values

OfficePersonNameResolver is like this:

public class OfficePersonNameResolver : IValueResolver<PersonDto, OfficePersonDto, string>
{
    public string Resolve(PersonDto source, OfficePersonDto destination, string destMember, ResolutionContext context)
    {
        return (source as dynamic).Name;
    }
}

You are welcome to ask if you have questions about this.

Edit: Generalizing the resolver to IValueResolver<TSource, TDestination, TDestinationMember>

Change the creation of configuration like this:

var config = new MapperConfiguration(cfg =>
{
      cfg.CreateMap<PersonDto, OfficePersonDto>()
          .ForMember(d => d.Name, opt => opt.MapFrom(new DynamicObjectValueResolver<PersonDto, OfficePersonDto, string>("name")));
});

And The value resolver should be like this now:

public class DynamicObjectValueResolver<TSource, TDestination, TDestinationMember> : IValueResolver<TSource, TDestination, TDestinationMember>
   where TDestinationMember : class
{
    private readonly string _propertyName;

    public DynamicObjectValueResolver(string propertyName)
    {
       _propertyName = propertyName;
    }

    public TDestinationMember Resolve(TSource source, TDestination destination, TDestinationMember destMember, ResolutionContext context)
    {
        dynamic eo = JsonConvert.DeserializeObject<ExpandoObject>(JsonConvert.SerializeObject(source));
        IDictionary<string, object> dictionary = eo;
        return dictionary[_propertyName] as TDestinationMember;
     }
}

Working example: https://dotnetfiddle.net/KS91To

Koray Elbek
  • 794
  • 5
  • 13
  • Thanks @Koray Elbek, may I ask if I can generialise the resolver to IValueResolver, where S and D are of DocumentObject type? So that I do not have to create a resolver to every base class of DocumentObject. – chiaDev Sep 03 '21 at 10:17
  • Saw your update, is it possible if I can generalise the mapper config as well? cfg.CreateMap()? I tried. But get invalidCastException from DocumentObject to OfficePersonDto. – chiaDev Sep 03 '21 at 11:11
  • I don't think generalization of the config is also possible, to be honest. Even if it's possible somehow, it might require a lot of scaffolding. Which will result in a bad overengineering. – Koray Elbek Sep 03 '21 at 11:26