5

I want to map between two classes:

public class A {
    public IEnumerable<C> someList
}

and

public class B {
    public RepeatedField<D> someList
}

where RepeatedField is a class from Google.Protobuf.Collections that handles gRPC data.

EDIT: As it turns out, the way that gRPC creates classes via its prototype is not exactly like creating a class like B. See my answer.

I create an Automapper MappingConfiguration like this

return new MapperConfiguration(cfg =>
    {
        cfg.CreateMap<C, D>().ReverseMap();
        cfg.CreateMap<A, B>().ReverseMap();
    });

and then it gets registered via ASP.NET Startup class.

If I do something like this in another class

A instanceA; // assume A's list has values inside
var listofD = this.mapper.Map<List<D>>(A.someList)

it correctly returns a list with values inside. However:

A instanceA; // assume A's list has values inside
B instanceB = this.mapper.Map<B>(A);

returns an instance of B, but the list inside of instanceB is empty. How do I fix this?

more whirlpools
  • 335
  • 1
  • 4
  • 16

2 Answers2

7

You need to create a custom type converter for performing the conversion:

private class EnumerableToRepeatedFieldTypeConverter<TITemSource, TITemDest> : ITypeConverter<IEnumerable<TITemSource>, RepeatedField<TITemDest>>
{
    public RepeatedField<TITemDest> Convert(IEnumerable<TITemSource> source, RepeatedField<TITemDest> destination, ResolutionContext context)
    {
        destination = destination ?? new RepeatedField<TITemDest>();
        foreach (var item in source)
        {
            // obviously we haven't performed the mapping for the item yet
            // since AutoMapper didn't recognise the list conversion
            // so we need to map the item here and then add it to the new
            // collection
            destination.Add(context.Mapper.Map<TITemDest>(item));
        }
        return destination;
    }
}

And the other way, if required:

private class RepeatedFieldToListTypeConverter<TITemSource, TITemDest> : ITypeConverter<RepeatedField<TITemSource>, List<TITemDest>>
{
    public List<TITemDest> Convert(RepeatedField<TITemSource> source, List<TITemDest> destination, ResolutionContext context)
    {
        destination = destination ?? new List<TITemDest>();
        foreach (var item in source)
        {
            destination.Add(context.Mapper.Map<TITemDest>(item));
        }
        return destination;
    }
}

Which you can register like so:

ce.CreateMap(typeof(IEnumerable<>), typeof(RepeatedField<>)).ConvertUsing(typeof(EnumerableToRepeatedFieldTypeConverter<,>));
ce.CreateMap(typeof(RepeatedField<>), typeof(List<>)).ConvertUsing(typeof(RepeatedFieldToListTypeConverter<,>));

Try it online

ProgrammingLlama
  • 36,677
  • 7
  • 67
  • 86
  • This answer doesn't work, the destination RepeatedFields property remains empty. Copying the properties in AfterMap step works. See also: https://github.com/AutoMapper/AutoMapper/issues/3157 – vivainio Feb 19 '20 at 12:40
  • @vivainio I can't reproduce that, even with the latest AutoMapper and Google.Protobuf packages. The only issue I had was trying to map `List` to `RepeatedField` where one of the list items is `null`, but that's a limitation of the `RepeatedField` object and not the conversion code, and I wouldn't want to decide on someone else's behalf how that should be dealt with. Can you provide a reproduction case on DotNetFiddle so that I can improve my answer if needs be? – ProgrammingLlama Feb 20 '20 at 00:34
  • I have tried your answer. It work for simple object. But, I have modified class to complex object with inner List. And it not work. Please check [link](https://dotnetfiddle.net/58Wy66) – Nguyễn Văn Biên May 31 '20 at 09:01
  • My mistake, it works. [try it](https://dotnetfiddle.net/Uz5w7j) – Nguyễn Văn Biên May 31 '20 at 09:11
-1

I've solved the issue.

A Google.Protobuf.Collections.RepeatedField inside a C# class is readonly, meaning that directly assigning values into it won't work and will only return an empty list on the way back. Therefore, I created a custom type converter between the two larger classes to bring them together. What it does is add values directly into the RepeatedField rather than populating my own RepeatedField and assigning the value into the class.

public static class mapConfig
{
    public static ContainerBuilder RegisterObjectMappers(this ContainerBuilder builder)
    {
        builder.Register(c => GetV1MapperConfiguration().CreateMapper())
            .As<IMapper>().SingleInstance();

        return builder;
    }
    private static MapperConfiguration GetMapConfig()
    {
        return new MapperConfiguration(cfg =>
        {
            // some mappings here
            cfg.CreateMap<C, D>().ReverseMap();
            cfg.CreateMap<A, B>().ConvertUsing<AToBConverter>();
        });
    }
}
public class AToBConverter : ITypeConverter<A, B>
{
    public B Convert(A source, B destination, ResolutionContext context)
    {
        var b = new B
        {
            // internal values here aside from the repeated field(s)
        };

        // Need to use the Add method to add values rather than assign it with an '=' sign
        foreach (var someValue in source.someList)
        {
            b.someList.Add(context.Mapper.Map<D>(someValue));
        }
        return b;
    }
}

Converting from RepeatedField to a List or any other IEnumerable in a mapped class isn't any trouble and didn't require another converter for me.

more whirlpools
  • 335
  • 1
  • 4
  • 16
  • 3
    That's what the answer above does, but in a generic way. – Lucian Bargaoanu Jun 26 '19 at 04:48
  • As it turns out, the other answer works if you're just dealing with a class that just contains a RepeatedField. However, it turns out that the B class in my application makes all repeated fields readonly due to how gRPC's proto file generates repeated fields in classes. So just converting between the two didn't work for me unless I created a specific converter for the case of a repeated field existing. – more whirlpools Jul 02 '19 at 17:53
  • 1
    Instead of creating your own ITypeConverter for every type, it's easier to implement AfterMap that assigns all the RepeatedField properties. – vivainio Feb 19 '20 at 12:38