AdrianStrugala / AvroConvert

Rapid Avro serializer for C# .NET
Other
97 stars 27 forks source link

ReflectionSchemaBuilder: IEnumerable isn't recognized as array #146

Closed horato closed 4 months ago

horato commented 5 months ago

Probably caused by this line. IsArray only works for System.Array, but returns false for IEnumerable. https://github.com/AdrianStrugala/AvroConvert/blob/fe08cf09b28dec77848a29b06300c7e470deffb2/src/AvroConvert/AvroObjectServices/BuildSchema/ReflectionSchemaBuilder.cs#L247

AdrianStrugala commented 5 months ago

Hello @horato, Thank you for reporting the issue. Could you provide a little bit more context? What is your use case? Best, Adrian

horato commented 5 months ago

Hello, I've been tasked with Confluent Kafka integration. I use confluent-kafka-dotnet for comms, but their avro->csharp generator, for whatever reason, includes full avro schema in a string inside each class and parses the schema on demand. I've decided to write my own class generator to comply with code conventions and replace the json string with runtime schema generation from AvroConvert. Using the two sample classes/records below, AvroConvert.GenerateSchema fails to identify the IEnumerable attributes as an array and tries to serialize them as if they were another plain record. Replacing the IEnumerable with an IList seems to produce correct schema, but then fails on AvroConvert.Deserialize anyway.

/// <summary>  </summary>
[GeneratedCode("Avro.MSBuild", "1.11.3.1")]
public class Chapter : ISpecificRecord
{
    private static readonly Schema _schema = Schema.Parse(AvroConvert.GenerateSchema(typeof(Chapter)));

    /// <inheritdoc />
    [IgnoreDataMember]
    public Schema Schema => _schema;

    /// <summary>  </summary>
    [NullableSchema]
    public IEnumerable<Paragraph> Paragraphs { get; private set; }

    /// <summary>  </summary>
    [NullableSchema]
    public string Title { get; private set; }

    /// <summary> for serializer </summary>
    [EditorBrowsable(EditorBrowsableState.Never)]
    public Chapter()
    {

    }

    public Chapter(IEnumerable<Paragraph> paragraphs, string title)
    {
        Paragraphs = paragraphs;
        Title = title;
    }

    /// <inheritdoc />
    public object Get(int fieldPos)
    {
        return fieldPos switch
        {
            0 => Paragraphs,
            1 => Title,
            _ => throw new ArgumentOutOfRangeException(nameof(fieldPos), fieldPos, null)
        };
    }

    /// <inheritdoc />
    public void Put(int fieldPos, object fieldValue)
    {
        switch (fieldPos)
        {
            case 0: Paragraphs = (IEnumerable<Paragraph>)fieldValue; break;
            case 1: Title = (string)fieldValue; break;
            default: throw new ArgumentOutOfRangeException(nameof(fieldPos), fieldPos, null);
        };
    }
}

/// <summary>  </summary>
[GeneratedCode("Avro.MSBuild", "1.11.3.1")]
public class Paragraph : ISpecificRecord
{
    private static readonly Schema _schema = Schema.Parse(AvroConvert.GenerateSchema(typeof(Paragraph)));

    /// <inheritdoc />
    [IgnoreDataMember]
    public Schema Schema => _schema;

    /// <summary>  </summary>
    [NullableSchema]
    public string SentenceSeparator { get; private set; }

    /// <summary>  </summary>
    [NullableSchema]
    public string Style { get; private set; }

    /// <summary> for serializer </summary>
    [EditorBrowsable(EditorBrowsableState.Never)]
    protected Paragraph()
    {

    }

    public Paragraph(string sentenceSeparator, string style)
    {
        SentenceSeparator = sentenceSeparator;
        Style = style;
    }

    /// <inheritdoc />
    public object Get(int fieldPos)
    {
        return fieldPos switch
        {
            0 => SentenceSeparator,
            1 => Style,
            _ => throw new ArgumentOutOfRangeException(nameof(fieldPos), fieldPos, null)
        };
    }

    /// <inheritdoc />
    public void Put(int fieldPos, object fieldValue)
    {
        switch (fieldPos)
        {
            case 0: SentenceSeparator = (string)fieldValue; break;
            case 1: Style = (string)fieldValue; break;
            default: throw new ArgumentOutOfRangeException(nameof(fieldPos), fieldPos, null);
        };
    }
}
AdrianStrugala commented 5 months ago

Hey, I confirm, that the problem is with IEnumerable not being recognized as Avro array. I will work on a fix this week. Best, Adrian

AdrianStrugala commented 4 months ago

Hello @horato

Thank you for your contribution. IEnumberable should be handled correctly starting from v.3.4.5 of AvroConvert.

Regards, Adrian