Click here to Skip to main content
15,867,453 members
Articles / Web Development / ASP.NET / ASP.NET Core

LiteApi internals (or how to build your own WebAPI middleware)

Rate me:
Please Sign up or sign in to vote.
5.00/5 (3 votes)
1 Jul 2017CPOL19 min read 7.7K   3  
This article covers internal workings of LiteApi and explains aspects of creating MVC-like WebAPI middleware running on ASP.NET Core .

Introduction

LiteApi is open source ASP.NET Core middleware for creating (RESTful) HTTP web services. At the moment it supports only JSON and has planned extensibility points for XML and any other content types. This article will introduce you to high-level ideas behind workings of LiteApi, as well as some of the low-level implementation details. If you are interested in LiteApi documentation, you can find it here.

Background

ASP.NET Core applications are built around middlewares. First time I have encountered the idea of building web apps this way was when I was introduced to Node.js Express stack. Some time later I have encountered OWIN and Katana. With OWIN and Katana Microsoft has created a way to build web applications using middleware pipeline. OWIN and Katana have paved the way for ASP.NET Core architecture. 

When writing web apps on ASP.NET Core, middlewares are stacked one over the other. They are used to separate logical parts of the application. One middleware can be used to read cookies and inject authentication data into HTTP request object, another one can be used for logging exceptions, yet another one can be used for measuring the performance of the application, and so on... MVC 6 (or MVC Core) itself is built as a middleware, and it's running in ASP.NET Core pipeline.

This new architecture has opened new horizons for .NET web developers. We are now able more than ever to create pieces of reusable code that can be seamlessly integrated into web applications. Those pieces work on HTTP request/response level and they are called middlewares. If you want to find out more about middlewares there is a documentation page you should check.

LiteApi is a middleware. It's a middleware that can recognize controllers and actions. By following conventions and configuration LiteApi can respond to HTTP request by invoking an action and return response from that action. LiteApi is working with JSON, it does not support Razor or any other view engine. Even though it's possible to extend LiteApi and build HTML responses using LiteApi, it's purpose is only to serve as Web API middleware.

Creating a LiteApi application

Before we look into inner workings of LiteApi, we should first quickly get familiar with building applications with it. (Sample code that follows is taken from Getting Started article.) Steps for writing simple LiteApi app are:

  1. Create ASP.NET Core web app from empty template and add nuget package
  2. Register LiteApi in Startup.cs
  3. Write controller(s)

1. First, create an empty ASP.NET Core web application, and then add LiteApi to your application by installing .nupkg with the following command in package manager console.

PM> Install-Package LiteApi -Pre

2. After installing the package, register the middleware in Startup.cs file. (At this point Visual Studio can go crazy and report that UseLiteApi is not recognized method, just restart the IDE, hopefully it will be fixed soon.) Here is the Configure method from Startup.cs.

C#
public void Configure(IApplicationBuilder app, IHostingEnvironment env, ILoggerFactory loggerFactory)
{
    loggerFactory.AddConsole();

    if (env.IsDevelopment())
    {
        app.UseDeveloperExceptionPage();
    }

    app.UseLiteApi(); // <-- key line 
    
    app.Run(async (context) =>
    {
        await context.Response.WriteAsync("Hello World!");
    });
}

3. Now we are ready to write some controllers. The first controller we are going to write is simple MathController. For readability purposes using statements and namespace are omitted and all methods are written as expression-bodied methods (even though they can be classical, old school methods). Key namespaces we are using are LiteApi and LiteApi.Attributes.

C#
public class MathController: LiteController
{
    public int Add(int a, int b) => a + b;

    [ActionRoute("/{a}/minus/{b}")]
    public int Minus(int a, int b) => a - b;

    public int Sum(int[] ints) => ints.Sum();
}

Our MathController is a C# class that can be located in any folder, by convention it is usually located in API or Controllers folder. Controller has to inherit LiteController base class, and each action has to be public method. By convention all public methods are HTTP GET actions. Let's take a look at each of the action separately.

  • Add action will respond to /api/math/add?a=5&b=4 URL. This means that by default action name is included in the URL, how to omit action name and make our URLs more RESTful will be clear in the next sample controller.
  • Minus action will respond to /api/math/5/minus/4 URL. This behavior is achieved with ActionRouteAttribute. This attribute is critical for successfully forming nice RESTful URLs (e.g. /api/books/{id}).
  • Sum action is accepting array of integers, URL in this case would be /api/math/sum?ints=5&ints=4&ints=7. LiteApi can accept collections (IEnumerable<T>, T[], List<T>) as well as dictionaries (Dictionary<TKey, TValue> and IDictionary<TKey, TValue>). Details about how to pass a dictionary are out of scope of this article. If you want to find more about passing dictionaries please consult documentation.

Basics should now be clear. It's time to create a RESTful controller.

C#
// Restful will tell LiteApi to ignore action names when matching URL to action, 
// ControllerRoute will tell LiteApi to use specified path to target the controller
[Restful, ControllerRoute("/api/v2/actors")] 
public class ActorsController : LiteController, IActorsService
{
    private readonly IActorsService _service;

    public ActorsController(IActorsService service)
    {
        _service = service;
    }

    [HttpPost] // will respond to POST /api/v2/actors
    public ActorModel Add(ActorModel model) => _service.Add(model);

    [HttpDelete, ActionRoute("/{id}")] // will respond to DELETE /api/v2/actors/{id}
    public bool Delete(Guid id) => _service.Delete(id);

    [HttpGet, ActionRoute("/{id}")] // HttpGet is optional, will respond to GET /api/v2/actors/{id}
    public ActorModel Get(Guid id) => _service.Get(id);

