Design Patterns: Asp.Net Core Web API, services, and repositories
Part 7: the NinjaService

In the previous article, we defined most of the ninja sub-system and implemented the NinjaController
while in the next article, we will implement the repository, talk about Azure Table Storage and the ForEvolve Framework.
You could see this article as 6.2 since I originally planned to write a single article for both the NinjaController
and the NinjaService
.
Due to its size, I decided to split it in two.
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.
NinjaService
It is now the time to attack the NinjaService
class!
Creating the NinjaService
Again, we know the pattern, NinjaService
must implement INinjaService
and will use INinjaRepository
to access the data source.
But this time, using the power of Dependency Injection, we will get an implementation of IClanService
injected as well.
We will use IClanService
to validate the ninja’s clan later.
Let’s start with the empty class:
namespace ForEvolve.Blog.Samples.NinjaApi.Services
{
public class NinjaService : INinjaService
{
private readonly INinjaRepository _ninjaRepository;
private readonly IClanService _clanService;
public NinjaService(INinjaRepository ninjaRepository, IClanService clanService)
{
_ninjaRepository = ninjaRepository ?? throw new ArgumentNullException(nameof(ninjaRepository));
_clanService = clanService ?? throw new ArgumentNullException(nameof(clanService));
}
public Task<Ninja> CreateAsync(Ninja ninja)
{
throw new NotImplementedException();
}
public Task<Ninja> DeleteAsync(string clanName, string ninjaKey)
{
throw new NotImplementedException();
}
public Task<IEnumerable<Ninja>> ReadAllAsync()
{
throw new NotImplementedException();
}
public Task<IEnumerable<Ninja>> ReadAllInClanAsync(string clanName)
{
throw new NotImplementedException();
}
public Task<Ninja> ReadOneAsync(string clanName, string ninjaKey)
{
throw new NotImplementedException();
}
public Task<Ninja> UpdateAsync(Ninja ninja)
{
throw new NotImplementedException();
}
}
}
Testing the NinjaService
The tests pretty much speak for themselves, besides the introduction of .Verifiable()
and .Verify()
.
Moq verifiable
Moq can keep track of what method has been called on our mocks. To enable that, you have to call the
Verifiable()
method after theSetup()
method.Ex.:
NinjaRepositoryMock.Setup(x => x.DeleteAsync(clanName, ninjaKey)).Verifiable();
Then you can verify if that happened using one of the
Verify()
method overloads.Ex.:
NinjaRepositoryMock.Verify(x => x.DeleteAsync(clanName, ninjaKey));
would tell you theDeleteAsync
method with argumentsclanName
andninjaKey
has been called at least once.You can also be more precise and specify the expected amount of calls.
Ex.:
NinjaRepositoryMock.Verify(x => x.DeleteAsync(clanName, ninjaKey), Times.Never);
will be ok only if theDeleteAsync
method with argumentsclanName
andninjaKey
has never been called.For more information, see Moq quickstart verification section.
namespace ForEvolve.Blog.Samples.NinjaApi.Services
{
public class NinjaServiceTest
{
protected NinjaService ServiceUnderTest { get; }
protected Mock<INinjaRepository> NinjaRepositoryMock { get; }
protected Mock<IClanService> ClanServiceMock { get; }
public NinjaServiceTest()
{
NinjaRepositoryMock = new Mock<INinjaRepository>();
ClanServiceMock = new Mock<IClanService>();
ServiceUnderTest = new NinjaService(NinjaRepositoryMock.Object, ClanServiceMock.Object);
}
public class ReadAllAsync : NinjaServiceTest
{
[Fact]
public async void Should_return_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" }
};
NinjaRepositoryMock
.Setup(x => x.ReadAllAsync())
.ReturnsAsync(expectedNinjas);
// Act
var result = await ServiceUnderTest.ReadAllAsync();
// Assert
Assert.Same(expectedNinjas, result);
}
}
public class ReadAllInClanAsync : NinjaServiceTest
{
[Fact]
public async void Should_return_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" }
};
NinjaRepositoryMock
.Setup(x => x.ReadAllInClanAsync(clanName))
.ReturnsAsync(expectedNinjas)
.Verifiable();
ClanServiceMock
.Setup(x => x.IsClanExistsAsync(clanName))
.ReturnsAsync(true)
.Verifiable();
// Act
var result = await ServiceUnderTest.ReadAllInClanAsync(clanName);
// Assert
Assert.Same(expectedNinjas, result);
NinjaRepositoryMock
.Verify(x => x.ReadAllInClanAsync(clanName), Times.Once);
ClanServiceMock
.Verify(x => x.IsClanExistsAsync(clanName), Times.Once);
}
[Fact]
public async void Should_throw_ClanNotFoundException_when_clan_does_not_exist()
{
// Arrange
var unexistingClanName = "Some clan name";
NinjaRepositoryMock
.Setup(x => x.ReadAllInClanAsync(unexistingClanName))
.Verifiable();
ClanServiceMock
.Setup(x => x.IsClanExistsAsync(unexistingClanName))
.ReturnsAsync(false)
.Verifiable();
// Act & Assert
await Assert.ThrowsAsync<ClanNotFoundException>(() => ServiceUnderTest.ReadAllInClanAsync(unexistingClanName));
NinjaRepositoryMock
.Verify(x => x.ReadAllInClanAsync(unexistingClanName), Times.Never);
ClanServiceMock
.Verify(x => x.IsClanExistsAsync(unexistingClanName), Times.Once);
}
}
public class ReadOneAsync : NinjaServiceTest
{
[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" };
NinjaRepositoryMock
.Setup(x => x.ReadOneAsync(clanName, ninjaKey))
.ReturnsAsync(expectedNinja);
// Act
var result = await ServiceUnderTest.ReadOneAsync(clanName, ninjaKey);
// Assert
Assert.Same(expectedNinja, result);
}
[Fact]
public async void Should_throw_NinjaNotFoundException_when_ninja_does_not_exist()
{
// Arrange
var unexistingClanName = "Some clan name";
var unexistingNinjaKey = "Some ninja key";
NinjaRepositoryMock
.Setup(x => x.ReadOneAsync(unexistingClanName, unexistingNinjaKey))
.ReturnsAsync(default(Ninja));
// Act & Assert
await Assert.ThrowsAsync<NinjaNotFoundException>(() => ServiceUnderTest.ReadOneAsync(unexistingClanName, unexistingNinjaKey));
}
}
public class CreateAsync : NinjaServiceTest
{
[Fact]
public async void Should_create_and_return_the_created_Ninja()
{
// Arrange
var expectedNinja = new Ninja();
NinjaRepositoryMock
.Setup(x => x.CreateAsync(expectedNinja))
.ReturnsAsync(expectedNinja)
.Verifiable();
// Act
var result = await ServiceUnderTest.CreateAsync(expectedNinja);
// Assert
Assert.Same(expectedNinja, result);
NinjaRepositoryMock.Verify(x => x.CreateAsync(expectedNinja), Times.Once);
}
}
public class UpdateAsync : NinjaServiceTest
{
[Fact]
public async void Should_update_and_return_the_updated_Ninja()
{
// Arrange
const string ninjaKey = "Some key";
const string clanKey = "Some clan";
var expectedNinja = new Ninja
{
Key = ninjaKey,
Clan = new Clan { Name = clanKey }
};
NinjaRepositoryMock
.Setup(x => x.ReadOneAsync(clanKey, ninjaKey))
.ReturnsAsync(expectedNinja);
NinjaRepositoryMock
.Setup(x => x.UpdateAsync(expectedNinja))
.ReturnsAsync(expectedNinja)
.Verifiable();
// Act
var result = await ServiceUnderTest.UpdateAsync(expectedNinja);
// Assert
Assert.Same(expectedNinja, result);
NinjaRepositoryMock.Verify(x => x.UpdateAsync(expectedNinja), Times.Once);
}
[Fact]
public async void Should_throw_NinjaNotFoundException_when_ninja_does_not_exist()
{
// Arrange
var unexistingNinja = new Ninja { Key = "SomeKey", Clan = new Clan { Name = "Some clan" } };
NinjaRepositoryMock
.Setup(x => x.UpdateAsync(unexistingNinja))
.Verifiable();
NinjaRepositoryMock
.Setup(x => x.ReadOneAsync("Some clan", "SomeKey"))
.ReturnsAsync(default(Ninja))
.Verifiable();
// Act & Assert
await Assert.ThrowsAsync<NinjaNotFoundException>(() => ServiceUnderTest.UpdateAsync(unexistingNinja));
// Make sure UpdateAsync is never hit
NinjaRepositoryMock
.Verify(x => x.UpdateAsync(unexistingNinja), Times.Never);
// Make sure we read the ninja from the repository before attempting an update.
NinjaRepositoryMock
.Verify(x => x.ReadOneAsync("Some clan", "SomeKey"), Times.Once);
}
}
public class DeleteAsync : NinjaServiceTest
{
[Fact]
public async void Should_delete_and_return_the_deleted_Ninja()
{
// Arrange
var clanName = "My clan";
var ninjaKey = "Some key";
var expectedNinja = new Ninja { Name = "Test Ninja 1" };
NinjaRepositoryMock
.Setup(x => x.DeleteAsync(clanName, ninjaKey))
.ReturnsAsync(expectedNinja)
.Verifiable();
NinjaRepositoryMock
.Setup(x => x.ReadOneAsync(clanName, ninjaKey))
.ReturnsAsync(expectedNinja);
// Act
var result = await ServiceUnderTest.DeleteAsync(clanName, ninjaKey);
// Assert
Assert.Same(expectedNinja, result);
NinjaRepositoryMock.Verify(x => x.DeleteAsync(clanName, ninjaKey), Times.Once);
}
[Fact]
public async void Should_throw_NinjaNotFoundException_when_ninja_does_not_exist()
{
// Arrange
const string clanName = "Some clan name";
const string ninjaKey = "Some ninja key";
NinjaRepositoryMock
.Setup(x => x.DeleteAsync(clanName, ninjaKey))
.Verifiable();
NinjaRepositoryMock
.Setup(x => x.ReadOneAsync(clanName, ninjaKey))
.ReturnsAsync(default(Ninja))
.Verifiable();
// Act & Assert
await Assert.ThrowsAsync<NinjaNotFoundException>(() => ServiceUnderTest.DeleteAsync(clanName, ninjaKey));
// Make sure UpdateAsync is never hit
NinjaRepositoryMock
.Verify(x => x.DeleteAsync(clanName, ninjaKey), Times.Never);
// Make sure we read the ninja from the repository before attempting an update.
NinjaRepositoryMock
.Verify(x => x.ReadOneAsync(clanName, ninjaKey), Times.Once);
}
}
}
}
Making the NinjaService tests pass
Let’s do the same exercise again and make all NinjaService
tests pass.
ReadAllAsync
Should_return_all_Ninja
public Task<IEnumerable<Ninja>> ReadAllAsync()
{
return _ninjaRepository.ReadAllAsync();
}
Bam! down from 10 to 9 failing tests.
ReadAllInClanAsync
Should_return_all_Ninja_in_Clan
public Task<IEnumerable<Ninja>> ReadAllInClanAsync(string clanName)
{
return _ninjaRepository.ReadAllInClanAsync(clanName);
}
And down from 9 to 8 failing tests.
Should_throw_ClanNotFoundException_when_clan_does_not_exist
public async Task<IEnumerable<Ninja>> ReadAllInClanAsync(string clanName)
{
var isClanExist = await _clanService.IsClanExistsAsync(clanName);
if (!isClanExist)
{
throw new ClanNotFoundException(clanName);
}
return await _ninjaRepository.ReadAllInClanAsync(clanName);
}
And now only 7 failing tests.
ReadOneAsync
Should_return_OkObjectResult_with_a_Ninja
public Task<Ninja> ReadOneAsync(string clanName, string ninjaKey)
{
return _ninjaRepository.ReadOneAsync(clanName, ninjaKey);
}
And only 6 failing tests.
Should_throw_NinjaNotFoundException_when_ninja_does_not_exist
public async Task<Ninja> ReadOneAsync(string clanName, string ninjaKey)
{
var ninja = await _ninjaRepository.ReadOneAsync(clanName, ninjaKey);
if(ninja == null)
{
throw new NinjaNotFoundException(clanName, ninjaKey);
}
return ninja;
}
Down to 5 failing tests.
CreateAsync
Should_create_and_return_the_created_Ninja
public async Task<Ninja> CreateAsync(Ninja ninja)
{
var createdNinja = await _ninjaRepository.CreateAsync(ninja);
return createdNinja;
}
Down to 4 failing tests.
UpdateAsync
Should_update_and_return_the_updated_Ninja
public async Task<Ninja> UpdateAsync(Ninja ninja)
{
var updatedNinja = await _ninjaRepository.UpdateAsync(ninja);
return updatedNinja;
}
Only 3 failing tests are remaining.
Should_throw_NinjaNotFoundException_when_ninja_does_not_exist
public async Task<Ninja> UpdateAsync(Ninja ninja)
{
var remoteNinja = await _ninjaRepository.ReadOneAsync(ninja.Clan.Name, ninja.Key);
if (remoteNinja == null)
{
throw new NinjaNotFoundException(ninja.Clan.Name, ninja.Key);
}
var updatedNinja = await _ninjaRepository.UpdateAsync(ninja);
return updatedNinja;
}
Almost done, only 2 failing tests are remaining.
DeleteAsync
Should_delete_and_return_the_deleted_Ninja
public Task<Ninja> DeleteAsync(string clanName, string ninjaKey)
{
return _ninjaRepository.DeleteAsync(clanName, ninjaKey);
}
Only 1 failing test to go!
Should_throw_NinjaNotFoundException_when_ninja_does_not_exist
public async Task<Ninja> DeleteAsync(string clanName, string ninjaKey)
{
var remoteNinja = await _ninjaRepository.ReadOneAsync(clanName, ninjaKey);
if (remoteNinja == null)
{
throw new NinjaNotFoundException(clanName, ninjaKey);
}
var deletedNinja = await _ninjaRepository.DeleteAsync(clanName, ninjaKey);
return deletedNinja;
}
Yeah! All tests are passing again!
Some refactoring
If we take a look at the NinjaService
, there is some code that is there multiple times.
Armed with our unit test suite, we can now start to refactor the NinjaService
with confidence!
The refactoring target is:
var ninja = await _ninjaRepository.ReadOneAsync(clanName, ninjaKey);
if(ninja == null)
{
throw new NinjaNotFoundException(clanName, ninjaKey);
}
We use a similar code block in ReadOneAsync
, UpdateAsync
and DeleteAsync
methods.
Let’s create a method named EnforceNinjaExistenceAsync
to keep our code DRY!
EnforceNinjaExistenceAsync method
private async Task<Ninja> EnforceNinjaExistenceAsync(string clanName, string ninjaKey)
{
var remoteNinja = await _ninjaRepository.ReadOneAsync(clanName, ninjaKey);
if (remoteNinja == null)
{
throw new NinjaNotFoundException(clanName, ninjaKey);
}
return remoteNinja;
}
ReadOneAsync, UpdateAsync and DeleteAsync updates
Here are the updated methods:
public async Task<Ninja> DeleteAsync(string clanName, string ninjaKey)
{
await EnforceNinjaExistenceAsync(clanName, ninjaKey);
var deletedNinja = await _ninjaRepository.DeleteAsync(clanName, ninjaKey);
return deletedNinja;
}
public async Task<Ninja> ReadOneAsync(string clanName, string ninjaKey)
{
var ninja = await EnforceNinjaExistenceAsync(clanName, ninjaKey);
return ninja;
}
public async Task<Ninja> UpdateAsync(Ninja ninja)
{
await EnforceNinjaExistenceAsync(ninja.Clan.Name, ninja.Key);
var updatedNinja = await _ninjaRepository.UpdateAsync(ninja);
return updatedNinja;
}
If we run our tests, all 39 tests are still passing. This means that we broke nothing!
Adding clan validation
Here is where it becomes more interesting.
At the moment, the NinjaService
is pretty dumb; we can create ninja in unexisting clans.
This should not be tolerated!
Let’s add this domain logic to the API.
As a reminder, the domain logic goes in the Service
.
You may or may not remember it, but the shot was already called, and the tooling is already here.
The IClanService.IsClanExistsAsync
method does what we need to check the clan’s existences.
Adding our tests first
In both CreateAsync
and UpdateAsync
methods, there is no clan validation at all.
A user could create some ninja in unexisting clans, which could cause some problems.
To make sure that this cannot happen, we will start by adding the following two tests:
CreateAsync+Should_throw_a_ClanNotFoundException_when_clan_does_not_exist
UpdateAsync+Should_throw_a_ClanNotFoundException_when_clan_does_not_exist
We will update both CreateAsync
and UpdateAsync
methods one by one (I believe the writing will be clearer this way).
CreateAsync
First we will update the Should_create_and_return_the_created_Ninja
test method to support the call to the IClanService.IsClanExistsAsync
method.
Here is the updated method:
[Fact]
public async void Should_create_and_return_the_created_Ninja()
{
// Arrange
const string clanName = "Some clan name";
var expectedNinja = new Ninja { Clan = new Clan { Name = clanName } };
NinjaRepositoryMock
.Setup(x => x.CreateAsync(expectedNinja))
.ReturnsAsync(expectedNinja)
.Verifiable();
ClanServiceMock
.Setup(x => x.IsClanExistsAsync(clanName))
.ReturnsAsync(true);
// Act
var result = await ServiceUnderTest.CreateAsync(expectedNinja);
// Assert
Assert.Same(expectedNinja, result);
NinjaRepositoryMock.Verify(x => x.CreateAsync(expectedNinja), Times.Once);
}
All tests should still pass.
Should_throw_a_ClanNotFoundException_when_clan_does_not_exist
Let’s add the new failing test that backs our new specification.
[Fact]
public async void Should_throw_a_ClanNotFoundException_when_clan_does_not_exist()
{
const string clanName = "Some clan name";
var expectedNinja = new Ninja { Clan = new Clan { Name = clanName } };
NinjaRepositoryMock
.Setup(x => x.CreateAsync(expectedNinja))
.ReturnsAsync(expectedNinja)
.Verifiable();
ClanServiceMock
.Setup(x => x.IsClanExistsAsync(clanName))
.ReturnsAsync(false);
// Act & Assert
await Assert.ThrowsAsync<ClanNotFoundException>(() => ServiceUnderTest.CreateAsync(expectedNinja));
// Make sure CreateAsync is never called
NinjaRepositoryMock.Verify(x => x.CreateAsync(expectedNinja), Times.Never);
}
Our new test is failing (obviously).
NinjaService.CreateAsync Let’s make the new test pass!
public async Task<Ninja> CreateAsync(Ninja ninja)
{
var clanExist = await _clanService.IsClanExistsAsync(ninja.Clan.Name);
if (!clanExist)
{
throw new ClanNotFoundException(ninja.Clan);
}
var createdNinja = await _ninjaRepository.CreateAsync(ninja);
return createdNinja;
}
Ok, all tests are passing again!
UpdateAsync
Now we will update both Should_update_and_return_the_updated_Ninja
and Should_throw_NinjaNotFoundException_when_ninja_does_not_exist
test methods to support the call to the IClanService.IsClanExistsAsync
method by adding the following instruction to the “Arrange” section:
ClanServiceMock
.Setup(x => x.IsClanExistsAsync(clanKey))
.ReturnsAsync(true);
Should_throw_a_ClanNotFoundException_when_clan_does_not_exist
Then, we will add the following new failing test that backs our new specification.
[Fact]
public async void Should_throw_a_ClanNotFoundException_when_clan_does_not_exist()
{
// Arrange
const string ninjaKey = "SomeKey";
const string clanKey = "Some clan";
var unexistingNinja = new Ninja { Key = ninjaKey, Clan = new Clan { Name = clanKey } };
NinjaRepositoryMock
.Setup(x => x.UpdateAsync(unexistingNinja))
.Verifiable();
ClanServiceMock
.Setup(x => x.IsClanExistsAsync(clanKey))
.ReturnsAsync(false);
// Act & Assert
await Assert.ThrowsAsync<ClanNotFoundException>(() => ServiceUnderTest.UpdateAsync(unexistingNinja));
// Make sure UpdateAsync is never called
NinjaRepositoryMock
.Verify(x => x.UpdateAsync(unexistingNinja), Times.Never);
}
Again, back to one failing test.
NinjaService.UpdateAsync
Let’s make the new test pass!
public async Task<Ninja> UpdateAsync(Ninja ninja)
{
var clanExist = await _clanService.IsClanExistsAsync(ninja.Clan.Name);
if (!clanExist)
{
throw new ClanNotFoundException(ninja.Clan);
}
await EnforceNinjaExistenceAsync(ninja.Clan.Name, ninja.Key);
var updatedNinja = await _ninjaRepository.UpdateAsync(ninja);
return updatedNinja;
}
And all tests are now passing again!
Some more refactoring
We repeated the same code block a few times; let’s extract it to a method to keep our application DRY!
Then, lets refactor ReadAllInClanAsync
, CreateAsync
and UpdateAsync
methods to use the following new method:
private async Task EnforceClanExistenceAsync(string clanName)
{
var clanExist = await _clanService.IsClanExistsAsync(clanName);
if (!clanExist)
{
throw new ClanNotFoundException(clanName);
}
}
ReadAllInClanAsync
, CreateAsync
and UpdateAsync
public async Task<IEnumerable<Ninja>> ReadAllInClanAsync(string clanName)
{
await EnforceClanExistenceAsync(clanName);
return await _ninjaRepository.ReadAllInClanAsync(clanName);
}
public async Task<Ninja> CreateAsync(Ninja ninja)
{
await EnforceClanExistenceAsync(ninja.Clan.Name);
var createdNinja = await _ninjaRepository.CreateAsync(ninja);
return createdNinja;
}
public async Task<Ninja> UpdateAsync(Ninja ninja)
{
await EnforceClanExistenceAsync(ninja.Clan.Name);
await EnforceNinjaExistenceAsync(ninja.Clan.Name, ninja.Key);
var updatedNinja = await _ninjaRepository.UpdateAsync(ninja);
return updatedNinja;
}
Ain’t that code cleaner than our previous version?
I believe that code like
await EnforceClanExistenceAsync(clanName);
is easier to read and to understand than it was in its previous form. It is almost plain English now, instead of raw code.Do not over extract methods either. As a rule of thumb, create a private method when it is used more than once.
That said, if for some reason creating a private method would significantly improve the readability of the code, do it. Be however careful because too many private methods called only once can sometimes make the code harder to read than simply keeping the code in the caller method.
The end of this article
Running all tests gives us the green light to continue toward the repository implementation, with 41 passing tests and no failing one.
What have we covered in this article?
In this article, we implemented and added some domain logic to the NinjaService
.
We also used our unit tests to ensure that our refactoring had not broken anything.
What’s next?
In the next articles, we will implement the missing piece of the Ninja subsystem: the NinjaRepository
.
We will use Azure Table Storage to store our ninja’s data and create a mapping system.
I will also introduce my in-development ForEvolve Framework that will help us access our data.
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.