How to write good, testable ASP.NET Core Web API code quickly

Last Updated: July 31, 2020 | Created: August 7, 2018

This article is about ASP.NET Core 2.1 Web API code and ways to write code that is easy to understand, easy to test and quick to write. You might think that is quite a task, but I introduce a number of libraries that make that feat possible. Also there is an example application that you can clone from GihHub and run locally to see the libraries in action.

If you are new Web API I do start with a general introduction which covers the difference between Web APIs and using ASP.NET Core to directly deliver HTML pages. I then show you how to write simple Web API actions and set the correct HTTP response type back to the caller.

TL;DR; – summary

I describe a way to write Web API controllers that moves the database access code out to a library called GenericServices – this reduces the code needed in a Web API to a few lines. I also have another library called GenericBizRunner that does the same for business logic, which I cover in this article too. I describe a companion library called EfCore.GenericServices.AspNetCore that handles the creation of the correct response with a HTTP status code and json payload.

The overall effect of using the described pattern and libraries is that your Web API actions are small, easy to test and quick to develop.

NOTE: There is an example application on GitHub, which you can clone and run locally – it uses an in-memory database and includes Swagger, so its easy to run.

Introduction to ASP.NET Core Web API

Note: if you already know about ASP.NET Core Web API then you can skip this section.

There are now three main approaches to building a web application using ASP.NET Core version 2.1:

  1. NET Core MVC, which uses controllers, actions and views
  2. NET Core Razor Pages, which has pages with associated code
  3. NET Core Web API, which can be queried by a framework, like React.js, or an external system

The first two have the ASP.NET Core web app deliver HTML pages, with the help of the built-in Razor syntax. In these cases, the ASP.NET Core web app is in charge of what is shown on the screen – see diagram below:

But in the ASP.NET Core Web API case what is shown to the user is controlled by something outside ASP.NET Core. For the human-user example its either framework (like AngularJS or React.js) or mobile application makes requests to the ASP.NET Core application to either get data or execute some business logic – see the diagram below:

There are pros and cons to both approaches, but nowadays more applications are moving over some sort of front-end code running in the browser or mobile which uses Web API to access the backend system. Also, application-to-application traffic can also use the Web API approach. Therefore, being aware on how to write good, testable Web API code quickly is a useful skill.

Going beyond Microsoft’s Web API examples

Microsoft has some good examples of writing Web API applications, including one using EF Core for the database (VS Code version, VS2017/Win version and VS2017/Mac version). These are a great place to start as they clearly show what is going. Here is an example taken from Microsoft’s example (Note: I have left out the constructor of the Web API controller, where the EF Core DbContext for the database is injected and put in the private field called _context).

[HttpPut("{id}")]
public IActionResult Update(long id, TodoItem item)
{
    var todo = _context.TodoItems.Find(id);
    if (todo == null)
    {
        return NotFound();
    }

    todo.IsComplete = item.IsComplete;
    todo.Name = item.Name;

    _context.TodoItems.Update(todo);
    _context.SaveChanges();
    return NoContent();
}

The example is very clear about what is happening, as you can see each part of the process, which is why Microsoft wrote it that way. But when the databases accesses and business logic get more complex, then putting the code in the Web API action isn’t such a good idea, for a number of reasons:

  1. The code needed to achieve a particular action (in the example it’s a simple update) can become many lines of code, which means your controller is long and hard to understand.
  2. Your database or business logic is likely to have extra validation/checks which it needs to return as errors, which not covered in the Microsoft example.
  3. There is a danger of duplication of code. For instance, if another action needed to update the TodoItem you would you need to duplicate part of the code.
  4. It wouldn’t be hard to unit test the Web API action in Microsoft’s example, but in more complex applications with say extra data injected via tokens or other parts of the HttpContext then it can get more difficult.

