While this article is technically part of my series in a fun gamedev lite side project I'm working on, my activities on the project this evening made for a good opportunity to share how to create a new ASP .NET Core Web API.
This article will walk you through some simple steps in creating, running, and testing a new ASP .NET Core Web API.
Prerequisites
I will be using .NET Core 2.1 as it's what I have installed on my machine, though today Release Candidate 1 of .NET Core 3 came out.
To get started:
Download and Install Visual Studio 2019 Community Edition. You will need to install the .NET Core cross-platform development tools.
Download and Install the .NET Core 2.1 SDK
Project Setup
Open Visual Studio 2019 and create a new project.
When prompted, choose ASP .NET Core Web Application and click Next.
Give your project a meaningful name. The solution name will auto-generate itself.
Next Visual Studio will ask you what starting template you want to choose. These choices do not exclude you from going down other paths later. For now, we'll choose API and uncheck all the boxes on the right for the purposes of a simple demo application.
Click Create and your project should be created and opened.
Running the API
To verify that everything worked properly, go to the Debug menu on the top of the screen and click Start without Debugging. This will launch a web browser and give you a blank web page with the text ["value1", "value2"].
Believe it or not, this means everything is working. Your browser navigated to the ValuesController
class and hit its HTTP GET route, which returned that content.
Here's a snippet of ValuesController.cs
located in the Controllers
folder:
[Route("api/[controller]")]
[ApiController]
public class ValuesController : ControllerBase
{
// GET api/values
[HttpGet]
public ActionResult<IEnumerable<string>> Get()
{
return new string[] { "value1", "value2" };
}
// Other code omitted...
}
Here the browser navigated to /api/values
which matched to ValuesController
by name prefix (see the Route
attribute on the ValuesController
class). Inside of this controller, we mapped to the Get
method listed above because the method used was GET (browser navigation does a GET request) and we weren't looking any further into api/values
.
Because of this, ASP .NET Core ran the Get()
method and returned a 200 OK result with the content defined in the string array in the listing above.
So, cool! Our code works. Now it's time to set up for some deeper development.
Adding Projects to the Solution
The first thing I like to do when adding a new project is create two new library projects and add them to the solution. The first will be a library to hold all of our application logic and the second will be a unit test library.
In Solution Explorer right click on your solution (the topmost item that contains the project) and choose Add and then New Project inside of that.
Select Class Library (.NET Standard), Click Next, give it a meaningful name (I named mine MattEland.Starship.Logic) and click Create.
Now that the library is created, we'll right click on the main Web API project in Solution Explorer, select Add and then select Reference. From here, we'll check the name of the library we added and click OK.
This allows the main API project to use code defined in the library, which helps separate the API-specific logic from the domain logic and makes it easier to port the application logic over to a console, desktop, or mobile application if that is ever needed.
Now, click on the solution explorer and add another new project. This time we'll select either a new XUnit test project or an NUnit test project. For the purposes of this tutorial, I'll be demonstrating using the NUnit Test Project (.NET Core) template.
Name that project whatever you wish (mine is MattEland.Starship.Tests) and click Create.
Next we'll right click on the test project and add dependencies like we did above. This time we'll add a dependency to both the library and the web application. This way our tests can invoke methods directly on the controller for integration testing.
Adding classes to the library
Next, let's create a few sample domain classes and put them in our logic library. With the project selected, right click and click Add and then Class...
From here, leave the selection as a Class, but give it a meaningful name. Mine is going to be GameState.cs
to represent the state of a turn-based game.
Put some simple code inside of this class - enough to test a simple object structure.
My data will be the following:
namespace MattEland.Starship.Logic
{
public class GameState
{
public GameState(int id)
{
Id = id;
}
public int Id { get; }
public int ClosedCount { get; set; }
}
}
I'm also going to create a GameRepository
to store the GameState instances. This class is what our controller will interact with.
A very simple demo-oriented repository is listed below:
using System.Collections.Generic;
using System.Linq;
namespace MattEland.Starship.Logic
{
public class GameRepository
{
private readonly IList<GameState> _games = new List<GameState>();
public GameRepository()
{
// Start with some sample data
CreateNewGame();
}
public IEnumerable<GameState> Games => _games;
public GameState GetGame(int id) => _games.FirstOrDefault(g => g.Id == id);
public GameState CreateNewGame()
{
int id = _games.Count + 1;
var game = new GameState(id);
_games.Add(game);
return game;
}
public bool DeleteGame(int id)
{
var game = _games.FirstOrDefault(g => g.Id == id);
return game != null && _games.Remove(game);
}
}
}
Now that we have some basic logic and a repository class to manage operations, lets see how this plugs in with the controller.
Creating our first controller
Next, let's delete the ValuesController.cs
file (or leave it in if you want to keep it as a reference) and add a new controller to the Web API project. In my case, this is called GamesController
to manage various game states available.
This class will hold on to a new instance of the repository class we created before and relay operations to it.
My controller is listed below:
using System.Collections.Generic;
using MattEland.Starship.Logic;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
namespace MattEland.Starship.ProcessingService.Controllers
{
[Route("api/[controller]")]
[ApiController]
public class GamesController : ControllerBase
{
private readonly GameRepository _repository = new GameRepository();
// GET api/games
[HttpGet]
public ActionResult<IEnumerable<GameState>> LoadGame()
{
return Ok(_repository.Games);
}
// GET api/games/42
[HttpGet("{id}")]
public ActionResult<GameState> LoadGame(int id)
{
var game = _repository.GetGame(id);
if (game != null)
{
return Ok(game);
}
return new NotFoundResult();
}
// POST api/games
[HttpPost]
public CreatedResult NewGame()
{
var game = _repository.CreateNewGame();
return new CreatedResult($"/api/games/{game.Id}", game);
}
// DELETE api/games/42
[HttpDelete("{id}")]
public StatusCodeResult Delete(int id)
{
bool deleted = _repository.DeleteGame(id);
if (deleted)
{
return new StatusCodeResult(StatusCodes.Status204NoContent);
}
return new NotFoundResult();
}
}
}
Note that I define a standard GET method to GET all games as well as a specific GET method for getting a single game by its ID. These methods are differentiated by the parameters fed in to the HttpGet
attribute with the get specific game one taking in a variable {id}
that is mapped in as the int id
parameter.
Also note that I define HttpPost
and HttpDelete
verbs for methods to create a new game and to delete an existing game.
Note again that this is extremely minimal. In a real application I'd have things like request validation and error handling baked into the API layer (if not handled by middleware).
Testing it in the browser
Now that the logic is all ready, you'd think we could just run without debugging and see our new response, but remember that Visual Studio navigated to the /api/values
path on startup last time. This is because the project's debug settings are configured to look at that path.
We can change the default path by going to the Web API project's properties node and double clicking that, then choosing the Debug tab, then changing the URL in the Start Browser text box to match the name of your new controller.
Once everything is configured and you've saved the project (File > Save All), Run without Debugging and verify you see data as you'd expect.
In my case, I see: [{"id":1,"closedCount":0}]
which looks correct based on my simple object definition.
At this point you can make HTTP requests against your local instance and it will respond with the appropriate response.
Testing it via code
I like to have a bit more security in testing my applications than having to manually test every API call, so I like to have at least one or two integration level tests that simulate direct calls on the Controller
classes. The majority of my tests will be unit tests aimed at things like GameState
or GameRepository
, but it is helpful to test that the API logic is functioning as well.
In the UnitTest1.cs
(which you can rename as you like), I'm going to modify the existing test to read as follows:
using MattEland.Starship.Logic;
using MattEland.Starship.ProcessingService.Controllers;
using NUnit.Framework;
namespace Tests
{
public class Tests
{
[Test]
public void Test1()
{
// Arrange
var controller = new GamesController();
// Act
var result = controller.NewGame();
// Assert
Assert.IsNotNull(result);
GameState state = (GameState) result.Value;
Assert.AreEqual(2, state.Id);
}
}
}
This invokes the NewGame method on my controller (a HTTP POST verb in my case) and examines the result to see that a new game was created and returned and it had an ID consistent with what I'd expect.
Note that in order to support referencing the controller directly I had to follow a Visual Studio action suggestion and add a reference to Microsoft.AspNetCore.Mvc.Core
.
I also found my tests were initially failing until I added a NuGet reference to Microsoft.AspNetCore.MVC.Abstractions
. Do this by right clicking on the test project in Solution Explorer and choosing Manage NuGet Packages... and then searching for the assembly and clicking Install with it selected.
From here, you can run your test by clicking on Test in the upper menu, then Windows, then File Explorer. This will make the testing pane available and you can click the test case to run and run the selected test.
Note: Your UI may look different than mine. I use ReSharper which adds on extra testing tools in the user interface
A simple unit test around the GameRepository
starting with state inside of it might look like this:
using System.Linq;
using MattEland.Starship.Logic;
using NUnit.Framework;
namespace Tests
{
public class RepositoryTests
{
[Test]
public void RepositoryShouldStartWithGameState()
{
// Arrange
var repository = new GameRepository();
// Act
var games = repository.Games;
// Assert
Assert.Greater(games.Count(), 0);
}
}
}
Closing
In this article we created a new API project, a shared logic library, and a test project and verified that everything functioned properly.
While there's still so much in this example that is very basic and could be drastically improved, this should help you get started. Stay tuned for subsequent articles on optimizing this application and dealing with common scenarios. There's a lot to learn in ASP .NET Core, but it's a fantastic platform for API development.
Top comments (0)