Showing posts with label Reflection. Show all posts
Showing posts with label Reflection. Show all posts

Thursday, May 18, 2023

Generate action urls from C# lambda expressions in ASP.Net MVC Core: good news for those who missed it there

In ASP.Net MVC (on top on .Net Framework) there was useful mechanism for generating actions urls from C# lambda expressions. Let's say we have controller UserController for managing users which has List action with 3 params:

  • page number (in case if users list is large and we need to use pagination)
  • sort by (first name, last name, etc)
  • sort direction (asc or desc)
public enum SortDirection
{
    Asc,
    Desc
}

public class UserController : Controller
{
    public ActionResult List(int pageNumber, string sortBy, SortDirection sortDirection)
    {
        ...
    }
}

In this case we could generate url for this action using generic ActionLink<T> method like this:

Html.ActionLink<UserController>(c => c.List(0, "FirstName", SortDirection.Asc), "All users")

which will generate the following url:

/user/list?pageNumber=0&sortBy=FirstName&sortDirection=SortDirection.Asc

That is convenient method since we have compile-time check for the code. Compare it with classic way:

Html.ActionLink("All users", "List", "Users", new { pageNumber = 0, sortBy = "FirstName", sortDirection = SortDirection.Asc })

In the last case if e.g. action name, parameters names or number of params will be changed we won't get any errors or warning during compilation. Instead we may get unexpected behavior or runtime error (e.g. if new parameter was added it will have default value if we won't explicitly add it to link generation code).

The problem is that strongly-typed method is not available in ASP.Net Core. Don't know what was the reason for not adding it there (except mentioned advantage of having compile-time check it may also be a problem during migration of old ASP.Net MVC app to ASP.Net Core) but fortunately it is possible to get it back there.

Lets check steps which are needed for generating action url from expression: we need to get controller name (/user), action name (/list), list action parameters names (or get route values from expression how it is called in ASP.Net MVC) and (most tricky one) get value of each parameter passed to expression. And then concatenate all parts to one string. First 3 steps are relatively easy: they can be done by basic reflection. But last step (get values of parameters passed to lambda expression) needs extra attention. In the past I already faced with that need in Camlex.NET (open source library for Sharepoint developers which I maintain in free time): Runtime evaluation of lambda expressions. We can use the same technique here as well. Also (as I found out during experiments) code for generating routing values from expression (list parameters names) can be reused with small changes from internal method of ASP.Net MVC Microsoft.Web.Mvc.Internal.ExpressionHelper.GetRouteValuesFromExpression in ASP.NET Core - it will help to save our time.

Now if we will combine all of this we may create helper class for generating urls from expressions in ASP.Net Core:

public static class HtmlHelperExtensions
{
    public static string ActionLink<TController>(this IHtmlHelper html, Expression<Action<TController>> actionExpression)
    {
        return ActionLink(actionExpression, GetRouteValuesFromExpression(actionExpression));
    }

    public static string ActionLink<TController>(this IHtmlHelper html, Expression<Action<TController>> actionExpression, RouteValueDictionary routeValues)
    {
        return ActionLink(actionExpression, routeValues);
    }

    public static string ActionLink<TController>(Expression<Action<TController>> actionExpression, RouteValueDictionary routeValues)
    {
        string controllerName = typeof(TController).GetControllerName();
        string actionName = actionExpression.GetActionName();
        var sb = new StringBuilder($"/{controllerName}/{actionName}");
        if (routeValues != null)
        {
            bool isFirst = true;
            foreach (var routeValue in routeValues)
            {
                sb.Append(isFirst ? "?" : "&");
                sb.Append($"{routeValue.Key}={routeValue.Value}");
                isFirst = false;
            }
        }
        return sb.ToString();
    }

    private static string GetControllerName(this Type controllerType)
    {
        return controllerType.Name.Replace("Controller", string.Empty);
    }

    private static string GetActionName(this LambdaExpression actionExpression)
    {
        return ((MethodCallExpression)actionExpression.Body).Method.Name;
    }

    // copy of Microsoft.Web.Mvc.Internal.ExpressionHelper.GetRouteValuesFromExpression
    private static RouteValueDictionary GetRouteValuesFromExpression<TController>(
        Expression<Action<TController>> action)
    {
        if (action == null)
            throw new ArgumentNullException(nameof(action));
        if (!(action.Body is MethodCallExpression body))
            throw new ArgumentException("MustBeMethodCall");
        string name = typeof(TController).Name;
        string str = name.EndsWith("Controller", StringComparison.OrdinalIgnoreCase)
            ? name.Substring(0, name.Length - "Controller".Length)
            : throw new ArgumentException("TargetMustEndInController");
        if (str.Length == 0)
            throw new ArgumentException("CannotRouteToController");
        string targetActionName = GetTargetActionName(body.Method);
        var rvd = new RouteValueDictionary();
        AddParameterValuesFromExpressionToDictionary(rvd, body);
        return rvd;
    }