Over the years I have learnt to keep my controller actions as small as possible. This means moving the code out of the action into a separate method. At the same time, I have noticed patterns in database accesses and business logic calls which have culminated in my building libraries to handle the common parts. Here is my version of the TodoItem update using my GenericServices library, which is designed to handle CRUD (Create, Read, Update and Delete) functions to a EF Core database.

[HttpPut()]
public ActionResult<WebApiMessageOnly> Put(
    ChangeTodoDto dto,
    [FromServices]ICrudServices service)
{
    service.UpdateAndSave(dto);
    return service.Response();
}

The differences are:

  • Its short and to the point – my Web API controller will be small and easy to read
  • The library uses a general ‘update TodoItem’ method which is used throughout the application – no possibility of duplication of code.
  • My EfCore.GenericServices (injected via the [FromServices]ICrudServices service property on line 4) provides generic CRUD functions, including the mapping from the input class to the ToDoItem class.
  • The Response method on line 7 returns a response that is either OK (HTTP 200) with a success message, NoContent (HTTP 204) if the item wasn’t found, or if the update logic or database validation finds errors then it returns Invalid Input (HTTP 400) with the error messages (The format of these responses are shown later).

This approach also allows me to unit test the ‘update TodoItem’ on its own, which I prefer. As I said before testing Web API actions can work, but if you have complex HTTP things going on, like cookies or tokens providing data, then testing can get very hard. I find it much simpler and easier to test the service.UpdateAndSave(dto) code on its own in a unit test.

NOTE: If you are using EF Core then see my open-source library EfCore.TestSupport for which has lots of useful methods for testing code that uses EF Core. Also, consider using in-memory databases for unit testing as they are really quick – the article “Using in-memory databases for unit testing EF Core applications” to show you how.

So, by doing all this I have:

  • I wrote good code: is short, obvious and does not duplicate code.
  • I have written testable code: it’s easy to duplicate the same code in your unit tests.
  • I wrote the code quickly: The EfCore.GenericServices library handles the four CRUD patterns, including copying from a ViewModel/Dto class to the EF Core entity class.

It’s my EfCore.GenericServices open-source library that makes the code shorter and quicker to write. This library is a second-generation version of my original  GenericServices library built for EF6.x, and is significantly improvement over the original. My experience with the original GenericServices library was it has saved me months-per-year of development time. I expect my new library to give me a similar, if not better, productively as it now supports DDD-styled entity classes which centralises the create/update code.

I’m not going to describe the EfCore.GenericServices library in detail here, because I have already done that in the article “GenericServices: A library to provide CRUD front-end services from a EF Core database” (see also NuGet, GitHub, and documentation pages). But that article talks about MVC/Razor Pages usage.

In this article I am going to describe how to use my libraries with Web API, and to help I have updated the companion EfCore.GenericServices.AspNetCore library to convert the status output of my libraries library into the correctly formed HTTP responses.

What about business logic?

I have handled CRUD accesses to the database, but complex applications often have complex business logic. As I mentioned before I have another open-source library called EfCore.GenericBizRunner which runs business logic working with EF Core, which the companion EfCore.GenericServices.AspNetCore library also supports. You can find out more about the EfCore.GenericBizRunner library via the article called “A library to run your business logic when using Entity Framework Core”, and the GenericBizRunner’s documentation pages.

How I turn my libraries’ statuses into HTTP responses

My EfCore.GenericServices and EfCore.GenericBizRunner libraies are linked to EF Core, but could be used in any sort of application – web, desktop etc. But I mainly use them with ASP.NET Core, so I have a library called EfCore.GenericServices.AspNetCore which contains code to change the IGenericStatus that both these libraries use into either ASP.NET ModelState (for MVC and Razor Pages) into either

  • ActionResult<WebApiMessageOnly>, where it returns just a success message (or errors if incorrect).
  • ActionResult<WebApiMessageAndResult<T>>, if the Web API returns a success message and some results (or errors if incorrect).

In this article I want to look at the CreateResponse class that contain extension methods to turn my IGenericStatus into the two ActionResult forms.

NOTE: The EfCore.GenericServices.AspNetCore contains an ExampleWebApi project that contains an ASP.NET Core 2.1 Web API controller as an example of how the CreateResponse extension methods work. It uses an in-memory database and Swagger so that it’s easy to try out the Web API commands – just start the ExampleWebApi project and it should got directly to the swagger pages which lists each Web API command and allows you to test them.

Now I will describe the three main response types and what they look like. I start with the handling errors, as ASP.NET Core 2.1 has already defined that format.

InvalidInput (HTTP 400) – error return

If you create a new ASP.NET Core application the default template turns on 2.1 compatibility, which means that any Web API input is automatically validated, e.g. if a property had the attribute [Range(1,5)] then it would return an error if that property was outside that range. This is a really useful feature, as it saves you from having to add code to validate the input at the start of the Web API action.

The format that ASP.NET Core uses to return the errors is a dictionary of errors returned with Invalid Input (HTTP 400) status code. Here is an example of the format it uses:

{
    "": [
        "Global error message"
    ],    

    "MyPropery": [
        "The property is required",
        "Another error on the same property"
    ]
}

Therefore, my Response extension method in the EfCore.GenericServices.AspNetCore library turns any errors found by the EfCore.GenericServices or EfCore.GenericBizRunner libray into the same format. That means there is one common error format the calling code needs to process.

The other responses we need are:

OK (HTTP 200) – successful

This contains a message string, which you can show the user or log. You can set the message yourself, or my libraries will return a message relevant to the. Also, if there is any data to return, then there is a results property. Here is an example with results, which uses the WebApiMessageAndResult<T> response class.

{
  "message": "Success",
  "results": {
    "id": 1,
    "name": "Create ASP.NET Core API project",
    "difficulty": 1
  }
}

This type of response is has a result, as shown in this Get action method

[HttpGet("{id}", Name= "GetSingleTodo")]
public async Task<ActionResult<WebApiMessageAndResult<TodoItem>>>
    GetAsync(int id, [FromServices]ICrudServicesAsync service)
{
    return service.Response(
        await service.ReadSingleAsync<TodoItem>(id));
}

The other type of action that returns a result is a create. There are two ways to handle this.

1. Create, and return url

The standard Web API way of handling a create is to return a HTTP status of Created (201), with a url to get the newly created item. This library provides that feature, by linking into the ASP.NET Core’s CreatedAtRoute  method. Here is an example of this approach:

[HttpPost]
public ActionResult<CreateTodoDto> Post(CreateTodoDto item,
    [FromServices]IActionService<ICreateTodoBizLogic> service)
{
    var result = service.RunBizAction<TodoItem>(item);
    return service.Status.Response(this, "GetSingleTodo",
         new { id = result?.Id },  item);
}

NOTE: I have to allow for the result to be null, which can happens if there are errors. Therefore I use result?.Id  (see line 7) to stop me getting a null reference in the error state (thanks to Michael Wilson for spotting that).

On a successful creation this method returns a response header containing the URL you should use to obtain the newly created item. Here is an example of the header, with the url on the third line:

content-type: application/json; charset=utf-8
date: Thu, 30 Aug 2018 11:07:48 GMT
location: http://localhost:54074/api/ToDo/7
... other parts removed

NOTE: There is one “gotcha” that I need to warn you about. In ASP.NET Core you need to provide the Name of action method that provides the GET method to return the crated item. The Name isn’t the name of the method, but the name provided via the HttpGet attribute, for instance [HttpGet(“{id}”, Name= “GetSingleTodo”)]. If you don’t do that you get the dreaded “No route matches the supplied values” error.

2. Create, and return the created item

The other way to handle a Create is to return the created item. This has the advantage of removing the second HTTP to get the data. The library allows you to do this, and provide the 201 status code if required. Here is an example of a create a new TodoItem, where I return the created item, with its primary key:

