如何设置构造函数来反序列化只获取属性,而不必在c#中重复代码?

如何设置构造函数来反序列化只获取属性,而不必在c#中重复代码?,c#,json,deserialization,C#,Json,Deserialization,通过一个例子来描述这个问题 我有一个抽象基类框 abstract class Box { public Box(double panelThickness) : this(IDGenerator.GetNewID(), panelThickness) { } protected Box(int id, double panelThickness) { ID = id

通过一个例子来描述这个问题

我有一个抽象基类

    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; }
    }
继承的类
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; }
    }
和一个简单的
IDGenerator

    static class IDGenerator
    {
        private static int _id = 0;

        internal static int GetNewID()
        {
            _id++;
            return _id;
        }
    }
可通过此测试方法运行示例:

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);
    }
使用Newtonsoft.Json;
[测试方法]
public void BoxJsonDeserializationTest()
{
矩形框矩形框1=新矩形框(8,9);
JsonSerializerSettings serializationSettings=新的JsonSerializerSettings
{
TypeNameHandling=TypeNameHandling.Objects,
ConstructorHandling=ConstructorHandling.AllowNonPublicDefaultConstructor
};
string-boxJsonString=JsonConvert.SerializeObject
(矩形框1,格式化。缩进,序列化设置);
var rectangularBoxFromJson=JsonConvert.DeserializeObject
(boxJsonString,序列化设置);
}
实例化
RectangularBox
的对象会在基类中生成一个ID,并在子类的公共构造函数中分配
XDimension
YDimension
属性。这里需要注意两件事:

  • XDimension
    YDimension
    都是只能获取的属性。因此,它只能在构造函数中赋值
  • 用户不能通过输入生成ID。基类中有一个Get only ID属性,这是禁止的。如果使用了
    RectangularBox
    的公共构造函数,则会自动生成一个新ID。但是,如果从JSON反序列化
    RectangularBox
    ,并且基类中有一个受保护的构造函数,该构造函数由子类中的私有构造函数调用,允许在从JSON反序列化时设置ID属性(使用Newtonsoft.JSON)
  • 将此对象序列化为JSON,然后在稍后进行反序列化时,不应为该对象生成新的
    ID
    ,而应从JSON中分配
    ID
    属性。类似地,
    XDimension
    YDimension
    属性也必须来自JSON。因此de>[JsonConstructor]属性,位于
    RectangularBox
    的私有构造函数上

    问题是我无法找到一种方法来消除
    RectangularBox
    的两个构造函数中的代码重复,但仍然能够从JSON中反序列化get only属性。这些属性可以有私有setter,并用
    [JsonProperty]标记
    属性,这将允许将属性分配从构造函数中删除到单独的方法中,但这是不需要的。创建
    RectangularBox
    对象后,不应允许用户更改这些属性


    任何帮助都将不胜感激。

    您可以使
    id
    参数为空:

    abstract class Box
    {
        public Box(double panelThickness) : this(null, panelThickness) { }
    
        protected Box(int? id, double panelThickness)
        {
            ID = id ?? IDGenerator.GetNewID();
            PanelThickness = panelThickness;
        }
    
        public int ID { get; }
        public double PanelThickness { get; }
    }
    
    class RectangularBox : Box
    {
        private static double _rectPanelThickness = 0.2;
    
        public RectangularBox(double xDimension, double yDimension)
            : this(null, xDimension, yDimension) { }
    
        [JsonConstructor]
        private RectangularBox(int? id, double xDimension, double yDimension)
            : base(id, _rectPanelThickness)
        {
            XDimension = xDimension;
            YDimension = yDimension;
        }
    
        public double XDimension { get; }
        public double YDimension { get; }
    }
    

    显然,如果您的JSON不包含
    id
    ,则会生成一个新id,但这似乎是您可以控制的。

    作为一个选项,您可以将get-only属性拆分为property和readonly字段(主要是编译器在使用
    SomeProperty{get;}
    时为您所做的),然后执行以下操作:

    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;
    }
    
    它之所以能工作是因为JSON.NET可以设置只读字段没有问题,但是当您使用自动生成的只读属性时——它不知道对应的只读字段,它看到的只是一个没有setter的属性,因此无法设置它的值。在这里,我们显式地告诉它要使用哪个字段

    它是否比构造函数中的代码复制“更好”是有争议的,但至少达到了您想要的结果(属性仍然是只读的,构造函数中没有代码复制)

    编辑:我注意到C#7.3引入了一个特性-您可以使用
    [field:AttributeHere]标记自动生成的属性
    注释,并将该属性应用于自动生成的字段。但是,自动生成的字段具有
    CompilerGenerated
    属性,因此默认情况下JSON.NET将忽略该属性。但是,有一个设置可更改该属性,此设置在
    ContractResolver
    上,例如:

    JsonSerializerSettings serializationSettings = new JsonSerializerSettings
    {
        TypeNameHandling = TypeNameHandling.Objects,
        ConstructorHandling = ConstructorHandling.AllowNonPublicDefaultConstructor,
        ContractResolver = new DefaultContractResolver() {
            SerializeCompilerGeneratedMembers = true
        }
    };
    
    如果您使用的是C#7.3+编译器,那么您可能不会拆分readonly属性,而是用属性标记它:


    不幸的是,这并不容易(直到C#9)。处理此问题的常用方法是为序列化创建一组类型,为业务逻辑创建一组类型。这通常还可以解决序列化输出中的格式与业务逻辑类型允许的格式略有不同的问题。在C#9中,可以将属性设置器设置为
    init
    ,这意味着它们将在初始化表达式中可用,但也可以在反序列化使用的反射中使用。请注意,您发布的代码不正确,因为私有json构造函数调用
    base(id)
    ,而调用的父构造函数实际上是
    公共框(双面板厚度)
    。应该是
    base(id,_)面板厚度)
    我猜。你不能使用一个只从JSON反序列化程序调用的私有构造函数吗?如下面的回答@Evk。谢谢。你是正确的。我更正了上面的代码示例。@Johnathan Barclay。谢谢。已经有一段时间了,但最终,我使用了这个解决方案,最初认为它与JSON反序列化程序相比会是一个更简单的重构@Evk提供的解决方案也会很好地工作。我后来意识到,两种解决方案的重构都需要相似的时间。我本想将两个答案都标记为正确,但可惜只能标记一个。谢谢你的帮助。
    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; }
    }