AdrianStrugala / AvroConvert

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

Support get only properties #138

Closed Mitkee closed 7 months ago

Mitkee commented 10 months ago

Describe the solution you'd like Currently, get only properties is not supported in deserialization. For example:

public class MyThing {
  public int Number {get; set; }
  public int NextNumber => Number +1;
}

We can correctly write this schema, but we'll be unable to read it since NextNumber does not have a setter. As a workaround for this, the following can be done:

public class MyThing {
 public int Number {get; set;}
 public int NextNumber  {
        get => Number +1;
        init
        {
            //Ignored, set method only here to not make deserialization fail
        }
    }
}

It would be nice if get only properties would be ignored either out of the box, or if a special attribute could be added to opt in for this behavior.

Describe implementation idea How it could be done? Either we make the following case work out of the box;

public class MyThing {
  public int Number {get; set; }
  public int NextNumber => Number +1;
}

Or, a custom attribute is added to support this case:

public class MyThing {
  public int Number {get; set; }
  [ReadOnly]
  public int NextNumber => Number +1;
}

The expected outcome of both options would be that the value is written when deserializing, but it won't be populated when deserializing.

AdrianStrugala commented 10 months ago

Hello! Thank you for reporting this problem. I will investigate the solution. Best, Adrian

AdrianStrugala commented 10 months ago

Hello @Mitkee

The fix is ready. From v.3.4.2 of AvroConvert the get-only properties should not be throwing errors during deserialization.

Regards, Adrian

Mitkee commented 10 months ago

Awesome, thanks! 🙏

Mitkee commented 10 months ago

While the basic cases seem to work, it seems certain chains of interactions are causing deserialization to fail still.

using NUnit.Framework;
using SolTechnology.Avro;

namespace TestNestedReadonly;

public record Parent
{
    public List<Child> Children { get; set; } = new();
}

public record Child
{
    public List<Toy> Toys { get; set; } = new();

    //Need to clean this up
    public int? TotalPrice => Toys != null && Toys.Any() ? Toys.Sum(v => v.Price) : null;

    public int? TotalWeight => Toys!= null && Toys.Any() ? Toys.Sum(v => v.Weight) : null;
    // Uncomment this for an interesting process crash
    //public decimal? AveragePricePerWeight => Toys?.Average(t => t.PricePerWeightUnit);
}

public record Toy
{
    public string Name { get; set; }
    public int Weight { get; set; }
    public int Price { get; set; }

    public decimal PricePerWeightUnit => decimal.Round(Price / Weight, 2);
}

public class AvroReadonlyIssue
{

    [Test]
    public void TestReadonlySerializationAndDeserialization()
    {
        var parent = new Parent()
        {
            Children = new List<Child>
            {
                new Child()
                {
                    Toys = new List<Toy>()
                    {
                        new Toy()
                        {
                            Weight = 10, Price = 11, Name = "Toy1"
                        },
                        new Toy()
                        {
                            Weight = 6, Price = 2, Name = "Toy2"
                        }
                    },
                }
            }
        };
        var serialized = AvroConvert.Serialize(parent);
        var deserialized = AvroConvert.Deserialize<Parent>(serialized);
        Assert.Pass();
    }
}

Take the above test - the model is serialized properly, but deserializing gives