[HttpPost]
public ActionResult<WebApiMessageAndResult<TodoItem>> Post(
     CreateTodoDto item,
     [FromServices]IActionService<ICreateTodoBizLogic> service)
{
    var result = service.RunBizAction<TodoItem>(item);
    return service.Status.ResponseWithValidCode(result, 201);
}

NOTE: You will see I use the [FromServices] attribute to allow the dependency injection (DI) provider to inject a service as a parameter in the action. I find this more efficient than the normal constructor injection, as it means each action creates just the service it needs.

NoContent (HTTP 204) – when the requested data lookup returned null

In Web APIs if something like a Get(id) is used, and no data is found (i.e. its null) then then it returns a status code of NoContent (HTTP 204). Therefore, if that happens then you get a NoContent status with a message, but no results. Here is an example of the json that is returned.

{
  "message": "The Todo Item was not found."
}

UPDATE: In version 2.0.0 of the EfCore.GenericServices.AspNetCore library I returned the NotFound status (HTTP 404) because Microsoft used NoFound status for nulls. But following feedback from others, and looking at other documentation, I released version 3.0.0 which returns a NoContent (HTTP 204) return on a null result. Version 3.0.0 also has a more fully defined return types, which helps Swagger to properly describe the API.

If you want to set your own Success and SuccessWithNullResult status you can the ResponseWithValidCode method, e.g. ResponseWithValidCode (result, 201, 404). The second parameter is the status code to return on a successful, non-null result, while the third parameter (which is optional – it defaults to 204) is the status code to return on a successful, but null result.

Telling GenericServices that null is not an error

To make the null handler to work you need to set a configuration parameter when you set up the GenericServices library. This is because in MVC or Razor Pages applications not finding the data asked for is an error, but in a Web API it isn’t. Here is an example of how you would configure GenericServices for WebAPI in the Configuration method in the Startup class – see line 4 for the important configuration option

services.GenericServicesSimpleSetup<ExampleDbContext>(
    new GenericServicesConfig
    {
        NoErrorOnReadSingleNull = true
    }
    ,Assembly.GetAssembly(typeof(ChangeNameDto)));

UPDATE: Now with unit test support

I’m working on a client’s project which has lots of Web APIs, and we want to check them, because we found errors could creep up if the dtos weren’t properly setup.  I therefore added a new set of extension methods to the EfCore.GenericService.AspNetCore project (look for version 2.0.0 or higher on NuGet) to help test these.

The new extension methods decode the HTTP IActionResult and ActionResult<T> into a form that allows you to access all the parts of the information you sent from your GenericService/GenericBizRunner methods. This makes unit testing or integration testing much easier to do.

Here is an example integration test taken from the IntegrationTestToDoController, which you can find in the the EfCore.GenericService.AspNetCore GitHub project.

[Fact]
public void TestPutNameOk()
{
    //SETUP
    var options = SqliteInMemory.CreateOptions<ExampleDbContext>();
    using (var context = new ExampleDbContext(options))
    {
        context.Database.EnsureCreated();
        context.SeedDatabase();

        var controller = new ToDoController();
        var utData = context.SetupSingleDtoAndEntities
             <ChangeNameDto>(_genericServiceConfig);
        var service = new CrudServices(context, utData.ConfigAndMapper);

        //ATTEMPT
        var dto = new ChangeNameDto()
        {
            Id = 2,
            Name = "Test",
        };
        var response = controller.PutName(dto, service);

        //VERIFY
        response.GetStatusCode().ShouldEqual(200);
        var rStatus = response.CopyToStatus();
        rStatus.IsValid.ShouldBeTrue(rStatus.GetAllErrors());
        rStatus.Message.ShouldEqual("Successfully updated the Todo Item");
    }
}

