ch-robinson / dotnet-avro

An Avro implementation for .NET
https://engineering.chrobinson.com/dotnet-avro/
MIT License
135 stars 51 forks source link

No coercion operator is defined between types 'System.String' and 'System.Char[]'. on a simple string property #251

Closed koncq closed 1 year ago

koncq commented 1 year ago

Hi, this is really a question regarding 2 seperate issues, but I did not want to create 2 issues.

The first thing, it looks like a bug to me. So I have a following class (which is nested in the main class TestClassRoot):

[DataContract]
public class TestClass
{
    [DataMember]
    public string IdType { get; set; }

    // other properties
}

During deserialization process I am getting following error message: No coercion operator is defined between types 'System.String' and 'System.Char[]'.

Which, to me, is kinda strange, when I have couple dozen of similar properties and they deserialize correctly. I am using following code to build schema:

var schemaBuilder = new SchemaBuilder(
    memberVisibility: BindingFlags.Public | BindingFlags.Instance,
    enumBehavior: EnumBehavior.Symbolic,
    nullableReferenceTypeBehavior: NullableReferenceTypeBehavior.Annotated,
    temporalBehavior: TemporalBehavior.Iso8601);

var schemaBuilderContext = new SchemaBuilderContext();
var schema = schemaBuilder.BuildSchema<TestClassRoot>(schemaBuilderContext);

Also, deserializer are basic, deserializer for reference:

var deserializer = new BinaryDeserializerBuilder(
                BinaryDeserializerBuilder
                    .CreateDefaultCaseBuilders()
            )
            .BuildDelegate<TestClassRoot>(schema);

The other issue is related to the serializing and deserializing C#'s Dictionary<int, T> . How can I embed a rule in SchemaBuilder to support that kind of conversion? Or should I create a custom deserializer? As a workaround I create another property with supported type (Dictionary<string, string>) and do parsing under the hood, which is not really sustainable...

dstelljes commented 1 year ago

For the first issue, could you provide the trace as well as the TestClassRoot class if you can?

For the second, it should work to create custom cases for T. See https://github.com/ch-robinson/dotnet-avro/blob/main/examples/Chr.Avro.UnionTypeExample/Program.cs for an example. If you want to use int as the dictionary key, you may also have to implement a custom case for int since Avro map keys are strings and Chr.Avro won’t convert int to string implicitly.

koncq commented 1 year ago

First issue - No coercion operator is defined between types 'System.String' and 'System.Char[]'.

Trace:

