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..d02125a
--- /dev/null
+++ b/AccountOwnerServer/AccountOwnerServer.csproj
@@ -0,0 +1,20 @@
+
+
+
+ netcoreapp3.0
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/AccountOwnerServer/Controllers/AccountController.cs b/AccountOwnerServer/Controllers/AccountController.cs
new file mode 100644
index 0000000..aeb0fa3
--- /dev/null
+++ b/AccountOwnerServer/Controllers/AccountController.cs
@@ -0,0 +1,61 @@
+using Contracts;
+using Entities.Extensions;
+using Entities.Models;
+using Microsoft.AspNetCore.Mvc;
+using Microsoft.AspNetCore.Routing;
+using Newtonsoft.Json;
+using System;
+
+namespace AccountOwnerServer.Controllers
+{
+ [Route("api/owners/{ownerId}/accounts")]
+ [ApiController]
+ public class AccountController : ControllerBase
+ {
+ private ILoggerManager _logger;
+ private IRepositoryWrapper _repository;
+
+ public AccountController(ILoggerManager logger,
+ IRepositoryWrapper repository)
+ {
+ _logger = logger;
+ _repository = repository;
+ }
+
+ [HttpGet]
+ 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.");
+
+ return Ok(accounts);
+ }
+
+ [HttpGet("{id}")]
+ public IActionResult GetAccountForOwner(Guid ownerId, Guid id)
+ {
+ var account = _repository.Account.GetAccountByOwner(ownerId, id);
+
+ if (account.IsEmptyObject())
+ {
+ _logger.LogError($"Account with id: {id}, hasn't been found in db.");
+ return NotFound();
+ }
+
+ return Ok(account);
+ }
+ }
+}
diff --git a/AccountOwnerServer/Controllers/OwnerController.cs b/AccountOwnerServer/Controllers/OwnerController.cs
new file mode 100644
index 0000000..19d4e30
--- /dev/null
+++ b/AccountOwnerServer/Controllers/OwnerController.cs
@@ -0,0 +1,133 @@
+using System;
+using System.Linq;
+using Contracts;
+using Entities.Extensions;
+using Entities.Models;
+using Microsoft.AspNetCore.Mvc;
+using Newtonsoft.Json;
+
+namespace AccountOwnerServer.Controllers
+{
+ [Route("api/owners")]
+ [ApiController]
+ public class OwnerController : ControllerBase
+ {
+ private ILoggerManager _logger;
+ private IRepositoryWrapper _repository;
+
+ public OwnerController(ILoggerManager logger, IRepositoryWrapper repository)
+ {
+ _logger = logger;
+ _repository = repository;
+ }
+
+ [HttpGet]
+ 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.");
+
+ return Ok(owners);
+ }
+
+ [HttpGet("{id}", Name = "OwnerById")]
+ public IActionResult GetOwnerById(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();
+ }
+ else
+ {
+ _logger.LogInfo($"Returned owner with id: {id}");
+ return Ok(owner);
+ }
+ }
+
+ [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();
+ }
+ }
+}
\ No newline at end of file
diff --git a/AccountOwnerServer/Extensions/ServiceExtensions.cs b/AccountOwnerServer/Extensions/ServiceExtensions.cs
new file mode 100644
index 0000000..08ca204
--- /dev/null
+++ b/AccountOwnerServer/Extensions/ServiceExtensions.cs
@@ -0,0 +1,55 @@
+using Contracts;
+using Entities;
+using Entities.Helpers;
+using Entities.Models;
+using LoggerService;
+using Microsoft.AspNetCore.Builder;
+using Microsoft.EntityFrameworkCore;
+using Microsoft.Extensions.Configuration;
+using Microsoft.Extensions.DependencyInjection;
+using Repository;
+
+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();
+ }
+ }
+}
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..767f354
--- /dev/null
+++ b/AccountOwnerServer/Startup.cs
@@ -0,0 +1,97 @@
+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.AddControllers();
+ }
+
+ // 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..6d2d127
--- /dev/null
+++ b/AccountOwnerServer/appsettings.json
@@ -0,0 +1,11 @@
+{
+ "Logging": {
+ "LogLevel": {
+ "Default": "Warning"
+ }
+ },
+ "mysqlconnection": {
+ "connectionString": "server=localhost;userid=root;password=root;database=accountowner;"
+ },
+ "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..1780c6a
--- /dev/null
+++ b/Contracts/IAccountRepository.cs
@@ -0,0 +1,12 @@
+using Entities.Helpers;
+using Entities.Models;
+using System;
+
+namespace Contracts
+{
+ public interface IAccountRepository : IRepositoryBase
+ {
+ PagedList GetAccountsByOwner(Guid ownerId, AccountParameters parameters);
+ 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..7182f49
--- /dev/null
+++ b/Contracts/IOwnerRepository.cs
@@ -0,0 +1,15 @@
+using Entities.Helpers;
+using Entities.Models;
+using System;
+
+namespace Contracts
+{
+ public interface IOwnerRepository : IRepositoryBase
+ {
+ PagedList GetOwners(OwnerParameters ownerParameters);
+ Owner GetOwnerById(Guid ownerId);
+ void CreateOwner(Owner owner);
+ void UpdateOwner(Owner dbOwner, Owner owner);
+ void DeleteOwner(Owner owner);
+ }
+}
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/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/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..67af09d
--- /dev/null
+++ b/Entities/Helpers/PagedList.cs
@@ -0,0 +1,35 @@
+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(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(IQueryable 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..b915817
--- /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 sortingOrder = param.EndsWith(" desc") ? "descending" : "ascending";
+
+ orderQueryBuilder.Append($"{objectProperty.Name} {sortingOrder}, ");
+ }
+
+ 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..879f660
--- /dev/null
+++ b/Entities/Models/AccountParameters.cs
@@ -0,0 +1,10 @@
+namespace Entities.Models
+{
+ public class AccountParameters : QueryStringParameters
+ {
+ public AccountParameters()
+ {
+ OrderBy = "DateCreated";
+ }
+ }
+}
diff --git a/Entities/Models/Owner.cs b/Entities/Models/Owner.cs
new file mode 100644
index 0000000..bed85f5
--- /dev/null
+++ b/Entities/Models/Owner.cs
@@ -0,0 +1,25 @@
+using System;
+using System.ComponentModel.DataAnnotations;
+using System.ComponentModel.DataAnnotations.Schema;
+
+namespace Entities.Models
+{
+ [Table("owner")]
+ public class Owner : IEntity
+ {
+ [Key]
+ [Column("OwnerId")]
+ public Guid Id { get; set; }
+
+ [Required(ErrorMessage = "Name is required")]
+ [StringLength(60, ErrorMessage = "Name can't be longer than 60 characters")]
+ public string Name { get; set; }
+
+ [Required(ErrorMessage = "Date of birth is required")]
+ public DateTime DateOfBirth { get; set; }
+
+ [Required(ErrorMessage = "Address is required")]
+ [StringLength(100, ErrorMessage = "Address can not be loner then 100 characters")]
+ public string Address { get; set; }
+ }
+}
diff --git a/Entities/Models/OwnerParameters.cs b/Entities/Models/OwnerParameters.cs
new file mode 100644
index 0000000..a5c9c2d
--- /dev/null
+++ b/Entities/Models/OwnerParameters.cs
@@ -0,0 +1,19 @@
+using System;
+
+namespace Entities.Models
+{
+ public class OwnerParameters : QueryStringParameters
+ {
+ public OwnerParameters()
+ {
+ OrderBy = "name";
+ }
+
+ public uint MinYearOfBirth { get; set; }
+ public uint MaxYearOfBirth { get; set; } = (uint)DateTime.Now.Year;
+
+ public bool ValidYearRange => MaxYearOfBirth > MinYearOfBirth;
+
+ public string Name { get; set; }
+ }
+}
diff --git a/Entities/Models/QueryStringParameters.cs b/Entities/Models/QueryStringParameters.cs
new file mode 100644
index 0000000..2ca8010
--- /dev/null
+++ b/Entities/Models/QueryStringParameters.cs
@@ -0,0 +1,23 @@
+namespace Entities.Models
+{
+ public abstract class QueryStringParameters
+ {
+ const int maxPageSize = 50;
+ public int PageNumber { get; set; } = 1;
+
+ private int _pageSize = 10;
+ public int PageSize
+ {
+ get
+ {
+ return _pageSize;
+ }
+ set
+ {
+ _pageSize = (value > maxPageSize) ? maxPageSize : value;
+ }
+ }
+
+ public string OrderBy { get; set; }
+ }
+}
diff --git a/Entities/RepositoryContext.cs b/Entities/RepositoryContext.cs
new file mode 100644
index 0000000..644825c
--- /dev/null
+++ b/Entities/RepositoryContext.cs
@@ -0,0 +1,16 @@
+using Entities.Models;
+using Microsoft.EntityFrameworkCore;
+
+namespace Entities
+{
+ public class RepositoryContext: DbContext
+ {
+ public RepositoryContext(DbContextOptions options)
+ :base(options)
+ {
+ }
+
+ public DbSet Owners { get; set; }
+ public DbSet Accounts { get; set; }
+ }
+}
diff --git a/LoggerService/LoggerManager.cs b/LoggerService/LoggerManager.cs
new file mode 100644
index 0000000..5ee74b9
--- /dev/null
+++ b/LoggerService/LoggerManager.cs
@@ -0,0 +1,34 @@
+using Contracts;
+using NLog;
+
+namespace LoggerService
+{
+ public class LoggerManager : ILoggerManager
+ {
+ private static ILogger logger = LogManager.GetCurrentClassLogger();
+
+ public LoggerManager()
+ {
+ }
+
+ public void LogDebug(string message)
+ {
+ logger.Debug(message);
+ }
+
+ public void LogError(string message)
+ {
+ logger.Error(message);
+ }
+
+ public void LogInfo(string message)
+ {
+ logger.Info(message);
+ }
+
+ public void LogWarn(string message)
+ {
+ logger.Warn(message);
+ }
+ }
+}
diff --git a/LoggerService/LoggerService.csproj b/LoggerService/LoggerService.csproj
new file mode 100644
index 0000000..953adf9
--- /dev/null
+++ b/LoggerService/LoggerService.csproj
@@ -0,0 +1,15 @@
+
+
+
+ netcoreapp3.0
+
+
+
+
+
+
+
+
+
+
+
diff --git a/Repository/AccountRepository.cs b/Repository/AccountRepository.cs
new file mode 100644
index 0000000..dea5120
--- /dev/null
+++ b/Repository/AccountRepository.cs
@@ -0,0 +1,37 @@
+using Contracts;
+using Entities;
+using Entities.Helpers;
+using Entities.Models;
+using System;
+using System.Collections.Generic;
+using System.Linq;
+
+namespace Repository
+{
+ public class AccountRepository : RepositoryBase, IAccountRepository
+ {
+ private ISortHelper _sortHelper;
+
+ public AccountRepository(RepositoryContext repositoryContext, ISortHelper sortHelper)
+ : base(repositoryContext)
+ {
+ _sortHelper = sortHelper;
+ }
+
+ public PagedList GetAccountsByOwner(Guid ownerId, AccountParameters parameters)
+ {
+ var accounts = FindByCondition(a => a.OwnerId.Equals(ownerId));
+
+ var sortedAccounts = _sortHelper.ApplySort(accounts, parameters.OrderBy);
+
+ return PagedList.ToPagedList(sortedAccounts,
+ parameters.PageNumber,
+ parameters.PageSize);
+ }
+
+ public Account GetAccountByOwner(Guid ownerId, Guid id)
+ {
+ return FindByCondition(a => a.OwnerId.Equals(ownerId) && a.Id.Equals(id)).SingleOrDefault();
+ }
+ }
+}
\ No newline at end of file
diff --git a/Repository/OwnerRepository.cs b/Repository/OwnerRepository.cs
new file mode 100644
index 0000000..1ee3fe8
--- /dev/null
+++ b/Repository/OwnerRepository.cs
@@ -0,0 +1,70 @@
+using Contracts;
+using Entities;
+using Entities.Extensions;
+using Entities.Helpers;
+using Entities.Models;
+using System;
+using System.Linq;
+using System.Linq.Dynamic.Core;
+
+namespace Repository
+{
+ public class OwnerRepository : RepositoryBase, IOwnerRepository
+ {
+ private ISortHelper _sortHelper;
+
+ public OwnerRepository(RepositoryContext repositoryContext, ISortHelper sortHelper)
+ : base(repositoryContext)
+ {
+ _sortHelper = sortHelper;
+ }
+
+ public PagedList GetOwners(OwnerParameters ownerParameters)
+ {
+ var owners = FindByCondition(o => o.DateOfBirth.Year >= ownerParameters.MinYearOfBirth &&
+ o.DateOfBirth.Year <= ownerParameters.MaxYearOfBirth);
+
+ SearchByName(ref owners, ownerParameters.Name);
+
+ _sortHelper.ApplySort(owners, ownerParameters.OrderBy);
+
+ return PagedList.ToPagedList(owners,
+ ownerParameters.PageNumber,
+ ownerParameters.PageSize);
+ }
+
+ private void SearchByName(ref IQueryable owners, string ownerName)
+ {
+ if (!owners.Any() || string.IsNullOrWhiteSpace(ownerName))
+ return;
+
+ if (string.IsNullOrEmpty(ownerName))
+ return;
+
+ owners = owners.Where(o => o.Name.ToLowerInvariant().Contains(ownerName.Trim().ToLowerInvariant()));
+ }
+
+ public Owner GetOwnerById(Guid ownerId)
+ {
+ return FindByCondition(owner => owner.Id.Equals(ownerId))
+ .DefaultIfEmpty(new Owner())
+ .FirstOrDefault();
+ }
+
+ public void CreateOwner(Owner owner)
+ {
+ Create(owner);
+ }
+
+ public void UpdateOwner(Owner dbOwner, Owner owner)
+ {
+ dbOwner.Map(owner);
+ Update(dbOwner);
+ }
+
+ public void DeleteOwner(Owner owner)
+ {
+ Delete(owner);
+ }
+ }
+}
\ No newline at end of file
diff --git a/Repository/Repository.csproj b/Repository/Repository.csproj
new file mode 100644
index 0000000..57127ad
--- /dev/null
+++ b/Repository/Repository.csproj
@@ -0,0 +1,15 @@
+
+
+
+ netcoreapp3.0
+
+
+
+
+
+
+
+
+
+
+
diff --git a/Repository/RepositoryBase.cs b/Repository/RepositoryBase.cs
new file mode 100644
index 0000000..5f6442c
--- /dev/null
+++ b/Repository/RepositoryBase.cs
@@ -0,0 +1,47 @@
+using Contracts;
+using Entities;
+using Microsoft.EntityFrameworkCore;
+using System;
+using System.Linq;
+using System.Linq.Expressions;
+
+namespace Repository
+{
+ public abstract class RepositoryBase : IRepositoryBase where T : class
+ {
+ protected RepositoryContext RepositoryContext { get; set; }
+
+ public RepositoryBase(RepositoryContext repositoryContext)
+ {
+ RepositoryContext = repositoryContext;
+ }
+
+ public IQueryable FindAll()
+ {
+ return RepositoryContext.Set()
+ .AsNoTracking();
+ }
+
+ public IQueryable FindByCondition(Expression> expression)
+ {
+ return RepositoryContext.Set()
+ .Where(expression)
+ .AsNoTracking();
+ }
+
+ public void Create(T entity)
+ {
+ RepositoryContext.Set().Add(entity);
+ }
+
+ public void Update(T entity)
+ {
+ RepositoryContext.Set().Update(entity);
+ }
+
+ public void Delete(T entity)
+ {
+ RepositoryContext.Set().Remove(entity);
+ }
+ }
+}
diff --git a/Repository/RepositoryWrapper.cs b/Repository/RepositoryWrapper.cs
new file mode 100644
index 0000000..b1eb340
--- /dev/null
+++ b/Repository/RepositoryWrapper.cs
@@ -0,0 +1,56 @@
+using Contracts;
+using Entities;
+using Entities.Helpers;
+using Entities.Models;
+
+namespace Repository
+{
+ public class RepositoryWrapper : IRepositoryWrapper
+ {
+ private RepositoryContext _repoContext;
+ private IOwnerRepository _owner;
+ private IAccountRepository _account;
+ private ISortHelper _ownerSortHelper;
+ private ISortHelper _accountSortHelper;
+
+ public IOwnerRepository Owner
+ {
+ get
+ {
+ if (_owner == null)
+ {
+ _owner = new OwnerRepository(_repoContext, _ownerSortHelper);
+ }
+
+ return _owner;
+ }
+ }
+
+ public IAccountRepository Account
+ {
+ get
+ {
+ if (_account == null)
+ {
+ _account = new AccountRepository(_repoContext, _accountSortHelper);
+ }
+
+ return _account;
+ }
+ }
+
+ public RepositoryWrapper(RepositoryContext repositoryContext,
+ ISortHelper ownerSortHelper,
+ ISortHelper accountSortHelper)
+ {
+ _repoContext = repositoryContext;
+ _ownerSortHelper = ownerSortHelper;
+ _accountSortHelper = accountSortHelper;
+ }
+
+ public void Save()
+ {
+ _repoContext.SaveChanges();
+ }
+ }
+}
diff --git a/_MySQL_Init_Script/init.sql b/_MySQL_Init_Script/init.sql
new file mode 100644
index 0000000..648857d
--- /dev/null
+++ b/_MySQL_Init_Script/init.sql
@@ -0,0 +1,81 @@
+-- MySQL dump 10.13 Distrib 5.7.17, for Win64 (x86_64)
+--
+-- Host: localhost Database: accountowner
+-- ------------------------------------------------------
+-- Server version 5.7.17-log
+
+/*!40101 SET @OLD_CHARACTER_SET_CLIENT=@@CHARACTER_SET_CLIENT */;
+/*!40101 SET @OLD_CHARACTER_SET_RESULTS=@@CHARACTER_SET_RESULTS */;
+/*!40101 SET @OLD_COLLATION_CONNECTION=@@COLLATION_CONNECTION */;
+/*!40101 SET NAMES utf8 */;
+/*!40103 SET @OLD_TIME_ZONE=@@TIME_ZONE */;
+/*!40103 SET TIME_ZONE='+00:00' */;
+/*!40014 SET @OLD_UNIQUE_CHECKS=@@UNIQUE_CHECKS, UNIQUE_CHECKS=0 */;
+/*!40014 SET @OLD_FOREIGN_KEY_CHECKS=@@FOREIGN_KEY_CHECKS, FOREIGN_KEY_CHECKS=0 */;
+/*!40101 SET @OLD_SQL_MODE=@@SQL_MODE, SQL_MODE='NO_AUTO_VALUE_ON_ZERO' */;
+/*!40111 SET @OLD_SQL_NOTES=@@SQL_NOTES, SQL_NOTES=0 */;
+
+--
+-- Table structure for table `account`
+--
+
+DROP TABLE IF EXISTS `account`;
+/*!40101 SET @saved_cs_client = @@character_set_client */;
+/*!40101 SET character_set_client = utf8 */;
+CREATE TABLE `account` (
+ `AccountId` char(36) NOT NULL,
+ `DateCreated` date NOT NULL,
+ `AccountType` varchar(45) NOT NULL,
+ `OwnerId` char(36) NOT NULL,
+ PRIMARY KEY (`AccountId`),
+ KEY `fk_Account_Owner_idx` (`OwnerId`),
+ CONSTRAINT `fk_Account_Owner` FOREIGN KEY (`OwnerId`) REFERENCES `owner` (`OwnerId`) ON UPDATE CASCADE
+) ENGINE=InnoDB DEFAULT CHARSET=utf8;
+/*!40101 SET character_set_client = @saved_cs_client */;
+
+--
+-- Dumping data for table `account`
+--
+
+LOCK TABLES `account` WRITE;
+/*!40000 ALTER TABLE `account` DISABLE KEYS */;
+INSERT INTO `account` VALUES ('03e91478-5608-4132-a753-d494dafce00b','2003-12-15','Domestic','f98e4d74-0f68-4aac-89fd-047f1aaca6b6'),('356a5a9b-64bf-4de0-bc84-5395a1fdc9c4','1996-02-15','Domestic','261e1685-cf26-494c-b17c-3546e65f5620'),('371b93f2-f8c5-4a32-894a-fc672741aa5b','1999-05-04','Domestic','24fd81f8-d58a-4bcc-9f35-dc6cd5641906'),('670775db-ecc0-4b90-a9ab-37cd0d8e2801','1999-12-21','Savings','24fd81f8-d58a-4bcc-9f35-dc6cd5641906'),('a3fbad0b-7f48-4feb-8ac0-6d3bbc997bfc','2010-05-28','Domestic','a3c1880c-674c-4d18-8f91-5d3608a2c937'),('aa15f658-04bb-4f73-82af-82db49d0fbef','1999-05-12','Foreign','24fd81f8-d58a-4bcc-9f35-dc6cd5641906'),('c6066eb0-53ca-43e1-97aa-3c2169eec659','1996-02-16','Foreign','261e1685-cf26-494c-b17c-3546e65f5620'),('eccadf79-85fe-402f-893c-32d3f03ed9b1','2010-06-20','Foreign','a3c1880c-674c-4d18-8f91-5d3608a2c937');
+/*!40000 ALTER TABLE `account` ENABLE KEYS */;
+UNLOCK TABLES;
+
+--
+-- Table structure for table `owner`
+--
+
+DROP TABLE IF EXISTS `owner`;
+/*!40101 SET @saved_cs_client = @@character_set_client */;
+/*!40101 SET character_set_client = utf8 */;
+CREATE TABLE `owner` (
+ `OwnerId` char(36) NOT NULL,
+ `Name` varchar(60) NOT NULL,
+ `DateOfBirth` date NOT NULL,
+ `Address` varchar(100) NOT NULL,
+ PRIMARY KEY (`OwnerId`)
+) ENGINE=InnoDB DEFAULT CHARSET=utf8;
+/*!40101 SET character_set_client = @saved_cs_client */;
+
+--
+-- Dumping data for table `owner`
+--
+
+LOCK TABLES `owner` WRITE;
+/*!40000 ALTER TABLE `owner` DISABLE KEYS */;
+INSERT INTO `owner` VALUES ('24fd81f8-d58a-4bcc-9f35-dc6cd5641906','John Keen','1980-12-05','61 Wellfield Road'),('261e1685-cf26-494c-b17c-3546e65f5620','Anna Bosh','1974-11-14','27 Colored Row'),('66774006-2371-4d5b-8518-2177bcf3f73e','Nick Somion','1998-12-15','North sunny address 102'),('a3c1880c-674c-4d18-8f91-5d3608a2c937','Sam Query','1990-04-22','91 Western Roads'),('f98e4d74-0f68-4aac-89fd-047f1aaca6b6','Martin Miller','1983-05-21','3 Edgar Buildings');
+/*!40000 ALTER TABLE `owner` ENABLE KEYS */;
+UNLOCK TABLES;
+/*!40103 SET TIME_ZONE=@OLD_TIME_ZONE */;
+
+/*!40101 SET SQL_MODE=@OLD_SQL_MODE */;
+/*!40014 SET FOREIGN_KEY_CHECKS=@OLD_FOREIGN_KEY_CHECKS */;
+/*!40014 SET UNIQUE_CHECKS=@OLD_UNIQUE_CHECKS */;
+/*!40101 SET CHARACTER_SET_CLIENT=@OLD_CHARACTER_SET_CLIENT */;
+/*!40101 SET CHARACTER_SET_RESULTS=@OLD_CHARACTER_SET_RESULTS */;
+/*!40101 SET COLLATION_CONNECTION=@OLD_COLLATION_CONNECTION */;
+/*!40111 SET SQL_NOTES=@OLD_SQL_NOTES */;
+
+-- Dump completed on 2017-12-24 15:53:17