From 0d7a781d48f18792b29d2c71682ee59223075abe Mon Sep 17 00:00:00 2001 From: Vladimir Pecanac Date: Sun, 29 Dec 2019 08:31:19 +0100 Subject: [PATCH 1/3] Initial commit --- .gitignore | 12 ++ AccountOwnerServer.sln | 49 +++++ AccountOwnerServer/AccountOwnerServer.csproj | 21 ++ .../Controllers/AccountController.cs | 117 +++++++++++ .../Controllers/OwnerController.cs | 197 ++++++++++++++++++ .../Extensions/ServiceExtensions.cs | 89 ++++++++ .../Filters/ValidateMediaTypeAttribute.cs | 36 ++++ AccountOwnerServer/Program.cs | 24 +++ .../Properties/launchSettings.json | 28 +++ AccountOwnerServer/Startup.cs | 106 ++++++++++ .../appsettings.Development.json | 9 + AccountOwnerServer/appsettings.json | 11 + AccountOwnerServer/nlog.config | 17 ++ Contracts/Contracts.csproj | 11 + Contracts/IAccountRepository.cs | 13 ++ Contracts/ILoggerManager.cs | 14 ++ Contracts/IOwnerRepository.cs | 16 ++ Contracts/IRepositoryBase.cs | 15 ++ Contracts/IRepositoryWrapper.cs | 13 ++ Entities/Entities.csproj | 12 ++ Entities/Enumerations/AccountType.cs | 13 ++ Entities/ExtendedModels/OwnerExtended.cs | 29 +++ Entities/Extensions/IEntityExtensions.cs | 19 ++ Entities/Extensions/OwnerExtensions.cs | 17 ++ Entities/Helpers/DataShaper.cs | 87 ++++++++ Entities/Helpers/IDataShaper.cs | 11 + Entities/Helpers/ISortHelper.cs | 9 + Entities/Helpers/PagedList.cs | 40 ++++ Entities/Helpers/SortHelper.cs | 46 ++++ Entities/IEntity.cs | 11 + Entities/Models/Account.cs | 26 +++ Entities/Models/AccountParameters.cs | 10 + Entities/Models/Entity.cs | 181 ++++++++++++++++ Entities/Models/Link.cs | 21 ++ Entities/Models/LinkCollectionWrapper.cs | 19 ++ Entities/Models/LinkResourceBase.cs | 14 ++ Entities/Models/Owner.cs | 25 +++ Entities/Models/OwnerParameters.cs | 19 ++ Entities/Models/QueryStringParameters.cs | 25 +++ Entities/Models/ShapedEntity.cs | 15 ++ Entities/RepositoryContext.cs | 16 ++ LoggerService/LoggerManager.cs | 34 +++ LoggerService/LoggerService.csproj | 15 ++ Repository/AccountRepository.cs | 46 ++++ Repository/OwnerRepository.cs | 83 ++++++++ Repository/Repository.csproj | 15 ++ Repository/RepositoryBase.cs | 48 +++++ Repository/RepositoryWrapper.cs | 62 ++++++ _MySQL_Init_Script/init.sql | 81 +++++++ 49 files changed, 1847 insertions(+) create mode 100644 .gitignore create mode 100644 AccountOwnerServer.sln create mode 100644 AccountOwnerServer/AccountOwnerServer.csproj create mode 100644 AccountOwnerServer/Controllers/AccountController.cs create mode 100644 AccountOwnerServer/Controllers/OwnerController.cs create mode 100644 AccountOwnerServer/Extensions/ServiceExtensions.cs create mode 100644 AccountOwnerServer/Filters/ValidateMediaTypeAttribute.cs create mode 100644 AccountOwnerServer/Program.cs create mode 100644 AccountOwnerServer/Properties/launchSettings.json create mode 100644 AccountOwnerServer/Startup.cs create mode 100644 AccountOwnerServer/appsettings.Development.json create mode 100644 AccountOwnerServer/appsettings.json create mode 100644 AccountOwnerServer/nlog.config create mode 100644 Contracts/Contracts.csproj create mode 100644 Contracts/IAccountRepository.cs create mode 100644 Contracts/ILoggerManager.cs create mode 100644 Contracts/IOwnerRepository.cs create mode 100644 Contracts/IRepositoryBase.cs create mode 100644 Contracts/IRepositoryWrapper.cs create mode 100644 Entities/Entities.csproj create mode 100644 Entities/Enumerations/AccountType.cs create mode 100644 Entities/ExtendedModels/OwnerExtended.cs create mode 100644 Entities/Extensions/IEntityExtensions.cs create mode 100644 Entities/Extensions/OwnerExtensions.cs create mode 100644 Entities/Helpers/DataShaper.cs create mode 100644 Entities/Helpers/IDataShaper.cs create mode 100644 Entities/Helpers/ISortHelper.cs create mode 100644 Entities/Helpers/PagedList.cs create mode 100644 Entities/Helpers/SortHelper.cs create mode 100644 Entities/IEntity.cs create mode 100644 Entities/Models/Account.cs create mode 100644 Entities/Models/AccountParameters.cs create mode 100644 Entities/Models/Entity.cs create mode 100644 Entities/Models/Link.cs create mode 100644 Entities/Models/LinkCollectionWrapper.cs create mode 100644 Entities/Models/LinkResourceBase.cs create mode 100644 Entities/Models/Owner.cs create mode 100644 Entities/Models/OwnerParameters.cs create mode 100644 Entities/Models/QueryStringParameters.cs create mode 100644 Entities/Models/ShapedEntity.cs create mode 100644 Entities/RepositoryContext.cs create mode 100644 LoggerService/LoggerManager.cs create mode 100644 LoggerService/LoggerService.csproj create mode 100644 Repository/AccountRepository.cs create mode 100644 Repository/OwnerRepository.cs create mode 100644 Repository/Repository.csproj create mode 100644 Repository/RepositoryBase.cs create mode 100644 Repository/RepositoryWrapper.cs create mode 100644 _MySQL_Init_Script/init.sql 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..ae305f8 --- /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/owner/{ownerId}/account")] + [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..d49094e --- /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/owner")] + [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..b658000 --- /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.ToString()} {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 Values + { + get { return expando.Values; } + } + + public object this[string key] + { + get + { + return expando[key]; + } + set + { + expando[key] = value; + } + } + + public void Add(KeyValuePair item) + { + expando.Add(item); + } + + public void Clear() + { + expando.Clear(); + } + + public bool Contains(KeyValuePair item) + { + return expando.Contains(item); + } + + public void CopyTo(KeyValuePair[] array, int arrayIndex) + { + expando.CopyTo(array, arrayIndex); + } + + public int Count + { + get { return expando.Count; } + } + + public bool IsReadOnly + { + get { return expando.IsReadOnly; } + } + + public bool Remove(KeyValuePair item) + { + return expando.Remove(item); + } + + public IEnumerator> GetEnumerator() + { + return expando.GetEnumerator(); + } + + IEnumerator IEnumerable.GetEnumerator() + { + return GetEnumerator(); + } + } +} diff --git a/Entities/Models/Link.cs b/Entities/Models/Link.cs new file mode 100644 index 0000000..320d132 --- /dev/null +++ b/Entities/Models/Link.cs @@ -0,0 +1,21 @@ +namespace Entities.Models +{ + public class Link + { + public string Href { get; set; } + public string Rel { get; set; } + public string Method { get; set; } + + public Link() + { + + } + + public Link(string href, string rel, string method) + { + Href = href; + Rel = rel; + Method = method; + } + } +} diff --git a/Entities/Models/LinkCollectionWrapper.cs b/Entities/Models/LinkCollectionWrapper.cs new file mode 100644 index 0000000..7f2f369 --- /dev/null +++ b/Entities/Models/LinkCollectionWrapper.cs @@ -0,0 +1,19 @@ +using System.Collections.Generic; + +namespace Entities.Models +{ + public class LinkCollectionWrapper : LinkResourceBase + { + public List Value { get; set; } = new List(); + + public LinkCollectionWrapper() + { + + } + + public LinkCollectionWrapper(List value) + { + Value = value; + } + } +} diff --git a/Entities/Models/LinkResourceBase.cs b/Entities/Models/LinkResourceBase.cs new file mode 100644 index 0000000..71a6107 --- /dev/null +++ b/Entities/Models/LinkResourceBase.cs @@ -0,0 +1,14 @@ +using System.Collections.Generic; + +namespace Entities.Models +{ + public class LinkResourceBase + { + public LinkResourceBase() + { + + } + + public List Links { get; set; } = new List(); + } +} 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..301cc31 --- /dev/null +++ b/Entities/Models/QueryStringParameters.cs @@ -0,0 +1,25 @@ +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; } + + public string Fields { get; set; } + } +} \ No newline at end of file diff --git a/Entities/Models/ShapedEntity.cs b/Entities/Models/ShapedEntity.cs new file mode 100644 index 0000000..5239540 --- /dev/null +++ b/Entities/Models/ShapedEntity.cs @@ -0,0 +1,15 @@ +using System; + +namespace Entities.Models +{ + public class ShapedEntity + { + public ShapedEntity() + { + Entity = new Entity(); + } + + public Guid Id { get; set; } + public Entity Entity { 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..160af0d --- /dev/null +++ b/Repository/AccountRepository.cs @@ -0,0 +1,46 @@ +using Contracts; +using Entities; +using Entities.Helpers; +using Entities.Models; +using System; +using System.Linq; + +namespace Repository +{ + public class AccountRepository : RepositoryBase, IAccountRepository + { + private ISortHelper _sortHelper; + private IDataShaper _dataShaper; + + public AccountRepository(RepositoryContext repositoryContext, ISortHelper sortHelper, IDataShaper dataShaper) + : base(repositoryContext) + { + _sortHelper = sortHelper; + _dataShaper = dataShaper; + } + + public PagedList GetAccountsByOwner(Guid ownerId, AccountParameters parameters) + { + var accounts = FindByCondition(a => a.OwnerId.Equals(ownerId)); + + _sortHelper.ApplySort(accounts, parameters.OrderBy); + + var shapedAccounts = _dataShaper.ShapeData(accounts, parameters.Fields); + + return PagedList.ToPagedList(shapedAccounts, + parameters.PageNumber, + parameters.PageSize); + } + + public ShapedEntity GetAccountByOwner(Guid ownerId, Guid id, string fields) + { + var account = FindByCondition(a => a.OwnerId.Equals(ownerId) && a.Id.Equals(id)).SingleOrDefault(); + return _dataShaper.ShapeData(account, fields); + } + + 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..41e9aec --- /dev/null +++ b/Repository/OwnerRepository.cs @@ -0,0 +1,83 @@ +using Contracts; +using Entities; +using Entities.Extensions; +using Entities.Helpers; +using Entities.Models; +using System; +using System.Linq; + +namespace Repository +{ + public class OwnerRepository : RepositoryBase, IOwnerRepository + { + private ISortHelper _sortHelper; + private IDataShaper _dataShaper; + + public OwnerRepository(RepositoryContext repositoryContext, + ISortHelper sortHelper, + IDataShaper dataShaper) + : base(repositoryContext) + { + _sortHelper = sortHelper; + _dataShaper = dataShaper; + } + + 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); + var shapedOwners = _dataShaper.ShapeData(owners, ownerParameters.Fields); + + return PagedList.ToPagedList(shapedOwners, + 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 ShapedEntity GetOwnerById(Guid ownerId, string fields) + { + var owner = FindByCondition(owner => owner.Id.Equals(ownerId)) + .DefaultIfEmpty(new Owner()) + .FirstOrDefault(); + + return _dataShaper.ShapeData(owner, fields); + } + + 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..ac324da --- /dev/null +++ b/Repository/RepositoryBase.cs @@ -0,0 +1,48 @@ +using Contracts; +using Entities; +using Entities.Helpers; +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..33c1d6d --- /dev/null +++ b/Repository/RepositoryWrapper.cs @@ -0,0 +1,62 @@ +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; + private IDataShaper _ownerDataShaper; + private IDataShaper _accountDataShaper; + + public IOwnerRepository Owner + { + get + { + if (_owner == null) + { + _owner = new OwnerRepository(_repoContext, _ownerSortHelper, _ownerDataShaper); + } + + return _owner; + } + } + + public IAccountRepository Account + { + get + { + if (_account == null) + { + _account = new AccountRepository(_repoContext, _accountSortHelper, _accountDataShaper); + } + + return _account; + } + } + + public RepositoryWrapper(RepositoryContext repositoryContext, + ISortHelper ownerSortHelper, + ISortHelper accountSortHelper, + IDataShaper ownerDataShaper, + IDataShaper accountDataShaper) + { + _repoContext = repositoryContext; + _ownerSortHelper = ownerSortHelper; + _accountSortHelper = accountSortHelper; + _ownerDataShaper = ownerDataShaper; + _accountDataShaper = accountDataShaper; + } + + 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 From c9da93cb55225183e89f39c3ae63b3ef28f22e90 Mon Sep 17 00:00:00 2001 From: CodeMazeBlog Date: Wed, 1 Apr 2020 20:12:45 +0200 Subject: [PATCH 2/3] Sorting fix --- Entities/Helpers/SortHelper.cs | 2 +- Repository/AccountRepository.cs | 4 ++-- Repository/OwnerRepository.cs | 4 ++-- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/Entities/Helpers/SortHelper.cs b/Entities/Helpers/SortHelper.cs index b658000..58ca35c 100644 --- a/Entities/Helpers/SortHelper.cs +++ b/Entities/Helpers/SortHelper.cs @@ -35,7 +35,7 @@ public IQueryable ApplySort(IQueryable entities, string orderByQueryString var descending = param.EndsWith(" desc") ? "descending" : "ascending"; - orderQueryBuilder.Append($"{objectProperty.Name.ToString()} {descending}, "); + orderQueryBuilder.Append($"{objectProperty.Name} {descending}, "); } var orderQuery = orderQueryBuilder.ToString().TrimEnd(',', ' '); diff --git a/Repository/AccountRepository.cs b/Repository/AccountRepository.cs index 160af0d..7dd852c 100644 --- a/Repository/AccountRepository.cs +++ b/Repository/AccountRepository.cs @@ -23,9 +23,9 @@ public PagedList GetAccountsByOwner(Guid ownerId, AccountParameter { var accounts = FindByCondition(a => a.OwnerId.Equals(ownerId)); - _sortHelper.ApplySort(accounts, parameters.OrderBy); + var sortedAccounts = _sortHelper.ApplySort(accounts, parameters.OrderBy); - var shapedAccounts = _dataShaper.ShapeData(accounts, parameters.Fields); + var shapedAccounts = _dataShaper.ShapeData(sortedAccounts, parameters.Fields); return PagedList.ToPagedList(shapedAccounts, parameters.PageNumber, diff --git a/Repository/OwnerRepository.cs b/Repository/OwnerRepository.cs index 41e9aec..c457f7d 100644 --- a/Repository/OwnerRepository.cs +++ b/Repository/OwnerRepository.cs @@ -29,8 +29,8 @@ public PagedList GetOwners(OwnerParameters ownerParameters) SearchByName(ref owners, ownerParameters.Name); - _sortHelper.ApplySort(owners, ownerParameters.OrderBy); - var shapedOwners = _dataShaper.ShapeData(owners, ownerParameters.Fields); + var sortedOwners = _sortHelper.ApplySort(owners, ownerParameters.OrderBy); + var shapedOwners = _dataShaper.ShapeData(sortedOwners, ownerParameters.Fields); return PagedList.ToPagedList(shapedOwners, ownerParameters.PageNumber, From 8b3adc8cfef94f0dd0cfe9857826dc278d4ef010 Mon Sep 17 00:00:00 2001 From: Code Maze Date: Fri, 2 Oct 2020 19:09:26 +0200 Subject: [PATCH 3/3] Fixed controller routes --- AccountOwnerServer/Controllers/AccountController.cs | 2 +- AccountOwnerServer/Controllers/OwnerController.cs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/AccountOwnerServer/Controllers/AccountController.cs b/AccountOwnerServer/Controllers/AccountController.cs index ae305f8..6b21f1d 100644 --- a/AccountOwnerServer/Controllers/AccountController.cs +++ b/AccountOwnerServer/Controllers/AccountController.cs @@ -11,7 +11,7 @@ namespace AccountOwnerServer.Controllers { - [Route("api/owner/{ownerId}/account")] + [Route("api/owners/{ownerId}/accounts")] [ApiController] public class AccountController : ControllerBase { diff --git a/AccountOwnerServer/Controllers/OwnerController.cs b/AccountOwnerServer/Controllers/OwnerController.cs index d49094e..3caab07 100644 --- a/AccountOwnerServer/Controllers/OwnerController.cs +++ b/AccountOwnerServer/Controllers/OwnerController.cs @@ -13,7 +13,7 @@ namespace AccountOwnerServer.Controllers { - [Route("api/owner")] + [Route("api/owners")] [ApiController] public class OwnerController : ControllerBase {