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

sal*_*uce 5 c# json.net deserialization asp.net-web-api

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

具有抽象成员的类如下所示(为了清晰起见,我包含了 Xml 装饰器,因为它们非常适合使用 XmlSerializer 反序列化 xml)

[Serializable]
public class FormulaStructure {
    [XmlElement("column", typeof(ColumnStructure))]
    [XmlElement("function", typeof(FunctionStructure))]
    [XmlElement("operand", typeof(OperandStructure))]
    public AFormulaItemStructure FormulaItem;
}
Run Code Online (Sandbox Code Playgroud)

抽象类非常基本:

[Serializable]
public abstract class AFormulaItemStructure { }
Run Code Online (Sandbox Code Playgroud)

抽象类有以下三个派生类:

[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;
}
Run Code Online (Sandbox Code Playgroud)

目前,使用[DataContract]属性时,Json.NET 格式化程序无法填充派生类,留下了 property null


问题

我可以将 XmlSerializer 属性与 DataContractSerializer 同一类上的属性混合使用吗? 我使用 是XmlSerializer因为我在设计的 xml 中使用了 xml 属性,但如果需要的话可以更改,因为我自己开发 xml 模式。

在 Json.NET 中相当于什么 [KnownType()] Json.NET 似乎不DataContractSerializer尊重KnownType. 我需要滚动自己的 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"}}
Run Code Online (Sandbox Code Playgroud)

这对于我的目的来说是理想的。然而,让它返回到 API 并反序列化为正确的派生类型是行不通的(它为属性生成 null)。

测试Tommy Grovnes 的答案如下,DataContractSerializer他用于测试生成:

{"FormulaItem":{"__type":"column:#ExpressionStructureExperimentation.Models","display":"My Field","field":"my_field","type":"int"}}
Run Code Online (Sandbox Code Playgroud)

这对我来说不起作用,也不适合代码可维护性(如果我将整个命名空间硬编码到 JavaScript 中以生成这些对象,那么重构就变成了 PITA)。

sal*_*uce 2

在我们按照之前的答案遇到一些问题之后,我发现了SerializationBinderJSON 可用于序列化/反序列化命名空间的类。

代码优先

我生成了一个类来继承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;
    }
}
Run Code Online (Sandbox Code Playgroud)

怎么运行的

假设我有一组这样定义的类:

public class Class1 {
    public string Text { get; set; }
}

public class Class2 {
    public int Value { get; set; }
}

public class MyClass {
    public Class1 Text { get; set; }
    public Class2 Value { get; set; }
}
Run Code Online (Sandbox Code Playgroud)

别名类型

这样做的作用是允许我为将要序列化/反序列化的类生成自己的名称。在我的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;
Run Code Online (Sandbox Code Playgroud)

现在,每当我序列化(例如MyClassJSON)时,我都会得到以下信息:

{ 
    item: { 
        $type: "Project1.MyClass",  
        Text: {
            $type: "Class1",
            Text: "some value"
        },
        Value: {
            $type: "WhateverStringIWant",
            Value: 88
        }
    } 
}
Run Code Online (Sandbox Code Playgroud)

已知类型

我还可以选择去掉程序集信息并通过将信息添加到以下内容来严格使用类名KnownTypesBinder

KnownTypesBinder binder = new KnownTypesBinder()
binder.KnownTypes.Add(typeof(Project1.Class1));
binder.KnownTypes.Add(typeof(Project1.Class1));
Run Code Online (Sandbox Code Playgroud)

在给出的两个示例中,Class1以相同的方式引用。但是,如果我重构Class1为,比如说,NewClass1那么第二个示例将开始发送不同的名称。这可能是也可能不是一个大问题,具体取决于您是否使用这些类型。

最后的想法

AliasedTypes 的优点是我可以给它任何我想要的字符串,并且无论我重构代码多少,.NET 和 JavaScript(或任何消费者)之间的通信都不会中断。

注意不要混合具有完全相同类名的AliasedTypes 和s,因为代码中写的会胜过。当绑定器无法识别类型(别名或已知)时,它将提供该类型的完整程序集名称。KnownTypeAliasTypeKnownType