diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..530220b
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,12 @@
+/AccountOwnerServer/obj
+/AccountOwnerServer/bin
+/Contracts/obj
+/Contracts/bin
+/Entities/obj
+/Entities/bin
+/LoggerService/obj
+/LoggerService/bin
+/Repository/obj
+/Repository/bin
+/.vs
+/AccountOwnerServer/*.user
diff --git a/AccountOwnerServer.sln b/AccountOwnerServer.sln
new file mode 100644
index 0000000..297e2e6
--- /dev/null
+++ b/AccountOwnerServer.sln
@@ -0,0 +1,49 @@
+
+Microsoft Visual Studio Solution File, Format Version 12.00
+# Visual Studio Version 16
+VisualStudioVersion = 16.0.29318.209
+MinimumVisualStudioVersion = 10.0.40219.1
+Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "AccountOwnerServer", "AccountOwnerServer\AccountOwnerServer.csproj", "{D6116708-5D20-43C2-A796-9716AC8885F3}"
+EndProject
+Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Contracts", "Contracts\Contracts.csproj", "{C6A5877A-BCBF-487C-B9EE-F678F921D3F2}"
+EndProject
+Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "LoggerService", "LoggerService\LoggerService.csproj", "{8F7CB858-3196-4D0C-A3DB-A850443CA462}"
+EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Entities", "Entities\Entities.csproj", "{CA018972-3651-4802-9930-69E855892B7A}"
+EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Repository", "Repository\Repository.csproj", "{A1DEC662-9D40-45E3-A8DE-22AF6A1BE970}"
+EndProject
+Global
+ GlobalSection(SolutionConfigurationPlatforms) = preSolution
+ Debug|Any CPU = Debug|Any CPU
+ Release|Any CPU = Release|Any CPU
+ EndGlobalSection
+ GlobalSection(ProjectConfigurationPlatforms) = postSolution
+ {D6116708-5D20-43C2-A796-9716AC8885F3}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {D6116708-5D20-43C2-A796-9716AC8885F3}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {D6116708-5D20-43C2-A796-9716AC8885F3}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {D6116708-5D20-43C2-A796-9716AC8885F3}.Release|Any CPU.Build.0 = Release|Any CPU
+ {C6A5877A-BCBF-487C-B9EE-F678F921D3F2}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {C6A5877A-BCBF-487C-B9EE-F678F921D3F2}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {C6A5877A-BCBF-487C-B9EE-F678F921D3F2}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {C6A5877A-BCBF-487C-B9EE-F678F921D3F2}.Release|Any CPU.Build.0 = Release|Any CPU
+ {8F7CB858-3196-4D0C-A3DB-A850443CA462}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {8F7CB858-3196-4D0C-A3DB-A850443CA462}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {8F7CB858-3196-4D0C-A3DB-A850443CA462}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {8F7CB858-3196-4D0C-A3DB-A850443CA462}.Release|Any CPU.Build.0 = Release|Any CPU
+ {CA018972-3651-4802-9930-69E855892B7A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {CA018972-3651-4802-9930-69E855892B7A}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {CA018972-3651-4802-9930-69E855892B7A}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {CA018972-3651-4802-9930-69E855892B7A}.Release|Any CPU.Build.0 = Release|Any CPU
+ {A1DEC662-9D40-45E3-A8DE-22AF6A1BE970}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {A1DEC662-9D40-45E3-A8DE-22AF6A1BE970}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {A1DEC662-9D40-45E3-A8DE-22AF6A1BE970}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {A1DEC662-9D40-45E3-A8DE-22AF6A1BE970}.Release|Any CPU.Build.0 = Release|Any CPU
+ EndGlobalSection
+ GlobalSection(SolutionProperties) = preSolution
+ HideSolutionNode = FALSE
+ EndGlobalSection
+ GlobalSection(ExtensibilityGlobals) = postSolution
+ SolutionGuid = {9552CCB3-10D6-4D12-8C27-FDD7440E23EB}
+ EndGlobalSection
+EndGlobal
diff --git a/AccountOwnerServer/AccountOwnerServer.csproj b/AccountOwnerServer/AccountOwnerServer.csproj
new file mode 100644
index 0000000..7ab1c71
--- /dev/null
+++ b/AccountOwnerServer/AccountOwnerServer.csproj
@@ -0,0 +1,21 @@
+
+
+
+ netcoreapp3.0
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/AccountOwnerServer/Controllers/AccountController.cs b/AccountOwnerServer/Controllers/AccountController.cs
new file mode 100644
index 0000000..6b21f1d
--- /dev/null
+++ b/AccountOwnerServer/Controllers/AccountController.cs
@@ -0,0 +1,117 @@
+using AccountOwnerServer.Filters;
+using Contracts;
+using Entities.Models;
+using Microsoft.AspNetCore.Mvc;
+using Microsoft.AspNetCore.Routing;
+using Microsoft.Net.Http.Headers;
+using Newtonsoft.Json;
+using System;
+using System.Collections.Generic;
+using System.Linq;
+
+namespace AccountOwnerServer.Controllers
+{
+ [Route("api/owners/{ownerId}/accounts")]
+ [ApiController]
+ public class AccountController : ControllerBase
+ {
+ private ILoggerManager _logger;
+ private IRepositoryWrapper _repository;
+ private readonly LinkGenerator _linkGenerator;
+
+ public AccountController(ILoggerManager logger,
+ IRepositoryWrapper repository,
+ LinkGenerator linkGenerator)
+ {
+ _logger = logger;
+ _repository = repository;
+ _linkGenerator = linkGenerator;
+ }
+
+ [HttpGet]
+ [ServiceFilter(typeof(ValidateMediaTypeAttribute))]
+ public IActionResult GetAccountsForOwner(Guid ownerId, [FromQuery] AccountParameters parameters)
+ {
+ var accounts = _repository.Account.GetAccountsByOwner(ownerId, parameters);
+
+ var metadata = new
+ {
+ accounts.TotalCount,
+ accounts.PageSize,
+ accounts.CurrentPage,
+ accounts.TotalPages,
+ accounts.HasNext,
+ accounts.HasPrevious
+ };
+
+ Response.Headers.Add("X-Pagination", JsonConvert.SerializeObject(metadata));
+
+ _logger.LogInfo($"Returned {accounts.TotalCount} accounts from database.");
+
+ var shapedAccounts = accounts.Select(o => o.Entity).ToList();
+
+ var mediaType = (MediaTypeHeaderValue)HttpContext.Items["AcceptHeaderMediaType"];
+
+ if (!mediaType.SubTypeWithoutSuffix.EndsWith("hateoas", StringComparison.InvariantCultureIgnoreCase))
+ {
+ return Ok(shapedAccounts);
+ }
+
+ for (var index = 0; index < accounts.Count(); index++)
+ {
+ var accountLinks = CreateLinksForAccount(ownerId, accounts[index].Id, parameters.Fields);
+ shapedAccounts[index].Add("Links", accountLinks);
+ }
+
+ var accountsWrapper = new LinkCollectionWrapper(shapedAccounts);
+
+ return Ok(CreateLinksForAccounts(accountsWrapper));
+ }
+
+ [HttpGet("{id}")]
+ [ServiceFilter(typeof(ValidateMediaTypeAttribute))]
+ public IActionResult GetAccountForOwner(Guid ownerId, Guid id, [FromQuery] string fields)
+ {
+ var account = _repository.Account.GetAccountByOwner(ownerId, id, fields);
+
+ if (account.Id == Guid.Empty)
+ {
+ _logger.LogError($"Account with id: {id}, hasn't been found in db.");
+ return NotFound();
+ }
+
+ var mediaType = (MediaTypeHeaderValue)HttpContext.Items["AcceptHeaderMediaType"];
+
+ if (!mediaType.SubTypeWithoutSuffix.EndsWith("hateoas", StringComparison.InvariantCultureIgnoreCase))
+ {
+ _logger.LogInfo($"Returned a shaped account with id: {id}");
+ return Ok(account.Entity);
+ }
+
+ account.Entity.Add("Links", CreateLinksForAccount(account.Id, id, fields));
+
+ return Ok(account.Entity);
+ }
+
+ private List CreateLinksForAccount(Guid ownerId, Guid id, string fields = "")
+ {
+ var links = new List
+ {
+ new Link(_linkGenerator.GetUriByAction(HttpContext, nameof(GetAccountForOwner), values: new { ownerId, id, fields }),
+ "self",
+ "GET"),
+ };
+
+ return links;
+ }
+
+ private LinkCollectionWrapper CreateLinksForAccounts(LinkCollectionWrapper accountsWrapper)
+ {
+ accountsWrapper.Links.Add(new Link(_linkGenerator.GetUriByAction(HttpContext, nameof(GetAccountsForOwner), values: new { }),
+ "self",
+ "GET"));
+
+ return accountsWrapper;
+ }
+ }
+}
diff --git a/AccountOwnerServer/Controllers/OwnerController.cs b/AccountOwnerServer/Controllers/OwnerController.cs
new file mode 100644
index 0000000..3caab07
--- /dev/null
+++ b/AccountOwnerServer/Controllers/OwnerController.cs
@@ -0,0 +1,197 @@
+using AccountOwnerServer.Filters;
+using Contracts;
+using Entities.Extensions;
+using Entities.Helpers;
+using Entities.Models;
+using Microsoft.AspNetCore.Mvc;
+using Microsoft.AspNetCore.Routing;
+using Microsoft.Net.Http.Headers;
+using Newtonsoft.Json;
+using System;
+using System.Collections.Generic;
+using System.Linq;
+
+namespace AccountOwnerServer.Controllers
+{
+ [Route("api/owners")]
+ [ApiController]
+ public class OwnerController : ControllerBase
+ {
+ private ILoggerManager _logger;
+ private IRepositoryWrapper _repository;
+ private LinkGenerator _linkGenerator;
+
+ public OwnerController(ILoggerManager logger,
+ IRepositoryWrapper repository,
+ LinkGenerator linkGenerator)
+ {
+ _logger = logger;
+ _repository = repository;
+ _linkGenerator = linkGenerator;
+ }
+
+ [HttpGet]
+ [ServiceFilter(typeof(ValidateMediaTypeAttribute))]
+ public IActionResult GetOwners([FromQuery] OwnerParameters ownerParameters)
+ {
+ if (!ownerParameters.ValidYearRange)
+ {
+ return BadRequest("Max year of birth cannot be less than min year of birth");
+ }
+
+ var owners = _repository.Owner.GetOwners(ownerParameters);
+
+ var metadata = new
+ {
+ owners.TotalCount,
+ owners.PageSize,
+ owners.CurrentPage,
+ owners.TotalPages,
+ owners.HasNext,
+ owners.HasPrevious
+ };
+
+ Response.Headers.Add("X-Pagination", JsonConvert.SerializeObject(metadata));
+
+ _logger.LogInfo($"Returned {owners.TotalCount} owners from database.");
+
+ var shapedOwners = owners.Select(o => o.Entity).ToList();
+
+ var mediaType = (MediaTypeHeaderValue)HttpContext.Items["AcceptHeaderMediaType"];
+
+ if (!mediaType.SubTypeWithoutSuffix.EndsWith("hateoas", StringComparison.InvariantCultureIgnoreCase))
+ {
+ return Ok(shapedOwners);
+ }
+
+ for (var index = 0; index < owners.Count(); index++)
+ {
+ var ownerLinks = CreateLinksForOwner(owners[index].Id, ownerParameters.Fields);
+ shapedOwners[index].Add("Links", ownerLinks);
+ }
+
+ var ownersWrapper = new LinkCollectionWrapper(shapedOwners);
+
+ return Ok(CreateLinksForOwners(ownersWrapper));
+ }
+
+ [HttpGet("{id}", Name = "OwnerById")]
+ [ServiceFilter(typeof(ValidateMediaTypeAttribute))]
+ public IActionResult GetOwnerById(Guid id, [FromQuery] string fields)
+ {
+ var owner = _repository.Owner.GetOwnerById(id, fields);
+
+ if (owner.Id == Guid.Empty)
+ {
+ _logger.LogError($"Owner with id: {id}, hasn't been found in db.");
+ return NotFound();
+ }
+
+ var mediaType = (MediaTypeHeaderValue)HttpContext.Items["AcceptHeaderMediaType"];
+
+ if (!mediaType.SubTypeWithoutSuffix.EndsWith("hateoas", StringComparison.InvariantCultureIgnoreCase))
+ {
+ _logger.LogInfo($"Returned shaped owner with id: {id}");
+ return Ok(owner.Entity);
+ }
+
+ owner.Entity.Add("Links", CreateLinksForOwner(owner.Id, fields));
+
+ return Ok(owner.Entity);
+ }
+
+ [HttpPost]
+ public IActionResult CreateOwner([FromBody]Owner owner)
+ {
+ if (owner.IsObjectNull())
+ {
+ _logger.LogError("Owner object sent from client is null.");
+ return BadRequest("Owner object is null");
+ }
+
+ if (!ModelState.IsValid)
+ {
+ _logger.LogError("Invalid owner object sent from client.");
+ return BadRequest("Invalid model object");
+ }
+
+ _repository.Owner.CreateOwner(owner);
+ _repository.Save();
+
+ return CreatedAtRoute("OwnerById", new { id = owner.Id }, owner);
+ }
+
+ [HttpPut("{id}")]
+ public IActionResult UpdateOwner(Guid id, [FromBody]Owner owner)
+ {
+ if (owner.IsObjectNull())
+ {
+ _logger.LogError("Owner object sent from client is null.");
+ return BadRequest("Owner object is null");
+ }
+
+ if (!ModelState.IsValid)
+ {
+ _logger.LogError("Invalid owner object sent from client.");
+ return BadRequest("Invalid model object");
+ }
+
+ var dbOwner = _repository.Owner.GetOwnerById(id);
+ if (dbOwner.IsEmptyObject())
+ {
+ _logger.LogError($"Owner with id: {id}, hasn't been found in db.");
+ return NotFound();
+ }
+
+ _repository.Owner.UpdateOwner(dbOwner, owner);
+ _repository.Save();
+
+ return NoContent();
+ }
+
+ [HttpDelete("{id}")]
+ public IActionResult DeleteOwner(Guid id)
+ {
+ var owner = _repository.Owner.GetOwnerById(id);
+ if (owner.IsEmptyObject())
+ {
+ _logger.LogError($"Owner with id: {id}, hasn't been found in db.");
+ return NotFound();
+ }
+
+ _repository.Owner.DeleteOwner(owner);
+ _repository.Save();
+
+ return NoContent();
+ }
+
+ private IEnumerable CreateLinksForOwner(Guid id, string fields = "")
+ {
+ var links = new List
+ {
+ new Link(_linkGenerator.GetUriByAction(HttpContext, nameof(GetOwnerById), values: new { id, fields }),
+ "self",
+ "GET"),
+
+ new Link(_linkGenerator.GetUriByAction(HttpContext, nameof(DeleteOwner), values: new { id }),
+ "delete_owner",
+ "DELETE"),
+
+ new Link(_linkGenerator.GetUriByAction(HttpContext, nameof(UpdateOwner), values: new { id }),
+ "update_owner",
+ "PUT")
+ };
+
+ return links;
+ }
+
+ private LinkCollectionWrapper CreateLinksForOwners(LinkCollectionWrapper ownersWrapper)
+ {
+ ownersWrapper.Links.Add(new Link(_linkGenerator.GetUriByAction(HttpContext, nameof(GetOwners), values: new { }),
+ "self",
+ "GET"));
+
+ return ownersWrapper;
+ }
+ }
+}
\ No newline at end of file
diff --git a/AccountOwnerServer/Extensions/ServiceExtensions.cs b/AccountOwnerServer/Extensions/ServiceExtensions.cs
new file mode 100644
index 0000000..6f07bcc
--- /dev/null
+++ b/AccountOwnerServer/Extensions/ServiceExtensions.cs
@@ -0,0 +1,89 @@
+using AccountOwnerServer.Filters;
+using Contracts;
+using Entities;
+using Entities.Helpers;
+using Entities.Models;
+using LoggerService;
+using Microsoft.AspNetCore.Builder;
+using Microsoft.AspNetCore.Mvc;
+using Microsoft.AspNetCore.Mvc.Formatters;
+using Microsoft.EntityFrameworkCore;
+using Microsoft.Extensions.Configuration;
+using Microsoft.Extensions.DependencyInjection;
+using Repository;
+using System.Linq;
+
+namespace AccountOwnerServer.Extensions
+{
+ public static class ServiceExtensions
+ {
+ public static void ConfigureCors(this IServiceCollection services)
+ {
+ services.AddCors(options =>
+ {
+ options.AddPolicy("CorsPolicy",
+ builder => builder.WithOrigins("http://localhost:5000", "https://localhost:5001")
+ .AllowAnyMethod()
+ .AllowAnyHeader()
+ .AllowCredentials());
+ });
+ }
+
+ public static void ConfigureIISIntegration(this IServiceCollection services)
+ {
+ services.Configure(options =>
+ {
+
+ });
+ }
+
+ public static void ConfigureLoggerService(this IServiceCollection services)
+ {
+ services.AddSingleton();
+ }
+
+ public static void ConfigureMySqlContext(this IServiceCollection services, IConfiguration config)
+ {
+ var connectionString = config["mysqlconnection:connectionString"];
+ services.AddDbContext(o => o.UseMySql(connectionString));
+ }
+
+ public static void ConfigureRepositoryWrapper(this IServiceCollection services)
+ {
+ services.AddScoped, SortHelper>();
+ services.AddScoped, SortHelper>();
+
+ services.AddScoped, DataShaper>();
+ services.AddScoped, DataShaper>();
+
+ services.AddScoped();
+ }
+
+ public static void RegisterFilters(this IServiceCollection services)
+ {
+ services.AddScoped();
+ }
+
+ public static void AddCustomMediaTypes(this IServiceCollection services)
+ {
+ services.Configure(config =>
+ {
+ var newtonsoftJsonOutputFormatter = config.OutputFormatters
+ .OfType()?.FirstOrDefault();
+
+ if (newtonsoftJsonOutputFormatter != null)
+ {
+ newtonsoftJsonOutputFormatter.SupportedMediaTypes.Add("application/vnd.codemaze.hateoas+json");
+ }
+
+ var xmlOutputFormatter = config.OutputFormatters
+ .OfType()?.FirstOrDefault();
+
+ if (xmlOutputFormatter != null)
+ {
+ xmlOutputFormatter.SupportedMediaTypes.Add("application/vnd.codemaze.hateoas+xml");
+ }
+ });
+ }
+ }
+}
diff --git a/AccountOwnerServer/Filters/ValidateMediaTypeAttribute.cs b/AccountOwnerServer/Filters/ValidateMediaTypeAttribute.cs
new file mode 100644
index 0000000..d365dca
--- /dev/null
+++ b/AccountOwnerServer/Filters/ValidateMediaTypeAttribute.cs
@@ -0,0 +1,36 @@
+using Microsoft.AspNetCore.Mvc;
+using Microsoft.AspNetCore.Mvc.Filters;
+using Microsoft.Net.Http.Headers;
+using System.Linq;
+
+namespace AccountOwnerServer.Filters
+{
+ public class ValidateMediaTypeAttribute : IActionFilter
+ {
+ public void OnActionExecuting(ActionExecutingContext context)
+ {
+ var acceptHeaderPresent = context.HttpContext.Request.Headers.ContainsKey("Accept");
+
+ if (!acceptHeaderPresent)
+ {
+ context.Result = new BadRequestObjectResult($"Accept header is missing.");
+ return;
+ }
+
+ var mediaType = context.HttpContext.Request.Headers["Accept"].FirstOrDefault();
+
+ if (!MediaTypeHeaderValue.TryParse(mediaType, out MediaTypeHeaderValue outMediaType))
+ {
+ context.Result = new BadRequestObjectResult($"Media type not present. Please add Accept header with the required media type.");
+ return;
+ }
+
+ context.HttpContext.Items.Add("AcceptHeaderMediaType", outMediaType);
+ }
+
+ public void OnActionExecuted(ActionExecutedContext context)
+ {
+
+ }
+ }
+}
\ No newline at end of file
diff --git a/AccountOwnerServer/Program.cs b/AccountOwnerServer/Program.cs
new file mode 100644
index 0000000..5baa7d0
--- /dev/null
+++ b/AccountOwnerServer/Program.cs
@@ -0,0 +1,24 @@
+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 AccountOwnerServer
+{
+ public class Program
+ {
+ public static void Main(string[] args)
+ {
+ CreateWebHostBuilder(args).Build().Run();
+ }
+
+ public static IWebHostBuilder CreateWebHostBuilder(string[] args) =>
+ WebHost.CreateDefaultBuilder(args)
+ .UseStartup();
+ }
+}
diff --git a/AccountOwnerServer/Properties/launchSettings.json b/AccountOwnerServer/Properties/launchSettings.json
new file mode 100644
index 0000000..7c8de30
--- /dev/null
+++ b/AccountOwnerServer/Properties/launchSettings.json
@@ -0,0 +1,28 @@
+{
+ "$schema": "http://json.schemastore.org/launchsettings.json",
+ "iisSettings": {
+ "windowsAuthentication": false,
+ "anonymousAuthentication": true,
+ "iisExpress": {
+ "applicationUrl": "http://localhost:5000",
+ "sslPort": 0
+ }
+ },
+ "profiles": {
+ "IIS Express": {
+ "commandName": "IISExpress",
+ "launchBrowser": false,
+ "environmentVariables": {
+ "ASPNETCORE_ENVIRONMENT": "Development"
+ }
+ },
+ "AccountOwnerServer": {
+ "commandName": "Project",
+ "launchBrowser": false,
+ "applicationUrl": "https://localhost:5001;http://localhost:5000",
+ "environmentVariables": {
+ "ASPNETCORE_ENVIRONMENT": "Development"
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/AccountOwnerServer/Startup.cs b/AccountOwnerServer/Startup.cs
new file mode 100644
index 0000000..efc4590
--- /dev/null
+++ b/AccountOwnerServer/Startup.cs
@@ -0,0 +1,106 @@
+using AccountOwnerServer.Extensions;
+using Microsoft.AspNetCore.Builder;
+using Microsoft.AspNetCore.Diagnostics;
+using Microsoft.AspNetCore.Hosting;
+using Microsoft.AspNetCore.Http;
+using Microsoft.AspNetCore.HttpOverrides;
+using Microsoft.AspNetCore.Mvc;
+using Microsoft.Extensions.Configuration;
+using Microsoft.Extensions.DependencyInjection;
+using Microsoft.Extensions.Hosting;
+using NLog;
+using System;
+using System.IO;
+using System.Net;
+
+namespace AccountOwnerServer
+{
+ public class Startup
+ {
+ public Startup(IConfiguration configuration)
+ {
+ LogManager.LoadConfiguration(String.Concat(Directory.GetCurrentDirectory(), "/nlog.config"));
+ 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)
+ {
+ services.ConfigureCors();
+
+ services.ConfigureIISIntegration();
+
+ services.ConfigureLoggerService();
+
+ services.ConfigureMySqlContext(Configuration);
+
+ services.ConfigureRepositoryWrapper();
+
+ services.RegisterFilters();
+
+ services.AddControllers(config =>
+ {
+ config.RespectBrowserAcceptHeader = true;
+ config.ReturnHttpNotAcceptable = true;
+ }).AddXmlDataContractSerializerFormatters()
+ .AddNewtonsoftJson();
+
+ services.AddCustomMediaTypes();
+ }
+
+ // This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
+ public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
+ {
+ if (env.IsDevelopment())
+ {
+ app.UseDeveloperExceptionPage();
+ }
+ else
+ {
+ // The default HSTS value is 30 days. You may want to change this for production scenarios, see https://aka.ms/aspnetcore-hsts.
+ app.UseHsts();
+ }
+
+ app.UseExceptionHandler(appError =>
+ {
+ appError.Run(async context =>
+ {
+ context.Response.StatusCode = (int)HttpStatusCode.InternalServerError;
+ context.Response.ContentType = "application/json";
+
+ var contextFeature = context.Features.Get();
+ if (contextFeature != null)
+ {
+ Console.WriteLine($"Something went wrong: {contextFeature.Error}");
+
+ await context.Response.WriteAsync(new
+ {
+ context.Response.StatusCode,
+ Message = "Internal Server Error."
+ }.ToString());
+ }
+ });
+ });
+
+ app.UseHttpsRedirection();
+
+ app.UseCors("CorsPolicy");
+
+ app.UseForwardedHeaders(new ForwardedHeadersOptions
+ {
+ ForwardedHeaders = ForwardedHeaders.All
+ });
+
+ app.UseStaticFiles();
+
+ app.UseRouting();
+
+ app.UseEndpoints(endpoints =>
+ {
+ endpoints.MapControllers();
+ });
+ }
+ }
+}
diff --git a/AccountOwnerServer/appsettings.Development.json b/AccountOwnerServer/appsettings.Development.json
new file mode 100644
index 0000000..e203e94
--- /dev/null
+++ b/AccountOwnerServer/appsettings.Development.json
@@ -0,0 +1,9 @@
+{
+ "Logging": {
+ "LogLevel": {
+ "Default": "Debug",
+ "System": "Information",
+ "Microsoft": "Information"
+ }
+ }
+}
diff --git a/AccountOwnerServer/appsettings.json b/AccountOwnerServer/appsettings.json
new file mode 100644
index 0000000..0c61fec
--- /dev/null
+++ b/AccountOwnerServer/appsettings.json
@@ -0,0 +1,11 @@
+{
+ "Logging": {
+ "LogLevel": {
+ "Default": "Warning"
+ }
+ },
+ "mysqlconnection": {
+ "connectionString": "server=localhost;userid=root;password=Popokatapetl12;database=codemaze;"
+ },
+ "AllowedHosts": "*"
+}
diff --git a/AccountOwnerServer/nlog.config b/AccountOwnerServer/nlog.config
new file mode 100644
index 0000000..47744e0
--- /dev/null
+++ b/AccountOwnerServer/nlog.config
@@ -0,0 +1,17 @@
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/Contracts/Contracts.csproj b/Contracts/Contracts.csproj
new file mode 100644
index 0000000..ea0a350
--- /dev/null
+++ b/Contracts/Contracts.csproj
@@ -0,0 +1,11 @@
+
+
+
+ netcoreapp3.0
+
+
+
+
+
+
+
diff --git a/Contracts/IAccountRepository.cs b/Contracts/IAccountRepository.cs
new file mode 100644
index 0000000..cea7568
--- /dev/null
+++ b/Contracts/IAccountRepository.cs
@@ -0,0 +1,13 @@
+using Entities.Helpers;
+using Entities.Models;
+using System;
+
+namespace Contracts
+{
+ public interface IAccountRepository : IRepositoryBase
+ {
+ PagedList GetAccountsByOwner(Guid ownerId, AccountParameters parameters);
+ ShapedEntity GetAccountByOwner(Guid ownerId, Guid id, string fields);
+ Account GetAccountByOwner(Guid ownerId, Guid id);
+ }
+}
diff --git a/Contracts/ILoggerManager.cs b/Contracts/ILoggerManager.cs
new file mode 100644
index 0000000..7dcd872
--- /dev/null
+++ b/Contracts/ILoggerManager.cs
@@ -0,0 +1,14 @@
+using System;
+using System.Collections.Generic;
+using System.Text;
+
+namespace Contracts
+{
+ public interface ILoggerManager
+ {
+ void LogInfo(string message);
+ void LogWarn(string message);
+ void LogDebug(string message);
+ void LogError(string message);
+ }
+}
diff --git a/Contracts/IOwnerRepository.cs b/Contracts/IOwnerRepository.cs
new file mode 100644
index 0000000..71d4aa1
--- /dev/null
+++ b/Contracts/IOwnerRepository.cs
@@ -0,0 +1,16 @@
+using Entities.Helpers;
+using Entities.Models;
+using System;
+
+namespace Contracts
+{
+ public interface IOwnerRepository : IRepositoryBase
+ {
+ PagedList GetOwners(OwnerParameters ownerParameters);
+ ShapedEntity GetOwnerById(Guid ownerId, string fields);
+ Owner GetOwnerById(Guid ownerId);
+ void CreateOwner(Owner owner);
+ void UpdateOwner(Owner dbOwner, Owner owner);
+ void DeleteOwner(Owner owner);
+ }
+}
\ No newline at end of file
diff --git a/Contracts/IRepositoryBase.cs b/Contracts/IRepositoryBase.cs
new file mode 100644
index 0000000..75c817a
--- /dev/null
+++ b/Contracts/IRepositoryBase.cs
@@ -0,0 +1,15 @@
+using System;
+using System.Linq;
+using System.Linq.Expressions;
+
+namespace Contracts
+{
+ public interface IRepositoryBase
+ {
+ IQueryable FindAll();
+ IQueryable FindByCondition(Expression> expression);
+ void Create(T entity);
+ void Update(T entity);
+ void Delete(T entity);
+ }
+}
diff --git a/Contracts/IRepositoryWrapper.cs b/Contracts/IRepositoryWrapper.cs
new file mode 100644
index 0000000..4337480
--- /dev/null
+++ b/Contracts/IRepositoryWrapper.cs
@@ -0,0 +1,13 @@
+using System;
+using System.Collections.Generic;
+using System.Text;
+
+namespace Contracts
+{
+ public interface IRepositoryWrapper
+ {
+ IOwnerRepository Owner { get; }
+ IAccountRepository Account { get; }
+ void Save();
+ }
+}
diff --git a/Entities/Entities.csproj b/Entities/Entities.csproj
new file mode 100644
index 0000000..c398694
--- /dev/null
+++ b/Entities/Entities.csproj
@@ -0,0 +1,12 @@
+
+
+
+ netcoreapp3.0
+
+
+
+
+
+
+
+
diff --git a/Entities/Enumerations/AccountType.cs b/Entities/Enumerations/AccountType.cs
new file mode 100644
index 0000000..7cad9c9
--- /dev/null
+++ b/Entities/Enumerations/AccountType.cs
@@ -0,0 +1,13 @@
+using System;
+using System.Collections.Generic;
+using System.Text;
+
+namespace Entities.Enumerations
+{
+ public enum AccountType
+ {
+ Domestic,
+ Savings,
+ Foreign
+ }
+}
diff --git a/Entities/ExtendedModels/OwnerExtended.cs b/Entities/ExtendedModels/OwnerExtended.cs
new file mode 100644
index 0000000..96b2dde
--- /dev/null
+++ b/Entities/ExtendedModels/OwnerExtended.cs
@@ -0,0 +1,29 @@
+using Entities.Models;
+using System;
+using System.Collections.Generic;
+using System.Text;
+
+namespace Entities.ExtendedModels
+{
+ public class OwnerExtended : IEntity
+ {
+ public Guid Id { get; set; }
+ public string Name { get; set; }
+ public DateTime DateOfBirth { get; set; }
+ public string Address { get; set; }
+
+ public IEnumerable Accounts { get; set; }
+
+ public OwnerExtended()
+ {
+ }
+
+ public OwnerExtended(Owner owner)
+ {
+ Id = owner.Id;
+ Name = owner.Name;
+ DateOfBirth = owner.DateOfBirth;
+ Address = owner.Address;
+ }
+ }
+}
diff --git a/Entities/Extensions/IEntityExtensions.cs b/Entities/Extensions/IEntityExtensions.cs
new file mode 100644
index 0000000..992fca0
--- /dev/null
+++ b/Entities/Extensions/IEntityExtensions.cs
@@ -0,0 +1,19 @@
+using System;
+using System.Collections.Generic;
+using System.Text;
+
+namespace Entities.Extensions
+{
+ public static class IEntityExtensions
+ {
+ public static bool IsObjectNull(this IEntity entity)
+ {
+ return entity == null;
+ }
+
+ public static bool IsEmptyObject(this IEntity entity)
+ {
+ return entity.Id.Equals(Guid.Empty);
+ }
+ }
+}
diff --git a/Entities/Extensions/OwnerExtensions.cs b/Entities/Extensions/OwnerExtensions.cs
new file mode 100644
index 0000000..03e8a0a
--- /dev/null
+++ b/Entities/Extensions/OwnerExtensions.cs
@@ -0,0 +1,17 @@
+using Entities.Models;
+using System;
+using System.Collections.Generic;
+using System.Text;
+
+namespace Entities.Extensions
+{
+ public static class OwnerExtensions
+ {
+ public static void Map(this Owner dbOwner, Owner owner)
+ {
+ dbOwner.Name = owner.Name;
+ dbOwner.Address = owner.Address;
+ dbOwner.DateOfBirth = owner.DateOfBirth;
+ }
+ }
+}
diff --git a/Entities/Helpers/DataShaper.cs b/Entities/Helpers/DataShaper.cs
new file mode 100644
index 0000000..ac8ae49
--- /dev/null
+++ b/Entities/Helpers/DataShaper.cs
@@ -0,0 +1,87 @@
+using Entities.Models;
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Reflection;
+
+namespace Entities.Helpers
+{
+ public class DataShaper : IDataShaper
+ {
+ public PropertyInfo[] Properties { get; set; }
+
+ public DataShaper()
+ {
+ Properties = typeof(T).GetProperties(BindingFlags.Public | BindingFlags.Instance);
+ }
+
+ public IEnumerable ShapeData(IEnumerable entities, string fieldsString)
+ {
+ var requiredProperties = GetRequiredProperties(fieldsString);
+
+ return FetchData(entities, requiredProperties);
+ }
+
+ public ShapedEntity ShapeData(T entity, string fieldsString)
+ {
+ var requiredProperties = GetRequiredProperties(fieldsString);
+
+ return FetchDataForEntity(entity, requiredProperties);
+ }
+
+ private IEnumerable GetRequiredProperties(string fieldsString)
+ {
+ var requiredProperties = new List();
+
+ if (!string.IsNullOrWhiteSpace(fieldsString))
+ {
+ var fields = fieldsString.Split(',', StringSplitOptions.RemoveEmptyEntries);
+
+ foreach (var field in fields)
+ {
+ var property = Properties.FirstOrDefault(pi => pi.Name.Equals(field.Trim(), StringComparison.InvariantCultureIgnoreCase));
+
+ if (property == null)
+ continue;
+
+ requiredProperties.Add(property);
+ }
+ }
+ else
+ {
+ requiredProperties = Properties.ToList();
+ }
+
+ return requiredProperties;
+ }
+
+ private IEnumerable FetchData(IEnumerable entities, IEnumerable requiredProperties)
+ {
+ var shapedData = new List();
+
+ foreach (var entity in entities)
+ {
+ var shapedObject = FetchDataForEntity(entity, requiredProperties);
+ shapedData.Add(shapedObject);
+ }
+
+ return shapedData;
+ }
+
+ private ShapedEntity FetchDataForEntity(T entity, IEnumerable requiredProperties)
+ {
+ var shapedObject = new ShapedEntity();
+
+ foreach (var property in requiredProperties)
+ {
+ var objectPropertyValue = property.GetValue(entity);
+ shapedObject.Entity.TryAdd(property.Name, objectPropertyValue);
+ }
+
+ var objectProperty = entity.GetType().GetProperty("Id");
+ shapedObject.Id = (Guid)objectProperty.GetValue(entity);
+
+ return shapedObject;
+ }
+ }
+}
diff --git a/Entities/Helpers/IDataShaper.cs b/Entities/Helpers/IDataShaper.cs
new file mode 100644
index 0000000..840d36a
--- /dev/null
+++ b/Entities/Helpers/IDataShaper.cs
@@ -0,0 +1,11 @@
+using Entities.Models;
+using System.Collections.Generic;
+
+namespace Entities.Helpers
+{
+ public interface IDataShaper
+ {
+ IEnumerable ShapeData(IEnumerable entities, string fieldsString);
+ ShapedEntity ShapeData(T entity, string fieldsString);
+ }
+}
\ No newline at end of file
diff --git a/Entities/Helpers/ISortHelper.cs b/Entities/Helpers/ISortHelper.cs
new file mode 100644
index 0000000..62d59d3
--- /dev/null
+++ b/Entities/Helpers/ISortHelper.cs
@@ -0,0 +1,9 @@
+using System.Linq;
+
+namespace Entities.Helpers
+{
+ public interface ISortHelper
+ {
+ IQueryable ApplySort(IQueryable entities, string orderByQueryString);
+ }
+}
\ No newline at end of file
diff --git a/Entities/Helpers/PagedList.cs b/Entities/Helpers/PagedList.cs
new file mode 100644
index 0000000..2e4720a
--- /dev/null
+++ b/Entities/Helpers/PagedList.cs
@@ -0,0 +1,40 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+
+namespace Entities.Helpers
+{
+ public class PagedList : List
+ {
+ public int CurrentPage { get; private set; }
+ public int TotalPages { get; private set; }
+ public int PageSize { get; private set; }
+ public int TotalCount { get; private set; }
+
+ public bool HasPrevious => CurrentPage > 1;
+ public bool HasNext => CurrentPage < TotalPages;
+
+ public PagedList()
+ {
+
+ }
+
+ public PagedList(List items, int count, int pageNumber, int pageSize)
+ {
+ TotalCount = count;
+ PageSize = pageSize;
+ CurrentPage = pageNumber;
+ TotalPages = (int)Math.Ceiling(count / (double)pageSize);
+
+ AddRange(items);
+ }
+
+ public static PagedList ToPagedList(IEnumerable source, int pageNumber, int pageSize)
+ {
+ var count = source.Count();
+ var items = source.Skip((pageNumber - 1) * pageSize).Take(pageSize).ToList();
+
+ return new PagedList(items, count, pageNumber, pageSize);
+ }
+ }
+}
diff --git a/Entities/Helpers/SortHelper.cs b/Entities/Helpers/SortHelper.cs
new file mode 100644
index 0000000..58ca35c
--- /dev/null
+++ b/Entities/Helpers/SortHelper.cs
@@ -0,0 +1,46 @@
+using System;
+using System.Linq;
+using System.Reflection;
+using System.Text;
+using System.Linq.Dynamic.Core;
+
+namespace Entities.Helpers
+{
+ public class SortHelper : ISortHelper
+ {
+ public IQueryable ApplySort(IQueryable entities, string orderByQueryString)
+ {
+ if (!entities.Any())
+ return entities;
+
+ if (string.IsNullOrWhiteSpace(orderByQueryString))
+ {
+ return entities;
+ }
+
+ var orderParams = orderByQueryString.Trim().Split(',');
+ var propertyInfos = typeof(T).GetProperties(BindingFlags.Public | BindingFlags.Instance);
+ var orderQueryBuilder = new StringBuilder();
+
+ foreach (var param in orderParams)
+ {
+ if (string.IsNullOrWhiteSpace(param))
+ continue;
+
+ var propertyFromQueryName = param.Split(" ")[0];
+ var objectProperty = propertyInfos.FirstOrDefault(pi => pi.Name.Equals(propertyFromQueryName, StringComparison.InvariantCultureIgnoreCase));
+
+ if (objectProperty == null)
+ continue;
+
+ var descending = param.EndsWith(" desc") ? "descending" : "ascending";
+
+ orderQueryBuilder.Append($"{objectProperty.Name} {descending}, ");
+ }
+
+ var orderQuery = orderQueryBuilder.ToString().TrimEnd(',', ' ');
+
+ return entities.OrderBy(orderQuery);
+ }
+ }
+}
diff --git a/Entities/IEntity.cs b/Entities/IEntity.cs
new file mode 100644
index 0000000..8dcadc4
--- /dev/null
+++ b/Entities/IEntity.cs
@@ -0,0 +1,11 @@
+using System;
+using System.Collections.Generic;
+using System.Text;
+
+namespace Entities
+{
+ public interface IEntity
+ {
+ Guid Id { get; set; }
+ }
+}
diff --git a/Entities/Models/Account.cs b/Entities/Models/Account.cs
new file mode 100644
index 0000000..105d588
--- /dev/null
+++ b/Entities/Models/Account.cs
@@ -0,0 +1,26 @@
+using Entities.Enumerations;
+using System;
+using System.Collections.Generic;
+using System.ComponentModel.DataAnnotations;
+using System.ComponentModel.DataAnnotations.Schema;
+using System.Text;
+
+namespace Entities.Models
+{
+ [Table("account")]
+ public class Account : IEntity
+ {
+ [Key]
+ [Column("AccountId")]
+ public Guid Id { get; set; }
+
+ [Required(ErrorMessage = "Date created is required")]
+ public DateTime DateCreated { get; set; }
+
+ [Required(ErrorMessage = "Account type is required")]
+ public string AccountType { get; set; }
+
+ [Required(ErrorMessage = "Owner Id is required")]
+ public Guid OwnerId { get; set; }
+ }
+}
diff --git a/Entities/Models/AccountParameters.cs b/Entities/Models/AccountParameters.cs
new file mode 100644
index 0000000..2cd3f5d
--- /dev/null
+++ b/Entities/Models/AccountParameters.cs
@@ -0,0 +1,10 @@
+namespace Entities.Models
+{
+ public class AccountParameters : QueryStringParameters
+ {
+ public AccountParameters()
+ {
+ OrderBy = "DateCreated";
+ }
+ }
+}
\ No newline at end of file
diff --git a/Entities/Models/Entity.cs b/Entities/Models/Entity.cs
new file mode 100644
index 0000000..e7a81d3
--- /dev/null
+++ b/Entities/Models/Entity.cs
@@ -0,0 +1,181 @@
+using System;
+using System.Collections;
+using System.Collections.Generic;
+using System.Dynamic;
+using System.Xml;
+using System.Xml.Schema;
+using System.Xml.Serialization;
+
+namespace Entities.Models
+{
+ public class Entity : DynamicObject, IXmlSerializable, IDictionary
+ {
+ private readonly string root = "EntityWithLinks";
+ private readonly IDictionary expando = null;
+
+ public Entity()
+ {
+ expando = new ExpandoObject();
+ }
+
+ public override bool TryGetMember(GetMemberBinder binder, out object result)
+ {
+ if (expando.TryGetValue(binder.Name, out object value))
+ {
+ result = value;
+ return true;
+ }
+
+ return base.TryGetMember(binder, out result);
+ }
+
+ public override bool TrySetMember(SetMemberBinder binder, object value)
+ {
+ expando[binder.Name] = value;
+
+ return true;
+ }
+
+ public XmlSchema GetSchema()
+ {
+ throw new NotImplementedException();
+ }
+
+ public void ReadXml(XmlReader reader)
+ {
+ reader.ReadStartElement(root);
+
+ while (!reader.Name.Equals(root))
+ {
+ string typeContent;
+ Type underlyingType;
+ var name = reader.Name;
+
+ reader.MoveToAttribute("type");
+ typeContent = reader.ReadContentAsString();
+ underlyingType = Type.GetType(typeContent);
+ reader.MoveToContent();
+ expando[name] = reader.ReadElementContentAs(underlyingType, null);
+ }
+ }
+
+ public void WriteXml(XmlWriter writer)
+ {
+ foreach (var key in expando.Keys)
+ {
+ var value = expando[key];
+ WriteXmlElement(key, value, writer);
+ }
+ }
+
+ private void WriteXmlElement(string key, object value, XmlWriter writer)
+ {
+ writer.WriteStartElement(key);
+
+ if (value.GetType() == typeof(List))
+ {
+ foreach (var val in value as List)
+ {
+ writer.WriteStartElement(nameof(Link));
+ WriteXmlElement(nameof(val.Href), val.Href, writer);
+ WriteXmlElement(nameof(val.Method), val.Method, writer);
+ WriteXmlElement(nameof(val.Rel), val.Rel, writer);
+ writer.WriteEndElement();
+ }
+ }
+ else
+ {
+ writer.WriteString(value.ToString());
+ }
+
+ writer.WriteEndElement();
+ }
+
+ public void Add(string key, object value)
+ {
+ expando.Add(key, value);
+ }
+
+ public bool ContainsKey(string key)
+ {
+ return expando.ContainsKey(key);
+ }
+
+ public ICollection Keys
+ {
+ get { return expando.Keys; }
+ }
+
+ public bool Remove(string key)
+ {
+ return expando.Remove(key);
+ }
+
+ public bool TryGetValue(string key, out object value)
+ {
+ return expando.TryGetValue(key, out value);
+ }
+
+ public ICollection