    [HttpGet] // HttpGet is optional, will respond to GET /api/v2/actors
    public IEnumerable<ActorModel> GetAll() => _service.GetAll();

    [HttpPut, ActionRoute("/{id}")] // will respond to PUT /api/v2/actors/{id}
    public ActorModel Update(Guid id, ActorModel model) => _service.Update(id, model);
}

ActorsController is depending on IActorsService and it's using ActorModel for passing around some data. Details of this classes are not important for this article, instead, we will focus on the controller. Here are some points we can conclude by looking at the code above:

  • Constructor works with built-in DI system. We can request any service that is registered within DI container.
  • Restful attribute will tell the middleware to exclude action names when they are matched against URLs.
  • ControllerRoute attribute will set base URL on which controller should respond.
  • HttpGetHttpPostHttpPut and HttpDelete are attributes used to tell the middleware on which HTTP methods should action respond. HttpGet is optional. By default, when no Http...Attribute is set, public method is considered to be HTTP GET action.

Now that we saw some basics of what LiteApi can do, it's time to see how it works.

LiteApi internals

Internal workings of this middleware can be separated into two aspects. The first aspect is initialization and the second one is handling of HTTP requests in runtime. Initialization is done on the first run and is responsible for finding and validating controllers, actions and action parameters. The second aspect of the middleware is responsible for finding a matching controller and action when HTTP request is received. If matching controller/action is found, LiteApi will interpret parameters, invoke the action and return response. If no matching controller/action is found, LiteApi will invoke next middleware (if there is one registered after the LiteApi).

Please note that LiteApi consists of about 50 files (mostly interfaces and classes, not counting test classes and sample projects), it is not possible to cover them all in this article, that's why you will often see presented interfaces and particular methods. The following content will try to explain how the middleware is working from a high-level perspective where only some of the implementation details will be provided. If you are interested in all of the details you can dig into the code repository.

Initialization of the middleware

Following steps are performed during initialization of LiteApi:

  1. Register controllers
    • Look into application assembly/assemblies and find controllers
    • For each controller find actions
    • For each action find input parameters and resulting response types
  2. Perform validation
    • Validate all controllers
    • For each controller validate actions
    • For each action validate parameters

Registration of the controllers, actions and action parameters

For registration of the controller, action, and parameters we have following interfaces:

C#
public interface IControllerDiscoverer
{
    ControllerContext[] GetControllers(Assembly assembly);
}

public interface IActionDiscoverer
{
    ActionContext[] GetActions(ControllerContext controllerCtx);
}

public interface IParametersDiscoverer
{
    ActionParameter[] GetParameters(ActionContext actionCtx);
}

ControllerContextActionContext and ActionParameter are classes that hold metadata about controllers. Interfaces should be self-explanatory. The default implementation of IControllerDiscoverer will check provided assembly and find controller classes. Here is some interesting code from default controller discoverer implementation:

C#
public ControllerContext[] GetControllers(Assembly assembly)
{
    var types = assembly.GetTypes()
        .Where(x => typeof(LiteController).IsAssignableFrom(x) && !x.GetTypeInfo().IsAbstract)
        .ToArray();
    ControllerContext[] ctrls = new ControllerContext[types.Length];
    for (int i = 0; i < ctrls.Length; i++)
    {
        ctrls[i] = new ControllerContext
        {
            ControllerType = types[i],
            RouteAndName = GetControllerRute(types[i]),
            IsRestful = types[i].GetTypeInfo().GetCustomAttributes<RestfulAttribute>().Any()
        };
        ctrls[i].Actions = _actionDiscoverer.GetActions(ctrls[i]);
        ctrls[i].Init();
    }
    return ctrls;
}

As we can see, reflection is used to retrieve all types in an assembly and find controllers. In order to distinguish controllers from other classes GetControllers method is checking if class is inheriting LiteController class and making sure that found controllers are not abstract controllers. When controllers are found, for each controller we are calling action discoverer method to retrieve actions. Relevant methods from action discoverer look like this:

C#
public ActionContext[] GetActions(ControllerContext controllerCtx)
{
    var properties = controllerCtx.ControllerType
        .GetProperties(BindingFlags.Instance | BindingFlags.Public);
    var propertyMethods = new List<string>();
    propertyMethods.AddRange(properties.Where(x => x.GetMethod?.Name != null)
        .Select(x => x.GetMethod.Name));
    propertyMethods.AddRange(properties.Where(x => x.SetMethod?.Name != null)
        .Select(x => x.SetMethod.Name));

    return controllerCtx.ControllerType
        .GetMethods(BindingFlags.Instance | BindingFlags.Public)
        .Where(x => !propertyMethods.Contains(x.Name))
        .Where(MethodIsAction)
        .Select(x => GetActionContext(x, controllerCtx))
        .ToArray();
}

private ActionContext GetActionContext(MethodInfo method, ControllerContext ctrlCtx)
{
    string methodName = method.Name.ToLowerInvariant();
    var segmentsAttr = method.GetCustomAttribute<ActionRouteAttribute>();
    RouteSegment[] segments = new RouteSegment[0] ;
    if (!ctrlCtx.IsRestful)
    {
        segments = new [] { new RouteSegment(methodName) };
    }
    if (segmentsAttr != null)
    {
        segments = segmentsAttr.RouteSegments.ToArray();
    }
    var actionCtx = new ActionContextan
    {
        HttpMethod = GetHttpAttribute(method).Method,
        Method = method,
        ParentController = ctrlCtx,
        RouteSegments = segments
    };
    actionCtx.Parameters = _parameterDiscoverer.GetParameters(actionCtx);
    return actionCtx;
}

