5

I am looking at building an API using WebAPI in ASP.Net.

I have a requirement to conditionally exclude properties from the XML or JSON based on some custom logic at RunTime and not Compile Time.

I have to remove the xml or json from the response, it is no good just including the tags with a null or empty value.

I have tried various approaches, none of which I seem to be able to get to work.

I have tried the following

Delegating Handler from here

public class ResponseDataFilterHandler : DelegatingHandler
{
    protected override System.Threading.Tasks.Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
    {
        return base.SendAsync(request, cancellationToken)
            .ContinueWith(task =>
            {
                var response = task.Result;

                //Manipulate content here
                var content = response.Content as ObjectContent;
                if (content != null && content.Value != null)
                {

                }

                //Or replace the content
                //response.Content = new ObjectContent(typeof(object), new object(), new MyFormatter());

                return response;
            });
    }


}

Sure I can null properties out here, but they still appear in the response.

DataContractSurrogate similar to this

 public class MySurrogate: IDataContractSurrogate
{
    public object GetCustomDataToExport(Type clrType, Type dataContractType)
    {
        return null;
    }

    public object GetCustomDataToExport(System.Reflection.MemberInfo memberInfo, Type dataContractType)
    {
        return null;
    }

    public Type GetDataContractType(Type type)
    {
        return null;
    }

    public object GetDeserializedObject(object obj, Type targetType)
    {
        return null;
    }

    public void GetKnownCustomDataTypes(System.Collections.ObjectModel.Collection<Type> customDataTypes)
    {

    }

    public object GetObjectToSerialize(object obj, Type targetType)
    {
        if (obj == null) return null;

        var type = obj.GetType();
        type.GetProperties().ToList()
          .ForEach(prop =>
          {
              try
              {
                  var attr = prop.GetCustomAttributes(typeof(ConditionalDataMemberAttribute), false);
                  if (attr.Any())
                  {
                      var proptype = prop.PropertyType;
                      //Set the property value to its default value
                      prop.GetSetMethod().Invoke(obj,
                                                 new[] { proptype.IsValueType ? Activator.CreateInstance(proptype) : null });
                  }
              }
              catch { }
          });


        return obj;

    }

    public Type GetReferencedTypeOnImport(string typeName, string typeNamespace, object customData)
    {
        return null;
    }

    public System.CodeDom.CodeTypeDeclaration ProcessImportedType(System.CodeDom.CodeTypeDeclaration typeDeclaration, System.CodeDom.CodeCompileUnit compileUnit)
    {
        return null;
    }
}

Again, I can just null the properties out, I cannot remove the xml or json from the output.

I have an idea where I can dynamically compile a specific class with the required attributes then use the DataContractSurrogate to swap out the original instance with an instance of my new dynamic compiled class - but I don't like it.

Ive tried looking at DataContractSerializer but it is sealed so I cant derive from it - i've also looked to decompile it and make some changes but again it uses internal classes such as DataContract - I feel I need to hook into the serialization but I don't know how to ?

Community
  • 1
  • 1
Richard Friend
  • 15,800
  • 1
  • 42
  • 60
  • You could implement `ISerializable` http://msdn.microsoft.com/en-us/library/ty01x675%28v=vs.110%29.aspx – paul Dec 10 '14 at 09:39
  • Should of mentioned, I have tried that and to some degree it works - but you cant use it with `DataContract` attributes and with the amount of nested collections and DTOs I have it becomes a headache to maintain and get the ouput to look anything like I want it to - I will take another look though :) – Richard Friend Dec 10 '14 at 09:49
  • This will remove it from the json response http://stackoverflow.com/questions/14486667/suppress-properties-with-null-value-on-asp-net-web-api – peco Dec 10 '14 at 09:55

3 Answers3

4

You should use Json.NET and write your own converter

public class MyJsonConverter : JsonConverter
{
    public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer)
    {
        writer.WriteStartArray();

        // write your object here based on your custom logic
        writer.WriteRawValue(value);

        writer.WriteEndArray();
    }

    public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer)
    {
        throw new NotImplementedException();
    }

    public override bool CanConvert(Type objectType)
    {
        return true;
    }
}

