C# 使用Web API反序列化为具有抽象属性的类

C# 使用Web API反序列化为具有抽象属性的类,c#,asp.net-web-api,json.net,deserialization,C#,Asp.net Web Api,Json.net,Deserialization,我试图编写一组类来表示一个特别复杂的对象,在其中一个类中,我有一个属性,它被设置为三个可能的派生类的基类(抽象类)。我正在设置一个ASP.NETWebAPI来处理序列化和反序列化,这意味着,默认情况下,它使用Json.NET作为Json。如何让Web API正确地反序列化通过POST发送的JSON或将其放入正确的派生类中 带有抽象成员的类如下所示(为了清晰起见,我加入了Xml装饰器,因为它们在使用XmlSerializer反序列化Xml时工作得非常好) 抽象类非常基本: [Serializabl

我试图编写一组类来表示一个特别复杂的对象,在其中一个类中,我有一个属性,它被设置为三个可能的派生类的基类(抽象类)。我正在设置一个ASP.NETWebAPI来处理序列化和反序列化,这意味着,默认情况下,它使用Json.NET作为Json。如何让Web API正确地反序列化通过POST发送的JSON或将其放入正确的派生类中

带有抽象成员的类如下所示(为了清晰起见,我加入了Xml装饰器,因为它们在使用XmlSerializer反序列化Xml时工作得非常好)

抽象类非常基本:

[Serializable]
public abstract class AFormulaItemStructure { }
抽象类有三个派生类:

[Serializable]
public class ColumnStructure: AFormulaItemStructure {
    [XmlAttribute("type")]
    public string Type;

    [XmlAttribute("field")]
    public string Field;

    [XmlAttribute("display")]
    public string Display;
}

[Serializable]
public class FunctionStructure: AFormulaItemStructure {
    [XmlAttribute("type")]
    public string Type;

    [XmlAttribute("name")]
    public string Name;

    [XmlElement("parameters")]
    public string Parameters;
}


[Serializable]
public class OperandStructure: AFormulaItemStructure {
    [XmlAttribute("type")]
    public string Type;

    [XmlElement("left")]
    public string Left;

    [XmlElement("right")]
    public string Right;
}
目前,使用
[DataContract]
属性,Json.NET格式化程序无法填充派生类,导致属性
为null


问题 我可以在同一个类中将
XmlSerializer
属性与
DataContractSerializer
属性混合使用吗?我使用
XmlSerializer
是因为我在设计的xml中使用了xml属性,但这可以在必要时进行更改,因为我自己正在开发xml模式

Json.NET中与
[KnownType()]
的等价物是什么?Json.NET似乎不尊重
KnownType的
DataContractSerializer
版本。我是否需要使用自己的JsonConverter来确定正确的类型

我该如何装饰类,以便
DataContractSerializer
DataContractJsonSerializer
能够正确地反序列化Xml和Json中的对象?我的目标是将其放入ASP.NET Web API中,因此我希望能够灵活地生成Xml或Json,根据所请求的类型而定。如果Json.NET不起作用,我是否需要使用其他格式化程序来处理这个复杂的类

我需要能够在客户端生成一个对象,而不必在对象中包含.NET类名


测试和改进 在我对Web API的测试中,默认序列化会向下发送到客户端:

{"FormulaItem":{"type":"int","field":"my_field","display":"My Field"}}
这对我的目的来说很理想。但是,让它返回API并反序列化为正确的派生类型是不起作用的(它为属性生成null)

在下面的测试答案中,他用于测试的
DataContractSerializer
生成:

{"FormulaItem":{"__type":"column:#ExpressionStructureExperimentation.Models","display":"My Field","field":"my_field","type":"int"}}

这对我和代码的可维护性都不起作用(如果我将整个名称空间硬编码到JavaScript中以生成这些对象,重构就成了一个PITA)

您可以像前面提到的那样进行混合,但我认为您不需要这样做,我自己也没有使用WEB api,但WCF Rest从DataContracts(不带xml..tags)生成xml和json,对您的类进行如下标记:

[DataContract]
public class FormulaStructure
{
    [DataMember]
    public AFormulaItemStructure FormulaItem;
}

[DataContract]
[KnownType(typeof(ColumnStructure))]
[KnownType(typeof(FunctionStructure))]
[KnownType(typeof(OperandStructure))]
public abstract class AFormulaItemStructure { }

[DataContract(Name="column")]
public class ColumnStructure : AFormulaItemStructure
{
    [DataMember(Name="type")]
    public string Type;

    [DataMember(Name = "field")]
    public string Field;

    [DataMember(Name = "display")]
    public string Display;
}

[DataContract(Name="function")]
public class FunctionStructure : AFormulaItemStructure
{
    [DataMember(Name = "type")]
    public string Type;

    [DataMember(Name = "name")]
    public string Name;

    [DataMember(Name = "parameters")]
    public string Parameters;
}

[DataContract(Name = "operand")]
public class OperandStructure : AFormulaItemStructure
{
    [DataMember(Name = "type")]
    public string Type;

    [DataMember(Name = "left")]
    public string Left;

    [DataMember(Name = "right")]
    public string Right;
}
如果您需要对生成的XML/JSON进行更多控制,那么可能需要进一步调整。我使用此代码测试:

    public static string Serialize(FormulaStructure structure)
    {
        using (MemoryStream memoryStream = new MemoryStream())
        using (StreamReader reader = new StreamReader(memoryStream))
        {
            var serializer = new DataContractSerializer(typeof(FormulaStructure));
            serializer.WriteObject(memoryStream, structure);
            memoryStream.Position = 0;
            return reader.ReadToEnd();
        }
    }

    public static FormulaStructure Deserialize(string xml)
    {
        using (Stream stream = new MemoryStream())
        {
            byte[] data = System.Text.Encoding.UTF8.GetBytes(xml);
            stream.Write(data, 0, data.Length);
            stream.Position = 0;
            var deserializer = new DataContractSerializer(typeof(FormulaStructure));
            return (FormulaStructure)deserializer.ReadObject(stream);
        }
    }

最后,我分解并在字符串变量中将.NET类信息添加到模块中,以使重构更容易

module.net = {};
module.net.classes = {};
module.net.classes['column'] = "ColumnStructure";
module.net.classes['function'] = "FunctionStructure";
module.net.classes['operand'] = "OperandStructure";
module.net.getAssembly = function (className) {
    return "MyNamespace.Models." + module.net.classes[className] + ", MyAssembly";
}
并将JSON生成为

{
    "FormulaItem": {
        "$type": module.net.getAssembly('column'),
        "type": "int",
        "field": "my_field",
        "display": "My Field"
    }
}

在我前面的回答中,我们进一步遇到了一些问题之后,我发现了JSON可以用于序列化/反序列化名称空间的
SerializationBinder

代码优先 我生成了一个类来继承
SerializationBinder

public class KnownTypesBinder : System.Runtime.Serialization.SerializationBinder {
    public KnownTypesBinder() {
        KnownTypes = new List<Type>();
        AliasedTypes = new Dictionary<string, Type>();
    }
    public IList<Type> KnownTypes { get; set; }
    public IDictionary<string, Type> AliasedTypes { get; set; }
    public override Type BindToType(string assemblyName, string typeName) {
        if (AliasedTypes.ContainsKey(typeName)) { return AliasedTypes[typeName]; }
        var type = KnownTypes.SingleOrDefault(t => t.Name == typeName);
        if (type == null) {
            type = Type.GetType(Assembly.CreateQualifiedName(assemblyName, typeName));
            if (type == null) {
                throw new InvalidCastException("Unknown type encountered while deserializing JSON.  This can happen if class names have changed but the database or the JavaScript references the old class name.");
            }
        }

        return type;
    }

