peteroupc / CBOR

A C# implementation of Concise Binary Object Representation (RFC 8949).
The Unlicense
206 stars 29 forks source link

Object serialization/deserialization and using integers as map keys #50

Closed finnkam closed 3 years ago

finnkam commented 3 years ago

First, thanks for making this cbor implementation available. 😊

I am working on a C# application which needs to communicate cbor with an embedded device (which is under development). Due to constraints on the embedded device it has been suggested that the cbor map keys could be integers rather than utf8 strings. I have been playing around with PeterO.Cbor, and what I would like to do is to use C# pocos and have them serialized/deserialized to/from cbor. So far the poco property names gets serialized as utf8 keys in the cbor maps, but what I am hoping for is integer keys.

Is there a way with the current PeterO.Cbor package to do C# poco serialization/deserialization using integers as keys in the cbor maps? If not, is it something that the package could support out of the box in a future release – or something that the package could allow through client side extensions? 😊

peteroupc commented 3 years ago

Take a look at CBORTypeMapper, which allows you to implement a custom way to convert objects of a given type to and from CBOR objects (ICBORToFromConverter).

finnkam commented 3 years ago

Hi Peter,

Thanks for your fast response. :)

I have been playing around with the CBORTypeMapper and the following example model:

    public class SimpleDto
    {
        [CborKey(3)]
        public int PropA { get; set; }

        [CborKey(4)]
        public int PropB { get; set; }
    }

    public class CompositeDto
    {
        [CborKey(2)]
        public SimpleDto Simple { get; set; }

        [CborKey(1)]
        public List<string> Strings { get; set; }

        [CborKey(5)]
        public ICollection<SimpleDto> Simples { get; set; }
    }

Notice the custom attributes, implemented using:

    [AttributeUsage(AttributeTargets.Property)]
    public sealed class CborKeyAttribute : Attribute
    {
        public CborKeyAttribute(int key)
        {
            Key = key;
        }

        public int Key { get; }
    }

I am hoping to avoid keeping a redundant reflection model for the integer key values, so I made an experiment tapping into PropertyMap and PropertyData. I have sketched this generic converter:

    public class MyConverter<T> : ICBORConverter<T> where T: class, new()
    {
        private CBORTypeMapper typeMapper;

        public MyConverter(CBORTypeMapper typeMapper)
        {
            this.typeMapper = typeMapper;
        }

        public CBORObject ToCBORObject(T obj)
        {
            var cborObject = CBORObject.NewMap();
            foreach (var propertyData in PropertyMap.GetPropertyList(obj.GetType()))
                cborObject[propertyData.KeyAttribute.Key] = CBORObject.FromObject(propertyData.GetValue(obj), typeMapper);
            return cborObject;
        }

        public object FromCBORObject(CBORObject cborObject)
        {
            if (cborObject.Type != CBORType.Map)
            {
                throw new CBORException($"Expected map type object. Was:{cborObject.Type}");
            }

            var result = new T();
            var properties = PropertyMap.GetPropertyList(typeof(T))
                .ToDictionary(x => x.KeyAttribute.Key);

            foreach (var item in cborObject.Entries)
            {
                var key = item.Key.AsNumber().ToByteUnchecked();
                if (!properties.TryGetValue(key, out var pd)) continue;

                var value = item.Value.ToObject(pd.PropertyType, typeMapper);
                pd.SetValue(result, value);
            }
            return result;
        }
    }

.. naturally a converter for each dto type must be registered in the CBORTypeMapper. :)

But, in order to pull this off I had to make local changes to PropertyData and PropertyMap. My copy PropertyData also carries the CborKeyAttribute and PropertyMap.GetPropertyList knows how to work with CborKeyAttribute.

My main problem with this is I am taking dependency on non-public API, which likely causes maintenance down the road as new versions of PeterO.Cbor may have changes to PropertyData and/or PropertyMap.

So I am hoping you might provide guidance how to proceed. :) I'd like to retain the approach with annotating the properties with attributes for the CBOR key values. And if at all possible I'd like to not maintain my own reflection model. Would it make sense to somehow make PropertyMap and PropertyData accessible outside the library (inside converters)?

peteroupc commented 3 years ago

Your example types have properties that are all annotated with CborKey attribute. In addition, it appears you're using PropertyMap only to find out which properties have the CborKey attribute.

Thus, it seems to me that you can do this using the reflection API directly, rather than resorting to the non-public PropertyMap.GetPropertyList method. In any case, the purpose of PropertyMap is to hold functionality that is highly specific to the C# version of this project and is difficult to "translate" to the Java version of this project. This includes the use of .NET-specific reflection APIs.

finnkam commented 3 years ago

In the example I am using PropertyMap and PropertyData changes to find the CborKey attribute, associate it to PropertyData and invoking GetValue and SetValue in context of the cbor key value. So the changes are a bit more involved than just finding the CborKey attribute.

I agree that it is not good to depend on non-public stuff, which is why I was thinking that the lib might aid/open for this way of use. But I didn’t consider the java implementation and the possible difference such a change might cause in the common api. :/

So I am going to implement my own classes which does the same as PropertyMap and PropertyData but also works with the CborKey attribute.

Thanks for taking the time to comment on this thread so far. 😊