You can use your custom converter like this

string json = JsonConvert.SerializeObject(SomeObject, new MyJsonConverter());

Then, in order to avoid writing a custom Converter for both Json and XML, you can convert your Json to XML

XmlDocument doc = JsonConvert.DeserializeXmlNode(json);
Mihai Dinculescu
  • 19,743
  • 8
  • 55
  • 70
  • Json.NET supports XML as well: http://james.newtonking.com/json/help/index.html?topic=html/ConvertingJSONandXML.htm – Mihai Dinculescu Dec 10 '14 at 10:04
  • Cool, I didn't know this. But will this formatting be happening in the json- and xml-mediatypeformatter? – peco Dec 10 '14 at 10:11
  • You can convert your Json to XML, so you don't have to write two custom Converters. – Mihai Dinculescu Dec 10 '14 at 10:13
  • Interesting, I will take a look at this now :) – Richard Friend Dec 10 '14 at 10:14
  • This pointed me in the right direction but I wasn't keen on writing all the logic to serialize myself - I managed to just attach to the ShouldSerialize predicate of each property instead.http://james.newtonking.com/json/help/index.html?topic=html/ConditionalProperties.htm – Richard Friend Dec 10 '14 at 11:35
3

Okay I managed to do this using a bit of what I had already done, plus some of the suggestions on here, I also stumbled upon this

First we start by adding a DelegatingHandler into the pipeline.

config.MessageHandlers.Add(new ResponseDataFilterHandler());

And the class itself

public class ResponseDataFilterHandler : DelegatingHandler
{
    protected override System.Threading.Tasks.Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
    {
        return base.SendAsync(request, cancellationToken)
            .ContinueWith(task =>
            {
                var response = task.Result;
                var content = response.Content as ObjectContent;
                if (content != null && content.Value != null)
                {
                    var isJson = response.RequestMessage.GetQueryNameValuePairs().Any(r => r.Key == "json" && r.Value == "true");
                    response.Content = new StringContent(Helper.GetResponseData(content.Value, isJson));
                }
                return response;
            });
    }
}

Then we have a helper class method to get the new serialized string (this is not prod code ;p)

public static class Helper
{
    public  static string GetResponseData(object root,bool isJson)
    {
        string json = JsonConvert.SerializeObject(root, new JsonSerializerSettings {  ContractResolver = new ShouldSerializeContractResolver()});

        if (!isJson)
        {
            XmlDocument doc = JsonConvert.DeserializeXmlNode(json,"response");
            json = doc.OuterXml;
        }
        return json;
    }
}

And finally the ContractReoslver

public class ShouldSerializeContractResolver : DefaultContractResolver
{
    protected override JsonProperty CreateProperty(MemberInfo member, MemberSerialization memberSerialization)
    {
        JsonProperty property = base.CreateProperty(member, memberSerialization);
        property.ShouldSerialize = (i) =>
            {
                //Your logic goes here
                var r = !property.PropertyName.StartsWith("block-ref");
                return r;
            };

        return property;
    }
}

this roots everything through Json and the converts to xml if required, for my test project I am using a querystring (json=true) to specify if the format should be json instead of xml.

Richard Friend
  • 15,800
  • 1
  • 42
  • 60
1

Just specify EmitDefaultValue = false like this

[DataContract]
public class MyClass
{
    [DataMember]
    public int Id { get; set; }

    [DataMember(EmitDefaultValue = false)]
    public string Name { get; set; }
}

With that, when Name is null, it will not shown up in XML/JSON.

If you want to dynamically null out specific properties, you can provide a method like this.

[OnSerializing]
void OnSerializing(StreamingContext context)
{
    if(someConditionIsMet)
        this.Name = null;
}
  • I like this, very simple - however there are times when I need to emit the null value. User wont know if the element is not there because of permissions or it is genuinely null. – Richard Friend Dec 10 '14 at 10:45