Looking through the lines of code:

  • Lines 5 to 9: I create a in-memory Sqlite database a seed it with some known data for the test.
  • Line 11: I create an instance of the Web API controller I want to test. Because I inject the services via a parameter on the action method call, then there aren’t any parameters on the Web API controller.
  • Lines 12 to 14: Here I create the service that the Web API action method needs. The code you see is for GenericServices, but you can find an example of setting up GenericBizRunner here.
  • Lines 17 to 22: I call my Web API method, with the required dto class.
  • Line 25: The GetStatusCode extension method gets the HTTP status code, which I then check.
  • Line 26: The CopyToStatus method turns the Web API IActionResult/ActionResult<T> response back into a IStatusGeneric/IStatusGeneric<T> status (it started in from, so its easy to convert it back). This gives you access to any Errors, the Message, and the Result (if it has one).
  • Line 27 and 28 I check the parts of the IStatusGeneric to see what was send

NOTE: I could have added a test of the database to check that the name of the TodoItem with primary key 2 was changed too. It depends on whether you have unit tested the underlying GenericService UpdateAndSave of this entity elsewhere.

The extensions method can be found in UnitTesting.ResponseDecoders class.

NOTE: If you want to do a full integration test of your Web APIs then have a look at the articles “How to Test ASP.NET Core Web API” which uses the Microsoft.AspNetCore.TestHost package, or “Painless Integration Testing with ASP.NET Core Web API”  which uses Microsoft’s Microsoft.AspNetCore.Mvc.Testing package.

Conclusion