    private static void AddParameterValuesFromExpressionToDictionary(
        RouteValueDictionary rvd,
        MethodCallExpression call)
    {
        ParameterInfo[] parameters = call.Method.GetParameters();
        if (parameters.Length <= 0)
            return;
        for (int index = 0; index < parameters.Length; ++index)
        {
            Expression expression = call.Arguments[index];
            object obj = !(expression is ConstantExpression constantExpression)
                ? Expression.Lambda<Func<object, object>>((Expression)Expression.Convert(expression, typeof(object)), Expression.Parameter(typeof(object), "_unused")).Compile().Invoke((object)null)
                : constantExpression.Value;
            rvd.Add(parameters[index].Name, obj);
        }
    }

    private static string GetTargetActionName(MethodInfo methodInfo)
    {
        string name = methodInfo.Name;
        ActionNameAttribute actionNameAttribute = !methodInfo.IsDefined(typeof(NonActionAttribute), true)
            ? methodInfo.GetCustomAttributes(typeof(ActionNameAttribute), true).OfType<ActionNameAttribute>().FirstOrDefault<ActionNameAttribute>()
            : throw new InvalidOperationException("CannotCallNonAction");
        if (actionNameAttribute != null)
            return actionNameAttribute.Name;
        return name;
    }
}

Using this helper class we may again generate actions links from C# expressions in ASP.Net Core.


Friday, March 18, 2022

Several ways to reduce JSON response size from Web API

If you have Web API which returns some data in JSON format at some point you may need to optimize its performance by reducing response size. It doesn't important on which technology Web API is implemented (ASP.Net Web API, .Net Azure Functions or something else). For this article the only important thing is that it is implemented on .Net stack.

Let's assume that we have an endpoint which returns array of objects of the following class which have many public properties:

public class Foo
{
    public string PropA { get; set; }
    public string PropB { get; set; }
    public string PropC { get; set; }
    public string PropD { get; set; }
    ...
}

By default all these properties will be returned from our API (including those properties which contain nulls):

[{
        "PropA": "test1",
        "PropB": null,
        "PropC": null,
        "PropD": null,
        ...
    }, {
        "PropA": "test2",
        "PropB": null,
        "PropC": null,
        "PropD": null,
        ...
    },
    ...
]

As you can see these properties with nulls still add many bytes to the response. If we want to exclude properties which contain nulls from response (which in turn may significantly reduce response size) we may add special class-level and property-level attributes to our class. Note however that it will help only if you serialize response with Json.Net lib (Newtonsoft.Json):

[JsonObject(MemberSerialization.OptIn)]
public class Foo
{
    [JsonProperty(NullValueHandling = NullValueHandling.Ignore)]
    public string PropA { get; set; }
    [JsonProperty(NullValueHandling = NullValueHandling.Ignore)]
    public string PropB { get; set; }
    [JsonProperty(NullValueHandling = NullValueHandling.Ignore)]
    public string PropC { get; set; }
    [JsonProperty(NullValueHandling = NullValueHandling.Ignore)]
    public string PropD { get; set; }
}

Attribute JsonObject(MemberSerialization.OptIn) means that it should serialize only those properties which are explicitly decorated by JsonProperty attribute. After that response will look like that:

[{
        "PropA": "test1",
    }, {
        "PropA": "test2",
    },
	...
]

i.e. it will only contain those properties which have not-null value.

As you can see response size got significantly reduced. The drawback of this approach is that it will work only with Json.Net lib (JsonObject and JsonProperty attributes classes are defined in Newtonsoft.Json assembly). If we don't use Newtonsoft.Json we need to use another approach which is based on .Net reflection and on the fact that Dictionary is serialized to JSON in the same way as object with the properties. Here is the code which does it:

// get array of items of Foo class
var items = ...;

// map function for removing null values
var propertiesMembers = new List<PropertyInfo>(typeof(Foo).GetProperties(BindingFlags.Instance | BindingFlags.Public));
Func<Foo, Dictionary<string, object>> map = (item) =>
{
    if (item == null)
    {
        return new Dictionary<string, object>();
    }
    var dict = new Dictionary<string, object>();
    foreach (string prop in selectProperties)
    {
        if (string.IsNullOrEmpty(prop) || !propertiesMembers.Any(p => p.Name == prop))
        {
            continue;
        }
		
        var val = item.GetPublicInstancePropertyValue(prop);
        if (val == null)
        {
            // skip properties with null
            continue;
        }
        dict.Add(prop, val);
    }
    return dict;
};

// convert original array to array of Dictionary objects each of which contains only not null properties
return JsonConvert.SerializeObject(items.Select(i => map(i)));

At first we get original array of items of Foo class. Then we define map function which creates Dictionary from Foo object and this dictionary contains only those properties which don't contain null value (it is done via reflection). Here I used helper method GetPublicInstancePropertyValue from PnP.Framework but it is quite easy to implement by yourself also:

public static Object GetPublicInstancePropertyValue(this object source, string propertyName)
{
    return (source?.GetType()?.GetProperty(propertyName,
            System.Reflection.BindingFlags.Instance |
            System.Reflection.BindingFlags.Public |
            System.Reflection.BindingFlags.IgnoreCase)?
        .GetValue(source));
}

With this approach we will also get reduced response which will contain only not-null properties and it will work also without Newtonsoft.Json:

[{
        "PropA": "test1",
    }, {
        "PropA": "test2",
    },
	...
]