Chr.Avro.UnsupportedTypeException: The RequestInfoContext member on DataContract.EngineRequest could not be mapped to the RequestInfoContext field on DataContract.EngineRequest.
 ---> Chr.Avro.UnsupportedTypeException: The Parameter member on DataContract.Data.InfoContext could not be mapped to the Parameter field on DataContract.Data.InfoContext.
 ---> Chr.Avro.UnsupportedTypeException: Failed to map StringSchema to System.Char[].
 ---> System.InvalidOperationException: No coercion operator is defined between types 'System.String' and 'System.Char[]'.
   at System.Linq.Expressions.Expression.GetUserDefinedCoercionOrThrow(ExpressionType coercionType, Expression expression, Type convertToType)
   at Chr.Avro.Serialization.ExpressionBuilder.BuildStaticConversion(Expression value, Type target)
   at Chr.Avro.Serialization.StringDeserializerBuilderCase.BuildStaticConversion(Expression value, Type target)
   at Chr.Avro.Serialization.ExpressionBuilder.BuildConversion(Expression value, Type target)
   at Chr.Avro.Serialization.BinaryStringDeserializerBuilderCase.BuildExpression(Type type, Schema schema, BinaryDeserializerBuilderContext context)
   --- End of inner exception stack trace ---
   at Chr.Avro.Serialization.BinaryStringDeserializerBuilderCase.BuildExpression(Type type, Schema schema, BinaryDeserializerBuilderContext context)
   at Chr.Avro.Serialization.BinaryDeserializerBuilder.BuildExpression(Type type, Schema schema, BinaryDeserializerBuilderContext context)
   at Chr.Avro.Serialization.BinaryRecordDeserializerBuilderCase.<>c__DisplayClass7_1.<BuildExpression>b__0(RecordField field)
   at System.Linq.Enumerable.SelectEnumerableIterator`2.MoveNext()
   at System.Linq.Enumerable.ToDictionary[TSource,TKey,TElement](IEnumerable`1 source, Func`2 keySelector, Func`2 elementSelector, IEqualityComparer`1 comparer)
   at System.Linq.Enumerable.ToDictionary[TSource,TKey,TElement](IEnumerable`1 source, Func`2 keySelector, Func`2 elementSelector)
   at Chr.Avro.Serialization.BinaryRecordDeserializerBuilderCase.BuildExpression(Type type, Schema schema, BinaryDeserializerBuilderContext context)
   at Chr.Avro.Serialization.BinaryDeserializerBuilder.BuildExpression(Type type, Schema schema, BinaryDeserializerBuilderContext context)
   at Chr.Avro.Serialization.BinaryRecordDeserializerBuilderCase.<>c__DisplayClass7_1.<BuildExpression>b__0(RecordField field)
   at System.Linq.Enumerable.SelectEnumerableIterator`2.MoveNext()
   at System.Linq.Enumerable.ToDictionary[TSource,TKey,TElement](IEnumerable`1 source, Func`2 keySelector, Func`2 elementSelector, IEqualityComparer`1 comparer)
   at System.Linq.Enumerable.ToDictionary[TSource,TKey,TElement](IEnumerable`1 source, Func`2 keySelector, Func`2 elementSelector)
   at Chr.Avro.Serialization.BinaryRecordDeserializerBuilderCase.BuildExpression(Type type, Schema schema, BinaryDeserializerBuilderContext context)
   at Chr.Avro.Serialization.BinaryDeserializerBuilder.BuildExpression(Type type, Schema schema, BinaryDeserializerBuilderContext context)
   at Chr.Avro.Serialization.BinaryRecordDeserializerBuilderCase.<>c__DisplayClass7_3.<BuildExpression>b__7(RecordField field)
   --- End of inner exception stack trace ---
   at Chr.Avro.Serialization.BinaryRecordDeserializerBuilderCase.<>c__DisplayClass7_3.<BuildExpression>b__7(RecordField field)
   at System.Linq.Enumerable.SelectEnumerableIterator`2.MoveNext()
   at System.Collections.Generic.LargeArrayBuilder`1.AddRange(IEnumerable`1 items)
   at System.Collections.Generic.SparseArrayBuilder`1.ReserveOrAdd(IEnumerable`1 items)
   at System.Linq.Enumerable.ConcatNIterator`1.LazyToArray()
   at System.Dynamic.Utils.CollectionExtensions.ToReadOnly[T](IEnumerable`1 enumerable)
   at System.Linq.Expressions.Expression.Block(IEnumerable`1 variables, IEnumerable`1 expressions)
   at Chr.Avro.Serialization.BinaryRecordDeserializerBuilderCase.BuildExpression(Type type, Schema schema, BinaryDeserializerBuilderContext context)
   at Chr.Avro.Serialization.BinaryDeserializerBuilder.BuildExpression(Type type, Schema schema, BinaryDeserializerBuilderContext context)
   at Chr.Avro.Serialization.BinaryRecordDeserializerBuilderCase.<>c__DisplayClass7_3.<BuildExpression>b__7(RecordField field)
   --- End of inner exception stack trace ---
   at Chr.Avro.Serialization.BinaryRecordDeserializerBuilderCase.<>c__DisplayClass7_3.<BuildExpression>b__7(RecordField field)
   at System.Linq.Enumerable.SelectEnumerableIterator`2.MoveNext()
   at System.Collections.Generic.LargeArrayBuilder`1.AddRange(IEnumerable`1 items)
   at System.Collections.Generic.SparseArrayBuilder`1.ReserveOrAdd(IEnumerable`1 items)
   at System.Linq.Enumerable.ConcatNIterator`1.LazyToArray()
   at System.Dynamic.Utils.CollectionExtensions.ToReadOnly[T](IEnumerable`1 enumerable)
   at System.Linq.Expressions.Expression.Block(IEnumerable`1 variables, IEnumerable`1 expressions)
   at Chr.Avro.Serialization.BinaryRecordDeserializerBuilderCase.BuildExpression(Type type, Schema schema, BinaryDeserializerBuilderContext context)
   at Chr.Avro.Serialization.BinaryDeserializerBuilder.BuildExpression(Type type, Schema schema, BinaryDeserializerBuilderContext context)
   at Chr.Avro.Serialization.BinaryDeserializerBuilder.BuildDelegateExpression[T](Schema schema, BinaryDeserializerBuilderContext context)
   at Chr.Avro.Serialization.BinaryDeserializerBuilder.BuildDelegate[T](Schema schema, BinaryDeserializerBuilderContext context)
   at KafkaMessageConsumer.Deserialize(Byte[] data, Schema schema) in C:\Repos\AvroGen\KafkaMessageConsumer.cs:line 29
   at KafkaMessageConsumer.Consume(ConsumeContext`1 context) in C:\Repos\AvroGen\KafkaMessageConsumer.cs:line 20

Simplified class, without multiple, additional properties, stripped of ProtoMemberAttributes:


[DataContract]
public sealed class Request
{
    [DataMember] public bool Debug { get; set; }
    [DataMember] public int NetworkId { get; set; } = -1;
    [DataMember] public int SiteId { get; set; } = -1;
    [DataMember] public int PageId { get; set; }
    [DataMember] public IEnumerable<Size> RequestedSizes { get; set; }
    [DataMember] public string RequestUrl { get; set; }
    [DataMember] public string PageUrl { get; set; }
    [DataMember] public string PageDomain { get; set; }
    [DataMember] public string Keywords { get; set; }
    [DataMember] public InfoContext RequestInfoContext { get; set; }
    [DataMember] public bool HasIfa { get; set; }
    [DataMember] public bool HasIfv { get; set; }
    [DataMember] public string ImpressionUrl { get; set; }
    [DataMember] public string MraidOnePxImpressionUrl { get; set; }
    [DataMember] public string InventoryPartnerDomain { get; set; }
    [DataMember] public string UniqueId { get; set; }
}

[DataContract]
public class InfoContext
{
    [DataMember] public TestClass Parameter { get; set; }
    [DataMember] public int MaxDuration { get; set; }
    [DataMember] public int Size { get; set; }
}

[DataContract]
public class TestClass
{
    [DataMember] public string Platform { get; private set; }
    [DataMember] public int CurrentSpot { get; private set; }
    [DataMember] public string AdvertisingId { get; private set; }
    [DataMember] public string IdType { get; private set; }
    [DataMember] public bool IsLat { get; private set; }
    [DataMember] public string TransactionId { get; private set; }
    [DataMember] public TvDaiFeature DaiFeature { get; private set; }
}

Eveery other property gets mapped, except for that IdType in TestClass.

Second issue

I would like to use int as the key, I did not write it clear enough, my bad. Value Types are easy, it is basically handled automatically by SchemaBuilder. So what would I have to do to have int keys?

dstelljes commented 1 year ago

It looks like private set on those properties might be the issue here. To get around that, try adding BindingFlags.NonPublic to memberVisibility.

Custom serde cases would be the way to handle value type conversions as well: implement a case that matches the "int" schema and the string type.

koncq commented 1 year ago

Well, cannot really go with BindingFlags.NonPublic, but when I set a public setter, it did not really help...

dstelljes commented 1 year ago

Hmm, I'm unable to reproduce with the following code on .NET 7:

var schemaBuilder = new SchemaBuilder(
    memberVisibility: BindingFlags.Public | BindingFlags.Instance,
    enumBehavior: EnumBehavior.Symbolic,
    nullableReferenceTypeBehavior: NullableReferenceTypeBehavior.Annotated,
    temporalBehavior: TemporalBehavior.Iso8601);

var schemaBuilderContext = new SchemaBuilderContext();
var schema = schemaBuilder.BuildSchema<Request>(schemaBuilderContext);

var deserializer = new BinaryDeserializerBuilder(
        BinaryDeserializerBuilder
            .CreateDefaultCaseBuilders()
    )
    .BuildDelegate<Request>(schema);

Which version of .NET and which version of Chr.Avro are you using?

koncq commented 1 year ago

.net 7 and 9.4.1 - latest. i am unable to provide the whole project with repro today. I assume it won't be so easy to reproduce it without the whole context...

koncq commented 1 year ago

I have sent you link to the repo with reproduced issue.

EDIT: Could you explain what did you mean by?

Custom serde cases would be the way to handle value type conversions as well: implement a case that matches the "int" schema and the string type

dstelljes commented 1 year ago

When I run the reproduction locally, this is the trace I see:

Creating random SSPEngineRequest
Serializing request...
Deserializing request...
Unhandled exception. Chr.Avro.UnsupportedTypeException: The RequestContext member on AvroRepro.Request could not be mapped to the RequestContext field on AvroRepro.Request.
 ---> Chr.Avro.UnsupportedTypeException: The Parameter member on AvroRepro.Context could not be mapped to the Parameter field on AvroRepro.Context.
 ---> System.ArgumentException: Expression of type 'System.Object' cannot be used for constructor parameter of type 'System.String' (Parameter 'arguments[14]')
   at System.Dynamic.Utils.ExpressionUtils.ValidateOneArgument(MethodBase method, ExpressionType nodeKind, Expression arguments, ParameterInfo pi, String methodParamName, String argumentParamName, Int32 index)

If I remove the spotId parameter on the Parameter constructor, the program completes successfully. There’s no corresponding SpotId property on the Parameter class.


Custom serde cases would be the way to handle value type conversions as well: implement a case that matches the "int" schema and the string type

I’ll try to be a little more detailed. The easiest path would probably be to create a class that extends BinaryStringSerializerBuilderCase and override BuildStaticConversion to support conversion from int.