I have described the pattern and libraries that allow me to build good, testable Web API code quickly. Previous experience says that this approach makes me a much faster developer – I developed a MVC5 web application around the AdventureWorks 2012 Lite database in 10 days, which is quick (see http://complex.samplemvcwebapp.net/). Real applications that use Web APIs can have hundreds, if not thousands of Web API actions – saving even 25% development time on each Web API action could equate to a saving of many months of your time and the project’s time.

This article and the example Web API in the EfCore.GenericServices.AspNetCore GitHub repo give you a quick way to see the use of EfCore.GenericServices and I threw in an example of calling business logic via my EfCore.GenericBizRunner library (see the Post action which creates a new TodoItem) to build Web APIs.

The examples uses DDD-styled entity classes because I like the way it centralises the create/update code inside the entity class. Using DDD-styled entity classes is optional, as EfCore.GenericServices can work with normal EF Core entity classes as well, but I find it adds an extra level of the robustness of the final solution.

Do clone the the example code and look at the code and run the ExampleWebApi application – maybe using this approach will save you time too.

Thanks to the readers who left comments on the NotFound/NoContent argument! I have updated the EfCore.GenericServices.AspNetCore library to use NoContent, and better response definitions, which helps Swagger to define the API.

Happy coding!

2 1 vote
Article Rating
Subscribe
Notify of
guest
17 Comments
Newest
Oldest Most Voted
Inline Feedbacks
View all comments
Willem Luijk
Willem Luijk
5 months ago

Hi Jon, i am new here. Have you already combined building API’s with multi-tenant support?

Architectd
Architectd
2 years ago

Given this is an article is about how to write API code which is easily testable, have you considered using the repository pattern so that you can mock out your db context and run unit tests without needing a connection to the database?

Ieuan Walker
Ieuan Walker
2 years ago

Good article but i do disagree with your last point about the 404 not found.
I personally find it better practice to returns a 204 no content. It tells the client that everything was successful, but couldn’t find anything for their lookup.

The 400 range is for errors, either client side or server side

Paul Speranza
Paul Speranza
2 years ago
Reply to  Ieuan Walker

204 is perfect.

Fabricio
2 years ago
Reply to  Ieuan Walker

I strongly agree with you. Return a 404 when no content is found is just wrong.

Jon P Smith
4 years ago
Reply to  Fabricio

Hi guys,

Thanks for the feedback. I was taking my cue from Microsoft’s documentation, where they return NotFound (404) – see https://docs.microsoft.com/en-us/aspnet/core/tutorials/first-web-api?view=aspnetcore-2.1#get-to-do-items . I can look at making the status on null variable, like I did with the success response. Maybe I should create an optional config with the status codes for each option.

Busy on a contract at the moment (I wrote this article on holiday 🙂 ), so I won’t do it straight away.

Jon P Smith
2 years ago

Thanks for all the feedback on 204/404 for a null result. I have changed over to returning 204 (NoContent) in version 3.0.0 of JonPSmith/EfCore.GenericServices.AspNetCore. You can still define your own status code for a success, but null result if you want to use 404.

Michelle Tan
Michelle Tan
2 years ago

I am new to EF core and want to use DBQuery instead of DBSet for the get API. I but got error on casting. How should I resolve this? Thanks.

InvalidCastException: Unable to cast object of type ‘Microsoft.EntityFrameworkCore.Internal.InternalDbQuery`1[FII_WF.Database.AllConfirm]’ to type ‘Microsoft.EntityFrameworkCore.DbSet`1[FII_WF.Database.AllConfirm]’.

Jon P Smith
2 years ago
Reply to  Michelle Tan

Hi Michelle,

Is this an error when using my EfCore.GenericServices? If so then its because I use Set in that library. I’ll have to have a look at how I can support DbQuery. I’m busy on a contract at the moment so I won’t be able to look at this straight away.

Could I ask you to raise an issue in the EfCore.GenericServices project on GitHub – use this link https://github.com/JonPSmith/EfCore.GenericServices/issues an click the “New Issue” button. If you can give me the full stack trace, and the version of the EfCore.GenericServices you are using I will look at it. That will help me and you will get a email when I fix it.

Jeremy Navarro
Jeremy Navarro
2 years ago

Jon,

Thank you for your work. We are using your libraries and ideas in our project, and it has helped me (new to web programming) to keep things organized and easy to work with.

I have a question about Many-to-Many with EfCore.GenericServices…
We have many Projects with many Contacts. In our API, we wish to return the contacts for a certain project… i.e.:
projects/{id}/contacts
I would prefer not to include the ProjectId with each Contact like so:
return services.Response(await services.ReadManyNoTracked().Where(p => p.ProjectId == id).ToListAsync());
but, I can’t find a way to easily get the List without including the ProjectId in the Dto. Am I missing something obvious, or am I misguided in my attempt to not include the ProjectId in each item in the response (since it is specified in the route)?

Any suggestions would be appreciated. Thanks again for your work, and book.

Jon P Smith
2 years ago
Reply to  Jeremy Navarro

Hi Jeremy,

You are in luck, as I just added a new method called ProjectFromEntityToDto to GenericServices version 2 (see the Wiki Doc for this method). As the docs say you can filter by something in the entity class that doesn’t turn up in the DTO.

I hope that helps.

mohamed AIT LAHMID
mohamed AIT LAHMID
2 years ago

Thank you for your work Jon

i need an exemple showing howto consume API in a client project (Razor pages).

Thank you

Jon P Smith
2 years ago

Not quite sure what you are asking. This article is about building ASP.NET Core Web APIs, not Razor pages.

mohamed AIT LAHMID
mohamed AIT LAHMID
3 years ago
Reply to  Jon P Smith

I need an example that shows how to call this API and display the list of TodoItem elements in a web page, in C #

I use the code:
string baseUrl = “https://localhost:44374/”;
HttpClient http = new HttpClient();
var request = new HttpRequestMessage();
request.RequestUri = new Uri($”{baseUrl}api/Todo”);
var response = http.SendAsync(request).Result;
var var1 = response.Content.ReadAsAsync>>();
return var1.Result.Results;

But i get the error:
Newtonsoft.Json.JsonSerializationException: Unable to find a constructor to use for type GenericServices.AspNetCore.WebApiMessageAndResult`1[System.Collections.Generic.List`1[ExampleDatabase.TodoItem]]. A class should either have a default constructor, one constructor with arguments or a constructor marked with the JsonConstructor attribute.

Thank you.