    public override void BindToName(Type serializedType, out string assemblyName, out string typeName) {
        assemblyName = null;
        typeName = serializedType.Name;
    }
}
KnownTypesBinder binder = new KnownTypesBinder()
binder.KnownTypes.Add(typeof(Project1.Class1));
binder.KnownTypes.Add(typeof(Project1.Class1));
别名类型 这允许我为将被序列化/反序列化的类生成自己的名称。在我的
global.asax
文件中,我应用活页夹如下:

KnownTypesBinder binder = new KnownTypesBinder()
binder.AliasedTypes["Class1"] = typeof(Project1.Class1);
binder.AliasedTypes["WhateverStringIWant"] = typeof(Project1.Class2);

var json = GlobalConfiguration.Configuration.Formatters.JsonFormatter;
json.SerializerSettings.Binder = binder;
现在,每当我序列化,比如说,
MyClass
为JSON时,我都会得到以下结果:

{ 
    item: { 
        $type: "Project1.MyClass",  
        Text: {
            $type: "Class1",
            Text: "some value"
        },
        Value: {
            $type: "WhateverStringIWant",
            Value: 88
        }
    } 
}
已知类型 我还可以通过向
KnownTypesBinder
添加信息来选择剥离程序集信息并严格使用类名:

public class KnownTypesBinder : System.Runtime.Serialization.SerializationBinder {
    public KnownTypesBinder() {
        KnownTypes = new List<Type>();
        AliasedTypes = new Dictionary<string, Type>();
    }
    public IList<Type> KnownTypes { get; set; }
    public IDictionary<string, Type> AliasedTypes { get; set; }
    public override Type BindToType(string assemblyName, string typeName) {
        if (AliasedTypes.ContainsKey(typeName)) { return AliasedTypes[typeName]; }
        var type = KnownTypes.SingleOrDefault(t => t.Name == typeName);
        if (type == null) {
            type = Type.GetType(Assembly.CreateQualifiedName(assemblyName, typeName));
            if (type == null) {
                throw new InvalidCastException("Unknown type encountered while deserializing JSON.  This can happen if class names have changed but the database or the JavaScript references the old class name.");
            }
        }

        return type;
    }

    public override void BindToName(Type serializedType, out string assemblyName, out string typeName) {
        assemblyName = null;
        typeName = serializedType.Name;
    }
}
KnownTypesBinder binder = new KnownTypesBinder()
binder.KnownTypes.Add(typeof(Project1.Class1));
binder.KnownTypes.Add(typeof(Project1.Class1));
在给出的两个示例中,
Class1
的引用方式相同。但是,如果我将
Class1
重构为,比如说,
NewClass1
,那么第二个示例将开始发送不同的名称。这可能是一件大事,也可能不是,这取决于您是否在使用这些类型

最后的想法 AliasedTypes的优点是,我可以给它任何我想要的字符串,不管我重构代码多少,在.NET和JavaScript(或任何用户)之间的通信都不会中断


小心不要将类名称完全相同的
AliasedType
s和
KnownType
s混合在一起,因为编写代码时,
AliasedType
将胜过
KnownType
。当绑定器无法识别某个类型(别名或已知)时,它将提供该类型的完整程序集名称。

“我可以在同一个类上混合XmlSerializer属性和DataContractSerializer属性吗?”是的。为什么不发布使用DataContractSerializer属性的代码?@luk,因为不幸的是,我现在没有它……当我无法让它工作时,我又回到了没有它的代码,因为我已经花了一个星期的时间把头撞在桌子上试图让它工作。我最终使用了Newtonsoft.Json库中的一个松散类型的
JObject
,并手动转换Json(这排除了接受Xml),但现在我正在对类进行更改,我还必须记住修复我的手动转换器…这意味着手动转换降低了代码的可维护性。我将再次尝试使用
DataContractSerializer
,并尽快发布该代码,以感谢您为此所做的工作!我已经确定问题出在Json.NET(它是ASP.NET Web API使用的默认序列化程序)上,现在我需要知道如何解决Json.NET不存在的问题