SolTechnology.Avro.Infrastructure.Exceptions.AvroTypeMismatchException : Unable to deserialize [Parent] of schema [Record] to the target type [TestNes...

SolTechnology.Avro.Infrastructure.Exceptions.AvroTypeMismatchException : Unable to deserialize [Parent] of schema [Record] to the target type [TestNestedReadonly.Parent]. Inner exception:
  ----> SolTechnology.Avro.Infrastructure.Exceptions.AvroTypeMismatchException : Unable to deserialize [array] of schema [Array] to the target type [System.Collections.Generic.List`1[TestNestedReadonly.Child]]. Inner exception:
  ----> SolTechnology.Avro.Infrastructure.Exceptions.AvroTypeMismatchException : Unable to deserialize [Child] of schema [Record] to the target type [TestNestedReadonly.Child]. Inner exception:
  ----> SolTechnology.Avro.Infrastructure.Exceptions.AvroTypeMismatchException : Unable to deserialize [array] of schema [Array] to the target type [System.Collections.Generic.List`1[TestNestedReadonly.Toy]]. Inner exception:
  ----> SolTechnology.Avro.Infrastructure.Exceptions.AvroTypeMismatchException : Unable to deserialize [Toy] of schema [Record] to the target type [TestNestedReadonly.Toy]. Inner exception:
  ----> SolTechnology.Avro.Infrastructure.Exceptions.AvroTypeMismatchException : Unable to deserialize [int] of schema [Int] to the target type [System.Int32]. Inner exception:
  ----> System.IO.EndOfStreamException : Attempted to read past the end of the stream.
   at SolTechnology.Avro.AvroObjectServices.Read.Resolver.Resolve(TypeSchema writerSchema, TypeSchema readerSchema, IReader reader, Type type)
   at SolTechnology.Avro.AvroObjectServices.Read.Resolver.Resolve[T](IReader reader, Int64 itemsCount)
   at SolTechnology.Avro.Features.Deserialize.Decoder.Read[T](Reader reader, Header header, AbstractCodec codec, Resolver resolver)
   at SolTechnology.Avro.Features.Deserialize.Decoder.Decode[T](Stream stream, TypeSchema readSchema)
   at SolTechnology.Avro.AvroConvert.Deserialize[T](Byte[] avroBytes)
   at TestNestedReadonly.AvroReadonlyIssue.TestReadonlySerializationAndDeserialization() in C:\Projects\nps.dataservices.auctions\NPS.DataServices.Auction.IntegrationTests\TestRepositories\AvroReadonlyIssue.cs:line 63
--AvroTypeMismatchException
   at SolTechnology.Avro.AvroObjectServices.Read.Resolver.Resolve(TypeSchema writerSchema, TypeSchema readerSchema, IReader reader, Type type)
   at SolTechnology.Avro.AvroObjectServices.Read.Resolver.ResolveRecord(RecordSchema writerSchema, RecordSchema readerSchema, IReader reader, Type type)
   at SolTechnology.Avro.AvroObjectServices.Read.Resolver.Resolve(TypeSchema writerSchema, TypeSchema readerSchema, IReader reader, Type type)
--AvroTypeMismatchException
   at SolTechnology.Avro.AvroObjectServices.Read.Resolver.Resolve(TypeSchema writerSchema, TypeSchema readerSchema, IReader reader, Type type)
   at SolTechnology.Avro.AvroObjectServices.Read.Resolver.ResolveArray(TypeSchema writerSchema, TypeSchema readerSchema, IReader reader, Type type, Int64 itemsCount)
   at SolTechnology.Avro.AvroObjectServices.Read.Resolver.Resolve(TypeSchema writerSchema, TypeSchema readerSchema, IReader reader, Type type)
--AvroTypeMismatchException
   at SolTechnology.Avro.AvroObjectServices.Read.Resolver.Resolve(TypeSchema writerSchema, TypeSchema readerSchema, IReader reader, Type type)
   at SolTechnology.Avro.AvroObjectServices.Read.Resolver.ResolveRecord(RecordSchema writerSchema, RecordSchema readerSchema, IReader reader, Type type)
   at SolTechnology.Avro.AvroObjectServices.Read.Resolver.Resolve(TypeSchema writerSchema, TypeSchema readerSchema, IReader reader, Type type)
--AvroTypeMismatchException
   at SolTechnology.Avro.AvroObjectServices.Read.Resolver.Resolve(TypeSchema writerSchema, TypeSchema readerSchema, IReader reader, Type type)
   at SolTechnology.Avro.AvroObjectServices.Read.Resolver.ResolveArray(TypeSchema writerSchema, TypeSchema readerSchema, IReader reader, Type type, Int64 itemsCount)
   at SolTechnology.Avro.AvroObjectServices.Read.Resolver.Resolve(TypeSchema writerSchema, TypeSchema readerSchema, IReader reader, Type type)
--AvroTypeMismatchException
   at SolTechnology.Avro.AvroObjectServices.Read.Resolver.Resolve(TypeSchema writerSchema, TypeSchema readerSchema, IReader reader, Type type)
   at SolTechnology.Avro.AvroObjectServices.Read.Resolver.ResolveRecord(RecordSchema writerSchema, RecordSchema readerSchema, IReader reader, Type type)
   at SolTechnology.Avro.AvroObjectServices.Read.Resolver.Resolve(TypeSchema writerSchema, TypeSchema readerSchema, IReader reader, Type type)
--EndOfStreamException
   at SolTechnology.Avro.AvroObjectServices.Read.Reader.Read()
   at SolTechnology.Avro.AvroObjectServices.Read.Reader.ReadLong()
   at SolTechnology.Avro.AvroObjectServices.Read.Reader.ReadInt()
   at SolTechnology.Avro.AvroObjectServices.Read.Resolver.ResolveInt(Type readType, IReader reader)
   at SolTechnology.Avro.AvroObjectServices.Read.Resolver.Resolve(TypeSchema writerSchema, TypeSchema readerSchema, IReader reader, Type type)

-----

One or more child tests had errors
  Exception doesn't have a stacktrace

Even more interesting - uncommenting //public decimal? AveragePricePerWeight => Toys?.Average(t => t.PricePerWeightUnit); causes the whole process to fail when deserializing. It doesn't throw out, but instead this happens: image

I've a feeling something weird is still happening with the read only properties here? In basic cases, it works as expected.

AdrianStrugala commented 10 months ago

Well, I need to dig deeper into that. Thank you for providing the test case

AdrianStrugala commented 10 months ago

Hey,

The problem was with caching during the deserialization of a collection of classes with read-only properties. It should be fixed now. As for the calculated decimal property you might want to add AvroDecimal attribute:

[AvroDecimal(Precision = 28, Scale = 28)]
public decimal? AveragePricePerWeight => Toys?.Average(t => t.PricePerWeightUnit);

It's to ensure the result not exceeding max precision of avro decimal.

Best, Adrian