Design Patterns: Asp.Net Core Web API, services, and repositories
Part 6: the NinjaController and the ninja sub-system

In the previous articles, we covered all the patterns needed to create a system where each responsibility is isolated.
We implemented a Controller
, a Service
and a Repository
.
We also created unit and integration tests covering our specifications (as basic as they were).
Our Clan sub-system is pretty basic indeed, but it allowed us to learn the patterns without bothering too much about external dependencies.
In this article we will define most of the ninja sub-system and implement the NinjaController
while in the next articles, we will implement the service, the repository, talk about Azure Table Storage and the ForEvolve Framework.
Revisiting all the patterns in a more complex subsystem should help you learn them. Do not worry; I will also add a few more concepts along the way, this is not a copy/paste of my previous articles.
The series (shared section)
In the series, we will create an Asp.Net Core 2.0 Web API, and we will focus on the following major concerns:
- The web part; the HTTP request and response handling.
- The business logic; the domain.
- The data access logic; reading and writing data.
During the article, I will try to include the thinking process behind the code.
Technology-wise, we will use Asp.Net Core, Azure Table Storage and ForEvolve Framework to build the Web API.
To use the ForEvolve Framework (or let’s say toolbox), you will need to install packages from a custom NuGet feed.
If you dont know How to use a custom NuGet feed in Visual Studio 2017, feel free to take a look at this article.
If you do, the ForEvolve NuGet feed URI is https://www.myget.org/F/forevolve/api/v3/index.json
.
We will also use XUnit and Moq for both unit and integration testing.
Table of content
I will update the table of content as the series progress.
“Prerequisites”
In the series, I will cover multiple subjects, more or less in details, and I will assume that you have a little idea about what a Web API is, that you know C# and that you already have a development environment setup (i.e.: Visual Studio, Asp.Net Core, etc.).
The goal
At the end of this article series, you should be able to program an Asp.Net Core Web API in a structured and testable way using the explained techniques (design patterns). These design patterns offer a clean way to follow the Single Responsibility Principle.
Since design patterns are language-agnostic, you can use them in different applications and languages. In an Angular application, you will most likely use Dependency Injection for example.
This is one of the beauties of design patterns; they are tools to be used, not feared!
Asp.Net Core 2.0
At the time of the writing, Asp.Net Core 2.0 was still in prerelease, and I updated the code samples to use the release version.
You will need the .NET Core 2.0.0 SDK and Visual Studio 2017 update 3 or the IDE/code editor of your choosing.
Defining the interfaces
Now that we understand the patterns and know where we are heading, we will start by defining both our interfaces.
INinjaService
The ninja service should give us access to the Ninja objects. Once again, it will only support CRUD operations.
namespace ForEvolve.Blog.Samples.NinjaApi.Services
{
public interface INinjaService
{
Task<IEnumerable<Ninja>> ReadAllAsync();
Task<IEnumerable<Ninja>> ReadAllInClanAsync(string clanName);
Task<Ninja> ReadOneAsync(string clanName, string ninjaKey);
Task<Ninja> CreateAsync(Ninja ninja);
Task<Ninja> UpdateAsync(Ninja ninja);
Task<Ninja> DeleteAsync(string clanName, string ninjaKey);
}
}
INinjaRepository
The INinjaRepository
look the same as the INinjaService
but their responsibilities are different.
The repository’s goal is to read and write data while the service’s goal is to handle the domain logic.
As you can see, once again, we are building a simple CRUD data access interface.
namespace ForEvolve.Blog.Samples.NinjaApi.Repositories
{
public interface INinjaRepository
{
Task<IEnumerable<Ninja>> ReadAllAsync();
Task<IEnumerable<Ninja>> ReadAllInClanAsync(string clanName);
Task<Ninja> ReadOneAsync(string clanName, string ninjaKey);
Task<Ninja> CreateAsync(Ninja ninja);
Task<Ninja> UpdateAsync(Ninja ninja);
Task<Ninja> DeleteAsync(string clanName, string ninjaKey);
}
}
Controller
Having our interfaces defined will allow us to create the NinjaController
and unit test it.
As a reminder of the patterns, here is the schema from the first article of the series:

