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

时间:2012-08-29 16:10:45

标签: c# asp.net-web-api json.net deserialization

我正在尝试编写一组类来表示一个特别复杂的对象,在其中一个类中,我有一个属性被设置为三个可能的派生类的基类(抽象)类。我正在设置一个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;
}

抽象类很基本:

[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 属性混合使用吗?我使用{{因为我在我设计的xml中使用xml属性,但是如果有必要可以更改,因为我自己开发了xml模式。

Json.NET中的等效内容 XmlSerializer Json.NET似乎不尊重[KnownType()]版本的{ {1}}。我是否需要滚动自己的JsonConverter来确定正确的类型?

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

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


测试和改进

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

DataContractSerializer

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

在下面测试Tommy Grovnes回答,他用于测试的DataContractJsonSerializer生成:

{"FormulaItem":{"type":"int","field":"my_field","display":"My Field"}}

对我来说不起作用,或者代码可维护性(如果我将整个命名空间硬编码到JavaScript中以生成这些对象,则重构成为PITA)。

3 个答案:

答案 0 :(得分:2)

你可以像已经提到的那样混合,但我认为你不需要,我自己没有使用WEB api,但是WCF Rest从DataContracts(没有Xml ..标签)生成xml和json,标记你的类如下: / p>

[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);
        }
    }

答案 1 :(得分:2)

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

Code First

我生成了一个继承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;
    }
}

如何运作

我们说我有一组定义的类:

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; }
}

别名类型

这样做可以让我为将被序列化/反序列化的类生成自己的名称。在我的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添加信息来严格使用类名:

KnownTypesBinder binder = new KnownTypesBinder()
binder.KnownTypes.Add(typeof(Project1.Class1));
binder.KnownTypes.Add(typeof(Project1.Class1));

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

最后的想法

AliasedTypes的优点是我可以给它任何我想要的字符串,并且重构代码的重要性,.NET和JavaScript之间的通信(或者任何消费者都没有)那里是完整的。

请注意不要混用具有完全相同类名的AliasedTypeKnownType,因为编写的代码AliasType将胜过KnownType 。当活页夹没有识别出类型(别名或已知)时,它将提供该类型的完整程序集名称。

答案 2 :(得分:0)

最后,我分解并在字符串变量中将.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"
    }
}