简体   繁体   中英

Using Web API to deserialize into class with abstract property

I'm attempting to write a set of classes to represent a particularly complex object, and in one of those classes, I have a property that is set as the base (abstract) class of three possible derived classes. I'm setting up an ASP.NET Web API to handle the serialization and deserialization, which means that, by default, it uses Json.NET for JSON. How can I get the Web API to properly deserialize JSON sent via POST or PUT into the proper derived class?

The class with the abstract member looks like this (I'm including the Xml decorators for clarity and because they work perfectly well for deserializing xml using the XmlSerializer)

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

The abstract class is pretty basic:

[Serializable]
public abstract class AFormulaItemStructure { }

And there are three derivatives of the abstract class:

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

At present, using [DataContract] attributes, the Json.NET formatter fails to populate the derived class, leaving the property null .


Questions

Can I mix XmlSerializer attributes with DataContractSerializer attributes on the same class? I use the XmlSerializer because I use xml attributes in the xml I designed, but that can be changed if necessary since I am developing the xml schema myself.

What is the equivalent in Json.NET to [KnownType()] ? Json.NET doesn't appear to respect the DataContractSerializer version of KnownType . Will I need to roll my own JsonConverter to determine the proper type?

How would I decorate the classes so that DataContractSerializer or DataContractJsonSerializer will properly deserialize the objects in both Xml and Json? My goal is to put this into an ASP.NET Web API, so I want the flexibility to generate Xml or Json, as appropriate to the requested type. Is there an alternative formatter that I need to use to work with this complex class, if Json.NET won't work?

I need the ability to generate an object on the client side without necessarily including the .NET class names into the object.


Testing and Refinement

In my testing of the Web API, the default serialization sends down to the client:

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

which is ideal for my purposes. Getting this to go back to the API and deserialize into the proper derived types, though, isn't working (it's generating null for the property).

Testing Tommy Grovnes answer below, the DataContractSerializer he used for testing generates:

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

which doesn't work for me, or for code maintainability (refactoring becomes a PITA if I hard-code the entire namespace into the JavaScript for generating these objects).

You can mix as mentioned already but I don't think you need to, haven't used WEB api myself but WCF Rest produces xml and json from DataContracts (without Xml.. tags), tag your classes like this:

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

If you need more control over the XML/JSON generated you might have to tweak this further. I used this code to test:

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

After we ran into some issues much further down the line with my previous answer, I discovered the SerializationBinder class that JSON can use for serializing/deserializing namespaces.

Code First

I generated a class to inherit the 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;
    }
}

How it works

Let's say I have a set of classes defined thus:

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

Aliased Types

What this does is allows me to generate my own names for classes that will be serialized/deserialized. In my global.asax file, I apply the binder as such:

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;

Now, whenever I serialize, say, MyClass as JSON, I get the following:

{ 
    item: { 
        $type: "Project1.MyClass",  
        Text: {
            $type: "Class1",
            Text: "some value"
        },
        Value: {
            $type: "WhateverStringIWant",
            Value: 88
        }
    } 
}

Known Types

I can also choose to strip off the assembly information and strictly use the class name by adding information to the KnownTypesBinder :

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

In the two examples given, Class1 is referenced the same way. However, if I refactor Class1 to, say, NewClass1 , then this second example will start sending a different name. That may or may not be a big deal, depending on whether you are using the types or not.

Final Thoughts

The advantage of the AliasedTypes is that I can give it any string that I want, and it doesn't matter how much I refactor the code, the communication between the .NET and the JavaScript (or whatever consumer is out there) is unbroken.

Be careful not to mix AliasedType s and KnownType s that have the exact same class name, because the code is written that the AliasType will win out over KnownType . When the binder doesn't recognize a type (aliased or known), it will provide the full assembly name of the type.

In the end, I broke down and added the .NET class information to the module in string variables to make refactoring easier.

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

and generated the JSON as

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

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM