LiteApi - Alternative WEB API .NET Core Middleware

The what and why?

LiteApi is an alternative to MVC Core WEB API. It's .NET Core middleware that can provide REST(ish) capabilities for your .NET Core web app. Currently LiteApi supports only JSON data and following HTTP methods:

  • GET
  • POST
  • PUT
  • DELETE

You might be wondering why would someone write another WEB API middleware which functionalities are already covered in MVC Core. Well fellow developer... read on...

I got the idea when one colleague complaint to me how all of the new web apps he is working on, or will be working on, are SPA applications which does not need any MVC server side rendering. That complaint implies that (if you are writing SPA using .NET Core) you are using middleware that have a lot of unused code, which means you have a lot of extra binaries to deploy with your application. In addition to smaller number of deploy dependencies (hence smaller number of binaries), LiteApi supports some scenarios that MVC WEB API doesn't and it should be able to handle more requests per second (here is post about performance and I will write another one about scenarios that are supported by LiteApi and not by MVC Core).

The how?

There are two "the how" questions: 1) "How to use it" and 2) "How it works". This article will cover only the basics of the second question and primarily focus on the first one.

How it works?

Before I begun working on this middleware I have already created an implementation of controller discoverer for a MVC 4/5 project that can load controllers in runtime (something like simple plugin based web app). Later on I was curious so I wrote MVC/WEB-API implementation that works with HttpListener class. I had some code and knowlege to begin with.

Every (strongly typed) MVC type of framework/middleware works like this, including LiteApi:

  1. Load third party assemblies/code and find controllers
  2. For each found controller find actions/methods/functions
  3. For each found action find parameters that are used for the action
  4. Validate that all controllers/actions/parameters are supported (optional, this can be done on each HTTP request)
  5. Wait for HTTP request
  6. For each HTTP request find matching controller and action, after that construct controller
  7. If there is matching controller and action handle the request and if there isn't matching controller and action let the next middleware handle it (if there is one)

If you disagree about the list please post comment, I might have missed something. In any case this is how LiteApi is working.

Details about implementation will be covered in some other posts, now let's see how to use the middleware.

How to use it?

Step 1 - Create project and add reference

Before we create a sample project please note that LiteApi is still in beta phase of development. Now let's create a new project:

LiteApiTryout-create-project

Then we need to select template:

LiteApiTryout-project-template

Select empty template and set No Authentication (should be default and not changeable when empty template is selected).

After creating the project go to project.json and under dependencies add LiteApi. Intellisense will offer you version to add, you should add latest version, or if you want to follow this steps as I am doing add 0.1.1-beta as in image below:

LiteApiTryout-add-dependency

Now in Startup.cs add using LiteApi; at the top of the file and in Configure method add app.UseLiteApi();. Full file content:

using Microsoft.AspNetCore.Builder;  
using Microsoft.AspNetCore.Hosting;  
using Microsoft.AspNetCore.Http;  
using Microsoft.Extensions.DependencyInjection;  
using Microsoft.Extensions.Logging;  
using LiteApi;

namespace LiteApiTryout  
{
    public class Startup
    {
        public void ConfigureServices(IServiceCollection services)
        {
        }

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

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

            app.UseLiteApi();

            app.Run(async (context) =>
            {
                await context.Response.WriteAsync("Hello World!");
            });
        }
    }
}

Now make sure your project builds, if not, please leave a comment and I, or someone else, will try to help you.

Step 2 - Create a controller

