Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How to set up constructors for deserialization of Get only properties without having to duplicate code in c#?

The problem is described by means of an example.

I have an abstract base class Box,

    abstract class Box
    {
        public Box(double panelThickness) : 
            this(IDGenerator.GetNewID(), panelThickness)
        { }

        protected Box(int id, double panelThickness)
        {
            ID = id;
            PanelThickness = panelThickness;
        }

        public int ID { get; }
        public double PanelThickness { get; }
    }

an inherited class RectangularBox

    class RectangularBox : Box
    {
        private static double _rectPanelThickness = 0.2;

        public RectangularBox(double xDimension, double yDimension) : 
            base(_rectPanelThickness)
        {
            // ---- Code duplication:
            XDimension = xDimension;
            YDimension = yDimension;
        }

        [JsonConstructor]
        private RectangularBox(int id, double xDimension, double yDimension) : 
            base (id, _rectPanelThickness)
        {
            // ---- Code duplication:
            XDimension = xDimension;
            YDimension = yDimension;
        }

        public double XDimension { get; }
        public double YDimension { get; }
    }

and a simple IDGenerator:

    static class IDGenerator
    {
        private static int _id = 0;

        internal static int GetNewID()
        {
            _id++;
            return _id;
        }
    }

An example can be run by means of this test method:

using Newtonsoft.Json;    
[TestMethod]
    public void BoxJsonDeserializationTest()
    {
        RectangularBox rectangularBox1 = new RectangularBox(8, 9);

        JsonSerializerSettings serializationSettings = new JsonSerializerSettings
        {
            TypeNameHandling = TypeNameHandling.Objects,
            ConstructorHandling = ConstructorHandling.AllowNonPublicDefaultConstructor
        };

        string boxJsonString = JsonConvert.SerializeObject
            (rectangularBox1, Formatting.Indented, serializationSettings);

        var rectangularBoxFromJson = JsonConvert.DeserializeObject<RectangularBox>
            (boxJsonString, serializationSettings);
    }

Instantiating an object of RectangularBox causes an ID to be generated in the base class and the XDimension and YDimension properties to be assigned in the public constructor of the sub-class. Two things to note here:

  1. Both XDimension and YDimension are Get only properties. Hence, its only assignable in the constructor.
  2. The user should not be able to generate an ID by input. This is prohibited by having a Get only ID property in the base class. If the public constructor of RectangularBox is used, a new ID is generated automatically. However, upon deserializing RectangularBox from JSON and having a protected constructor in the base class called by the private constructor in the child class allowing the ID property to be set when deserializing from JSON (using Newtonsoft.Json.

When serializing this object to JSON and then deserializing at a later stage, a new ID should not be generated for the object, but instead the ID property be assigned from the JSON. Similarly, the XDimension and YDimension properties must also come from the JSON. Hence the reason for the [JsonConstructor] attribute over the private constructor of RectangularBox.

The problem is that I cannot find a way to get rid of the code duplication in both constructors of RectangularBox, but still maintain the ability to deserialize the Get only properties from a JSON. The properties could have private setters and marked with [JsonProperty] attributes, which would allow the properties assignments to be removed from the constructors into a separate method, but this is not desired. The user should not be allowed to change those properties once a RectangularBox object is created.

Any help would be appreciated.

like image 372
Francois Louw Avatar asked Feb 02 '26 11:02

Francois Louw


1 Answers

As an option, you can split your get-only property into property and readonly field (bascially what compiler does for you when you use SomeProperty {get;}), and then do this:

abstract class Box
{
    [JsonProperty(nameof(ID))]
    private readonly int _id;
    [JsonProperty(nameof(PanelThickness))]
    private readonly double _panelThickness;
    protected Box(double panelThickness)
    {
        _id = IDGenerator.GetNewID();
        _panelThickness = panelThickness;
    }

    protected Box()
    {
        // default contstructor for deserialization
    }

    [JsonIgnore]
    public int ID => _id;
    [JsonIgnore]
    public double PanelThickness => _panelThickness;
}

class RectangularBox : Box
{
    private static double _rectPanelThickness = 0.2;
    [JsonProperty(nameof(XDimension))]
    private readonly double _xDimension;
    [JsonProperty(nameof(YDimension))]
    private readonly double _yDimension;

    public RectangularBox(double xDimension, double yDimension) :
        base(_rectPanelThickness)
    {
        _xDimension = xDimension;
        _yDimension = yDimension;
    }

    protected RectangularBox()
    {
        // default contstructor for deserialization
    }

    [JsonIgnore]
    public double XDimension => _xDimension;
    [JsonIgnore]
    public double YDimension => _yDimension;
}

It works because JSON.NET can set readonly fields no problem, but when you use autogenerated readonly property - it has no idea about corresponding readonly field, all it sees is just a property which has no setter and so no means to set it's value. Here we tell it explicitly which field to use.

Whether it's "better" than code duplication in constructor is arguable, but at least achieves your desired result (properties are still readonly, no code duplication in constructors".

EDIT: I noticed that C# 7.3 introduced a feature - you can mark an autogenerated property with [field: AttributeHere] annotation and it will apply that attribute to autogenerated field. BUT, autogenerated field has CompilerGenerated attribute, so it will be ignored by default by JSON.NET. However, there is a setting to change that, this setting is on ContractResolver, for example:

JsonSerializerSettings serializationSettings = new JsonSerializerSettings
{
    TypeNameHandling = TypeNameHandling.Objects,
    ConstructorHandling = ConstructorHandling.AllowNonPublicDefaultConstructor,
    ContractResolver = new DefaultContractResolver() {
        SerializeCompilerGeneratedMembers = true
    }
};

Then you might not split your readonly property, and instead mark it with attributes, if you are using compiler for C# 7.3+:

abstract class Box
{
    protected Box(double panelThickness)
    {
        ID = IDGenerator.GetNewID();
        PanelThickness = panelThickness;
    }

    protected Box()
    {
        // default contstructor for deserialization
    }

    [JsonIgnore]
    [field: JsonProperty(nameof(ID))]
    public int ID { get; }

    [JsonIgnore]
    [field: JsonProperty(nameof(PanelThickness))]
    public double PanelThickness { get; }
}

class RectangularBox : Box
{
    private static double _rectPanelThickness = 0.2;

    public RectangularBox(double xDimension, double yDimension) :
        base(_rectPanelThickness)
    {
        XDimension = xDimension;
        YDimension = yDimension;
    }

    protected RectangularBox()
    {
        // default contstructor for deserialization
    }

    [JsonIgnore]
    [field: JsonProperty(nameof(XDimension))]
    public double XDimension { get; }

    [JsonIgnore]
    [field: JsonProperty(nameof(YDimension))]
    public double YDimension { get; }
}
like image 188
Evk Avatar answered Feb 03 '26 23:02

Evk