8

I am creating a new web API and would like to allow the user to specify what fields get returned to them in the URL.

My current thoughts are:

For a sample model like this:

public class Value
{
    public string ValueId { get; set; }

    public int Number { get; set; }

    public ValueInternal Internal { get; set; }
}
public class ValueInternal
{
    public int Number { get; set; }

    public string Something { get; set; }
}

and a URL like this

http://example.com/api/values/?_fields=Number,Internal(Something)

would return this

[
   {
       "Number": 0,
       "Internal": {
           "Number": 0
       }
   }
]

I have come up with the below method of achieving this, but it has some flaws. I.e. it couldn't handle if Internal was an enumerable of ValueInternal or has no support for include all or include all except, or if T and TResult are different types. Does anyone have any suggestions on how I can improve this or if there already exists a way of doing it that I am missing.

public static Expression<Func<T, TResult>> CreateSelector<T, TResult>() where TResult : new()
    {
        var property = "Number,Internal(Something)";
        return arg => Process<T, TResult>(arg, default(TResult), property);
    }

    private static TResult Process<T, TResult>(T arg, TResult output, string propertyList) where TResult : new()
    {
        if (output == null)
        {
            output = new TResult();
        }
        if (string.IsNullOrEmpty(propertyList))
        {
            return output;
        }
        var properties = Regex.Split(propertyList, @"(?<!,[^(]+\([^)]+),");
        foreach (var property in properties)
        {
            var propertyName = property;
            var propertyInternalsMatch = Regex.Match(property, @"\(.*(?<!,[^(]+\([^)]+)\)");
            var internalPropertyList = propertyInternalsMatch.Value;
            if (!string.IsNullOrEmpty(internalPropertyList))
            {
                propertyName = property.Replace(internalPropertyList, "");
                internalPropertyList = internalPropertyList.Replace("(", "");
                internalPropertyList = internalPropertyList.Replace(")", "");
            }
            var tProperty = arg.GetType().GetProperty(propertyName);
            if(tProperty == null) continue;
            var tResultProperty = output.GetType().GetProperty(propertyName);
            if(tResultProperty == null) continue;

            if (tProperty.PropertyType.IsPrimitive || tProperty.PropertyType.IsValueType || (tProperty.PropertyType == typeof(string)))
            {
                tResultProperty.SetValue(output, tProperty.GetValue(arg));
            }
            else
            {
                var propertyInstance = Activator.CreateInstance(tResultProperty.PropertyType);
                tResultProperty.SetValue(output, Process(tProperty.GetValue(arg), propertyInstance, internalPropertyList));
            }
        }

        return output;
    }

After a bit more reading I think I want to do something like the answer to this question LINQ : Dynamic select but that still has the same flaws my solution had

Community
  • 1
  • 1
Mike Norgate
  • 2,393
  • 3
  • 24
  • 45

1 Answers1

2

If you use OData support on your ASP .NET Web API you can jus use $select, but if you don't want to use it or your underlying system can't be easy queried using Linq, you can use a custom contract resolver, but in this case you are just reducing the serialization size, not the internal data traffic.

public class FieldsSelectContractResolver : CamelCasePropertyNamesContractResolver
{
    protected override JsonProperty CreateProperty(MemberInfo member, MemberSerialization memberSerialization)
    {
        var property = base.CreateProperty(member, memberSerialization);

        property.GetIsSpecified = (t) =>
        {
        var fields = HttpContext.Current.Request["fields"];

        if (fields != null)
        {
            return fields.IndexOf(member.Name, StringComparison.OrdinalIgnoreCase) > -1;
        }

        return true;
        };

        return property;
    }
}

and in WebApiConfig.cs set the custom contract resolver:

var jsonFormatter = GlobalConfiguration.Configuration.Formatters.JsonFormatter;
jsonFormatter.SerializerSettings.ContractResolver = new FieldsSelectContractResolver();
giacomelli
  • 7,287
  • 2
  • 27
  • 31
  • I did look at using OData and everything I saw about it was related to entity framework. I am not using entity framework. Is there a way to integrate OData with a generic LINQ provider? – Mike Norgate Aug 13 '14 at 11:25