In presented code, it's visible that LiteApi first looks for properties. The reason for looking for properties is that in .NET Core (and NET Standard) properties are compiled to methods, so we have to filter them out. On filtered methods, LiteApi is applying another filter called MethodIsAction. This filter is checking for presence of DontMapToApiAttribute which can be used to prevent registering method as action. After the correct methods are filtered, LiteApi is creating ActionContext for each method. ActionContext is taking name of the method which can be set as route segments. If parent controller is RESTful than route segments are empty, except in case when ActionRouteAttribute is present. Route segments are used for matching request route to controller and action, more about route segments matching will be described later on in the article. For each action context GetActionContext method is calling IParametersDiscoverer.GetParameters.

Parameters in LiteApi actions can be received from:

  • Query (default source of parameters for simple types
  • Route segment (set in ActionRouteAttribute)
  • Body (default source of parameters for all types that are not considered simple types)
  • Header (can be set with FromHeaderAttribute)
  • Dependency injection container (can be set with FromServicesAttribute)

Discovering and parsing parameters could possibly be the most challenging work performed by the middleware. Parameters can come from different sources, and there are rules for different sources of parameters. For example, nullable parameters are not allowed in route segments, body parameter is not allowed on GET and DELETE, collections and dictionaries are not allowed in headers... Besides those rules further complication is to determine rules for action overloading. For example, let's take a look at following three actions.

public Book Get(string id) => _bookService.Get(id);

public Book Get(Guid id) => _bookService.Get(id);

public Book Get(int id) => _bookService.Get(id);

All three actions are responding to the same URL, so how will LiteApi know which one to invoke? Rule of thumb is to try to parse parameter id as Guid and as int, if parameter is successfully parsed as Guid it will invoke the action that expects Guid, if parameters is successfully parsed as integer, it will invoke action that expects int, if no parsing is successful if will finally call action that expects string. More complicated are samples where actions are overloaded with int, int?int?[] and int[]. In this case, LiteApi will always call int?[] because this type of parameter is superset of other three parameter types. In case of overloading with int, int?, int[], LiteApi will call int? action because there is the least chance of failing when parsing int?, even if there is more than one value, only one will be parsed. When overloading actions with parameters, it would be best to name parameters differently, this way LiteApi will know exactly which action to invoke without parsing all possible parameter types.

ParameterDiscoverer.GetParameters method follows:

C#
public ActionParameter[] GetParameters(ActionContext actionCtx)
{
    var methodParams = actionCtx.Method.GetParameters();
    ActionParameter[] parameters = new ActionParameter[methodParams.Length];
    for (int i = 0; i < methodParams.Length; i++)
    {
        var param = actionCtx.Method.GetParameters()[i];
        bool isFromQuery = false;
        bool isFromBody = false;
        bool isFromRoute = false;
        bool isFromService = false;
        bool isFromHeader = false;
        string overridenName = null;

        if (param.GetCustomAttribute<FromServicesAttribute>() != null)
        {
            isFromService = true;
        }
        else
        {
            isFromQuery = param.GetCustomAttribute<FromQueryAttribute>() != null;
            isFromBody = param.GetCustomAttribute<FromBodyAttribute>() != null;
            isFromRoute = param.GetCustomAttribute<FromRouteAttribute>() != null;
            var headerAttrib = param.GetCustomAttribute<FromHeaderAttribute>();
            if (headerAttrib != null)
            {
                isFromHeader = true;
                overridenName = headerAttrib.HeaderName;
            }
        }

        ParameterSources source = ParameterSources.Unknown;

        if (isFromService) source = ParameterSources.Service;
        else if (isFromHeader) source = ParameterSources.Header;
        else if (isFromQuery && !isFromBody && !isFromRoute) source = ParameterSources.Query;
        else if (!isFromQuery && isFromBody && !isFromRoute) source = ParameterSources.Body;
        else if (!isFromQuery && !isFromBody && isFromRoute) source = ParameterSources.RouteSegment;

        parameters[i] = new ActionParameter(
            actionCtx, 
            new ModelBinders.ModelBinderCollection(new JsonSerializer(), _services))
        {
            Name = param.Name.ToLower(),
            DefaultValue = param.DefaultValue,
            HasDefaultValue = param.HasDefaultValue,
            Type = param.ParameterType,
            ParameterSource = source,
            OverridenName = overridenName
        };

        if (parameters[i].ParameterSource == ParameterSources.Unknown)
        {
            if (parameters[i].IsComplex)
            {
                parameters[i].ParameterSource = ParameterSources.Body;
            }
            else if (actionCtx.RouteSegments
                     .Any(x => x.IsParameter && x.ParameterName == parameters[i].Name))
            {
                parameters[i].ParameterSource = ParameterSources.RouteSegment;
            }
            else
            {
                parameters[i].ParameterSource = ParameterSources.Query;
            }
        }
    }

    return parameters;
}

GetParameters method is a bit complex. The reason for the complexity is the fact that parameters can be retrieved from different sources. Source of the parameter can be set explicitly with an attribute, or LiteApi can determine for you from where to read the parameters. For example if parameter is type of string, default source for that parameter type is query, however, if there is parameter with same name set in ActionRouteAttribute, then parameter is expected to come from route segment. Yet another example with string would be to have parameter with explicitly set FromBodyAttribute in which case parameter should be received from request body. Following parameters source related attributes are available:

  • FromQueryAttribute
  • FromRouteAttribute
  • FromBodyAttribute
  • FromHeaderAttribute
  • FromServicesAttribute

As a sample here is a controller that receives parameters from headers:

C#
public class HeaderParametersController: LiteController
{
    // parameter values will be retrieved from headers "i" and "x-overriden-param-name-j"
    public int Add([FromHeader]int i, [FromHeader("x-overriden-param-name-j")]int j) => i + j;
}

Source of the parameter is set in ParameterSource (enum) property of ActionParameter object. As a precaution default value for ParameterSource is Unknown. GetParameters method will change Unknown value to actual value of the parameter source. If the actual value of parameter source is not discovered, exception will be thrown during validation (on the first run of the web application).

Validation of registered controllers, actions and action parameters

For validating controllers/actions/parameters LiteApi is using following interfaces:

C#
public interface IControllersValidator
{
    IEnumerable<string> GetValidationErrors(ControllerContext[] controllerCtxs);
}

public interface IActionsValidator
{
    IEnumerable<string> GetValidationErrors(ActionContext[] actionCtxs, bool isControllerRestful);
}

public interface IParametersValidator
{
    IEnumerable<string> GetParametersErrors(ActionContext actionCtx);
}

Implementation of those interfaces will check if there are controllers that have same base URL, if all parameters rules are met, if authorization rules are valid and so on... I will not go into details about validation, I consider it to be the non-critical part of the middleware, however, if you are interested, the source can be found here and the list of expected errors can be found here.

Responding to HTTP requests

Once initialization (registration and validation) is done, LiteApi is ready to respond to HTTP requests. Here are steps performed by the middleware when a request is received:

  1. Create logger for the request (one logger per request)
  2. Find action to invoke
    • If action is found
      • Check if HTTPS is required (return 400 if it is and request is not HTTPS)
      • Invoke the action
    • If no action is found, find next middleware and invoke it (if there is next middleware)

Invoking the action is not a single step, it consists of running filters (e.g. authorization), creating the controller instance, parsing parameters and invoking the action. Following diagram describes high-level overview of steps taken when responding to an HTTP request.

LiteApi

Here is the code that receives HTTP request located in the middleware class (for clarity and readability code is stripped of the lines that perform logging operations):

C#
public async Task Invoke(HttpContext context)
{
    ActionContext action = _pathResolver.ResolveAction(context.Request, log);
    if (action == null)
    {
        if (_next != null)
        {
            await _next?.Invoke(context);
        }
    }
    else
    {
        if (Options.RequiresHttps && !context.Request.IsHttps)
        {
            context.Response.StatusCode = 400;
            await context.Response.WriteAsync("Bad request, HTTPS request was expected.");
        }
        else
        {
            await _actionInvoker.Invoke(context, action, log);
        }
    }
}

Invoke method is invoked by ASP.NET Core itself. It's entry point for the middleware when HTTP request is received. Some of the called methods are expecting ILogger as parameter. Code that is creating ILogger is removed for readability purposes. HttpContext parameter is provided by ASP.NET Core, that parameter is used by all middlewares to check if specific middleware should respond to the request and to write the response if there is any response to be written by specific middleware.

Finding the appropriate action

For finding the right action implementation of IPathResolver (really bad name, should be changed) is used. Again, logging code lines are removed. Here is the code from default implementation of IPathResolver.

C#
public ActionContext ResolveAction(HttpRequest request, ILogger logger = null)
{
    ActionContext[] actions = GetActionsForPathAndMethod(request).ToArray();
    if (actions.Length == 1)
    {
        return actions[0];
    }
    if (actions.Length == 0) return null;
    return ResolveActionContextByQueryParameterTypes(request, actions, logger);
}

private IEnumerable<ActionContext> GetActionsForPathAndMethod(HttpRequest request)
{
    string path = request.Path.Value.ToLower();
    string[] segments = path.TrimStart('/').TrimEnd('/').Split(_separator, StringSplitOptions.None);
    var method = (SupportedHttpMethods)Enum.Parse(typeof(SupportedHttpMethods), request.Method, true);
    foreach (var ctrl in _controllerContrxts)
    {
        var actions = ctrl.GetActionsBySegments(segments, method).ToArray();
        foreach (var a in actions)
        {
            yield return a;
        }
    }
}

private ActionContext ResolveActionContextByQueryParameterTypes(
    HttpRequest request, 
    ActionContext[] actions, 
    ILogger logger)
{
    // performs parameter parsing in order to find appropriate action
    // parameter parsing will be described later on in the article
}

LiteApi is looking through all controllers and calling GetActionsBySegments. If only one action is found, it will be returned to be invoked, if multiple actions are found, parsing of the parameters is performed in order to find appropriate overloaded action.

GetActionsBySegments is checking URL and HTTP method and returning actions which are matching the URL and the method. URL is checked by splitting it into segments and finding which action segments are matched against request route segments. Segments are used because they can be holding parameters, hence they are not always constants. Here is the code behind the GetActionsBySegments method:

C#
public IEnumerable<ActionContext> GetActionsBySegments(
    string[] requestSegments, 
    SupportedHttpMethods method)
{
    if (RequestSegmentsMatchController(requestSegments))
    {
        string[] requestSegmentsWithoutController = 
            requestSegments.Skip(RouteSegments.Length).ToArray();
        var actions = Actions.Where(x => 
            x.RouteSegments.Length == requestSegmentsWithoutController.Length 
                && x.HttpMethod == method)
            .ToArray();
        if (actions.Length > 0)
        {
            if (IsActionMatchedToRequestSegments(action, requestSegmentsWithoutController))
            {
                yield return action;
            }
        }
    }
}

private bool RequestSegmentsMatchController(string[] requestSegments)
{
    if (requestSegments.Length < RouteSegments.Length) return false;

    for (int i = 0; i < RouteSegments.Length; i++)
    {
        if (RouteSegments[i] != requestSegments[i]) return false;
    }
    return true;
}

private bool IsActionMatchedToRequestSegments(ActionContext action, string[] requestSegments)
{
    for (int i = 0; i < action.RouteSegments.Length; i++)
    {
        if (action.RouteSegments[i].IsConstant)
        {
            if (action.RouteSegments[i].ConstantValue != requestSegments[i]) return false;
        }
    }
    return true;
}

GetActionsBySegments is a method located in ControllerContext class. This class is used to store metadata about controller. Among other information important property for finding the correct controller/action is RouteSegments. This property is storing controller route segments which can be compared to request route segments.

First LiteApi is checking if starting request route segments is matching route segments of the controller. For example, if controller route is /api/books/ and request is /someFile.js it will be clear that the number of controller route segments is larger than the number of request route segments. In such case, LiteApi will skip other checks for matching the controller. If number of request route segments is more or equal to the number of controller route segments LiteApi will check if first few request route segments match controller route segments. For example if request is /api/books/category/dramas and controller route is /api/books LiteApi will match /api to /api and /books to /books, after that LiteApi will try to match each action of the controller.

Action matching is done by IsActionMatchedToRequestSegments method. IsActionMatchedToRequestSegments method is checking again for route segments, however here are checked only constant route segments because action route segments can contain action parameters (as we saw on /api/math/5/minus/4 example).

Each ActionContext stores route segments of the action. Route segment on action level can be constant or parameter. As an example in /api/books/category/{categoryId} all segments are constants except {categoryId}. RouteSegment class is pretty simple and it looks like this:

C#
public class RouteSegment
{
    public string OriginalValue { get; private set; }
    public bool IsConstant { get; private set; }
    public bool IsParameter => !IsConstant;
    public string ParameterName { get; private set; }
    public string ConstantValue { get; private set; }

    public RouteSegment(string segment)
    {
        if (segment == null) throw new ArgumentNullException(nameof(segment));

        OriginalValue = segment;
        IsConstant = !(OriginalValue.StartsWith("{", StringComparison.Ordinal) 
                       && OriginalValue.EndsWith("}", StringComparison.Ordinal));
        if (!IsConstant)
        {
            ParameterName = OriginalValue.TrimStart('{').TrimEnd('}').ToLower();
        }
        else
        {
            ConstantValue = OriginalValue.ToLower();
        }
    }
}

Invoking the action

After the correct action is found it needs to be invoked. IActionInvoker is responsible for invoking the action.

C#
public interface IActionInvoker
{
    Task Invoke(HttpContext httpCtx, ActionContext actionCtx, ILogger logger);
}

Action invoker accepts HttpContext, ActionContext and ILoggerActionContext is the action that is found to match the request and ILogger is context aware logger which logs request Id along with any other information, so during debugging we can see and follow logs for single request. Default implementation of IActionInvoker is using reflection. Plain is to (for future release) implement faster invoker that works with compiled expressions, delegates or IL emitting. For now, LiteApi is faster than MVC, and once the new invoker is implemented it should be even faster.

Action invoker is returning Task because action can be async Task.

Here are steps taken when invoking an action:

  1. Run filters
    • If filters fail, write response with 4xx code and error description
  2. Construct the controller
  3. Parse parameters (if any)
  4. Invoke the action and get the result
  5. Check if the result is Task, void, or other object
    • If result is Task await it
      • If task awaiting result is not void serialize the response
    • Else if result is not void serialize the response
  6. Set response code (2xx)
  7. If there is serialized response set response content type and write response to response body

Methods and classes for invoking the action are lengthy so I'm splitting the code into separate steps. If you are interested in the whole action invoker class, please take a look at the file on GitHub.

1. Run filters step is responsible to checking RequireHttpsAttribute and authorization attributes. Those attributes are called filters. Each filter implements IApiFilter or IApiFilterAsync and as a result of the check is returning ApiFilterRunResult.

C#
public interface IApiFilter
{
    bool IgnoreSkipFilters { get; }
    ApiFilterRunResult ShouldContinue(HttpContext httpCtx);
}

public interface IApiFilterAsync
{
    bool IgnoreSkipFilters { get; }
    Task<ApiFilterRunResult> ShouldContinueAsync(HttpContext httpCtx);
}

public class ApiFilterRunResult
{
    public bool ShouldContinue { get; set; }
    public int? SetResponseCode { get; set; }
    public string SetResponseMessage { get; set; }

    public static ApiFilterRunResult Unauthorized
        => new ApiFilterRunResult
        {
            ShouldContinue = false,
            SetResponseCode = 403,
            SetResponseMessage = "User is unauthorized to access the resource"
        };

    public static ApiFilterRunResult Continue
        => new ApiFilterRunResult
        {
            ShouldContinue = true
        };

    public static ApiFilterRunResult Unauthenticated
        => new ApiFilterRunResult
        {
            ShouldContinue = false,
            SetResponseCode = 401,
            SetResponseMessage = "User is unauthenticated"
        };
}

Filters are applied as attributes on controller or action level. API filter interfaces have property IgnoreSkipFilters and method ShouldContinue (or ShouldContinueAsync). IgnoreSkipFilters is used to tell LiteApi that this filter should not be skipped even if SkipFiltersAttribute is present on action. This property is useful when we want to make sure filter is not skipped. One example where a filter should never be skipped is RequiresHttpsAttribute. As an example here is a controller that has applied filters:

C#
[RequiresHttps, RequiresAuthentication]
public class UsersController: LiteController
{
    public UserDetails GetCurrentUserDetails() => // ...

    [RequiresRoles("Admin")]
    public Task<List<User>> GetAllUsers() => // ...

    [SkipFilters]
    public Task<bool> GetAccessToken(AccessRequest model) => // ...
}

In the sample UsersController we have three actions. All actions will require HTTPS connection and for the user to be authenticated, except GetAccessToken action. GetAccessToken action has declared SkipFiltersAttribute which will invalidate requirement for authentication, but will not invalidate requirement for HTTPS connection since RequiresHttpsAttribute has IgnoreSkipFilters property set to true. Furthermore GetAllUsers action has additional filter requirement, for the user to access that action user has to be in role of Admin. All filter attributes can be applied to class (controller) or method (action) with exception of SkipFiltersAttribute which can be applied only to methods.

Here is the implementation of RequiresHttpsAttribute filter:

C#
[AttributeUsage(AttributeTargets.Method | AttributeTargets.Class)]
public class RequiresHttpsAttribute : Attribute, IApiFilter
{
    public bool IgnoreSkipFilters { get; set; } = true;

    public ApiFilterRunResult ShouldContinue(HttpContext httpCtx)
    {
        if (httpCtx.Request.IsHttps) return ApiFilterRunResult.Continue;

        return new ApiFilterRunResult
        {
            SetResponseCode = 400,
            SetResponseMessage = "Bad request, HTTPS request was expected.",
            ShouldContinue = false
        };
    }
}

A filter can also check is the user is authenticated, if the user has claims, roles, claim values, and so on... As another example here is RequireRolesAttribute.

C#
[AttributeUsage(AttributeTargets.Class | AttributeTargets.Method, AllowMultiple = true)]
public class RequiresRolesAttribute : RequiresAuthenticationAttribute
{
    private readonly string[] _roles;

    public RequiresRolesAttribute(params string[] roles)
    {
        if (roles == null) throw new ArgumentNullException(nameof(roles));
        if (roles.Any(string.IsNullOrWhiteSpace) || roles.Length == 0)
            throw new ArgumentException("Role cannot be null or empty or white space.");

        _roles = roles;
    }

    public override ApiFilterRunResult ShouldContinue(HttpContext httpCtx)
    {
        var result = base.ShouldContinue(httpCtx); // check if authenticated
        if (!result.ShouldContinue) return result;

        bool hasRoles = _roles.All(httpCtx.User.IsInRole);
        if (!hasRoles) result = ApiFilterRunResult.Unauthorized;
        
        return result;
    }
}

For checking filters ActionInvoker class has method called RunFiltersAndCheckIfShouldContinue.

C#
internal static async Task<ApiFilterRunResult> RunFiltersAndCheckIfShouldContinue(
    HttpContext httpCtx, 
    ActionContext action)
{
    if (action.SkipAuth)
    {
        var nonSkipable = action.ParentController.Filters.Where(x => x.IgnoreSkipFilter);
        foreach (var filter in nonSkipable)
        {
            var shouldContinue = await filter.ShouldContinueAsync(httpCtx);
            if (!shouldContinue.ShouldContinue)
            {
                return shouldContinue;
            }
        }
        return new ApiFilterRunResult { ShouldContinue = true };
    }

    ApiFilterRunResult result = await action.ParentController.ValidateFilters(httpCtx);
    if (!result.ShouldContinue)
    {
        return result;
    }

    foreach (var filter in action.Filters)
    {
        result = await filter.ShouldContinueAsync(httpCtx);
        if (!result.ShouldContinue)
        {
            return result;
        }
    }

    return new ApiFilterRunResult { ShouldContinue = true };
} 

Firstly method checks if the action has SkipFilters attribute. If it does, it checks for non-skippable filters and runs them. If the action does not have skip filters attribute, method checks all filters on controller level and then on the action level. Plan for the future release is to add support for global filters, so we can declare a filter that can be applied on middleware level.

2. Constructing the controller is done by IControllerBuilder. The default implementation is inheriting ObjectBuilder which is capable of constructing any object by retrieving constructor parameters from DI container.

C#
internal class ControllerBuilder : ObjectBuilder, IControllerBuilder
{
    public ControllerBuilder(IServiceProvider serviceProvider) : base(serviceProvider)
    {
    }

    public LiteController Build(ControllerContext controllerCtx, HttpContext httpContext)
    {
        var controller = BuildObject(controllerCtx.ControllerType) as LiteController;
        controller.HttpContext = httpContext;
        return controller;
    }
}

LiteController is the base class of all controllers, so in Build method we can always return an object of type LiteController. Another task of ControllerBuilder is to set HttpContext property of the controller. BuildObject method is defined in inherited ObjectBuilder class.

ObjectBuilder consists of few methods. Here is BuildObject method:

C#
public object BuildObject(Type objectType)
{
    ConstructorInfo constructor = GetConstructor(objectType);
    ParameterInfo[] parameters = GetConstructorParameters(constructor);
    object[] parameterValues = GetConstructorParameterValues(parameters);
    object objectInstance = constructor.Invoke(parameterValues);
    return objectInstance;
}

BuildObject is using reflection (I am considering to replace reflection with compiled expressions approach). First GetConstructor gets called. This method is using reflection to get all constructors of a class. If there is more than one constructor then check is performed if any constructor has PrimaryConstructorAttribute which declares which constructor should be used to construct the object. (For clarity code that performs caching is removed.)

C#
private ConstructorInfo GetConstructor(Type objectType)
{
    var constructors = objectType.GetConstructors(BindingFlags.Public | BindingFlags.Instance);
    if (constructors.Length > 1)
    {
        constructors = constructors
            .Where(x => x.GetCustomAttribute<PrimaryConstructorAttribute>() != null).ToArray();
    }

    if (constructors.Length != 1)
    {
        throw new Exception($"Cannot find constructor for {objectType.FullName}. Class has more than one constructor, or "
            + "more than one constructor is using ApiConstructorAttribute. If class has more than one constructor, only "
            + "one should be annotated with ApiConstructorAttribute.");
    }

    return constructors[0];
}

After constructor is retrieved, its' parameter types are retrieved with GetConstructorParameters. (Again, caching code is removed).

C#
private ParameterInfo[] GetConstructorParameters(ConstructorInfo constructor)
{
    ParameterInfo[] parameters = constructor.GetParameters();
    return parameters;
}

What's left is to get constructor parameter values. This is done by GetConstructorParameterValues.

C#
private object[] GetConstructorParameterValues(ParameterInfo[] parameters)
{
    object[] values = new object[parameters.Length];
    for (int i = 0; i < values.Length; i++)
    {
        values[i] = _serviceProvider.GetService(parameters[i].ParameterType);
    }
    return values;
}

3. Parsing parameters is done by IModelBinder implementations. IModelBinder looks like this:

C#
public interface IModelBinder
{
    object[] GetParameterValues(HttpRequest request, ActionContext actionCtx);
    bool DoesSupportType(Type type, ParameterSources source);   
}

Entry implementation of IModelBinder is ModelBinderCollection which contains all actual implementations of IModelBinder. Here are private fields and constructor of ModelBinderCollecton:

C#
private List<IQueryModelBinder> _queryBinders = new List<IQueryModelBinder>();
private List<IBodyModelBinder> _bodyBinders = new List<IBodyModelBinder>();
private readonly IJsonSerializer _jsonSerializer;
private readonly IServiceProvider _serviceProvider;

public ModelBinderCollection(IJsonSerializer jsonSerializer, IServiceProvider serviceProvider)
{
    _jsonSerializer = jsonSerializer ?? throw new ArgumentNullException(nameof(jsonSerializer));
    _serviceProvider = serviceProvider ?? throw new ArgumentNullException(nameof(serviceProvider));

    _queryBinders.Add(new BasicQueryModelBinder());
    _queryBinders.Add(new CollectionsQueryModelBinder());
    _queryBinders.Add(new DictionaryQueryModelBinder());

    _bodyBinders.Add(new FormFileBodyBinder());
}

As we can see ModelBinderCollection contains all default model binders. Additionally it contains FormFileBodyBinder which can read HTTP form posted file(s). Beside default model binders this class can accept custom model binders which can be added during initialization of the middleware. Adding custom binder is out of the scope of this article, if you are interested here is a sample on how to define custom query model binder.

Actual method that calls other model binders is GetParameterValues.

C#
public object[] GetParameterValues(HttpRequest request, ActionContext actionCtx)
{
    object[] values = new object[actionCtx.Parameters.Length];
    List<object> args = new List<object>();
    foreach (var param in actionCtx.Parameters)
    {
        if (param.ParameterSource == ParameterSources.Query || param.ParameterSource == ParameterSources.Header)
        {
            var binder = _queryBinders.FirstOrDefault(x => x.DoesSupportType(param.Type));
            if (binder != null)
            {
                args.Add(binder.ParseParameterValue(request, actionCtx, param));
            }
            else
            {
                throw new Exception($"No model binder supports type: {param.Type}");
            }
        }
        else if (param.ParameterSource == ParameterSources.Body)
        {
            IBodyModelBinder bodyBinder;
            if ((bodyBinder = _bodyBinders.FirstOrDefault(x => x.CanHandleType(param.Type))) != null)
            {
                args.Add(bodyBinder.CreateParameter(request));
                continue;
            }
            using (TextReader reader = new StreamReader(request.Body))
            {
                string json = reader.ReadToEnd();
                args.Add(_jsonSerializer.Deserialize(json, param.Type));
            }
            request.Body.Dispose();
        }
        else if (param.ParameterSource == ParameterSources.Service)
        {
            args.Add(_serviceProvider.GetService(param.Type));
        }
        else if (param.ParameterSource == ParameterSources.RouteSegment)
        {
            args.Add(RouteSegmentModelBinder.GetParameterValue(actionCtx, param, request));
        }
        else
        {
            throw new ArgumentException(
                $"Parameter {param.Name} in controller {actionCtx.ParentController.RouteAndName} in action {actionCtx.Name} "
                + "has unknown source (body or URL). " + AttributeConventions.ErrorResolutionSuggestion);
        }
    }
    return args.ToArray();
}

For each parameter, method checks it's source and finds appropriate model binder for it. As a sample here is some code from BasicQueryModelBinder that parses parameters:

C#
public virtual object ParseParameterValue(
    HttpRequest request, 
    ActionContext actionCtx, 
    ActionParameter parameter)
{
    string value = null;
    var paramName = parameter.Name;
    if (!string.IsNullOrWhiteSpace(parameter.OverridenName)) paramName = parameter.OverridenName;

    IEnumerable<KeyValuePair<string, StringValues>> source = null;
    if (parameter.ParameterSource == ParameterSources.Query) source = request.Query;
    else source = request.Headers;
    
    var keyValue = source.LastOrDefault(x => paramName.Equals(x.Key, StringComparison.OrdinalIgnoreCase));

    if (keyValue.Key != null)
    {
        value = keyValue.Value.LastOrDefault();
    }

    if (keyValue.Key == null)
    {
        if (parameter.HasDefaultValue) return parameter.DefaultValue;
        string message =
            $"Parameter '{parameter.Name}' from {parameter.ParameterSource.ToString().ToLower()} " +
            $"(action: '{parameter.ParentActionContext}') does not have default value and " +
            $"{parameter.ParameterSource.ToString().ToLower()} does not contain value.";
        throw new Exception(message);
    }

    if (parameter.HasDefaultValue && parameter.Type != typeof(string) && string.IsNullOrEmpty(value)) return parameter.DefaultValue;
    
    return ParseSingleQueryValue(value, parameter.Type, parameter.IsNullable, parameter.Name, new Lazy<string>(() => parameter.ParentActionContext.ToString()));
}

BasicQueryModelBinder is responsible for parsing all simple types. Method ParseParameterValue is checking if parameter source is query or header (it supports both) and checks if there is actually key present for the parameter that should be parsed. If the key is not present it checks if there is the default value for the parameter and if there is it returns default value. If key is present method is checking if value is present, if value is not present, it tries to return default value, at the end it calls ParseSingleQueryValue.

ParseSingleQueryValue is very simple method, I am looking to improve its' performance (if you have an idea, please let me know by commenting). Here is the code behind ParseSingleQueryValue.

C#
public static object ParseSingleQueryValue(
    string value, 
    Type type, 
    bool isNullable, 
    string parameterName, 
    Lazy<string> actionNameRetriever)
{
    if (type == typeof(string))
    {
        return value;
    }

    if (string.IsNullOrEmpty(value))
    {
        if (isNullable)
        {
            return null;
        }
        throw new ArgumentException($"Value is not provided for parameter: '{parameterName}' in action '{actionNameRetriever.Value}'");
    }
    // todo: check if using swith with Type.GUID.ToString() would be an option
    if (type == typeof(bool)) return bool.Parse(value);
    if (type == typeof(char)) return char.Parse(value);
    if (type == typeof(Guid)) return Guid.Parse(value);
    if (type == typeof(Int16)) return Int16.Parse(value);
    if (type == typeof(Int32)) return Int32.Parse(value);
    if (type == typeof(Int64)) return Int64.Parse(value);
    if (type == typeof(UInt16)) return UInt16.Parse(value);
    if (type == typeof(UInt32)) return UInt32.Parse(value);
    if (type == typeof(UInt64)) return UInt64.Parse(value);
    if (type == typeof(Byte)) return Byte.Parse(value);
    if (type == typeof(SByte)) return SByte.Parse(value);
    if (type == typeof(decimal)) return decimal.Parse(value);
    if (type == typeof(float)) return float.Parse(value);
    if (type == typeof(double)) return double.Parse(value);
    if (type == typeof(DateTime)) return DateTime.Parse(value);
    if (type == typeof(Guid)) return Guid.Parse(value);

    throw new ArgumentOutOfRangeException();
}

ParseSingleQueryValue is returning actual value if type of the parameter is string. If it's not it checks if value is null or empty and returns null if it is and type is nullable. If value is present and type of parameter is not string method is calling parse for matched type.

At this point most of the work is done, we have controller instance, parameter values and it's time to (4) invoke the action, (5) check result, write response code and (6) body. All of this steps are done in ActionInvoker class in Invoke method. Following code is interesting part of Invoke method. (For readability code is stripped of logging lines.)

C#
object result = null;
bool isVoid = true;
if (actionCtx.Method.ReturnType == typeof(void))
{
    actionCtx.Method.Invoke(ctrl, paramValues);
}
else if (actionCtx.Method.ReturnType == typeof(Task))
{
    var task = (actionCtx.Method.Invoke(ctrl, paramValues) as Task);
    await task;
}
else if (actionCtx.Method.ReturnType.IsConstructedGenericType 
         && actionCtx.Method.ReturnType.GetGenericTypeDefinition() == typeof(Task<>))
{ 
    isVoid = false;
    var task = (dynamic)(actionCtx.Method.Invoke(ctrl, paramValues));
    result = await task;
}
else
{
    isVoid = false;
    result = actionCtx.Method.Invoke(ctrl, paramValues);
}

int statusCode = 405; // method not allowed
switch (httpCtx.Request.Method.ToUpper())
{
    case "GET": statusCode = 200; break;
    case "POST": statusCode = 201; break;
    case "PUT": statusCode = 201; break;
    case "DELETE": statusCode = 204; break;
}
httpCtx.Response.StatusCode = statusCode;
httpCtx.Response.Headers.Add("X-Powered-By-Middleware", "LiteApi");
if (!isVoid)
{
    if (actionCtx.IsReturningLiteActionResult)
    {
        await (result as ILiteActionResult).WriteResponse(httpCtx, actionCtx);
    }
    else
    {
        httpCtx.Response.ContentType = "application/json";
        await httpCtx.Response.WriteAsync(GetJsonSerializer().Serialize(result));
    }
}

Invoke method checks if action return type is void, TaskTask<T> or some other type. Depending of the return type, Ivoke method is setting response code and writing response body. Currently, implementation for setting custom response code and headers is being done for v0.8.

The end... (or is it?)

If you manage to get this far I salute you, you have my respect! I was wondering if it would be better to post this article in multiple parts (it took weeks to assemble), in the end, I decided to go with one article that covers most interesting parts of the implementation, and possibly to follow with some more specific articles about using LiteApi (getting started, authorization, customization, etc...). Please let me know what do you think. Should there be more articles about LiteApi, or should I stop here?

Want to know more?

If you are interested in using LiteApi, or in contributing to it, please visit web site and GitHub repo. Also, don't hesitate to ask me anything in the the comments below.

 

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)


Written By
Serbia Serbia
This member has not yet provided a Biography. Assume it's interesting and varied, and probably something to do with programming.

Comments and Discussions

 
-- There are no messages in this forum --