diff --git a/1. NinjaApi - Starting point/src/ForEvolve.Blog.Samples.NinjaApi/ForEvolve.Blog.Samples.NinjaApi.csproj b/1. NinjaApi - Starting point/src/ForEvolve.Blog.Samples.NinjaApi/ForEvolve.Blog.Samples.NinjaApi.csproj
index 826c251..8cb226a 100644
--- a/1. NinjaApi - Starting point/src/ForEvolve.Blog.Samples.NinjaApi/ForEvolve.Blog.Samples.NinjaApi.csproj
+++ b/1. NinjaApi - Starting point/src/ForEvolve.Blog.Samples.NinjaApi/ForEvolve.Blog.Samples.NinjaApi.csproj
@@ -6,7 +6,7 @@
-
+
diff --git a/10. NinjaApi - NinjaRepository/ForEvolve.Blog.Samples.NinjaApi.sln b/10. NinjaApi - NinjaRepository/ForEvolve.Blog.Samples.NinjaApi.sln
new file mode 100644
index 0000000..48f81a9
--- /dev/null
+++ b/10. NinjaApi - NinjaRepository/ForEvolve.Blog.Samples.NinjaApi.sln
@@ -0,0 +1,46 @@
+
+Microsoft Visual Studio Solution File, Format Version 12.00
+# Visual Studio 15
+VisualStudioVersion = 15.0.26711.1
+MinimumVisualStudioVersion = 10.0.40219.1
+Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "src", "src", "{23BF447F-B872-4D4F-8F28-D443072AE93E}"
+EndProject
+Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "test", "test", "{2F55FE43-6EA6-4A7E-B70B-2E8267478F29}"
+EndProject
+Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ForEvolve.Blog.Samples.NinjaApi", "src\ForEvolve.Blog.Samples.NinjaApi\ForEvolve.Blog.Samples.NinjaApi.csproj", "{DD219D97-6BF6-4EE5-9E35-13185F4FA261}"
+EndProject
+Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ForEvolve.Blog.Samples.NinjaApi.Tests", "test\ForEvolve.Blog.Samples.NinjaApi.Tests\ForEvolve.Blog.Samples.NinjaApi.Tests.csproj", "{6705EB4C-3DF6-4385-AEAB-3EE2FE074E05}"
+EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ForEvolve.Blog.Samples.NinjaApi.IntegrationTests", "test\ForEvolve.Blog.Samples.NinjaApi.IntegrationTests\ForEvolve.Blog.Samples.NinjaApi.IntegrationTests.csproj", "{89C4A3D5-7272-4EC7-AC91-89ED8C1D00EA}"
+EndProject
+Global
+ GlobalSection(SolutionConfigurationPlatforms) = preSolution
+ Debug|Any CPU = Debug|Any CPU
+ Release|Any CPU = Release|Any CPU
+ EndGlobalSection
+ GlobalSection(ProjectConfigurationPlatforms) = postSolution
+ {DD219D97-6BF6-4EE5-9E35-13185F4FA261}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {DD219D97-6BF6-4EE5-9E35-13185F4FA261}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {DD219D97-6BF6-4EE5-9E35-13185F4FA261}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {DD219D97-6BF6-4EE5-9E35-13185F4FA261}.Release|Any CPU.Build.0 = Release|Any CPU
+ {6705EB4C-3DF6-4385-AEAB-3EE2FE074E05}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {6705EB4C-3DF6-4385-AEAB-3EE2FE074E05}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {6705EB4C-3DF6-4385-AEAB-3EE2FE074E05}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {6705EB4C-3DF6-4385-AEAB-3EE2FE074E05}.Release|Any CPU.Build.0 = Release|Any CPU
+ {89C4A3D5-7272-4EC7-AC91-89ED8C1D00EA}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {89C4A3D5-7272-4EC7-AC91-89ED8C1D00EA}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {89C4A3D5-7272-4EC7-AC91-89ED8C1D00EA}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {89C4A3D5-7272-4EC7-AC91-89ED8C1D00EA}.Release|Any CPU.Build.0 = Release|Any CPU
+ EndGlobalSection
+ GlobalSection(SolutionProperties) = preSolution
+ HideSolutionNode = FALSE
+ EndGlobalSection
+ GlobalSection(NestedProjects) = preSolution
+ {DD219D97-6BF6-4EE5-9E35-13185F4FA261} = {23BF447F-B872-4D4F-8F28-D443072AE93E}
+ {6705EB4C-3DF6-4385-AEAB-3EE2FE074E05} = {2F55FE43-6EA6-4A7E-B70B-2E8267478F29}
+ {89C4A3D5-7272-4EC7-AC91-89ED8C1D00EA} = {2F55FE43-6EA6-4A7E-B70B-2E8267478F29}
+ EndGlobalSection
+ GlobalSection(ExtensibilityGlobals) = postSolution
+ SolutionGuid = {47DBF882-4F3A-4C9F-95CC-1286279CC35B}
+ EndGlobalSection
+EndGlobal
diff --git a/10. NinjaApi - NinjaRepository/README.md b/10. NinjaApi - NinjaRepository/README.md
new file mode 100644
index 0000000..0976423
--- /dev/null
+++ b/10. NinjaApi - NinjaRepository/README.md
@@ -0,0 +1,4 @@
+# Dependency Injection
+This project contains the code sample of the
+[Design Patterns: Asp.Net Core Web API, services, and repositories | Part 10: the NinjaRepository and ForEvolve.Azure](http://www.forevolve.com/en/articles/2017/09/14/design-patterns-web-api-service-and-repository-part-10/)
+article.
\ No newline at end of file
diff --git a/10. NinjaApi - NinjaRepository/src/ForEvolve.Blog.Samples.NinjaApi/Controllers/ClansController.cs b/10. NinjaApi - NinjaRepository/src/ForEvolve.Blog.Samples.NinjaApi/Controllers/ClansController.cs
new file mode 100644
index 0000000..4e3d49d
--- /dev/null
+++ b/10. NinjaApi - NinjaRepository/src/ForEvolve.Blog.Samples.NinjaApi/Controllers/ClansController.cs
@@ -0,0 +1,29 @@
+using ForEvolve.Blog.Samples.NinjaApi.Models;
+using ForEvolve.Blog.Samples.NinjaApi.Services;
+using Microsoft.AspNetCore.Mvc;
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Threading.Tasks;
+
+namespace ForEvolve.Blog.Samples.NinjaApi.Controllers
+{
+ [Route("v1/[controller]")]
+ public class ClansController : Controller
+ {
+ private readonly IClanService _clanService;
+
+ public ClansController(IClanService clanService)
+ {
+ _clanService = clanService ?? throw new ArgumentNullException(nameof(clanService));
+ }
+
+ [HttpGet]
+ [ProducesResponseType(typeof(IEnumerable), 200)]
+ public async Task ReadAllAsync()
+ {
+ var allClans = await _clanService.ReadAllAsync();
+ return Ok(allClans);
+ }
+ }
+}
diff --git a/10. NinjaApi - NinjaRepository/src/ForEvolve.Blog.Samples.NinjaApi/Controllers/NinjaController.cs b/10. NinjaApi - NinjaRepository/src/ForEvolve.Blog.Samples.NinjaApi/Controllers/NinjaController.cs
new file mode 100644
index 0000000..7f6d7eb
--- /dev/null
+++ b/10. NinjaApi - NinjaRepository/src/ForEvolve.Blog.Samples.NinjaApi/Controllers/NinjaController.cs
@@ -0,0 +1,117 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Threading.Tasks;
+using Microsoft.AspNetCore.Mvc;
+using ForEvolve.Blog.Samples.NinjaApi.Models;
+using Microsoft.AspNetCore.Http;
+using ForEvolve.Blog.Samples.NinjaApi.Services;
+
+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), StatusCodes.Status200OK)]
+ public async Task ReadAllAsync()
+ {
+ var allNinja = await _ninjaService.ReadAllAsync();
+ return Ok(allNinja);
+ }
+
+ [HttpGet("{clan}")]
+ [ProducesResponseType(typeof(IEnumerable), StatusCodes.Status200OK)]
+ [ProducesResponseType(StatusCodes.Status404NotFound)]
+ public async Task ReadAllInClanAsync(string clan)
+ {
+ try
+ {
+ var clanNinja = await _ninjaService.ReadAllInClanAsync(clan);
+ return Ok(clanNinja);
+ }
+ catch (ClanNotFoundException)
+ {
+ return NotFound();
+ }
+ }
+
+ [HttpGet("{clan}/{key}")]
+ [ProducesResponseType(typeof(Ninja), StatusCodes.Status200OK)]
+ [ProducesResponseType(StatusCodes.Status404NotFound)]
+ public async Task ReadOneAsync(string clan, string key)
+ {
+ try
+ {
+ var ninja = await _ninjaService.ReadOneAsync(clan, key);
+ return Ok(ninja);
+ }
+ catch (NinjaNotFoundException)
+ {
+ return NotFound();
+ }
+ }
+
+ [HttpPost]
+ [ProducesResponseType(typeof(Ninja), StatusCodes.Status201Created)]
+ [ProducesResponseType(StatusCodes.Status400BadRequest)]
+ public async Task 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
+ );
+ }
+
+ [HttpPut]
+ [ProducesResponseType(typeof(Ninja), StatusCodes.Status200OK)]
+ [ProducesResponseType(StatusCodes.Status400BadRequest)]
+ [ProducesResponseType(StatusCodes.Status404NotFound)]
+ public async Task UpdateAsync([FromBody]Ninja ninja)
+ {
+ if (!ModelState.IsValid)
+ {
+ return BadRequest(ModelState);
+ }
+
+ try
+ {
+ var updatedNinja = await _ninjaService.UpdateAsync(ninja);
+ return Ok(updatedNinja);
+ }
+ catch (NinjaNotFoundException)
+ {
+ return NotFound();
+ }
+ }
+
+ [HttpDelete("{clan}/{key}")]
+ [ProducesResponseType(typeof(Ninja), StatusCodes.Status200OK)]
+ [ProducesResponseType(StatusCodes.Status404NotFound)]
+ public async Task DeleteAsync(string clan, string key)
+ {
+ try
+ {
+ var deletedNinja = await _ninjaService.DeleteAsync(clan, key);
+ return Ok(deletedNinja);
+ }
+ catch (NinjaNotFoundException)
+ {
+ return NotFound();
+ }
+ }
+ }
+}
diff --git a/10. NinjaApi - NinjaRepository/src/ForEvolve.Blog.Samples.NinjaApi/Exceptions/ClanNotFoundException.cs b/10. NinjaApi - NinjaRepository/src/ForEvolve.Blog.Samples.NinjaApi/Exceptions/ClanNotFoundException.cs
new file mode 100644
index 0000000..7d55c95
--- /dev/null
+++ b/10. NinjaApi - NinjaRepository/src/ForEvolve.Blog.Samples.NinjaApi/Exceptions/ClanNotFoundException.cs
@@ -0,0 +1,17 @@
+using ForEvolve.Blog.Samples.NinjaApi.Models;
+
+namespace ForEvolve.Blog.Samples.NinjaApi
+{
+ public class ClanNotFoundException : NinjaApiException
+ {
+ public ClanNotFoundException(Clan clan)
+ : this(clan.Name)
+ {
+ }
+
+ public ClanNotFoundException(string clanName)
+ : base($"Clan {clanName} was not found.")
+ {
+ }
+ }
+}
diff --git a/10. NinjaApi - NinjaRepository/src/ForEvolve.Blog.Samples.NinjaApi/Exceptions/NinjaApiException.cs b/10. NinjaApi - NinjaRepository/src/ForEvolve.Blog.Samples.NinjaApi/Exceptions/NinjaApiException.cs
new file mode 100644
index 0000000..ff81d64
--- /dev/null
+++ b/10. NinjaApi - NinjaRepository/src/ForEvolve.Blog.Samples.NinjaApi/Exceptions/NinjaApiException.cs
@@ -0,0 +1,27 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Runtime.Serialization;
+using System.Threading.Tasks;
+
+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)
+ {
+ }
+ }
+}
diff --git a/10. NinjaApi - NinjaRepository/src/ForEvolve.Blog.Samples.NinjaApi/Exceptions/NinjaNotFoundException.cs b/10. NinjaApi - NinjaRepository/src/ForEvolve.Blog.Samples.NinjaApi/Exceptions/NinjaNotFoundException.cs
new file mode 100644
index 0000000..54c1d8d
--- /dev/null
+++ b/10. NinjaApi - NinjaRepository/src/ForEvolve.Blog.Samples.NinjaApi/Exceptions/NinjaNotFoundException.cs
@@ -0,0 +1,17 @@
+using ForEvolve.Blog.Samples.NinjaApi.Models;
+
+namespace ForEvolve.Blog.Samples.NinjaApi
+{
+ 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.")
+ {
+ }
+ }
+}
diff --git a/10. NinjaApi - NinjaRepository/src/ForEvolve.Blog.Samples.NinjaApi/ForEvolve.Blog.Samples.NinjaApi.csproj b/10. NinjaApi - NinjaRepository/src/ForEvolve.Blog.Samples.NinjaApi/ForEvolve.Blog.Samples.NinjaApi.csproj
new file mode 100644
index 0000000..2272ce1
--- /dev/null
+++ b/10. NinjaApi - NinjaRepository/src/ForEvolve.Blog.Samples.NinjaApi/ForEvolve.Blog.Samples.NinjaApi.csproj
@@ -0,0 +1,18 @@
+
+
+
+ netcoreapp2.0
+ aspnet-ForEvolve.Blog.Samples.NinjaApi-F62B525A-ACF4-4C7C-BF23-1EB0F434DDE5
+ Full
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/10. NinjaApi - NinjaRepository/src/ForEvolve.Blog.Samples.NinjaApi/Mappers/EnumerableMapper.cs b/10. NinjaApi - NinjaRepository/src/ForEvolve.Blog.Samples.NinjaApi/Mappers/EnumerableMapper.cs
new file mode 100644
index 0000000..be141da
--- /dev/null
+++ b/10. NinjaApi - NinjaRepository/src/ForEvolve.Blog.Samples.NinjaApi/Mappers/EnumerableMapper.cs
@@ -0,0 +1,30 @@
+using ForEvolve.Blog.Samples.NinjaApi.Mappers;
+using System;
+using System.Collections.Generic;
+using System.Linq;
+
+namespace ForEvolve.Blog.Samples.NinjaApi.Mappers
+{
+ public class EnumerableMapper : IMapper, IEnumerable>
+ {
+ private readonly IMapper _singleMapper;
+
+ public EnumerableMapper(IMapper singleMapper)
+ {
+ _singleMapper = singleMapper ?? throw new ArgumentNullException(nameof(singleMapper));
+ }
+
+ public IEnumerable Map(IEnumerable source)
+ {
+ var count = source.Count();
+ var destination = new TDestination[count];
+ for (int i = 0; i < count; i++)
+ {
+ var currentSource = source.ElementAt(i);
+ var currentDestination = _singleMapper.Map(currentSource);
+ destination[i] = currentDestination;
+ }
+ return destination;
+ }
+ }
+}
diff --git a/10. NinjaApi - NinjaRepository/src/ForEvolve.Blog.Samples.NinjaApi/Mappers/IMapper.cs b/10. NinjaApi - NinjaRepository/src/ForEvolve.Blog.Samples.NinjaApi/Mappers/IMapper.cs
new file mode 100644
index 0000000..2628ecb
--- /dev/null
+++ b/10. NinjaApi - NinjaRepository/src/ForEvolve.Blog.Samples.NinjaApi/Mappers/IMapper.cs
@@ -0,0 +1,7 @@
+namespace ForEvolve.Blog.Samples.NinjaApi.Mappers
+{
+ public interface IMapper
+ {
+ TDestination Map(TSource entity);
+ }
+}
diff --git a/10. NinjaApi - NinjaRepository/src/ForEvolve.Blog.Samples.NinjaApi/Mappers/NinjaEntityToNinjaMapper.cs b/10. NinjaApi - NinjaRepository/src/ForEvolve.Blog.Samples.NinjaApi/Mappers/NinjaEntityToNinjaMapper.cs
new file mode 100644
index 0000000..745a6d9
--- /dev/null
+++ b/10. NinjaApi - NinjaRepository/src/ForEvolve.Blog.Samples.NinjaApi/Mappers/NinjaEntityToNinjaMapper.cs
@@ -0,0 +1,19 @@
+using ForEvolve.Blog.Samples.NinjaApi.Models;
+
+namespace ForEvolve.Blog.Samples.NinjaApi.Mappers
+{
+ public class NinjaEntityToNinjaMapper : IMapper
+ {
+ public Ninja Map(NinjaEntity entity)
+ {
+ var ninja = new Ninja
+ {
+ Key = entity.RowKey,
+ Clan = new Clan { Name = entity.PartitionKey },
+ Level = entity.Level,
+ Name = entity.Name
+ };
+ return ninja;
+ }
+ }
+}
diff --git a/10. NinjaApi - NinjaRepository/src/ForEvolve.Blog.Samples.NinjaApi/Mappers/NinjaToNinjaEntityMapper.cs b/10. NinjaApi - NinjaRepository/src/ForEvolve.Blog.Samples.NinjaApi/Mappers/NinjaToNinjaEntityMapper.cs
new file mode 100644
index 0000000..a4546f4
--- /dev/null
+++ b/10. NinjaApi - NinjaRepository/src/ForEvolve.Blog.Samples.NinjaApi/Mappers/NinjaToNinjaEntityMapper.cs
@@ -0,0 +1,19 @@
+using ForEvolve.Blog.Samples.NinjaApi.Models;
+
+namespace ForEvolve.Blog.Samples.NinjaApi.Mappers
+{
+ public class NinjaToNinjaEntityMapper : IMapper
+ {
+ public NinjaEntity Map(Ninja ninja)
+ {
+ var entity = new NinjaEntity
+ {
+ PartitionKey = ninja.Clan.Name,
+ RowKey = ninja.Key,
+ Name = ninja.Name,
+ Level = ninja.Level
+ };
+ return entity;
+ }
+ }
+}
diff --git a/10. NinjaApi - NinjaRepository/src/ForEvolve.Blog.Samples.NinjaApi/Models/Clan.cs b/10. NinjaApi - NinjaRepository/src/ForEvolve.Blog.Samples.NinjaApi/Models/Clan.cs
new file mode 100644
index 0000000..7b7c615
--- /dev/null
+++ b/10. NinjaApi - NinjaRepository/src/ForEvolve.Blog.Samples.NinjaApi/Models/Clan.cs
@@ -0,0 +1,12 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Threading.Tasks;
+
+namespace ForEvolve.Blog.Samples.NinjaApi.Models
+{
+ public class Clan
+ {
+ public string Name { get; set; }
+ }
+}
diff --git a/10. NinjaApi - NinjaRepository/src/ForEvolve.Blog.Samples.NinjaApi/Models/Ninja.cs b/10. NinjaApi - NinjaRepository/src/ForEvolve.Blog.Samples.NinjaApi/Models/Ninja.cs
new file mode 100644
index 0000000..110e285
--- /dev/null
+++ b/10. NinjaApi - NinjaRepository/src/ForEvolve.Blog.Samples.NinjaApi/Models/Ninja.cs
@@ -0,0 +1,15 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Threading.Tasks;
+
+namespace ForEvolve.Blog.Samples.NinjaApi.Models
+{
+ public class Ninja
+ {
+ public string Key { get; set; }
+ public Clan Clan { get; set; }
+ public string Name { get; set; }
+ public int Level { get; set; }
+ }
+}
diff --git a/10. NinjaApi - NinjaRepository/src/ForEvolve.Blog.Samples.NinjaApi/Models/NinjaEntity.cs b/10. NinjaApi - NinjaRepository/src/ForEvolve.Blog.Samples.NinjaApi/Models/NinjaEntity.cs
new file mode 100644
index 0000000..12906d9
--- /dev/null
+++ b/10. NinjaApi - NinjaRepository/src/ForEvolve.Blog.Samples.NinjaApi/Models/NinjaEntity.cs
@@ -0,0 +1,14 @@
+using Microsoft.WindowsAzure.Storage.Table;
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Threading.Tasks;
+
+namespace ForEvolve.Blog.Samples.NinjaApi.Models
+{
+ public class NinjaEntity : TableEntity
+ {
+ public string Name { get; set; }
+ public int Level { get; set; }
+ }
+}
diff --git a/10. NinjaApi - NinjaRepository/src/ForEvolve.Blog.Samples.NinjaApi/Program.cs b/10. NinjaApi - NinjaRepository/src/ForEvolve.Blog.Samples.NinjaApi/Program.cs
new file mode 100644
index 0000000..88c9273
--- /dev/null
+++ b/10. NinjaApi - NinjaRepository/src/ForEvolve.Blog.Samples.NinjaApi/Program.cs
@@ -0,0 +1,25 @@
+using System;
+using System.Collections.Generic;
+using System.IO;
+using System.Linq;
+using System.Threading.Tasks;
+using Microsoft.AspNetCore;
+using Microsoft.AspNetCore.Hosting;
+using Microsoft.Extensions.Configuration;
+using Microsoft.Extensions.Logging;
+
+namespace ForEvolve.Blog.Samples.NinjaApi
+{
+ public class Program
+ {
+ public static void Main(string[] args)
+ {
+ BuildWebHost(args).Run();
+ }
+
+ public static IWebHost BuildWebHost(string[] args) =>
+ WebHost.CreateDefaultBuilder(args)
+ .UseStartup()
+ .Build();
+ }
+}
diff --git a/10. NinjaApi - NinjaRepository/src/ForEvolve.Blog.Samples.NinjaApi/Properties/launchSettings.json b/10. NinjaApi - NinjaRepository/src/ForEvolve.Blog.Samples.NinjaApi/Properties/launchSettings.json
new file mode 100644
index 0000000..5285189
--- /dev/null
+++ b/10. NinjaApi - NinjaRepository/src/ForEvolve.Blog.Samples.NinjaApi/Properties/launchSettings.json
@@ -0,0 +1,29 @@
+{
+ "iisSettings": {
+ "windowsAuthentication": false,
+ "anonymousAuthentication": true,
+ "iisExpress": {
+ "applicationUrl": "http://localhost:2439/",
+ "sslPort": 0
+ }
+ },
+ "profiles": {
+ "IIS Express": {
+ "commandName": "IISExpress",
+ "launchBrowser": true,
+ "launchUrl": "v1/ninja",
+ "environmentVariables": {
+ "ASPNETCORE_ENVIRONMENT": "Development"
+ }
+ },
+ "ForEvolve.Blog.Samples.NinjaApi": {
+ "commandName": "Project",
+ "launchBrowser": true,
+ "launchUrl": "api/values",
+ "environmentVariables": {
+ "ASPNETCORE_ENVIRONMENT": "Development"
+ },
+ "applicationUrl": "http://localhost:2440/"
+ }
+ }
+}
\ No newline at end of file
diff --git a/10. NinjaApi - NinjaRepository/src/ForEvolve.Blog.Samples.NinjaApi/Repositories/ClanRepository.cs b/10. NinjaApi - NinjaRepository/src/ForEvolve.Blog.Samples.NinjaApi/Repositories/ClanRepository.cs
new file mode 100644
index 0000000..c3f5176
--- /dev/null
+++ b/10. NinjaApi - NinjaRepository/src/ForEvolve.Blog.Samples.NinjaApi/Repositories/ClanRepository.cs
@@ -0,0 +1,45 @@
+using ForEvolve.Blog.Samples.NinjaApi.Models;
+using System;
+using System.Linq;
+using System.Collections.Generic;
+using System.Threading.Tasks;
+
+namespace ForEvolve.Blog.Samples.NinjaApi.Repositories
+{
+ public class ClanRepository : IClanRepository
+ {
+ private readonly List _clans;
+
+ public ClanRepository(IEnumerable clans)
+ {
+ if (clans == null) { throw new ArgumentNullException(nameof(clans)); }
+ _clans = new List(clans);
+ }
+
+ public Task> ReadAllAsync()
+ {
+ return Task.FromResult(_clans.AsEnumerable());
+ }
+
+ public Task ReadOneAsync(string clanName)
+ {
+ var clan = _clans.FirstOrDefault(c => c.Name == clanName);
+ return Task.FromResult(clan);
+ }
+
+ public Task CreateAsync(Clan clan)
+ {
+ throw new NotSupportedException();
+ }
+
+ public Task UpdateAsync(Clan clan)
+ {
+ throw new NotSupportedException();
+ }
+
+ public Task DeleteAsync(string clanName)
+ {
+ throw new NotSupportedException();
+ }
+ }
+}
diff --git a/10. NinjaApi - NinjaRepository/src/ForEvolve.Blog.Samples.NinjaApi/Repositories/IClanRepository.cs b/10. NinjaApi - NinjaRepository/src/ForEvolve.Blog.Samples.NinjaApi/Repositories/IClanRepository.cs
new file mode 100644
index 0000000..e2e41ed
--- /dev/null
+++ b/10. NinjaApi - NinjaRepository/src/ForEvolve.Blog.Samples.NinjaApi/Repositories/IClanRepository.cs
@@ -0,0 +1,16 @@
+using ForEvolve.Blog.Samples.NinjaApi.Models;
+using System.Collections.Generic;
+using System.Linq;
+using System.Threading.Tasks;
+
+namespace ForEvolve.Blog.Samples.NinjaApi.Repositories
+{
+ public interface IClanRepository
+ {
+ Task> ReadAllAsync();
+ Task ReadOneAsync(string clanName);
+ Task CreateAsync(Clan clan);
+ Task UpdateAsync(Clan clan);
+ Task DeleteAsync(string clanName);
+ }
+}
diff --git a/10. NinjaApi - NinjaRepository/src/ForEvolve.Blog.Samples.NinjaApi/Repositories/INinjaRepository.cs b/10. NinjaApi - NinjaRepository/src/ForEvolve.Blog.Samples.NinjaApi/Repositories/INinjaRepository.cs
new file mode 100644
index 0000000..0075b92
--- /dev/null
+++ b/10. NinjaApi - NinjaRepository/src/ForEvolve.Blog.Samples.NinjaApi/Repositories/INinjaRepository.cs
@@ -0,0 +1,18 @@
+using ForEvolve.Blog.Samples.NinjaApi.Models;
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Threading.Tasks;
+
+namespace ForEvolve.Blog.Samples.NinjaApi.Repositories
+{
+ public interface INinjaRepository
+ {
+ Task> ReadAllAsync();
+ Task> ReadAllInClanAsync(string clanName);
+ Task ReadOneAsync(string clanName, string ninjaKey);
+ Task CreateAsync(Ninja ninja);
+ Task UpdateAsync(Ninja ninja);
+ Task DeleteAsync(string clanName, string ninjaKey);
+ }
+}
diff --git a/10. NinjaApi - NinjaRepository/src/ForEvolve.Blog.Samples.NinjaApi/Repositories/NinjaRepository.cs b/10. NinjaApi - NinjaRepository/src/ForEvolve.Blog.Samples.NinjaApi/Repositories/NinjaRepository.cs
new file mode 100644
index 0000000..747e861
--- /dev/null
+++ b/10. NinjaApi - NinjaRepository/src/ForEvolve.Blog.Samples.NinjaApi/Repositories/NinjaRepository.cs
@@ -0,0 +1,66 @@
+using ForEvolve.Azure.Storage.Table;
+using ForEvolve.Blog.Samples.NinjaApi.Models;
+using ForEvolve.Blog.Samples.NinjaApi.Services;
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Threading.Tasks;
+
+namespace ForEvolve.Blog.Samples.NinjaApi.Repositories
+{
+ public class NinjaRepository : INinjaRepository
+ {
+ private readonly INinjaMappingService _ninjaMappingService;
+ private readonly ITableStorageRepository _ninjaEntityTableStorageRepository;
+
+ public NinjaRepository(INinjaMappingService ninjaMappingService, ITableStorageRepository ninjaEntityTableStorageRepository)
+ {
+ _ninjaMappingService = ninjaMappingService ?? throw new ArgumentNullException(nameof(ninjaMappingService));
+ _ninjaEntityTableStorageRepository = ninjaEntityTableStorageRepository ?? throw new ArgumentNullException(nameof(ninjaEntityTableStorageRepository));
+ }
+
+ public async Task CreateAsync(Ninja ninja)
+ {
+ var entityToCreate = _ninjaMappingService.Map(ninja);
+ var createdEntity = await _ninjaEntityTableStorageRepository.InsertOrReplaceAsync(entityToCreate);
+ var createNinja = _ninjaMappingService.Map(createdEntity);
+ return createNinja;
+ }
+
+ public async Task DeleteAsync(string clanName, string ninjaKey)
+ {
+ var deletedEntity = await _ninjaEntityTableStorageRepository.DeleteOneAsync(clanName, ninjaKey);
+ var deletedNinja = _ninjaMappingService.Map(deletedEntity);
+ return deletedNinja;
+ }
+
+ public async Task> ReadAllAsync()
+ {
+ var entities = await _ninjaEntityTableStorageRepository.ReadAllAsync();
+ var ninja = _ninjaMappingService.Map(entities);
+ return ninja;
+ }
+
+ public async Task> ReadAllInClanAsync(string clanName)
+ {
+ var entities = await _ninjaEntityTableStorageRepository.ReadPartitionAsync(clanName);
+ var ninja = _ninjaMappingService.Map(entities);
+ return ninja;
+ }
+
+ public async Task ReadOneAsync(string clanName, string ninjaKey)
+ {
+ var entity = await _ninjaEntityTableStorageRepository.ReadOneAsync(clanName, ninjaKey);
+ var ninja = _ninjaMappingService.Map(entity);
+ return ninja;
+ }
+
+ public async Task UpdateAsync(Ninja ninja)
+ {
+ var entityToUpdate = _ninjaMappingService.Map(ninja);
+ var updatedEntity = await _ninjaEntityTableStorageRepository.InsertOrMergeAsync(entityToUpdate);
+ var updatedNinja = _ninjaMappingService.Map(updatedEntity);
+ return updatedNinja;
+ }
+ }
+}
diff --git a/10. NinjaApi - NinjaRepository/src/ForEvolve.Blog.Samples.NinjaApi/Services/ClanService.cs b/10. NinjaApi - NinjaRepository/src/ForEvolve.Blog.Samples.NinjaApi/Services/ClanService.cs
new file mode 100644
index 0000000..d6b6118
--- /dev/null
+++ b/10. NinjaApi - NinjaRepository/src/ForEvolve.Blog.Samples.NinjaApi/Services/ClanService.cs
@@ -0,0 +1,49 @@
+using ForEvolve.Blog.Samples.NinjaApi.Models;
+using ForEvolve.Blog.Samples.NinjaApi.Repositories;
+using System;
+using System.Collections.Generic;
+using System.Threading.Tasks;
+
+namespace ForEvolve.Blog.Samples.NinjaApi.Services
+{
+ public class ClanService : IClanService
+ {
+ private IClanRepository _clanRepository;
+
+ public ClanService(IClanRepository clanRepository)
+ {
+ _clanRepository = clanRepository ?? throw new ArgumentNullException(nameof(clanRepository));
+ }
+
+ public Task> ReadAllAsync()
+ {
+ return _clanRepository.ReadAllAsync();
+ }
+
+ public Task ReadOneAsync(string clanName)
+ {
+ return _clanRepository.ReadOneAsync(clanName);
+ }
+
+ public async Task IsClanExistsAsync(string clanName)
+ {
+ var clan = await _clanRepository.ReadOneAsync(clanName);
+ return clan != null;
+ }
+
+ public Task CreateAsync(Clan clan)
+ {
+ throw new NotSupportedException();
+ }
+
+ public Task UpdateAsync(Clan clan)
+ {
+ throw new NotSupportedException();
+ }
+
+ public Task DeleteAsync(string clanName)
+ {
+ throw new NotSupportedException();
+ }
+ }
+}
diff --git a/10. NinjaApi - NinjaRepository/src/ForEvolve.Blog.Samples.NinjaApi/Services/IClanService.cs b/10. NinjaApi - NinjaRepository/src/ForEvolve.Blog.Samples.NinjaApi/Services/IClanService.cs
new file mode 100644
index 0000000..54843b9
--- /dev/null
+++ b/10. NinjaApi - NinjaRepository/src/ForEvolve.Blog.Samples.NinjaApi/Services/IClanService.cs
@@ -0,0 +1,17 @@
+using ForEvolve.Blog.Samples.NinjaApi.Models;
+using System.Collections.Generic;
+using System.Linq;
+using System.Threading.Tasks;
+
+namespace ForEvolve.Blog.Samples.NinjaApi.Services
+{
+ public interface IClanService
+ {
+ Task> ReadAllAsync();
+ Task ReadOneAsync(string clanName);
+ Task IsClanExistsAsync(string clanName);
+ Task CreateAsync(Clan clan);
+ Task UpdateAsync(Clan clan);
+ Task DeleteAsync(string clanName);
+ }
+}
diff --git a/10. NinjaApi - NinjaRepository/src/ForEvolve.Blog.Samples.NinjaApi/Services/INinjaMappingService.cs b/10. NinjaApi - NinjaRepository/src/ForEvolve.Blog.Samples.NinjaApi/Services/INinjaMappingService.cs
new file mode 100644
index 0000000..5186991
--- /dev/null
+++ b/10. NinjaApi - NinjaRepository/src/ForEvolve.Blog.Samples.NinjaApi/Services/INinjaMappingService.cs
@@ -0,0 +1,13 @@
+using ForEvolve.Blog.Samples.NinjaApi.Mappers;
+using ForEvolve.Blog.Samples.NinjaApi.Models;
+using System.Collections.Generic;
+using System.Linq;
+using System.Threading.Tasks;
+
+namespace ForEvolve.Blog.Samples.NinjaApi.Services
+{
+ public interface INinjaMappingService : IMapper, IMapper, IMapper, IEnumerable>
+ {
+ }
+}
+
diff --git a/10. NinjaApi - NinjaRepository/src/ForEvolve.Blog.Samples.NinjaApi/Services/INinjaService.cs b/10. NinjaApi - NinjaRepository/src/ForEvolve.Blog.Samples.NinjaApi/Services/INinjaService.cs
new file mode 100644
index 0000000..841069d
--- /dev/null
+++ b/10. NinjaApi - NinjaRepository/src/ForEvolve.Blog.Samples.NinjaApi/Services/INinjaService.cs
@@ -0,0 +1,17 @@
+using ForEvolve.Blog.Samples.NinjaApi.Models;
+using System.Collections.Generic;
+using System.Linq;
+using System.Threading.Tasks;
+
+namespace ForEvolve.Blog.Samples.NinjaApi.Services
+{
+ public interface INinjaService
+ {
+ Task> ReadAllAsync();
+ Task> ReadAllInClanAsync(string clanName);
+ Task ReadOneAsync(string clanName, string ninjaKey);
+ Task CreateAsync(Ninja ninja);
+ Task UpdateAsync(Ninja ninja);
+ Task DeleteAsync(string clanName, string ninjaKey);
+ }
+}
diff --git a/10. NinjaApi - NinjaRepository/src/ForEvolve.Blog.Samples.NinjaApi/Services/NinjaMappingService.cs b/10. NinjaApi - NinjaRepository/src/ForEvolve.Blog.Samples.NinjaApi/Services/NinjaMappingService.cs
new file mode 100644
index 0000000..25aefbf
--- /dev/null
+++ b/10. NinjaApi - NinjaRepository/src/ForEvolve.Blog.Samples.NinjaApi/Services/NinjaMappingService.cs
@@ -0,0 +1,40 @@
+using ForEvolve.Blog.Samples.NinjaApi.Mappers;
+using ForEvolve.Blog.Samples.NinjaApi.Models;
+using System;
+using System.Collections.Generic;
+
+namespace ForEvolve.Blog.Samples.NinjaApi.Services
+{
+ public class NinjaMappingService : INinjaMappingService
+ {
+ private readonly IMapper _ninjaToNinjaEntityMapper;
+ private readonly IMapper _ninjaEntityToNinjaMapper;
+ private readonly IMapper, IEnumerable> _ninjaEntityEnumerableToNinjaMapper;
+
+ public NinjaMappingService(
+ IMapper ninjaToNinjaEntityMapper,
+ IMapper ninjaEntityToNinjaMapper,
+ IMapper, IEnumerable> ninjaEntityEnumerableToNinjaMapper
+ )
+ {
+ _ninjaToNinjaEntityMapper = ninjaToNinjaEntityMapper ?? throw new ArgumentNullException(nameof(ninjaToNinjaEntityMapper));
+ _ninjaEntityToNinjaMapper = ninjaEntityToNinjaMapper ?? throw new ArgumentNullException(nameof(ninjaEntityToNinjaMapper));
+ _ninjaEntityEnumerableToNinjaMapper = ninjaEntityEnumerableToNinjaMapper ?? throw new ArgumentNullException(nameof(ninjaEntityEnumerableToNinjaMapper));
+ }
+
+ public NinjaEntity Map(Ninja entity)
+ {
+ return _ninjaToNinjaEntityMapper.Map(entity);
+ }
+
+ public Ninja Map(NinjaEntity entity)
+ {
+ return _ninjaEntityToNinjaMapper.Map(entity);
+ }
+
+ public IEnumerable Map(IEnumerable entities)
+ {
+ return _ninjaEntityEnumerableToNinjaMapper.Map(entities);
+ }
+ }
+}
diff --git a/10. NinjaApi - NinjaRepository/src/ForEvolve.Blog.Samples.NinjaApi/Services/NinjaService.cs b/10. NinjaApi - NinjaRepository/src/ForEvolve.Blog.Samples.NinjaApi/Services/NinjaService.cs
new file mode 100644
index 0000000..2e96b1f
--- /dev/null
+++ b/10. NinjaApi - NinjaRepository/src/ForEvolve.Blog.Samples.NinjaApi/Services/NinjaService.cs
@@ -0,0 +1,78 @@
+using ForEvolve.Blog.Samples.NinjaApi.Models;
+using ForEvolve.Blog.Samples.NinjaApi.Repositories;
+using System;
+using System.Collections.Generic;
+using System.Threading.Tasks;
+
+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 async Task CreateAsync(Ninja ninja)
+ {
+ await EnforceClanExistenceAsync(ninja.Clan.Name);
+ var createdNinja = await _ninjaRepository.CreateAsync(ninja);
+ return createdNinja;
+ }
+
+ public async Task DeleteAsync(string clanName, string ninjaKey)
+ {
+ await EnforceNinjaExistenceAsync(clanName, ninjaKey);
+ var deletedNinja = await _ninjaRepository.DeleteAsync(clanName, ninjaKey);
+ return deletedNinja;
+ }
+
+ public Task> ReadAllAsync()
+ {
+ return _ninjaRepository.ReadAllAsync();
+ }
+
+ public async Task> ReadAllInClanAsync(string clanName)
+ {
+ await EnforceClanExistenceAsync(clanName);
+ return await _ninjaRepository.ReadAllInClanAsync(clanName);
+ }
+
+ public async Task ReadOneAsync(string clanName, string ninjaKey)
+ {
+ var ninja = await EnforceNinjaExistenceAsync(clanName, ninjaKey);
+ return ninja;
+ }
+
+ public async Task UpdateAsync(Ninja ninja)
+ {
+ await EnforceClanExistenceAsync(ninja.Clan.Name);
+ await EnforceNinjaExistenceAsync(ninja.Clan.Name, ninja.Key);
+ var updatedNinja = await _ninjaRepository.UpdateAsync(ninja);
+ return updatedNinja;
+ }
+
+ private async Task EnforceClanExistenceAsync(string clanName)
+ {
+ var clanExist = await _clanService.IsClanExistsAsync(clanName);
+ if (!clanExist)
+ {
+ throw new ClanNotFoundException(clanName);
+ }
+ }
+
+ private async Task EnforceNinjaExistenceAsync(string clanName, string ninjaKey)
+ {
+ var remoteNinja = await _ninjaRepository.ReadOneAsync(clanName, ninjaKey);
+ if (remoteNinja == null)
+ {
+ throw new NinjaNotFoundException(clanName, ninjaKey);
+ }
+ return remoteNinja;
+ }
+ }
+}
diff --git a/10. NinjaApi - NinjaRepository/src/ForEvolve.Blog.Samples.NinjaApi/Startup.cs b/10. NinjaApi - NinjaRepository/src/ForEvolve.Blog.Samples.NinjaApi/Startup.cs
new file mode 100644
index 0000000..dcfa445
--- /dev/null
+++ b/10. NinjaApi - NinjaRepository/src/ForEvolve.Blog.Samples.NinjaApi/Startup.cs
@@ -0,0 +1,39 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Threading.Tasks;
+using Microsoft.AspNetCore.Builder;
+using Microsoft.AspNetCore.Hosting;
+using Microsoft.Extensions.Configuration;
+using Microsoft.Extensions.DependencyInjection;
+using Microsoft.Extensions.Logging;
+using Microsoft.Extensions.Options;
+using Microsoft.Extensions.DependencyInjection.Extensions;
+using ForEvolve.Blog.Samples.NinjaApi.Services;
+using ForEvolve.Blog.Samples.NinjaApi.Repositories;
+using ForEvolve.Blog.Samples.NinjaApi.Models;
+
+namespace ForEvolve.Blog.Samples.NinjaApi
+{
+ public class Startup
+ {
+ // This method gets called by the runtime. Use this method to add services to the container.
+ public void ConfigureServices(IServiceCollection services)
+ {
+ services.TryAddSingleton();
+ services.TryAddSingleton();
+ services.TryAddSingleton>(new Clan[]{
+ new Clan { Name = "Iga" },
+ new Clan { Name = "Kōga" },
+ });
+ services.AddMvc();
+ }
+
+ // This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
+ public void Configure(IApplicationBuilder app, IHostingEnvironment env)
+ {
+ app.ApplicationServices.GetService();
+ app.UseMvc();
+ }
+ }
+}
diff --git a/10. NinjaApi - NinjaRepository/src/ForEvolve.Blog.Samples.NinjaApi/appsettings.Development.json b/10. NinjaApi - NinjaRepository/src/ForEvolve.Blog.Samples.NinjaApi/appsettings.Development.json
new file mode 100644
index 0000000..fa8ce71
--- /dev/null
+++ b/10. NinjaApi - NinjaRepository/src/ForEvolve.Blog.Samples.NinjaApi/appsettings.Development.json
@@ -0,0 +1,10 @@
+{
+ "Logging": {
+ "IncludeScopes": false,
+ "LogLevel": {
+ "Default": "Debug",
+ "System": "Information",
+ "Microsoft": "Information"
+ }
+ }
+}
diff --git a/10. NinjaApi - NinjaRepository/src/ForEvolve.Blog.Samples.NinjaApi/appsettings.json b/10. NinjaApi - NinjaRepository/src/ForEvolve.Blog.Samples.NinjaApi/appsettings.json
new file mode 100644
index 0000000..26bb0ac
--- /dev/null
+++ b/10. NinjaApi - NinjaRepository/src/ForEvolve.Blog.Samples.NinjaApi/appsettings.json
@@ -0,0 +1,15 @@
+{
+ "Logging": {
+ "IncludeScopes": false,
+ "Debug": {
+ "LogLevel": {
+ "Default": "Warning"
+ }
+ },
+ "Console": {
+ "LogLevel": {
+ "Default": "Warning"
+ }
+ }
+ }
+}
diff --git a/10. NinjaApi - NinjaRepository/test/ForEvolve.Blog.Samples.NinjaApi.IntegrationTests/BaseHttpTest.cs b/10. NinjaApi - NinjaRepository/test/ForEvolve.Blog.Samples.NinjaApi.IntegrationTests/BaseHttpTest.cs
new file mode 100644
index 0000000..e314434
--- /dev/null
+++ b/10. NinjaApi - NinjaRepository/test/ForEvolve.Blog.Samples.NinjaApi.IntegrationTests/BaseHttpTest.cs
@@ -0,0 +1,59 @@
+using Microsoft.AspNetCore.Hosting;
+using Microsoft.AspNetCore.TestHost;
+using Microsoft.Extensions.DependencyInjection;
+using System;
+using System.Collections.Generic;
+using System.Net.Http;
+using System.Text;
+
+namespace ForEvolve.Blog.Samples.NinjaApi.IntegrationTests
+{
+ public abstract class BaseHttpTest : IDisposable
+ {
+ protected TestServer Server { get; }
+ protected HttpClient Client { get; }
+
+ protected virtual Uri BaseAddress => new Uri("http://localhost");
+ protected virtual string Environment => "Development";
+
+ public BaseHttpTest()
+ {
+ var builder = new WebHostBuilder()
+ .UseEnvironment(Environment)
+ .ConfigureServices(ConfigureServices)
+ .UseStartup();
+
+ Server = new TestServer(builder);
+ Client = Server.CreateClient();
+ Client.BaseAddress = BaseAddress;
+ }
+
+ protected virtual void ConfigureServices(IServiceCollection services)
+ {
+ }
+
+ #region IDisposable Support
+
+ private bool disposedValue = false;
+
+ protected virtual void Dispose(bool disposing)
+ {
+ if (!disposedValue)
+ {
+ if (disposing)
+ {
+ Client.Dispose();
+ Server.Dispose();
+ }
+ disposedValue = true;
+ }
+ }
+
+ public void Dispose()
+ {
+ Dispose(true);
+ }
+
+ #endregion
+ }
+}
diff --git a/10. NinjaApi - NinjaRepository/test/ForEvolve.Blog.Samples.NinjaApi.IntegrationTests/ClansControllerTest.cs b/10. NinjaApi - NinjaRepository/test/ForEvolve.Blog.Samples.NinjaApi.IntegrationTests/ClansControllerTest.cs
new file mode 100644
index 0000000..b1f2950
--- /dev/null
+++ b/10. NinjaApi - NinjaRepository/test/ForEvolve.Blog.Samples.NinjaApi.IntegrationTests/ClansControllerTest.cs
@@ -0,0 +1,51 @@
+using System;
+using Xunit;
+using Microsoft.AspNetCore.TestHost;
+using System.Net.Http;
+using Microsoft.AspNetCore.Hosting;
+using Microsoft.Extensions.DependencyInjection;
+using System.Collections.Generic;
+using ForEvolve.Blog.Samples.NinjaApi.Models;
+using System.Threading.Tasks;
+using System.Linq;
+
+namespace ForEvolve.Blog.Samples.NinjaApi.IntegrationTests
+{
+ public class ClansControllerTest : BaseHttpTest
+ {
+ public class ReadAllAsync : ClansControllerTest
+ {
+ private IEnumerable Clans => new Clan[] {
+ new Clan { Name = "My clan" },
+ new Clan { Name = "Your clan" },
+ new Clan { Name = "His clan" }
+ };
+
+ protected override void ConfigureServices(IServiceCollection services)
+ {
+ services.AddSingleton(Clans);
+ }
+
+ [Fact]
+ public async Task Should_return_the_default_clans()
+ {
+ // Arrange
+ var expectedNumberOfClans = Clans.Count();
+
+ // Act
+ var result = await Client.GetAsync("v1/clans");
+
+ // Assert
+ result.EnsureSuccessStatusCode();
+ var clans = await result.Content.ReadAsJsonObjectAsync();
+ Assert.NotNull(clans);
+ Assert.Equal(expectedNumberOfClans, clans.Length);
+ Assert.Collection(clans,
+ clan => Assert.Equal(Clans.ElementAt(0).Name, clans[0].Name),
+ clan => Assert.Equal(Clans.ElementAt(1).Name, clans[1].Name),
+ clan => Assert.Equal(Clans.ElementAt(2).Name, clans[2].Name)
+ );
+ }
+ }
+ }
+}
diff --git a/10. NinjaApi - NinjaRepository/test/ForEvolve.Blog.Samples.NinjaApi.IntegrationTests/ForEvolve.Blog.Samples.NinjaApi.IntegrationTests.csproj b/10. NinjaApi - NinjaRepository/test/ForEvolve.Blog.Samples.NinjaApi.IntegrationTests/ForEvolve.Blog.Samples.NinjaApi.IntegrationTests.csproj
new file mode 100644
index 0000000..8ad286b
--- /dev/null
+++ b/10. NinjaApi - NinjaRepository/test/ForEvolve.Blog.Samples.NinjaApi.IntegrationTests/ForEvolve.Blog.Samples.NinjaApi.IntegrationTests.csproj
@@ -0,0 +1,20 @@
+
+
+
+ netcoreapp2.0
+
+ false
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/10. NinjaApi - NinjaRepository/test/ForEvolve.Blog.Samples.NinjaApi.IntegrationTests/StartupTest.cs b/10. NinjaApi - NinjaRepository/test/ForEvolve.Blog.Samples.NinjaApi.IntegrationTests/StartupTest.cs
new file mode 100644
index 0000000..637653f
--- /dev/null
+++ b/10. NinjaApi - NinjaRepository/test/ForEvolve.Blog.Samples.NinjaApi.IntegrationTests/StartupTest.cs
@@ -0,0 +1,34 @@
+using ForEvolve.Blog.Samples.NinjaApi.Models;
+using System;
+using System.Collections.Generic;
+using System.Text;
+using Xunit;
+using Microsoft.Extensions.DependencyInjection;
+using System.Linq;
+
+namespace ForEvolve.Blog.Samples.NinjaApi.IntegrationTests
+{
+ public class StartupTest : BaseHttpTest
+ {
+ public class ServiceProvider : StartupTest
+ {
+ [Fact]
+ public void Should_return_both_Iga_and_Kōga_clans()
+ {
+ // Arrange
+ var serviceProvider = Server.Host.Services;
+
+ // Act
+ var clans = serviceProvider.GetService>();
+
+ // Assert
+ Assert.NotNull(clans);
+ Assert.Equal(2, clans.Count());
+ Assert.Collection(clans,
+ clan => Assert.Equal("Iga", clan.Name),
+ clan => Assert.Equal("Kōga", clan.Name)
+ );
+ }
+ }
+ }
+}
diff --git a/10. NinjaApi - NinjaRepository/test/ForEvolve.Blog.Samples.NinjaApi.Tests/Controllers/ClansControllerTest.cs b/10. NinjaApi - NinjaRepository/test/ForEvolve.Blog.Samples.NinjaApi.Tests/Controllers/ClansControllerTest.cs
new file mode 100644
index 0000000..bd6cec4
--- /dev/null
+++ b/10. NinjaApi - NinjaRepository/test/ForEvolve.Blog.Samples.NinjaApi.Tests/Controllers/ClansControllerTest.cs
@@ -0,0 +1,47 @@
+using ForEvolve.Blog.Samples.NinjaApi.Models;
+using Microsoft.AspNetCore.Mvc;
+using System;
+using Xunit;
+using Moq;
+using ForEvolve.Blog.Samples.NinjaApi.Services;
+
+namespace ForEvolve.Blog.Samples.NinjaApi.Controllers
+{
+ public class ClansControllerTest
+ {
+ protected ClansController ControllerUnderTest { get; }
+ protected Mock ClanServiceMock { get; }
+
+ public ClansControllerTest()
+ {
+ ClanServiceMock = new Mock();
+ ControllerUnderTest = new ClansController(ClanServiceMock.Object);
+ }
+
+
+ public class ReadAllAsync : ClansControllerTest
+ {
+ [Fact]
+ public async void Should_return_OkObjectResult_with_clans()
+ {
+ // Arrange
+ var expectedClans = new Clan[]
+ {
+ new Clan { Name = "Test clan 1" },
+ new Clan { Name = "Test clan 2" },
+ new Clan { Name = "Test clan 3" }
+ };
+ ClanServiceMock
+ .Setup(x => x.ReadAllAsync())
+ .ReturnsAsync(expectedClans);
+
+ // Act
+ var result = await ControllerUnderTest.ReadAllAsync();
+
+ // Assert
+ var okResult = Assert.IsType(result);
+ Assert.Same(expectedClans, okResult.Value);
+ }
+ }
+ }
+}
diff --git a/10. NinjaApi - NinjaRepository/test/ForEvolve.Blog.Samples.NinjaApi.Tests/Controllers/NinjaControllerTest.cs b/10. NinjaApi - NinjaRepository/test/ForEvolve.Blog.Samples.NinjaApi.Tests/Controllers/NinjaControllerTest.cs
new file mode 100644
index 0000000..3fec6d2
--- /dev/null
+++ b/10. NinjaApi - NinjaRepository/test/ForEvolve.Blog.Samples.NinjaApi.Tests/Controllers/NinjaControllerTest.cs
@@ -0,0 +1,270 @@
+using ForEvolve.Blog.Samples.NinjaApi.Models;
+using ForEvolve.Blog.Samples.NinjaApi.Services;
+using Microsoft.AspNetCore.Mvc;
+using Microsoft.AspNetCore.Mvc.ModelBinding;
+using Moq;
+using System;
+using System.Collections.Generic;
+using Xunit;
+
+namespace ForEvolve.Blog.Samples.NinjaApi.Controllers
+{
+ public class NinjaControllerTest
+ {
+ protected NinjaController ControllerUnderTest { get; }
+ protected Mock NinjaServiceMock { get; }
+
+ public NinjaControllerTest()
+ {
+ NinjaServiceMock = new Mock();
+ 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(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(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(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(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(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(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(result);
+ Assert.IsType(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(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(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(result);
+ Assert.IsType(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(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(result);
+ }
+ }
+ }
+}
diff --git a/10. NinjaApi - NinjaRepository/test/ForEvolve.Blog.Samples.NinjaApi.Tests/ForEvolve.Blog.Samples.NinjaApi.Tests.csproj b/10. NinjaApi - NinjaRepository/test/ForEvolve.Blog.Samples.NinjaApi.Tests/ForEvolve.Blog.Samples.NinjaApi.Tests.csproj
new file mode 100644
index 0000000..cf0b996
--- /dev/null
+++ b/10. NinjaApi - NinjaRepository/test/ForEvolve.Blog.Samples.NinjaApi.Tests/ForEvolve.Blog.Samples.NinjaApi.Tests.csproj
@@ -0,0 +1,22 @@
+
+
+
+ netcoreapp2.0
+
+ false
+
+ ForEvolve.Blog.Samples.NinjaApi
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/10. NinjaApi - NinjaRepository/test/ForEvolve.Blog.Samples.NinjaApi.Tests/Mappers/EnumerableMapperTest.cs b/10. NinjaApi - NinjaRepository/test/ForEvolve.Blog.Samples.NinjaApi.Tests/Mappers/EnumerableMapperTest.cs
new file mode 100644
index 0000000..ec08439
--- /dev/null
+++ b/10. NinjaApi - NinjaRepository/test/ForEvolve.Blog.Samples.NinjaApi.Tests/Mappers/EnumerableMapperTest.cs
@@ -0,0 +1,48 @@
+using ForEvolve.Blog.Samples.NinjaApi.Models;
+using Moq;
+using System;
+using System.Collections.Generic;
+using System.Text;
+using Xunit;
+
+namespace ForEvolve.Blog.Samples.NinjaApi.Mappers
+{
+ public class EnumerableMapperTest
+ {
+ public class Map : EnumerableMapperTest
+ {
+ public class NinjaEntity2Ninja : Map
+ {
+ protected EnumerableMapper MapperUnderTest { get; }
+ protected Mock> NinjaEntityToNinjaMapperMock { get; }
+
+ public NinjaEntity2Ninja()
+ {
+ NinjaEntityToNinjaMapperMock = new Mock>();
+ MapperUnderTest = new EnumerableMapper(NinjaEntityToNinjaMapperMock.Object);
+ }
+
+ [Fact]
+ public void Should_delegate_mapping_to_the_single_entity_mapper()
+ {
+ // Arrange
+ var ninja1 = new NinjaEntity();
+ var ninja2 = new NinjaEntity();
+ var ninjaEntities = new List { ninja1, ninja2 };
+
+ NinjaEntityToNinjaMapperMock
+ .Setup(x => x.Map(It.IsAny()))
+ .Returns(new Ninja())
+ .Verifiable();
+
+ // Act
+ var result = MapperUnderTest.Map(ninjaEntities);
+
+ // Assert
+ NinjaEntityToNinjaMapperMock.Verify(x => x.Map(ninja1), Times.Once);
+ NinjaEntityToNinjaMapperMock.Verify(x => x.Map(ninja2), Times.Once);
+ }
+ }
+ }
+ }
+}
diff --git a/10. NinjaApi - NinjaRepository/test/ForEvolve.Blog.Samples.NinjaApi.Tests/Mappers/NinjaEntityToNinjaMapperTest.cs b/10. NinjaApi - NinjaRepository/test/ForEvolve.Blog.Samples.NinjaApi.Tests/Mappers/NinjaEntityToNinjaMapperTest.cs
new file mode 100644
index 0000000..699f0a4
--- /dev/null
+++ b/10. NinjaApi - NinjaRepository/test/ForEvolve.Blog.Samples.NinjaApi.Tests/Mappers/NinjaEntityToNinjaMapperTest.cs
@@ -0,0 +1,41 @@
+using ForEvolve.Blog.Samples.NinjaApi.Models;
+using Xunit;
+
+namespace ForEvolve.Blog.Samples.NinjaApi.Mappers
+{
+ public class NinjaEntityToNinjaMapperTest
+ {
+ protected NinjaEntityToNinjaMapper MapperUnderTest { get; }
+
+ public NinjaEntityToNinjaMapperTest()
+ {
+ MapperUnderTest = new NinjaEntityToNinjaMapper();
+ }
+
+ public class Map : NinjaEntityToNinjaMapperTest
+ {
+ [Fact]
+ public void Should_return_a_well_formatted_ninja()
+ {
+ // Arrange
+ var entity = new NinjaEntity
+ {
+ Level = 10,
+ Name = "Some fake name",
+ PartitionKey = "Some clan name",
+ RowKey = "Some ninja key"
+ };
+
+ // Act
+ var result = MapperUnderTest.Map(entity);
+
+ // Assert
+ Assert.Equal(10, result.Level);
+ Assert.Equal("Some fake name", result.Name);
+ Assert.NotNull(result.Clan);
+ Assert.Equal("Some clan name", result.Clan.Name);
+ Assert.Equal("Some ninja key", result.Key);
+ }
+ }
+ }
+}
diff --git a/10. NinjaApi - NinjaRepository/test/ForEvolve.Blog.Samples.NinjaApi.Tests/Mappers/NinjaToNinjaEntityMapperTest.cs b/10. NinjaApi - NinjaRepository/test/ForEvolve.Blog.Samples.NinjaApi.Tests/Mappers/NinjaToNinjaEntityMapperTest.cs
new file mode 100644
index 0000000..1a08e1c
--- /dev/null
+++ b/10. NinjaApi - NinjaRepository/test/ForEvolve.Blog.Samples.NinjaApi.Tests/Mappers/NinjaToNinjaEntityMapperTest.cs
@@ -0,0 +1,43 @@
+using ForEvolve.Blog.Samples.NinjaApi.Models;
+using System;
+using System.Collections.Generic;
+using System.Text;
+using Xunit;
+
+namespace ForEvolve.Blog.Samples.NinjaApi.Mappers
+{
+ public class NinjaToNinjaEntityMapperTest
+ {
+ protected NinjaToNinjaEntityMapper MapperUnderTest { get; }
+
+ public NinjaToNinjaEntityMapperTest()
+ {
+ MapperUnderTest = new NinjaToNinjaEntityMapper();
+ }
+
+ public class Map : NinjaToNinjaEntityMapperTest
+ {
+ [Fact]
+ public void Should_return_a_well_formatted_entity()
+ {
+ // Arrange
+ var ninja = new Ninja
+ {
+ Key = "Some key",
+ Name = "Some name",
+ Level = 45,
+ Clan = new Clan { Name = "Super clan" }
+ };
+
+ // Act
+ var result = MapperUnderTest.Map(ninja);
+
+ // Assert
+ Assert.Equal("Some key", result.RowKey);
+ Assert.Equal("Some name", result.Name);
+ Assert.Equal(45, result.Level);
+ Assert.Equal("Super clan", result.PartitionKey);
+ }
+ }
+ }
+}
diff --git a/10. NinjaApi - NinjaRepository/test/ForEvolve.Blog.Samples.NinjaApi.Tests/Repositories/ClanRepositoryTest.cs b/10. NinjaApi - NinjaRepository/test/ForEvolve.Blog.Samples.NinjaApi.Tests/Repositories/ClanRepositoryTest.cs
new file mode 100644
index 0000000..ea77e7b
--- /dev/null
+++ b/10. NinjaApi - NinjaRepository/test/ForEvolve.Blog.Samples.NinjaApi.Tests/Repositories/ClanRepositoryTest.cs
@@ -0,0 +1,103 @@
+using ForEvolve.Blog.Samples.NinjaApi.Models;
+using System;
+using System.Collections.Generic;
+using System.Text;
+using System.Threading.Tasks;
+using Xunit;
+
+namespace ForEvolve.Blog.Samples.NinjaApi.Repositories
+{
+ public class ClanRepositoryTest
+ {
+ protected ClanRepository RepositoryUnderTest { get; }
+ protected Clan[] Clans { get; }
+
+ public ClanRepositoryTest()
+ {
+ Clans = new Clan[]
+ {
+ new Clan { Name = "My clan" },
+ new Clan { Name = "Your clan" },
+ new Clan { Name = "His clan" }
+ };
+ RepositoryUnderTest = new ClanRepository(Clans);
+ }
+
+ public class ReadAllAsync : ClanRepositoryTest
+ {
+ [Fact]
+ public async Task Should_return_all_clans()
+ {
+ // Act
+ var result = await RepositoryUnderTest.ReadAllAsync();
+
+ // Assert
+ Assert.Collection(result,
+ clan => Assert.Same(Clans[0], clan),
+ clan => Assert.Same(Clans[1], clan),
+ clan => Assert.Same(Clans[2], clan)
+ );
+ }
+ }
+
+ public class ReadOneAsync : ClanRepositoryTest
+ {
+ [Fact]
+ public async Task Should_return_the_expected_clan()
+ {
+ // Arrange
+ var expectedClan = Clans[1];
+ var expectedClanName = expectedClan.Name;
+
+ // Act
+ var result = await RepositoryUnderTest.ReadOneAsync(expectedClanName);
+
+ // Assert
+ Assert.Same(expectedClan, result);
+ }
+
+ [Fact]
+ public async Task Should_return_null_if_the_clan_does_not_exist()
+ {
+ // Arrange
+ var unexistingClanName = "Unexisting clan";
+
+ // Act
+ var result = await RepositoryUnderTest.ReadOneAsync(unexistingClanName);
+
+ // Assert
+ Assert.Null(result);
+ }
+ }
+
+ public class CreateAsync : ClanRepositoryTest
+ {
+ [Fact]
+ public async Task Should_throw_a_NotSupportedException()
+ {
+ // Arrange, Act, Assert
+ var exception = await Assert.ThrowsAsync(() => RepositoryUnderTest.CreateAsync(null));
+ }
+ }
+
+ public class UpdateAsync : ClanRepositoryTest
+ {
+ [Fact]
+ public async Task Should_throw_a_NotSupportedException()
+ {
+ // Arrange, Act, Assert
+ var exception = await Assert.ThrowsAsync(() => RepositoryUnderTest.UpdateAsync(null));
+ }
+ }
+
+ public class DeleteAsync : ClanRepositoryTest
+ {
+ [Fact]
+ public async Task Should_throw_a_NotSupportedException()
+ {
+ // Arrange, Act, Assert
+ var exception = await Assert.ThrowsAsync(() => RepositoryUnderTest.DeleteAsync(null));
+ }
+ }
+ }
+}
diff --git a/10. NinjaApi - NinjaRepository/test/ForEvolve.Blog.Samples.NinjaApi.Tests/Repositories/NinjaRepositoryTest.cs b/10. NinjaApi - NinjaRepository/test/ForEvolve.Blog.Samples.NinjaApi.Tests/Repositories/NinjaRepositoryTest.cs
new file mode 100644
index 0000000..5b91745
--- /dev/null
+++ b/10. NinjaApi - NinjaRepository/test/ForEvolve.Blog.Samples.NinjaApi.Tests/Repositories/NinjaRepositoryTest.cs
@@ -0,0 +1,222 @@
+using ForEvolve.Azure.Storage.Table;
+using ForEvolve.Blog.Samples.NinjaApi.Models;
+using ForEvolve.Blog.Samples.NinjaApi.Services;
+using Moq;
+using System;
+using System.Collections.Generic;
+using System.Text;
+using System.Threading.Tasks;
+using Xunit;
+
+namespace ForEvolve.Blog.Samples.NinjaApi.Repositories
+{
+ public class NinjaRepositoryTest
+ {
+ protected NinjaRepository RepositoryUnderTest { get; }
+
+ protected Mock NinjaMappingServiceMock { get; }
+ protected Mock> NinjaEntityTableStorageRepositoryMock { get; }
+
+ public NinjaRepositoryTest()
+ {
+ NinjaMappingServiceMock = new Mock();
+ NinjaEntityTableStorageRepositoryMock = new Mock>();
+ RepositoryUnderTest = new NinjaRepository(
+ NinjaMappingServiceMock.Object,
+ NinjaEntityTableStorageRepositoryMock.Object
+ );
+ }
+
+ public class ReadAllAsync : NinjaRepositoryTest
+ {
+ [Fact]
+ public async Task Should_map_ReadAll_and_return_the_expected_ninja()
+ {
+ // Arrange
+ var entities = new NinjaEntity[0];
+ var expectedNinja = new Ninja[0];
+
+ NinjaEntityTableStorageRepositoryMock
+ .Setup(x => x.ReadAllAsync())
+ .ReturnsAsync(entities)
+ .Verifiable();
+ NinjaMappingServiceMock
+ .Setup(x => x.Map(entities))
+ .Returns(expectedNinja)
+ .Verifiable();
+
+ // Act
+ var result = await RepositoryUnderTest.ReadAllAsync();
+
+ // Assert
+ NinjaMappingServiceMock
+ .Verify(x => x.Map(entities), Times.Once);
+ NinjaEntityTableStorageRepositoryMock
+ .Verify(x => x.ReadAllAsync(), Times.Once);
+ Assert.Same(expectedNinja, result);
+ }
+ }
+
+ public class ReadAllInClanAsync : NinjaRepositoryTest
+ {
+ [Fact]
+ public async Task Should_map_ReadPartition_and_return_the_expected_ninja()
+ {
+ // Arrange
+ var clanName = "My clan";
+ var entities = new NinjaEntity[0];
+ var expectedNinja = new Ninja[0];
+ NinjaEntityTableStorageRepositoryMock
+ .Setup(x => x.ReadPartitionAsync(clanName))
+ .ReturnsAsync(entities)
+ .Verifiable();
+ NinjaMappingServiceMock
+ .Setup(x => x.Map(entities))
+ .Returns(expectedNinja)
+ .Verifiable();
+
+ // Act
+ var result = await RepositoryUnderTest.ReadAllInClanAsync(clanName);
+
+ // Assert
+ NinjaMappingServiceMock
+ .Verify(x => x.Map(entities), Times.Once);
+ NinjaEntityTableStorageRepositoryMock
+ .Verify(x => x.ReadPartitionAsync(clanName), Times.Once);
+ Assert.Same(expectedNinja, result);
+ }
+ }
+
+ public class ReadOneAsync : NinjaRepositoryTest
+ {
+ [Fact]
+ public async Task Should_map_ReadOne_and_return_the_expected_ninja()
+ {
+ // Arrange
+ var clanName = "My clan";
+ var ninjaKey = "123FB950-57DB-4CD0-B4D1-7E6B00A6163A";
+ var entity = new NinjaEntity();
+ var expectedNinja = new Ninja();
+
+ NinjaEntityTableStorageRepositoryMock
+ .Setup(x => x.ReadOneAsync(clanName, ninjaKey))
+ .ReturnsAsync(entity)
+ .Verifiable();
+ NinjaMappingServiceMock
+ .Setup(x => x.Map(entity))
+ .Returns(expectedNinja)
+ .Verifiable();
+
+ // Act
+ var result = await RepositoryUnderTest.ReadOneAsync(clanName, ninjaKey);
+
+ // Assert
+ NinjaMappingServiceMock
+ .Verify(x => x.Map(entity), Times.Once);
+ NinjaEntityTableStorageRepositoryMock
+ .Verify(x => x.ReadOneAsync(clanName, ninjaKey), Times.Once);
+ Assert.Same(expectedNinja, result);
+ }
+ }
+
+ public class CreateAsync : NinjaRepositoryTest
+ {
+ [Fact]
+ public async Task Should_map_InsertOrReplace_and_return_the_expected_ninja()
+ {
+ // Arrange
+ var ninjaToCreate = new Ninja();
+ var entityToCreate = new NinjaEntity();
+ var createdEntity = new NinjaEntity();
+ var expectedNinja = new Ninja();
+
+ NinjaMappingServiceMock
+ .Setup(x => x.Map(ninjaToCreate))
+ .Returns(entityToCreate)
+ .Verifiable();
+ NinjaEntityTableStorageRepositoryMock
+ .Setup(x => x.InsertOrReplaceAsync(entityToCreate))
+ .ReturnsAsync(createdEntity)
+ .Verifiable();
+ NinjaMappingServiceMock
+ .Setup(x => x.Map(createdEntity))
+ .Returns(expectedNinja)
+ .Verifiable();
+
+ // Act
+ var result = await RepositoryUnderTest.CreateAsync(ninjaToCreate);
+
+ // Assert
+ NinjaMappingServiceMock.Verify(x => x.Map(ninjaToCreate), Times.Once);
+ NinjaEntityTableStorageRepositoryMock.Verify(x => x.InsertOrReplaceAsync(entityToCreate), Times.Once);
+ NinjaMappingServiceMock.Verify(x => x.Map(createdEntity), Times.Once);
+ Assert.Same(expectedNinja, result);
+ }
+ }
+
+ public class UpdateAsync : NinjaRepositoryTest
+ {
+ [Fact]
+ public async Task Should_map_InsertOrMerge_and_return_the_expected_ninja()
+ {
+ // Arrange
+ var ninjaToUpdate = new Ninja();
+ var entityToUpdate = new NinjaEntity();
+ var updatedEntity = new NinjaEntity();
+ var expectedNinja = new Ninja();
+
+ NinjaMappingServiceMock
+ .Setup(x => x.Map(ninjaToUpdate))
+ .Returns(entityToUpdate)
+ .Verifiable();
+ NinjaEntityTableStorageRepositoryMock
+ .Setup(x => x.InsertOrMergeAsync(entityToUpdate))
+ .ReturnsAsync(updatedEntity)
+ .Verifiable();
+ NinjaMappingServiceMock
+ .Setup(x => x.Map(updatedEntity))
+ .Returns(expectedNinja)
+ .Verifiable();
+
+ // Act
+ var result = await RepositoryUnderTest.UpdateAsync(ninjaToUpdate);
+
+ // Assert
+ NinjaMappingServiceMock.Verify(x => x.Map(ninjaToUpdate), Times.Once);
+ NinjaEntityTableStorageRepositoryMock.Verify(x => x.InsertOrMergeAsync(entityToUpdate), Times.Once);
+ NinjaMappingServiceMock.Verify(x => x.Map(updatedEntity), Times.Once);
+ Assert.Same(expectedNinja, result);
+ }
+ }
+
+ public class DeleteAsync : NinjaRepositoryTest
+ {
+ [Fact]
+ public async Task Should_map_Remove_and_return_the_expected_ninja()
+ {
+ // Arrange
+ var clanName = "My clan";
+ var ninjaKey = "123FB950-57DB-4CD0-B4D1-7E6B00A6163A";
+ var deletedEntity = new NinjaEntity();
+ var expectedNinja = new Ninja();
+
+ NinjaEntityTableStorageRepositoryMock
+ .Setup(x => x.DeleteOneAsync(clanName, ninjaKey))
+ .ReturnsAsync(deletedEntity)
+ .Verifiable();
+ NinjaMappingServiceMock
+ .Setup(x => x.Map(deletedEntity))
+ .Returns(expectedNinja)
+ .Verifiable();
+
+ // Act
+ var result = await RepositoryUnderTest.DeleteAsync(clanName, ninjaKey);
+
+ // Assert
+ NinjaEntityTableStorageRepositoryMock.Verify(x => x.DeleteOneAsync(clanName, ninjaKey), Times.Once);
+ NinjaMappingServiceMock.Verify(x => x.Map(deletedEntity), Times.Once);
+ Assert.Same(expectedNinja, result);
+ }
+ }
+ }
+}
diff --git a/10. NinjaApi - NinjaRepository/test/ForEvolve.Blog.Samples.NinjaApi.Tests/Services/ClanServiceTest.cs b/10. NinjaApi - NinjaRepository/test/ForEvolve.Blog.Samples.NinjaApi.Tests/Services/ClanServiceTest.cs
new file mode 100644
index 0000000..bba7c70
--- /dev/null
+++ b/10. NinjaApi - NinjaRepository/test/ForEvolve.Blog.Samples.NinjaApi.Tests/Services/ClanServiceTest.cs
@@ -0,0 +1,149 @@
+using ForEvolve.Blog.Samples.NinjaApi.Models;
+using ForEvolve.Blog.Samples.NinjaApi.Repositories;
+using Moq;
+using System;
+using System.Collections.Generic;
+using System.Collections.ObjectModel;
+using System.Text;
+using System.Threading.Tasks;
+using Xunit;
+
+namespace ForEvolve.Blog.Samples.NinjaApi.Services
+{
+ public class ClanServiceTest
+ {
+ protected ClanService ServiceUnderTest { get; }
+ protected Mock ClanRepositoryMock { get; }
+
+ public ClanServiceTest()
+ {
+ ClanRepositoryMock = new Mock();
+ ServiceUnderTest = new ClanService(ClanRepositoryMock.Object);
+ }
+
+ public class ReadAllAsync : ClanServiceTest
+ {
+ [Fact]
+ public async Task Should_return_all_clans()
+ {
+ // Arrange
+ var expectedClans = new ReadOnlyCollection(new List
+ {
+ new Clan { Name = "My Clan" },
+ new Clan { Name = "Your Clan" },
+ new Clan { Name = "His Clan" }
+ });
+ ClanRepositoryMock
+ .Setup(x => x.ReadAllAsync())
+ .ReturnsAsync(expectedClans);
+
+ // Act
+ var result = await ServiceUnderTest.ReadAllAsync();
+
+ // Assert
+ Assert.Same(expectedClans, result);
+ }
+ }
+
+ public class ReadOneAsync : ClanServiceTest
+ {
+ [Fact]
+ public async Task Should_return_the_expected_clan()
+ {
+ // Arrange
+ var clanName = "My Clan";
+ var expectedClan = new Clan { Name = clanName };
+ ClanRepositoryMock
+ .Setup(x => x.ReadOneAsync(clanName))
+ .ReturnsAsync(expectedClan);
+
+ // Act
+ var result = await ServiceUnderTest.ReadOneAsync(clanName);
+
+ // Assert
+ Assert.Same(expectedClan, result);
+ }
+
+ [Fact]
+ public async Task Should_return_null_if_the_clan_does_not_exist()
+ {
+ // Arrange
+ var clanName = "My Clan";
+ ClanRepositoryMock
+ .Setup(x => x.ReadOneAsync(clanName))
+ .ReturnsAsync(default(Clan));
+
+ // Act
+ var result = await ServiceUnderTest.ReadOneAsync(clanName);
+
+ // Assert
+ Assert.Null(result);
+ }
+ }
+
+ public class IsClanExistsAsync : ClanServiceTest
+ {
+ [Fact]
+ public async Task Should_return_true_if_the_clan_exist()
+ {
+ // Arrange
+ var clanName = "Your Clan";
+ ClanRepositoryMock
+ .Setup(x => x.ReadOneAsync(clanName))
+ .ReturnsAsync(new Clan());
+
+ // Act
+ var result = await ServiceUnderTest.IsClanExistsAsync(clanName);
+
+ // Assert
+ Assert.True(result);
+ }
+
+ [Fact]
+ public async Task Should_return_false_if_the_clan_does_not_exist()
+ {
+ // Arrange
+ var clanName = "Unexisting Clan";
+ ClanRepositoryMock
+ .Setup(x => x.ReadOneAsync(clanName))
+ .ReturnsAsync(default(Clan));
+
+ // Act
+ var result = await ServiceUnderTest.IsClanExistsAsync(clanName);
+
+ // Assert
+ Assert.False(result);
+ }
+ }
+
+ public class CreateAsync : ClanServiceTest
+ {
+ [Fact]
+ public async Task Should_throw_a_NotSupportedException()
+ {
+ // Arrange, Act, Assert
+ var exception = await Assert.ThrowsAsync(() => ServiceUnderTest.CreateAsync(null));
+ }
+ }
+
+ public class UpdateAsync : ClanServiceTest
+ {
+ [Fact]
+ public async Task Should_throw_a_NotSupportedException()
+ {
+ // Arrange, Act, Assert
+ var exception = await Assert.ThrowsAsync(() => ServiceUnderTest.UpdateAsync(null));
+ }
+ }
+
+ public class DeleteAsync : ClanServiceTest
+ {
+ [Fact]
+ public async Task Should_throw_a_NotSupportedException()
+ {
+ // Arrange, Act, Assert
+ var exception = await Assert.ThrowsAsync(() => ServiceUnderTest.DeleteAsync(null));
+ }
+ }
+ }
+}
diff --git a/10. NinjaApi - NinjaRepository/test/ForEvolve.Blog.Samples.NinjaApi.Tests/Services/NinjaMappingServiceTest.cs b/10. NinjaApi - NinjaRepository/test/ForEvolve.Blog.Samples.NinjaApi.Tests/Services/NinjaMappingServiceTest.cs
new file mode 100644
index 0000000..b0cb5df
--- /dev/null
+++ b/10. NinjaApi - NinjaRepository/test/ForEvolve.Blog.Samples.NinjaApi.Tests/Services/NinjaMappingServiceTest.cs
@@ -0,0 +1,81 @@
+using ForEvolve.Blog.Samples.NinjaApi.Mappers;
+using ForEvolve.Blog.Samples.NinjaApi.Models;
+using Moq;
+using System;
+using System.Collections.Generic;
+using System.Text;
+using Xunit;
+
+namespace ForEvolve.Blog.Samples.NinjaApi.Services
+{
+ public class NinjaMappingServiceTest
+ {
+ protected NinjaMappingService ServiceUnderTest { get; }
+ protected Mock> NinjaToNinjaEntityMapperMock { get; }
+ protected Mock> NinjaEntityToNinjaMapperMock { get; }
+ protected Mock, IEnumerable>> NinjaEntityEnumerableToNinjaMapperMock { get; }
+
+ public NinjaMappingServiceTest()
+ {
+ NinjaToNinjaEntityMapperMock = new Mock>();
+ NinjaEntityToNinjaMapperMock = new Mock>();
+ NinjaEntityEnumerableToNinjaMapperMock = new Mock, IEnumerable>>();
+ ServiceUnderTest = new NinjaMappingService(
+ NinjaToNinjaEntityMapperMock.Object,
+ NinjaEntityToNinjaMapperMock.Object,
+ NinjaEntityEnumerableToNinjaMapperMock.Object
+ );
+ }
+
+ [Fact]
+ public void Map_Ninja_to_NinjaEntity_should_delegate_to_NinjaToNinjaEntityMapper()
+ {
+ // Arrange
+ var ninja = new Ninja();
+ var expectedEntity = new NinjaEntity();
+ NinjaToNinjaEntityMapperMock
+ .Setup(x => x.Map(ninja))
+ .Returns(expectedEntity);
+
+ // Act
+ var result = ServiceUnderTest.Map(ninja);
+
+ // Assert
+ Assert.Same(expectedEntity, result);
+ }
+
+ [Fact]
+ public void Map_NinjaEntity_to_Ninja_should_delegate_to_NinjaEntityToNinjaMapper()
+ {
+ // Arrange
+ var ninjaEntity = new NinjaEntity();
+ var expectedNinja = new Ninja();
+ NinjaEntityToNinjaMapperMock
+ .Setup(x => x.Map(ninjaEntity))
+ .Returns(expectedNinja);
+
+ // Act
+ var result = ServiceUnderTest.Map(ninjaEntity);
+
+ // Assert
+ Assert.Same(expectedNinja, result);
+ }
+
+ [Fact]
+ public void Map_NinjaEntityEnumerable_to_NinjaEnumerable_should_delegate_to_NinjaEntityEnumerableToNinjaMapper()
+ {
+ // Arrange
+ var ninjaEntities = new List();
+ var expectedNinja = new List();
+ NinjaEntityEnumerableToNinjaMapperMock
+ .Setup(x => x.Map(ninjaEntities))
+ .Returns(expectedNinja);
+
+ // Act
+ var result = ServiceUnderTest.Map(ninjaEntities);
+
+ // Assert
+ Assert.Same(expectedNinja, result);
+ }
+ }
+}
diff --git a/10. NinjaApi - NinjaRepository/test/ForEvolve.Blog.Samples.NinjaApi.Tests/Services/NinjaServiceTest.cs b/10. NinjaApi - NinjaRepository/test/ForEvolve.Blog.Samples.NinjaApi.Tests/Services/NinjaServiceTest.cs
new file mode 100644
index 0000000..95180fe
--- /dev/null
+++ b/10. NinjaApi - NinjaRepository/test/ForEvolve.Blog.Samples.NinjaApi.Tests/Services/NinjaServiceTest.cs
@@ -0,0 +1,323 @@
+using ForEvolve.Blog.Samples.NinjaApi.Models;
+using ForEvolve.Blog.Samples.NinjaApi.Repositories;
+using Microsoft.AspNetCore.Mvc;
+using Moq;
+using System;
+using System.Collections.Generic;
+using System.Text;
+using Xunit;
+
+namespace ForEvolve.Blog.Samples.NinjaApi.Services
+{
+ public class NinjaServiceTest
+ {
+ protected NinjaService ServiceUnderTest { get; }
+ protected Mock NinjaRepositoryMock { get; }
+ protected Mock ClanServiceMock { get; }
+
+ public NinjaServiceTest()
+ {
+ NinjaRepositoryMock = new Mock();
+ ClanServiceMock = new Mock();
+ 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(() => 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(() => ServiceUnderTest.ReadOneAsync(unexistingClanName, unexistingNinjaKey));
+ }
+ }
+
+ public class CreateAsync : NinjaServiceTest
+ {
+ [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);
+ }
+
+ [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(() => ServiceUnderTest.CreateAsync(expectedNinja));
+
+ // Make sure CreateAsync is never called
+ NinjaRepositoryMock.Verify(x => x.CreateAsync(expectedNinja), Times.Never);
+ }
+ }
+
+ 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();
+ ClanServiceMock
+ .Setup(x => x.IsClanExistsAsync(clanKey))
+ .ReturnsAsync(true);
+
+ // 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
+ 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();
+ NinjaRepositoryMock
+ .Setup(x => x.ReadOneAsync(clanKey, ninjaKey))
+ .ReturnsAsync(default(Ninja))
+ .Verifiable();
+ ClanServiceMock
+ .Setup(x => x.IsClanExistsAsync(clanKey))
+ .ReturnsAsync(true);
+
+ // Act & Assert
+ await Assert.ThrowsAsync(() => 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(clanKey, ninjaKey), Times.Once);
+ }
+
+ [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(() => ServiceUnderTest.UpdateAsync(unexistingNinja));
+
+ // Make sure UpdateAsync is never called
+ NinjaRepositoryMock
+ .Verify(x => x.UpdateAsync(unexistingNinja), Times.Never);
+ }
+ }
+
+ 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(() => 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);
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/11. NinjaApi - IntegrationTesting/ForEvolve.Blog.Samples.NinjaApi.sln b/11. NinjaApi - IntegrationTesting/ForEvolve.Blog.Samples.NinjaApi.sln
new file mode 100644
index 0000000..48f81a9
--- /dev/null
+++ b/11. NinjaApi - IntegrationTesting/ForEvolve.Blog.Samples.NinjaApi.sln
@@ -0,0 +1,46 @@
+
+Microsoft Visual Studio Solution File, Format Version 12.00
+# Visual Studio 15
+VisualStudioVersion = 15.0.26711.1
+MinimumVisualStudioVersion = 10.0.40219.1
+Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "src", "src", "{23BF447F-B872-4D4F-8F28-D443072AE93E}"
+EndProject
+Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "test", "test", "{2F55FE43-6EA6-4A7E-B70B-2E8267478F29}"
+EndProject
+Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ForEvolve.Blog.Samples.NinjaApi", "src\ForEvolve.Blog.Samples.NinjaApi\ForEvolve.Blog.Samples.NinjaApi.csproj", "{DD219D97-6BF6-4EE5-9E35-13185F4FA261}"
+EndProject
+Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ForEvolve.Blog.Samples.NinjaApi.Tests", "test\ForEvolve.Blog.Samples.NinjaApi.Tests\ForEvolve.Blog.Samples.NinjaApi.Tests.csproj", "{6705EB4C-3DF6-4385-AEAB-3EE2FE074E05}"
+EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ForEvolve.Blog.Samples.NinjaApi.IntegrationTests", "test\ForEvolve.Blog.Samples.NinjaApi.IntegrationTests\ForEvolve.Blog.Samples.NinjaApi.IntegrationTests.csproj", "{89C4A3D5-7272-4EC7-AC91-89ED8C1D00EA}"
+EndProject
+Global
+ GlobalSection(SolutionConfigurationPlatforms) = preSolution
+ Debug|Any CPU = Debug|Any CPU
+ Release|Any CPU = Release|Any CPU
+ EndGlobalSection
+ GlobalSection(ProjectConfigurationPlatforms) = postSolution
+ {DD219D97-6BF6-4EE5-9E35-13185F4FA261}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {DD219D97-6BF6-4EE5-9E35-13185F4FA261}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {DD219D97-6BF6-4EE5-9E35-13185F4FA261}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {DD219D97-6BF6-4EE5-9E35-13185F4FA261}.Release|Any CPU.Build.0 = Release|Any CPU
+ {6705EB4C-3DF6-4385-AEAB-3EE2FE074E05}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {6705EB4C-3DF6-4385-AEAB-3EE2FE074E05}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {6705EB4C-3DF6-4385-AEAB-3EE2FE074E05}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {6705EB4C-3DF6-4385-AEAB-3EE2FE074E05}.Release|Any CPU.Build.0 = Release|Any CPU
+ {89C4A3D5-7272-4EC7-AC91-89ED8C1D00EA}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {89C4A3D5-7272-4EC7-AC91-89ED8C1D00EA}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {89C4A3D5-7272-4EC7-AC91-89ED8C1D00EA}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {89C4A3D5-7272-4EC7-AC91-89ED8C1D00EA}.Release|Any CPU.Build.0 = Release|Any CPU
+ EndGlobalSection
+ GlobalSection(SolutionProperties) = preSolution
+ HideSolutionNode = FALSE
+ EndGlobalSection
+ GlobalSection(NestedProjects) = preSolution
+ {DD219D97-6BF6-4EE5-9E35-13185F4FA261} = {23BF447F-B872-4D4F-8F28-D443072AE93E}
+ {6705EB4C-3DF6-4385-AEAB-3EE2FE074E05} = {2F55FE43-6EA6-4A7E-B70B-2E8267478F29}
+ {89C4A3D5-7272-4EC7-AC91-89ED8C1D00EA} = {2F55FE43-6EA6-4A7E-B70B-2E8267478F29}
+ EndGlobalSection
+ GlobalSection(ExtensibilityGlobals) = postSolution
+ SolutionGuid = {47DBF882-4F3A-4C9F-95CC-1286279CC35B}
+ EndGlobalSection
+EndGlobal
diff --git a/11. NinjaApi - IntegrationTesting/README.md b/11. NinjaApi - IntegrationTesting/README.md
new file mode 100644
index 0000000..e9035b8
--- /dev/null
+++ b/11. NinjaApi - IntegrationTesting/README.md
@@ -0,0 +1,4 @@
+# Dependency Injection
+This project contains the code sample of the
+[Design Patterns: Asp.Net Core Web API, services, and repositories | Part 11: Integration testing](http://www.forevolve.com/en/articles/2017/09/18/design-patterns-web-api-service-and-repository-part-11/)
+article.
\ No newline at end of file
diff --git a/11. NinjaApi - IntegrationTesting/src/ForEvolve.Blog.Samples.NinjaApi/Controllers/ClansController.cs b/11. NinjaApi - IntegrationTesting/src/ForEvolve.Blog.Samples.NinjaApi/Controllers/ClansController.cs
new file mode 100644
index 0000000..4e3d49d
--- /dev/null
+++ b/11. NinjaApi - IntegrationTesting/src/ForEvolve.Blog.Samples.NinjaApi/Controllers/ClansController.cs
@@ -0,0 +1,29 @@
+using ForEvolve.Blog.Samples.NinjaApi.Models;
+using ForEvolve.Blog.Samples.NinjaApi.Services;
+using Microsoft.AspNetCore.Mvc;
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Threading.Tasks;
+
+namespace ForEvolve.Blog.Samples.NinjaApi.Controllers
+{
+ [Route("v1/[controller]")]
+ public class ClansController : Controller
+ {
+ private readonly IClanService _clanService;
+
+ public ClansController(IClanService clanService)
+ {
+ _clanService = clanService ?? throw new ArgumentNullException(nameof(clanService));
+ }
+
+ [HttpGet]
+ [ProducesResponseType(typeof(IEnumerable), 200)]
+ public async Task ReadAllAsync()
+ {
+ var allClans = await _clanService.ReadAllAsync();
+ return Ok(allClans);
+ }
+ }
+}
diff --git a/11. NinjaApi - IntegrationTesting/src/ForEvolve.Blog.Samples.NinjaApi/Controllers/NinjaController.cs b/11. NinjaApi - IntegrationTesting/src/ForEvolve.Blog.Samples.NinjaApi/Controllers/NinjaController.cs
new file mode 100644
index 0000000..7f6d7eb
--- /dev/null
+++ b/11. NinjaApi - IntegrationTesting/src/ForEvolve.Blog.Samples.NinjaApi/Controllers/NinjaController.cs
@@ -0,0 +1,117 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Threading.Tasks;
+using Microsoft.AspNetCore.Mvc;
+using ForEvolve.Blog.Samples.NinjaApi.Models;
+using Microsoft.AspNetCore.Http;
+using ForEvolve.Blog.Samples.NinjaApi.Services;
+
+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), StatusCodes.Status200OK)]
+ public async Task ReadAllAsync()
+ {
+ var allNinja = await _ninjaService.ReadAllAsync();
+ return Ok(allNinja);
+ }
+
+ [HttpGet("{clan}")]
+ [ProducesResponseType(typeof(IEnumerable), StatusCodes.Status200OK)]
+ [ProducesResponseType(StatusCodes.Status404NotFound)]
+ public async Task ReadAllInClanAsync(string clan)
+ {
+ try
+ {
+ var clanNinja = await _ninjaService.ReadAllInClanAsync(clan);
+ return Ok(clanNinja);
+ }
+ catch (ClanNotFoundException)
+ {
+ return NotFound();
+ }
+ }
+
+ [HttpGet("{clan}/{key}")]
+ [ProducesResponseType(typeof(Ninja), StatusCodes.Status200OK)]
+ [ProducesResponseType(StatusCodes.Status404NotFound)]
+ public async Task ReadOneAsync(string clan, string key)
+ {
+ try
+ {
+ var ninja = await _ninjaService.ReadOneAsync(clan, key);
+ return Ok(ninja);
+ }
+ catch (NinjaNotFoundException)
+ {
+ return NotFound();
+ }
+ }
+
+ [HttpPost]
+ [ProducesResponseType(typeof(Ninja), StatusCodes.Status201Created)]
+ [ProducesResponseType(StatusCodes.Status400BadRequest)]
+ public async Task 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
+ );
+ }
+
+ [HttpPut]
+ [ProducesResponseType(typeof(Ninja), StatusCodes.Status200OK)]
+ [ProducesResponseType(StatusCodes.Status400BadRequest)]
+ [ProducesResponseType(StatusCodes.Status404NotFound)]
+ public async Task UpdateAsync([FromBody]Ninja ninja)
+ {
+ if (!ModelState.IsValid)
+ {
+ return BadRequest(ModelState);
+ }
+
+ try
+ {
+ var updatedNinja = await _ninjaService.UpdateAsync(ninja);
+ return Ok(updatedNinja);
+ }
+ catch (NinjaNotFoundException)
+ {
+ return NotFound();
+ }
+ }
+
+ [HttpDelete("{clan}/{key}")]
+ [ProducesResponseType(typeof(Ninja), StatusCodes.Status200OK)]
+ [ProducesResponseType(StatusCodes.Status404NotFound)]
+ public async Task DeleteAsync(string clan, string key)
+ {
+ try
+ {
+ var deletedNinja = await _ninjaService.DeleteAsync(clan, key);
+ return Ok(deletedNinja);
+ }
+ catch (NinjaNotFoundException)
+ {
+ return NotFound();
+ }
+ }
+ }
+}
diff --git a/11. NinjaApi - IntegrationTesting/src/ForEvolve.Blog.Samples.NinjaApi/Exceptions/ClanNotFoundException.cs b/11. NinjaApi - IntegrationTesting/src/ForEvolve.Blog.Samples.NinjaApi/Exceptions/ClanNotFoundException.cs
new file mode 100644
index 0000000..7d55c95
--- /dev/null
+++ b/11. NinjaApi - IntegrationTesting/src/ForEvolve.Blog.Samples.NinjaApi/Exceptions/ClanNotFoundException.cs
@@ -0,0 +1,17 @@
+using ForEvolve.Blog.Samples.NinjaApi.Models;
+
+namespace ForEvolve.Blog.Samples.NinjaApi
+{
+ public class ClanNotFoundException : NinjaApiException
+ {
+ public ClanNotFoundException(Clan clan)
+ : this(clan.Name)
+ {
+ }
+
+ public ClanNotFoundException(string clanName)
+ : base($"Clan {clanName} was not found.")
+ {
+ }
+ }
+}
diff --git a/11. NinjaApi - IntegrationTesting/src/ForEvolve.Blog.Samples.NinjaApi/Exceptions/NinjaApiException.cs b/11. NinjaApi - IntegrationTesting/src/ForEvolve.Blog.Samples.NinjaApi/Exceptions/NinjaApiException.cs
new file mode 100644
index 0000000..ff81d64
--- /dev/null
+++ b/11. NinjaApi - IntegrationTesting/src/ForEvolve.Blog.Samples.NinjaApi/Exceptions/NinjaApiException.cs
@@ -0,0 +1,27 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Runtime.Serialization;
+using System.Threading.Tasks;
+
+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)
+ {
+ }
+ }
+}
diff --git a/11. NinjaApi - IntegrationTesting/src/ForEvolve.Blog.Samples.NinjaApi/Exceptions/NinjaNotFoundException.cs b/11. NinjaApi - IntegrationTesting/src/ForEvolve.Blog.Samples.NinjaApi/Exceptions/NinjaNotFoundException.cs
new file mode 100644
index 0000000..54c1d8d
--- /dev/null
+++ b/11. NinjaApi - IntegrationTesting/src/ForEvolve.Blog.Samples.NinjaApi/Exceptions/NinjaNotFoundException.cs
@@ -0,0 +1,17 @@
+using ForEvolve.Blog.Samples.NinjaApi.Models;
+
+namespace ForEvolve.Blog.Samples.NinjaApi
+{
+ 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.")
+ {
+ }
+ }
+}
diff --git a/11. NinjaApi - IntegrationTesting/src/ForEvolve.Blog.Samples.NinjaApi/ForEvolve.Blog.Samples.NinjaApi.csproj b/11. NinjaApi - IntegrationTesting/src/ForEvolve.Blog.Samples.NinjaApi/ForEvolve.Blog.Samples.NinjaApi.csproj
new file mode 100644
index 0000000..2272ce1
--- /dev/null
+++ b/11. NinjaApi - IntegrationTesting/src/ForEvolve.Blog.Samples.NinjaApi/ForEvolve.Blog.Samples.NinjaApi.csproj
@@ -0,0 +1,18 @@
+
+
+
+ netcoreapp2.0
+ aspnet-ForEvolve.Blog.Samples.NinjaApi-F62B525A-ACF4-4C7C-BF23-1EB0F434DDE5
+ Full
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/11. NinjaApi - IntegrationTesting/src/ForEvolve.Blog.Samples.NinjaApi/Mappers/EnumerableMapper.cs b/11. NinjaApi - IntegrationTesting/src/ForEvolve.Blog.Samples.NinjaApi/Mappers/EnumerableMapper.cs
new file mode 100644
index 0000000..be141da
--- /dev/null
+++ b/11. NinjaApi - IntegrationTesting/src/ForEvolve.Blog.Samples.NinjaApi/Mappers/EnumerableMapper.cs
@@ -0,0 +1,30 @@
+using ForEvolve.Blog.Samples.NinjaApi.Mappers;
+using System;
+using System.Collections.Generic;
+using System.Linq;
+
+namespace ForEvolve.Blog.Samples.NinjaApi.Mappers
+{
+ public class EnumerableMapper : IMapper, IEnumerable>
+ {
+ private readonly IMapper _singleMapper;
+
+ public EnumerableMapper(IMapper singleMapper)
+ {
+ _singleMapper = singleMapper ?? throw new ArgumentNullException(nameof(singleMapper));
+ }
+
+ public IEnumerable Map(IEnumerable source)
+ {
+ var count = source.Count();
+ var destination = new TDestination[count];
+ for (int i = 0; i < count; i++)
+ {
+ var currentSource = source.ElementAt(i);
+ var currentDestination = _singleMapper.Map(currentSource);
+ destination[i] = currentDestination;
+ }
+ return destination;
+ }
+ }
+}
diff --git a/11. NinjaApi - IntegrationTesting/src/ForEvolve.Blog.Samples.NinjaApi/Mappers/IMapper.cs b/11. NinjaApi - IntegrationTesting/src/ForEvolve.Blog.Samples.NinjaApi/Mappers/IMapper.cs
new file mode 100644
index 0000000..2628ecb
--- /dev/null
+++ b/11. NinjaApi - IntegrationTesting/src/ForEvolve.Blog.Samples.NinjaApi/Mappers/IMapper.cs
@@ -0,0 +1,7 @@
+namespace ForEvolve.Blog.Samples.NinjaApi.Mappers
+{
+ public interface IMapper
+ {
+ TDestination Map(TSource entity);
+ }
+}
diff --git a/11. NinjaApi - IntegrationTesting/src/ForEvolve.Blog.Samples.NinjaApi/Mappers/NinjaEntityToNinjaMapper.cs b/11. NinjaApi - IntegrationTesting/src/ForEvolve.Blog.Samples.NinjaApi/Mappers/NinjaEntityToNinjaMapper.cs
new file mode 100644
index 0000000..745a6d9
--- /dev/null
+++ b/11. NinjaApi - IntegrationTesting/src/ForEvolve.Blog.Samples.NinjaApi/Mappers/NinjaEntityToNinjaMapper.cs
@@ -0,0 +1,19 @@
+using ForEvolve.Blog.Samples.NinjaApi.Models;
+
+namespace ForEvolve.Blog.Samples.NinjaApi.Mappers
+{
+ public class NinjaEntityToNinjaMapper : IMapper
+ {
+ public Ninja Map(NinjaEntity entity)
+ {
+ var ninja = new Ninja
+ {
+ Key = entity.RowKey,
+ Clan = new Clan { Name = entity.PartitionKey },
+ Level = entity.Level,
+ Name = entity.Name
+ };
+ return ninja;
+ }
+ }
+}
diff --git a/11. NinjaApi - IntegrationTesting/src/ForEvolve.Blog.Samples.NinjaApi/Mappers/NinjaToNinjaEntityMapper.cs b/11. NinjaApi - IntegrationTesting/src/ForEvolve.Blog.Samples.NinjaApi/Mappers/NinjaToNinjaEntityMapper.cs
new file mode 100644
index 0000000..a4546f4
--- /dev/null
+++ b/11. NinjaApi - IntegrationTesting/src/ForEvolve.Blog.Samples.NinjaApi/Mappers/NinjaToNinjaEntityMapper.cs
@@ -0,0 +1,19 @@
+using ForEvolve.Blog.Samples.NinjaApi.Models;
+
+namespace ForEvolve.Blog.Samples.NinjaApi.Mappers
+{
+ public class NinjaToNinjaEntityMapper : IMapper
+ {
+ public NinjaEntity Map(Ninja ninja)
+ {
+ var entity = new NinjaEntity
+ {
+ PartitionKey = ninja.Clan.Name,
+ RowKey = ninja.Key,
+ Name = ninja.Name,
+ Level = ninja.Level
+ };
+ return entity;
+ }
+ }
+}
diff --git a/11. NinjaApi - IntegrationTesting/src/ForEvolve.Blog.Samples.NinjaApi/Models/Clan.cs b/11. NinjaApi - IntegrationTesting/src/ForEvolve.Blog.Samples.NinjaApi/Models/Clan.cs
new file mode 100644
index 0000000..7b7c615
--- /dev/null
+++ b/11. NinjaApi - IntegrationTesting/src/ForEvolve.Blog.Samples.NinjaApi/Models/Clan.cs
@@ -0,0 +1,12 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Threading.Tasks;
+
+namespace ForEvolve.Blog.Samples.NinjaApi.Models
+{
+ public class Clan
+ {
+ public string Name { get; set; }
+ }
+}
diff --git a/11. NinjaApi - IntegrationTesting/src/ForEvolve.Blog.Samples.NinjaApi/Models/Ninja.cs b/11. NinjaApi - IntegrationTesting/src/ForEvolve.Blog.Samples.NinjaApi/Models/Ninja.cs
new file mode 100644
index 0000000..110e285
--- /dev/null
+++ b/11. NinjaApi - IntegrationTesting/src/ForEvolve.Blog.Samples.NinjaApi/Models/Ninja.cs
@@ -0,0 +1,15 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Threading.Tasks;
+
+namespace ForEvolve.Blog.Samples.NinjaApi.Models
+{
+ public class Ninja
+ {
+ public string Key { get; set; }
+ public Clan Clan { get; set; }
+ public string Name { get; set; }
+ public int Level { get; set; }
+ }
+}
diff --git a/11. NinjaApi - IntegrationTesting/src/ForEvolve.Blog.Samples.NinjaApi/Models/NinjaEntity.cs b/11. NinjaApi - IntegrationTesting/src/ForEvolve.Blog.Samples.NinjaApi/Models/NinjaEntity.cs
new file mode 100644
index 0000000..12906d9
--- /dev/null
+++ b/11. NinjaApi - IntegrationTesting/src/ForEvolve.Blog.Samples.NinjaApi/Models/NinjaEntity.cs
@@ -0,0 +1,14 @@
+using Microsoft.WindowsAzure.Storage.Table;
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Threading.Tasks;
+
+namespace ForEvolve.Blog.Samples.NinjaApi.Models
+{
+ public class NinjaEntity : TableEntity
+ {
+ public string Name { get; set; }
+ public int Level { get; set; }
+ }
+}
diff --git a/11. NinjaApi - IntegrationTesting/src/ForEvolve.Blog.Samples.NinjaApi/Program.cs b/11. NinjaApi - IntegrationTesting/src/ForEvolve.Blog.Samples.NinjaApi/Program.cs
new file mode 100644
index 0000000..f1c4882
--- /dev/null
+++ b/11. NinjaApi - IntegrationTesting/src/ForEvolve.Blog.Samples.NinjaApi/Program.cs
@@ -0,0 +1,28 @@
+using System;
+using System.Collections.Generic;
+using System.IO;
+using System.Linq;
+using System.Threading.Tasks;
+using Microsoft.AspNetCore;
+using Microsoft.AspNetCore.Hosting;
+using Microsoft.Extensions.Configuration;
+using Microsoft.Extensions.Logging;
+
+namespace ForEvolve.Blog.Samples.NinjaApi
+{
+ public class Program
+ {
+ public static void Main(string[] args)
+ {
+ BuildWebHost(args).Run();
+ }
+
+ public static IWebHost BuildWebHost(string[] args) =>
+ CreateWebHostBuilder(args)
+ .Build();
+
+ public static IWebHostBuilder CreateWebHostBuilder(string[] args) =>
+ WebHost.CreateDefaultBuilder(args)
+ .UseStartup();
+ }
+}
diff --git a/11. NinjaApi - IntegrationTesting/src/ForEvolve.Blog.Samples.NinjaApi/Properties/launchSettings.json b/11. NinjaApi - IntegrationTesting/src/ForEvolve.Blog.Samples.NinjaApi/Properties/launchSettings.json
new file mode 100644
index 0000000..5285189
--- /dev/null
+++ b/11. NinjaApi - IntegrationTesting/src/ForEvolve.Blog.Samples.NinjaApi/Properties/launchSettings.json
@@ -0,0 +1,29 @@
+{
+ "iisSettings": {
+ "windowsAuthentication": false,
+ "anonymousAuthentication": true,
+ "iisExpress": {
+ "applicationUrl": "http://localhost:2439/",
+ "sslPort": 0
+ }
+ },
+ "profiles": {
+ "IIS Express": {
+ "commandName": "IISExpress",
+ "launchBrowser": true,
+ "launchUrl": "v1/ninja",
+ "environmentVariables": {
+ "ASPNETCORE_ENVIRONMENT": "Development"
+ }
+ },
+ "ForEvolve.Blog.Samples.NinjaApi": {
+ "commandName": "Project",
+ "launchBrowser": true,
+ "launchUrl": "api/values",
+ "environmentVariables": {
+ "ASPNETCORE_ENVIRONMENT": "Development"
+ },
+ "applicationUrl": "http://localhost:2440/"
+ }
+ }
+}
\ No newline at end of file
diff --git a/11. NinjaApi - IntegrationTesting/src/ForEvolve.Blog.Samples.NinjaApi/Repositories/ClanRepository.cs b/11. NinjaApi - IntegrationTesting/src/ForEvolve.Blog.Samples.NinjaApi/Repositories/ClanRepository.cs
new file mode 100644
index 0000000..c3f5176
--- /dev/null
+++ b/11. NinjaApi - IntegrationTesting/src/ForEvolve.Blog.Samples.NinjaApi/Repositories/ClanRepository.cs
@@ -0,0 +1,45 @@
+using ForEvolve.Blog.Samples.NinjaApi.Models;
+using System;
+using System.Linq;
+using System.Collections.Generic;
+using System.Threading.Tasks;
+
+namespace ForEvolve.Blog.Samples.NinjaApi.Repositories
+{
+ public class ClanRepository : IClanRepository
+ {
+ private readonly List _clans;
+
+ public ClanRepository(IEnumerable clans)
+ {
+ if (clans == null) { throw new ArgumentNullException(nameof(clans)); }
+ _clans = new List(clans);
+ }
+
+ public Task> ReadAllAsync()
+ {
+ return Task.FromResult(_clans.AsEnumerable());
+ }
+
+ public Task ReadOneAsync(string clanName)
+ {
+ var clan = _clans.FirstOrDefault(c => c.Name == clanName);
+ return Task.FromResult(clan);
+ }
+
+ public Task CreateAsync(Clan clan)
+ {
+ throw new NotSupportedException();
+ }
+
+ public Task UpdateAsync(Clan clan)
+ {
+ throw new NotSupportedException();
+ }
+
+ public Task DeleteAsync(string clanName)
+ {
+ throw new NotSupportedException();
+ }
+ }
+}
diff --git a/11. NinjaApi - IntegrationTesting/src/ForEvolve.Blog.Samples.NinjaApi/Repositories/IClanRepository.cs b/11. NinjaApi - IntegrationTesting/src/ForEvolve.Blog.Samples.NinjaApi/Repositories/IClanRepository.cs
new file mode 100644
index 0000000..e2e41ed
--- /dev/null
+++ b/11. NinjaApi - IntegrationTesting/src/ForEvolve.Blog.Samples.NinjaApi/Repositories/IClanRepository.cs
@@ -0,0 +1,16 @@
+using ForEvolve.Blog.Samples.NinjaApi.Models;
+using System.Collections.Generic;
+using System.Linq;
+using System.Threading.Tasks;
+
+namespace ForEvolve.Blog.Samples.NinjaApi.Repositories
+{
+ public interface IClanRepository
+ {
+ Task> ReadAllAsync();
+ Task ReadOneAsync(string clanName);
+ Task CreateAsync(Clan clan);
+ Task UpdateAsync(Clan clan);
+ Task DeleteAsync(string clanName);
+ }
+}
diff --git a/11. NinjaApi - IntegrationTesting/src/ForEvolve.Blog.Samples.NinjaApi/Repositories/INinjaRepository.cs b/11. NinjaApi - IntegrationTesting/src/ForEvolve.Blog.Samples.NinjaApi/Repositories/INinjaRepository.cs
new file mode 100644
index 0000000..3b6fee5
--- /dev/null
+++ b/11. NinjaApi - IntegrationTesting/src/ForEvolve.Blog.Samples.NinjaApi/Repositories/INinjaRepository.cs
@@ -0,0 +1,17 @@
+using ForEvolve.Blog.Samples.NinjaApi.Models;
+using System.Collections.Generic;
+using System.Linq;
+using System.Threading.Tasks;
+
+namespace ForEvolve.Blog.Samples.NinjaApi.Repositories
+{
+ public interface INinjaRepository
+ {
+ Task> ReadAllAsync();
+ Task> ReadAllInClanAsync(string clanName);
+ Task ReadOneAsync(string clanName, string ninjaKey);
+ Task CreateAsync(Ninja ninja);
+ Task UpdateAsync(Ninja ninja);
+ Task DeleteAsync(string clanName, string ninjaKey);
+ }
+}
diff --git a/11. NinjaApi - IntegrationTesting/src/ForEvolve.Blog.Samples.NinjaApi/Repositories/NinjaRepository.cs b/11. NinjaApi - IntegrationTesting/src/ForEvolve.Blog.Samples.NinjaApi/Repositories/NinjaRepository.cs
new file mode 100644
index 0000000..747e861
--- /dev/null
+++ b/11. NinjaApi - IntegrationTesting/src/ForEvolve.Blog.Samples.NinjaApi/Repositories/NinjaRepository.cs
@@ -0,0 +1,66 @@
+using ForEvolve.Azure.Storage.Table;
+using ForEvolve.Blog.Samples.NinjaApi.Models;
+using ForEvolve.Blog.Samples.NinjaApi.Services;
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Threading.Tasks;
+
+namespace ForEvolve.Blog.Samples.NinjaApi.Repositories
+{
+ public class NinjaRepository : INinjaRepository
+ {
+ private readonly INinjaMappingService _ninjaMappingService;
+ private readonly ITableStorageRepository _ninjaEntityTableStorageRepository;
+
+ public NinjaRepository(INinjaMappingService ninjaMappingService, ITableStorageRepository ninjaEntityTableStorageRepository)
+ {
+ _ninjaMappingService = ninjaMappingService ?? throw new ArgumentNullException(nameof(ninjaMappingService));
+ _ninjaEntityTableStorageRepository = ninjaEntityTableStorageRepository ?? throw new ArgumentNullException(nameof(ninjaEntityTableStorageRepository));
+ }
+
+ public async Task CreateAsync(Ninja ninja)
+ {
+ var entityToCreate = _ninjaMappingService.Map(ninja);
+ var createdEntity = await _ninjaEntityTableStorageRepository.InsertOrReplaceAsync(entityToCreate);
+ var createNinja = _ninjaMappingService.Map(createdEntity);
+ return createNinja;
+ }
+
+ public async Task DeleteAsync(string clanName, string ninjaKey)
+ {
+ var deletedEntity = await _ninjaEntityTableStorageRepository.DeleteOneAsync(clanName, ninjaKey);
+ var deletedNinja = _ninjaMappingService.Map(deletedEntity);
+ return deletedNinja;
+ }
+
+ public async Task> ReadAllAsync()
+ {
+ var entities = await _ninjaEntityTableStorageRepository.ReadAllAsync();
+ var ninja = _ninjaMappingService.Map(entities);
+ return ninja;
+ }
+
+ public async Task> ReadAllInClanAsync(string clanName)
+ {
+ var entities = await _ninjaEntityTableStorageRepository.ReadPartitionAsync(clanName);
+ var ninja = _ninjaMappingService.Map(entities);
+ return ninja;
+ }
+
+ public async Task ReadOneAsync(string clanName, string ninjaKey)
+ {
+ var entity = await _ninjaEntityTableStorageRepository.ReadOneAsync(clanName, ninjaKey);
+ var ninja = _ninjaMappingService.Map(entity);
+ return ninja;
+ }
+
+ public async Task UpdateAsync(Ninja ninja)
+ {
+ var entityToUpdate = _ninjaMappingService.Map(ninja);
+ var updatedEntity = await _ninjaEntityTableStorageRepository.InsertOrMergeAsync(entityToUpdate);
+ var updatedNinja = _ninjaMappingService.Map(updatedEntity);
+ return updatedNinja;
+ }
+ }
+}
diff --git a/11. NinjaApi - IntegrationTesting/src/ForEvolve.Blog.Samples.NinjaApi/Services/ClanService.cs b/11. NinjaApi - IntegrationTesting/src/ForEvolve.Blog.Samples.NinjaApi/Services/ClanService.cs
new file mode 100644
index 0000000..d6b6118
--- /dev/null
+++ b/11. NinjaApi - IntegrationTesting/src/ForEvolve.Blog.Samples.NinjaApi/Services/ClanService.cs
@@ -0,0 +1,49 @@
+using ForEvolve.Blog.Samples.NinjaApi.Models;
+using ForEvolve.Blog.Samples.NinjaApi.Repositories;
+using System;
+using System.Collections.Generic;
+using System.Threading.Tasks;
+
+namespace ForEvolve.Blog.Samples.NinjaApi.Services
+{
+ public class ClanService : IClanService
+ {
+ private IClanRepository _clanRepository;
+
+ public ClanService(IClanRepository clanRepository)
+ {
+ _clanRepository = clanRepository ?? throw new ArgumentNullException(nameof(clanRepository));
+ }
+
+ public Task> ReadAllAsync()
+ {
+ return _clanRepository.ReadAllAsync();
+ }
+
+ public Task ReadOneAsync(string clanName)
+ {
+ return _clanRepository.ReadOneAsync(clanName);
+ }
+
+ public async Task IsClanExistsAsync(string clanName)
+ {
+ var clan = await _clanRepository.ReadOneAsync(clanName);
+ return clan != null;
+ }
+
+ public Task CreateAsync(Clan clan)
+ {
+ throw new NotSupportedException();
+ }
+
+ public Task UpdateAsync(Clan clan)
+ {
+ throw new NotSupportedException();
+ }
+
+ public Task DeleteAsync(string clanName)
+ {
+ throw new NotSupportedException();
+ }
+ }
+}
diff --git a/11. NinjaApi - IntegrationTesting/src/ForEvolve.Blog.Samples.NinjaApi/Services/IClanService.cs b/11. NinjaApi - IntegrationTesting/src/ForEvolve.Blog.Samples.NinjaApi/Services/IClanService.cs
new file mode 100644
index 0000000..54843b9
--- /dev/null
+++ b/11. NinjaApi - IntegrationTesting/src/ForEvolve.Blog.Samples.NinjaApi/Services/IClanService.cs
@@ -0,0 +1,17 @@
+using ForEvolve.Blog.Samples.NinjaApi.Models;
+using System.Collections.Generic;
+using System.Linq;
+using System.Threading.Tasks;
+
+namespace ForEvolve.Blog.Samples.NinjaApi.Services
+{
+ public interface IClanService
+ {
+ Task> ReadAllAsync();
+ Task ReadOneAsync(string clanName);
+ Task IsClanExistsAsync(string clanName);
+ Task CreateAsync(Clan clan);
+ Task UpdateAsync(Clan clan);
+ Task DeleteAsync(string clanName);
+ }
+}
diff --git a/11. NinjaApi - IntegrationTesting/src/ForEvolve.Blog.Samples.NinjaApi/Services/INinjaMappingService.cs b/11. NinjaApi - IntegrationTesting/src/ForEvolve.Blog.Samples.NinjaApi/Services/INinjaMappingService.cs
new file mode 100644
index 0000000..5186991
--- /dev/null
+++ b/11. NinjaApi - IntegrationTesting/src/ForEvolve.Blog.Samples.NinjaApi/Services/INinjaMappingService.cs
@@ -0,0 +1,13 @@
+using ForEvolve.Blog.Samples.NinjaApi.Mappers;
+using ForEvolve.Blog.Samples.NinjaApi.Models;
+using System.Collections.Generic;
+using System.Linq;
+using System.Threading.Tasks;
+
+namespace ForEvolve.Blog.Samples.NinjaApi.Services
+{
+ public interface INinjaMappingService : IMapper, IMapper, IMapper, IEnumerable>
+ {
+ }
+}
+
diff --git a/11. NinjaApi - IntegrationTesting/src/ForEvolve.Blog.Samples.NinjaApi/Services/INinjaService.cs b/11. NinjaApi - IntegrationTesting/src/ForEvolve.Blog.Samples.NinjaApi/Services/INinjaService.cs
new file mode 100644
index 0000000..841069d
--- /dev/null
+++ b/11. NinjaApi - IntegrationTesting/src/ForEvolve.Blog.Samples.NinjaApi/Services/INinjaService.cs
@@ -0,0 +1,17 @@
+using ForEvolve.Blog.Samples.NinjaApi.Models;
+using System.Collections.Generic;
+using System.Linq;
+using System.Threading.Tasks;
+
+namespace ForEvolve.Blog.Samples.NinjaApi.Services
+{
+ public interface INinjaService
+ {
+ Task> ReadAllAsync();
+ Task> ReadAllInClanAsync(string clanName);
+ Task ReadOneAsync(string clanName, string ninjaKey);
+ Task CreateAsync(Ninja ninja);
+ Task UpdateAsync(Ninja ninja);
+ Task DeleteAsync(string clanName, string ninjaKey);
+ }
+}
diff --git a/11. NinjaApi - IntegrationTesting/src/ForEvolve.Blog.Samples.NinjaApi/Services/NinjaMappingService.cs b/11. NinjaApi - IntegrationTesting/src/ForEvolve.Blog.Samples.NinjaApi/Services/NinjaMappingService.cs
new file mode 100644
index 0000000..25aefbf
--- /dev/null
+++ b/11. NinjaApi - IntegrationTesting/src/ForEvolve.Blog.Samples.NinjaApi/Services/NinjaMappingService.cs
@@ -0,0 +1,40 @@
+using ForEvolve.Blog.Samples.NinjaApi.Mappers;
+using ForEvolve.Blog.Samples.NinjaApi.Models;
+using System;
+using System.Collections.Generic;
+
+namespace ForEvolve.Blog.Samples.NinjaApi.Services
+{
+ public class NinjaMappingService : INinjaMappingService
+ {
+ private readonly IMapper _ninjaToNinjaEntityMapper;
+ private readonly IMapper _ninjaEntityToNinjaMapper;
+ private readonly IMapper, IEnumerable> _ninjaEntityEnumerableToNinjaMapper;
+
+ public NinjaMappingService(
+ IMapper ninjaToNinjaEntityMapper,
+ IMapper ninjaEntityToNinjaMapper,
+ IMapper, IEnumerable> ninjaEntityEnumerableToNinjaMapper
+ )
+ {
+ _ninjaToNinjaEntityMapper = ninjaToNinjaEntityMapper ?? throw new ArgumentNullException(nameof(ninjaToNinjaEntityMapper));
+ _ninjaEntityToNinjaMapper = ninjaEntityToNinjaMapper ?? throw new ArgumentNullException(nameof(ninjaEntityToNinjaMapper));
+ _ninjaEntityEnumerableToNinjaMapper = ninjaEntityEnumerableToNinjaMapper ?? throw new ArgumentNullException(nameof(ninjaEntityEnumerableToNinjaMapper));
+ }
+
+ public NinjaEntity Map(Ninja entity)
+ {
+ return _ninjaToNinjaEntityMapper.Map(entity);
+ }
+
+ public Ninja Map(NinjaEntity entity)
+ {
+ return _ninjaEntityToNinjaMapper.Map(entity);
+ }
+
+ public IEnumerable Map(IEnumerable entities)
+ {
+ return _ninjaEntityEnumerableToNinjaMapper.Map(entities);
+ }
+ }
+}
diff --git a/11. NinjaApi - IntegrationTesting/src/ForEvolve.Blog.Samples.NinjaApi/Services/NinjaService.cs b/11. NinjaApi - IntegrationTesting/src/ForEvolve.Blog.Samples.NinjaApi/Services/NinjaService.cs
new file mode 100644
index 0000000..2e96b1f
--- /dev/null
+++ b/11. NinjaApi - IntegrationTesting/src/ForEvolve.Blog.Samples.NinjaApi/Services/NinjaService.cs
@@ -0,0 +1,78 @@
+using ForEvolve.Blog.Samples.NinjaApi.Models;
+using ForEvolve.Blog.Samples.NinjaApi.Repositories;
+using System;
+using System.Collections.Generic;
+using System.Threading.Tasks;
+
+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 async Task CreateAsync(Ninja ninja)
+ {
+ await EnforceClanExistenceAsync(ninja.Clan.Name);
+ var createdNinja = await _ninjaRepository.CreateAsync(ninja);
+ return createdNinja;
+ }
+
+ public async Task DeleteAsync(string clanName, string ninjaKey)
+ {
+ await EnforceNinjaExistenceAsync(clanName, ninjaKey);
+ var deletedNinja = await _ninjaRepository.DeleteAsync(clanName, ninjaKey);
+ return deletedNinja;
+ }
+
+ public Task> ReadAllAsync()
+ {
+ return _ninjaRepository.ReadAllAsync();
+ }
+
+ public async Task> ReadAllInClanAsync(string clanName)
+ {
+ await EnforceClanExistenceAsync(clanName);
+ return await _ninjaRepository.ReadAllInClanAsync(clanName);
+ }
+
+ public async Task ReadOneAsync(string clanName, string ninjaKey)
+ {
+ var ninja = await EnforceNinjaExistenceAsync(clanName, ninjaKey);
+ return ninja;
+ }
+
+ public async Task UpdateAsync(Ninja ninja)
+ {
+ await EnforceClanExistenceAsync(ninja.Clan.Name);
+ await EnforceNinjaExistenceAsync(ninja.Clan.Name, ninja.Key);
+ var updatedNinja = await _ninjaRepository.UpdateAsync(ninja);
+ return updatedNinja;
+ }
+
+ private async Task EnforceClanExistenceAsync(string clanName)
+ {
+ var clanExist = await _clanService.IsClanExistsAsync(clanName);
+ if (!clanExist)
+ {
+ throw new ClanNotFoundException(clanName);
+ }
+ }
+
+ private async Task EnforceNinjaExistenceAsync(string clanName, string ninjaKey)
+ {
+ var remoteNinja = await _ninjaRepository.ReadOneAsync(clanName, ninjaKey);
+ if (remoteNinja == null)
+ {
+ throw new NinjaNotFoundException(clanName, ninjaKey);
+ }
+ return remoteNinja;
+ }
+ }
+}
diff --git a/11. NinjaApi - IntegrationTesting/src/ForEvolve.Blog.Samples.NinjaApi/Startup.cs b/11. NinjaApi - IntegrationTesting/src/ForEvolve.Blog.Samples.NinjaApi/Startup.cs
new file mode 100644
index 0000000..9ce0cc9
--- /dev/null
+++ b/11. NinjaApi - IntegrationTesting/src/ForEvolve.Blog.Samples.NinjaApi/Startup.cs
@@ -0,0 +1,70 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Threading.Tasks;
+using Microsoft.AspNetCore.Builder;
+using Microsoft.AspNetCore.Hosting;
+using Microsoft.Extensions.Configuration;
+using Microsoft.Extensions.DependencyInjection;
+using Microsoft.Extensions.Logging;
+using Microsoft.Extensions.Options;
+using Microsoft.Extensions.DependencyInjection.Extensions;
+using ForEvolve.Blog.Samples.NinjaApi.Services;
+using ForEvolve.Blog.Samples.NinjaApi.Repositories;
+using ForEvolve.Blog.Samples.NinjaApi.Models;
+using ForEvolve.Blog.Samples.NinjaApi.Mappers;
+using ForEvolve.Azure.Storage.Table;
+
+namespace ForEvolve.Blog.Samples.NinjaApi
+{
+ public class Startup
+ {
+ public Startup(IConfiguration configuration)
+ {
+ Configuration = configuration;
+ }
+
+ public IConfiguration Configuration { get; }
+
+ // This method gets called by the runtime. Use this method to add services to the container.
+ public void ConfigureServices(IServiceCollection services)
+ {
+ // Clans
+ services.TryAddSingleton();
+ services.TryAddSingleton();
+ services.TryAddSingleton>(new Clan[]{
+ new Clan { Name = "Iga" },
+ new Clan { Name = "Kōga" },
+ });
+
+ // Mappers
+ services.TryAddSingleton, NinjaToNinjaEntityMapper>();
+ services.TryAddSingleton, NinjaEntityToNinjaMapper>();
+ services.TryAddSingleton, IEnumerable>, EnumerableMapper>();
+
+ // Ninja
+ services.TryAddSingleton();
+ services.TryAddSingleton();
+ services.TryAddSingleton();
+
+ // ForEvolve.Azure
+ services.TryAddSingleton, TableStorageRepository>();
+ services.TryAddSingleton