Controller
to the data source, fully decoupled.Creating the NinjaController
Once again, the NinjaController
only exposes CRUD actions.
namespace ForEvolve.Blog.Samples.NinjaApi.Controllers
{
[Route("v1/[controller]")]
public class NinjaController : Controller
{
private readonly INinjaService _ninjaService;
public NinjaController(INinjaService ninjaService)
{
_ninjaService = ninjaService ?? throw new ArgumentNullException(nameof(ninjaService));
}
[HttpGet]
[ProducesResponseType(typeof(IEnumerable<Ninja>), StatusCodes.Status200OK)]
public Task<IActionResult> ReadAllAsync()
{
throw new NotImplementedException();
}
[HttpGet("{clan}")]
[ProducesResponseType(typeof(IEnumerable<Ninja>), StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status404NotFound)]
public Task<IActionResult> ReadAllInClanAsync(string clan)
{
throw new NotImplementedException();
}
[HttpGet("{clan}/{key}")]
[ProducesResponseType(typeof(Ninja), StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status404NotFound)]
public Task<IActionResult> ReadOneAsync(string clan, string key)
{
throw new NotImplementedException();
}
[HttpPost]
[ProducesResponseType(typeof(Ninja), StatusCodes.Status201Created)]
[ProducesResponseType(StatusCodes.Status400BadRequest)]
public Task<IActionResult> CreateAsync([FromBody]Ninja ninja)
{
throw new NotImplementedException();
}
[HttpPut]
[ProducesResponseType(typeof(Ninja), StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status400BadRequest)]
[ProducesResponseType(StatusCodes.Status404NotFound)]
public Task<IActionResult> UpdateAsync([FromBody]Ninja value)
{
throw new NotImplementedException();
}
[HttpDelete("{clan}/{key}")]
[ProducesResponseType(typeof(Ninja), StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status404NotFound)]
public Task<IActionResult> DeleteAsync(string clan, string key)
{
throw new NotImplementedException();
}
}
}
Creating project specific exception classes
Before testing the controller, we will create some project specific exceptions.
Instead of using the built-in ones or returning null
we will be able to throw
and catch
NinjaApiException
s.
We will also create ClanNotFoundException
and NinjaNotFoundException
that inherits from NinjaApiException
, as follow:
namespace ForEvolve.Blog.Samples.NinjaApi
{
public class NinjaApiException : Exception
{
public NinjaApiException()
{
}
public NinjaApiException(string message) : base(message)
{
}
public NinjaApiException(string message, Exception innerException) : base(message, innerException)
{
}
protected NinjaApiException(SerializationInfo info, StreamingContext context) : base(info, context)
{
}
}
public class ClanNotFoundException : NinjaApiException
{
public ClanNotFoundException(Clan clan)
: this(clan.Name)
{
}
public ClanNotFoundException(string clanName)
: base($"Clan {clanName} was not found.")
{
}
}
public class NinjaNotFoundException : NinjaApiException
{
public NinjaNotFoundException(Ninja ninja)
: base($"Ninja {ninja.Name} ({ninja.Key}) of clan {ninja.Clan.Name} was not found.")
{
}
public NinjaNotFoundException(string clanName, string ninjaKey)
: base($"Ninja {ninjaKey} of clan {clanName} was not found.")
{
}
}
}
Why create NinjaApiException?
By having our exceptions inheriting from a project specific base exception, it is easier to differentiate the internal system exceptions from the external ones. For example, we could
catch (NinjaApiException)
to catch any of our application specific exceptions.As for the other two, for example (with no knowledge of the system), reading
catch (ClanNotFoundException)
is easier to understand than readingcatch (ArgumentException)
orif (someObject == null)
.
Exception
s are a good way to propagates errors from one component of your system up to the user interface (the controller in our case).
Testing the NinjaController
The NinjaController
already expect an INinjaService
interface upon instantiation.
The API contracts are also well defined, including different status code with ProducesResponseType
attribute.
Based on that, creating our unit tests should be relatively easy.
We only have to translate our already well-thought use cases to XUnit test code.
namespace ForEvolve.Blog.Samples.NinjaApi.Controllers
{
public class NinjaControllerTest
{
protected NinjaController ControllerUnderTest { get; }
protected Mock<INinjaService> NinjaServiceMock { get; }
public NinjaControllerTest()
{
NinjaServiceMock = new Mock<INinjaService>();
ControllerUnderTest = new NinjaController(NinjaServiceMock.Object);
}
public class ReadAllAsync : NinjaControllerTest
{
[Fact]
public async void Should_return_OkObjectResult_with_all_Ninja()
{
// Arrange
var expectedNinjas = new Ninja[]
{
new Ninja { Name = "Test Ninja 1" },
new Ninja { Name = "Test Ninja 2" },
new Ninja { Name = "Test Ninja 3" }
};
NinjaServiceMock
.Setup(x => x.ReadAllAsync())
.ReturnsAsync(expectedNinjas);
// Act
var result = await ControllerUnderTest.ReadAllAsync();
// Assert
var okResult = Assert.IsType<OkObjectResult>(result);
Assert.Same(expectedNinjas, okResult.Value);
}
}
public class ReadAllInClanAsync : NinjaControllerTest
{
[Fact]
public async void Should_return_OkObjectResult_with_all_Ninja_in_Clan()
{
// Arrange
var clanName = "Some clan name";
var expectedNinjas = new Ninja[]
{
new Ninja { Name = "Test Ninja 1" },
new Ninja { Name = "Test Ninja 2" },
new Ninja { Name = "Test Ninja 3" }
};
NinjaServiceMock
.Setup(x => x.ReadAllInClanAsync(clanName))
.ReturnsAsync(expectedNinjas);
// Act
var result = await ControllerUnderTest.ReadAllInClanAsync(clanName);
// Assert
var okResult = Assert.IsType<OkObjectResult>(result);
Assert.Same(expectedNinjas, okResult.Value);
}
[Fact]
public async void Should_return_NotFoundResult_when_ClanNotFoundException_is_thrown()
{
// Arrange
var unexistingClanName = "Some clan name";
NinjaServiceMock
.Setup(x => x.ReadAllInClanAsync(unexistingClanName))
.ThrowsAsync(new ClanNotFoundException(unexistingClanName));
// Act
var result = await ControllerUnderTest.ReadAllInClanAsync(unexistingClanName);
// Assert
Assert.IsType<NotFoundResult>(result);
}
}
public class ReadOneAsync : NinjaControllerTest
{
[Fact]
public async void Should_return_OkObjectResult_with_a_Ninja()
{
// Arrange
var clanName = "Some clan name";
var ninjaKey = "Some ninja key";
var expectedNinja = new Ninja { Name = "Test Ninja 1" };
NinjaServiceMock
.Setup(x => x.ReadOneAsync(clanName, ninjaKey))
.ReturnsAsync(expectedNinja);
// Act
var result = await ControllerUnderTest.ReadOneAsync(clanName, ninjaKey);
// Assert
var okResult = Assert.IsType<OkObjectResult>(result);
Assert.Same(expectedNinja, okResult.Value);
}
[Fact]
public async void Should_return_NotFoundResult_when_NinjaNotFoundException_is_thrown()
{
// Arrange
var unexistingClanName = "Some clan name";
var unexistingNinjaKey = "Some ninja key";
NinjaServiceMock
.Setup(x => x.ReadOneAsync(unexistingClanName, unexistingNinjaKey))
.ThrowsAsync(new NinjaNotFoundException(unexistingClanName, unexistingNinjaKey));
// Act
var result = await ControllerUnderTest.ReadOneAsync(unexistingClanName, unexistingNinjaKey);
// Assert
Assert.IsType<NotFoundResult>(result);
}
}
public class CreateAsync : NinjaControllerTest
{
[Fact]
public async void Should_return_CreatedAtActionResult_with_the_created_Ninja()
{
// Arrange
var expectedNinjaKey = "SomeNinjaKey";
var expectedClanName = "My Clan";
var expectedCreatedAtActionName = nameof(NinjaController.ReadOneAsync);
var expectedNinja = new Ninja { Name = "Test Ninja 1", Clan = new Clan { Name = expectedClanName } };
NinjaServiceMock
.Setup(x => x.CreateAsync(expectedNinja))
.ReturnsAsync(() =>
{
expectedNinja.Key = expectedNinjaKey;
return expectedNinja;
});
// Act
var result = await ControllerUnderTest.CreateAsync(expectedNinja);
// Assert
var createdResult = Assert.IsType<CreatedAtActionResult>(result);
Assert.Same(expectedNinja, createdResult.Value);
Assert.Equal(expectedCreatedAtActionName, createdResult.ActionName);
Assert.Equal(
expectedNinjaKey,
createdResult.RouteValues.GetValueOrDefault("key")
);
Assert.Equal(
expectedClanName,
createdResult.RouteValues.GetValueOrDefault("clan")
);
}
[Fact]
public async void Should_return_BadRequestResult()
{
// Arrange
var ninja = new Ninja { Name = "Test Ninja 1" };
ControllerUnderTest.ModelState.AddModelError("Key", "Some error");
// Act
var result = await ControllerUnderTest.CreateAsync(ninja);
// Assert
var badRequestResult = Assert.IsType<BadRequestObjectResult>(result);
Assert.IsType<SerializableError>(badRequestResult.Value);
}
}
public class UpdateAsync : NinjaControllerTest
{
[Fact]
public async void Should_return_OkObjectResult_with_the_updated_Ninja()
{
// Arrange
var expectedNinja = new Ninja { Name = "Test Ninja 1" };
NinjaServiceMock
.Setup(x => x.UpdateAsync(expectedNinja))
.ReturnsAsync(expectedNinja);
// Act
var result = await ControllerUnderTest.UpdateAsync(expectedNinja);
// Assert
var createdResult = Assert.IsType<OkObjectResult>(result);
Assert.Same(expectedNinja, createdResult.Value);
}
[Fact]
public async void Should_return_NotFoundResult_when_NinjaNotFoundException_is_thrown()
{
// Arrange
var unexistingNinja = new Ninja { Name = "Test Ninja 1", Clan = new Clan { Name = "Some clan" } };
NinjaServiceMock
.Setup(x => x.UpdateAsync(unexistingNinja))
.ThrowsAsync(new NinjaNotFoundException(unexistingNinja));
// Act
var result = await ControllerUnderTest.UpdateAsync(unexistingNinja);
// Assert
Assert.IsType<NotFoundResult>(result);
}
[Fact]
public async void Should_return_BadRequestResult()
{
// Arrange
var ninja = new Ninja { Name = "Test Ninja 1" };
ControllerUnderTest.ModelState.AddModelError("Key", "Some error");
// Act
var result = await ControllerUnderTest.UpdateAsync(ninja);
// Assert
var badRequestResult = Assert.IsType<BadRequestObjectResult>(result);
Assert.IsType<SerializableError>(badRequestResult.Value);
}
}
public class DeleteAsync : NinjaControllerTest
{
[Fact]
public async void Should_return_OkObjectResult_with_the_deleted_Ninja()
{
// Arrange
var clanName = "My clan";
var ninjaKey = "Some key";
var expectedNinja = new Ninja { Name = "Test Ninja 1" };
NinjaServiceMock
.Setup(x => x.DeleteAsync(clanName, ninjaKey))
.ReturnsAsync(expectedNinja);
// Act
var result = await ControllerUnderTest.DeleteAsync(clanName, ninjaKey);
// Assert
var createdResult = Assert.IsType<OkObjectResult>(result);
Assert.Same(expectedNinja, createdResult.Value);
}
[Fact]
public async void Should_return_NotFoundResult_when_NinjaNotFoundException_is_thrown()
{
// Arrange
var unexistingClanName = "Some clan name";
var unexistingNinjaKey = "Some ninja key";
NinjaServiceMock
.Setup(x => x.DeleteAsync(unexistingClanName, unexistingNinjaKey))
.ThrowsAsync(new NinjaNotFoundException(unexistingClanName, unexistingNinjaKey));
// Act
var result = await ControllerUnderTest.DeleteAsync(unexistingClanName, unexistingNinjaKey);
// Assert
Assert.IsType<NotFoundResult>(result);
}
}
}
}
Making the NinjaController tests pass
Now that we defined the expected behaviors of the NinjaController
in our automated tests, it is time to make those tests pass.
In general, it should be pretty strait forward: delegates the business logic responsibility to the service class. Our controller will need to handle a few more things here for non-OK paths.
Let’s implement the NinjaController
, method by method and test by test.
ReadAllAsync
The test says all: ReadAllAsync.Should_return_OkObjectResult_with_all_Ninja
.
[HttpGet]
[ProducesResponseType(typeof(IEnumerable<Ninja>), StatusCodes.Status200OK)]
public async Task<IActionResult> ReadAllAsync()
{
var allNinja = await _ninjaService.ReadAllAsync();
return Ok(allNinja);
}
With those two lines of code, we are now down from 12 to 11 failing tests. This is a start!
ReadAllInClanAsync
For this one, we have two tests
ReadAllInClanAsync.Should_return_OkObjectResult_with_all_Ninja_in_Clan
ReadAllInClanAsync.Should_return_NotFoundResult_when_ClanNotFoundException_is_thrown
Should_return_OkObjectResult_with_all_Ninja_in_Clan
[HttpGet("{clan}")]
[ProducesResponseType(typeof(IEnumerable<Ninja>), StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status404NotFound)]
public async Task<IActionResult> ReadAllInClanAsync(string clan)
{
var clanNinja = await _ninjaService.ReadAllInClanAsync(clan);
return Ok(clanNinja);
}
Again, pretty straight forward. We are now down from 11 to 10 failing tests.
Should_return_NotFoundResult_when_ClanNotFoundException_is_thrown
[HttpGet("{clan}")]
[ProducesResponseType(typeof(IEnumerable<Ninja>), StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status404NotFound)]
public async Task<IActionResult> ReadAllInClanAsync(string clan)
{
try
{
var clanNinja = await _ninjaService.ReadAllInClanAsync(clan);
return Ok(clanNinja);
}
catch (ClanNotFoundException)
{
return NotFound();
}
}
After this little update, we are down from 10 to 9 failing tests.
ReadOneAsync
For this one, we also have two tests
ReadOneAsync.Should_return_OkObjectResult_with_a_Ninja
ReadOneAsync.Should_return_NotFoundResult
Should_return_OkObjectResult_with_a_Ninja
[HttpGet("{clan}/{key}")]
[ProducesResponseType(typeof(Ninja), StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status404NotFound)]
public async Task<IActionResult> ReadOneAsync(string clan, string key)
{
var ninja = await _ninjaService.ReadOneAsync(clan, key);
return Ok(ninja);
}
Now down from 9 to 8 failing tests.
Should_return_NotFoundResult_when_NinjaNotFoundException_is_thrown
[HttpGet("{clan}/{key}")]
[ProducesResponseType(typeof(Ninja), StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status404NotFound)]
public async Task<IActionResult> ReadOneAsync(string clan, string key)
{
try
{
var ninja = await _ninjaService.ReadOneAsync(clan, key);
return Ok(ninja);
}
catch (NinjaNotFoundException)
{
return NotFound();
}
}
Now down from 8 to 7 failing tests.
CreateAsync
CreateAsync
also have two tests
CreateAsync.Should_return_CreatedAtActionResult_with_the_created_Ninja
CreateAsync.Should_return_BadRequestResult
Should_return_CreatedAtActionResult_with_the_created_Ninja
[HttpPost]
[ProducesResponseType(typeof(Ninja), StatusCodes.Status201Created)]
[ProducesResponseType(StatusCodes.Status400BadRequest)]
public async Task<IActionResult> CreateAsync([FromBody]Ninja ninja)
{
var createdNinja = await _ninjaService.CreateAsync(ninja);
return CreatedAtAction(
nameof(ReadOneAsync),
new { clan = createdNinja.Clan.Name, key = createdNinja.Key },
createdNinja
);
}
This one is a little more “complex” since the CreatedAtAction
(HTTP status code 201) must return the “read URL”.
Now down from 7 to 6 failing tests.
Magic strings
As you may have noticed, I am using the
nameof
operator to avoid hard coding strings. In my opinion, strings should never be hard-coded anywhere in your code.That said, out of the utopic world of endless money, time and workforce, it is tolerable to hard code your error messages (ex.: exceptions), test data (ex.: a dev database seeder), etc. Most of the time, you do not have an unlimited budget, and these will rarely change (creating a resource file ain’t that costly tho; just saying).
However, for code references (ex.: method name, class name, property name), DO NOT use a string, use the
nameof
operator.In our last code block,
nameof(ReadOneAsync)
will simply become the string"ReadOneAsync"
, but, it is not a string until compiled. Thenameof(...)
operator is a constant expression. You could see this as a “dynamic constant”. Like aconst
, all references are replaced by the constant’s value at compile time. You can also use thenameof(...)
operator anywhere you can use a constant, like in*Attribute
or as a default parameter value.One of the biggest advantages is that Visual Studio’s refactoring feature (renaming a method, a class, etc.) will also rename the
nameof(...)
references. More on that, if you use CodeLens or the “Find all references” on theReadOneAsync
method, Visual Studio will listnameof(...)
as references.
If you do not know the
nameof
operator or want more info: nameof (C# Reference).
Should_return_BadRequestResult
[HttpPost]
[ProducesResponseType(typeof(Ninja), StatusCodes.Status201Created)]
[ProducesResponseType(StatusCodes.Status400BadRequest)]
public async Task<IActionResult> CreateAsync([FromBody]Ninja ninja)
{
if (!ModelState.IsValid)
{
return BadRequest(ModelState);
}
var createdNinja = await _ninjaService.CreateAsync(ninja);
return CreatedAtAction(
nameof(ReadOneAsync),
new { clan = createdNinja.Clan.Name, key = createdNinja.Key },
createdNinja
);
}
If there is a validation error, the API will return an HTTP status code 400 with the errors in its body. We do not have any restriction for the ninja (yet); our API is highly permissive right now. However, the controller does not have to know that, which would allow us to add validation later.
That said, we are now down from 6 to 5 failing tests. More than half done!
UpdateAsync
UpdateAsync
has three tests:
- One if everything is fine
- One if the ninja’s validation fails
- And the last one in case the ninja was not found
Should_return_OkObjectResult_with_the_updated_Ninja
[HttpPut]
[ProducesResponseType(typeof(Ninja), StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status400BadRequest)]
[ProducesResponseType(StatusCodes.Status404NotFound)]
public async Task<IActionResult> UpdateAsync([FromBody]Ninja ninja)
{
var updatedNinja = await _ninjaService.UpdateAsync(ninja);
return Ok(updatedNinja);
}
Down from 5 to 4 failing tests.
Should_return_NotFoundResult
[HttpPut]
[ProducesResponseType(typeof(Ninja), StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status400BadRequest)]
[ProducesResponseType(StatusCodes.Status404NotFound)]
public async Task<IActionResult> UpdateAsync([FromBody]Ninja ninja)
{
try
{
var updatedNinja = await _ninjaService.UpdateAsync(ninja);
return Ok(updatedNinja);
}
catch (NinjaNotFoundException)
{
return NotFound();
}
}
Here we catch NinjaNotFoundException
thrown by INinjaService
in case the ninja to update does not exist.
We will need to ensure this is implemented in NinjaService
later.
Meanwhile, we are down from 4 to 3 failing tests.
Should_return_BadRequestResult
[HttpPut]
[ProducesResponseType(typeof(Ninja), StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status400BadRequest)]
[ProducesResponseType(StatusCodes.Status404NotFound)]
public async Task<IActionResult> UpdateAsync([FromBody]Ninja ninja)
{
if (!ModelState.IsValid)
{
return BadRequest(ModelState);
}
try
{
var updatedNinja = await _ninjaService.UpdateAsync(ninja);
return Ok(updatedNinja);
}
catch (NinjaNotFoundException)
{
return NotFound();
}
}
Down from 3 to 2 failing tests.
DeleteAsync
DeleteAsync
has the last two tests needed to complete the NinjaController
implementation.
Should_return_OkObjectResult_with_the_deleted_Ninja
[HttpDelete("{clan}/{key}")]
[ProducesResponseType(typeof(Ninja), StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status404NotFound)]
public async Task<IActionResult> DeleteAsync(string clan, string key)
{
var deletedNinja = await _ninjaService.DeleteAsync(clan, key);
return Ok(deletedNinja);
}
Down from 2 to 1 failing tests. Almost done!
Should_return_NotFoundResult
[HttpDelete("{clan}/{key}")]
[ProducesResponseType(typeof(Ninja), StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status404NotFound)]
public async Task<IActionResult> DeleteAsync(string clan, string key)
{
try
{
var deletedNinja = await _ninjaService.DeleteAsync(clan, key);
return Ok(deletedNinja);
}
catch (NinjaNotFoundException)
{
return NotFound();
}
}
Bam! 29 passing tests! 0 failing test!
The end of this article
Running all tests gives us the green light to continue toward the service implementation, with 29 passing tests and no failing one.
What have we covered in this article?
In this article, we defined our ninja subsystem and implemented the NinjaController
.
What’s next?
I originally planned to write a single article for both the NinjaController
and the NinjaService
.
However, due to the quantity of code, it was becoming super long, so I decided to create two distinct part instead.
In the next article, we will implement the NinjaService
and use our unit tests suite to do some refactoring.
Last word (shared section)
Table of content
Resources
Some additional resources used during the article (or not).
Articles & concepts
- Bounded Context
- Dependency Injection
- From STUPID to SOLID Code!
- Mocks Aren’t Stubs
- Testing and debugging ASP.NET Core
Tools & technologies
Code samples
Special thanks
I’d like to finish with special thanks to Emmanuel Genest who took the time to read my drafts and give me comments from a reader point of view.