Each controller that needs to be invoked by LiteApi needs to inherit LiteApi.LiteController class. By convention we are creating all controllers in Controllers directory of our project. This directory does not need to be called Controllers it can be Api or anything that makes sense in your project. Controllers does not have to reside in any directory and they can be placed in any directory of your choosing. Additionally controllers can be placed in multiple assemblies (in which case you need to provide list of assemblies to use when calling app.UseLiteApi(); Something like this:

// myAssemblies is System.Reflection.Assembly[]
app.UseLiteApi(LiteApiOptions.Default.AddControllerAssemblies(myAssemblies));  

By default when we add middleware with app.UseLiteApi() all controllers needs to be placed in the web app project.

When controller name ends in Controller that part will be ignored, for example if controller is named TryoutController it will by default respond to URL: /api/tryout/{actionName} where URL matching is done in case insensitive manner. Our sample controller will work with PersonModel object:

public class PersonModel  
{
    public PersonModel() { }

    public PersonModel(string name, int yearOfBirth)
        : this(Guid.NewGuid(), name, yearOfBirth)
    { }

    public PersonModel(Guid id, string name, int yearOfBirth)
    {
        Id = id;
        Name = name;
        YearOfBirth = yearOfBirth;
    }

    public Guid Id { get; set; }
    public string Name { get; set; }
    public int YearOfBirth { get; set; }

    public static IEnumerable<PersonModel> GetSampleData()
    {
        yield return new PersonModel("Isaac Asimov", 1920);
        yield return new PersonModel("Arthur C. Clarke", 1917);
        yield return new PersonModel("Philip K. Dick", 1928);
    }
}

And here is our controller:

public class PersonController : LiteApi.LiteController  
{
    private static List<PersonModel> PersonStore = PersonModel.GetSampleData().ToList();

    public List<PersonModel> All()
    {
        return PersonStore;
    }

    [LiteApi.Attributes.HttpGet] // optional, notice that All() does not have the attribute
    public PersonModel ById(Guid id)
    {
        return PersonStore.FirstOrDefault(x => x.Id == id);
    }

    [LiteApi.Attributes.HttpPost]
    public PersonModel Add(PersonModel model)
    {
        if (model.Id == default(Guid))
        {
            model.Id = Guid.NewGuid();
        }
        PersonStore.Add(model);
        return model;
    }

    [LiteApi.Attributes.HttpPut]
    public PersonModel Update(PersonModel model)
    {
        var fromStore = PersonStore.FirstOrDefault(x => x.Id == model.Id);
        if (fromStore != null)
        {
            fromStore.Name = model.Name;
            fromStore.YearOfBirth = model.YearOfBirth;
        }
        return fromStore;
    }

    [LiteApi.Attributes.HttpDelete]
    public PersonModel DeleteById(Guid id)
    {
        var person = ById(id);
        if (person != null)
        {
            PersonStore.Remove(person);
        }
        return person;
    }
}

Now if we run the app we will get just a page with Hello World! That's because root URL is no responding to our controller/action path, and next middleware is simply returning "Hello World!" string. That basic hello world middleware will serve us as basic 404 middleware (even if it's not 404 middleware).

app-first-run

Step 3 - Test our controller

For testing of our controller we can use any REST client, I will use Postman.

Now if we enter URL to get all persons http://localhost:49403/api/person/all in the rest client we should get array of persons as in the screenshot below. Notice that port might be different on your side and ids of the persons will be different as well.

get all

Same get method can be performed in web browser.

get all in chrome

Now we can get a specific person by matching GetById method (on my PC URL is http://localhost:49403/api/person/byid?id=3ae4d29d-dbeb-4441-8641-c9a303d52f8f).

get by id

Notice that parameter is provided by query ?id={id}. LiteApi doesn't support at the moment URL matching with parameters (like /api/{controller}/{action}/{parameter:id}).

Here are screenshots with POST, PUT and DELETE calls. We are updating name of 'Arthur C. Clarke', removing 'Philip K. Dick' and adding 'Robert A. Heinlein'.

postman put


postman post


postman delete

Now after those updates we can execute another get All()

postman get all after updates

You might notice that even if DELETE method has removed a person from our list, response didn't return deleted person even if our code said explicitly to return deleted person. This might be a bug or undefined behavior of the middleware, as I stated LiteApi is still in beta and there is room for improvements.

Step 4 - Play around

If you think this is something you can use in real world try it out and please let me know what you think, what should be improved and what features should be added to the middleware. Please bear in mind that this is just introductory post and we didn't cover authentication/authorization, performance, some specific features and so on...