diff --git a/dotnet/.gitignore b/dotnet/.gitignore index 6982429..6a33e58 100644 --- a/dotnet/.gitignore +++ b/dotnet/.gitignore @@ -2,3 +2,5 @@ Debug/ Release/ obj/ +appsettings.Development.json +*.csproj.user diff --git a/dotnet/KeepTrack.sln b/dotnet/KeepTrack.sln index e63bdfc..4fd01ba 100644 --- a/dotnet/KeepTrack.sln +++ b/dotnet/KeepTrack.sln @@ -15,12 +15,26 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Car", "Car", "{A52EF787-A7D EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Movie", "Movie", "{A6A8275A-6360-43E7-AB5A-D8A1E96BED06}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ConsoleApp", "src\ConsoleApp\ConsoleApp.csproj", "{09DBB876-546F-40EF-B1D8-D0FDAE02E80D}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ConsoleApp", "src\ConsoleApp\ConsoleApp.csproj", "{3C4E6452-A0A7-4860-83F1-4AF7A0CF1E57}" EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "CarComponent.Domain", "src\CarComponent.Domain\CarComponent.Domain.csproj", "{A2193B19-8191-4A28-9E64-23D9083A0531}" EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "CarComponent.Infrastructure.MongoDb", "src\CarComponent.Infrastructure.MongoDb\CarComponent.Infrastructure.MongoDb.csproj", "{F7250DAA-718B-44FB-990B-AD7DFC488AA7}" EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{73A074B8-D41E-4CCD-AC4B-95DB716EC6C6}" + ProjectSection(SolutionItems) = preProject + CodeCoverage.runsettings = CodeCoverage.runsettings + README.md = README.md + EndProjectSection +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Api", "src\Api\Api.csproj", "{5B65EF04-3F7A-4042-8D8B-DFB3AD7CC560}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "CarComponent.Domain.UnitTests", "test\CarComponent.Domain.UnitTests\CarComponent.Domain.UnitTests.csproj", "{2079D00A-8489-4A43-BC2E-0B7CFB772CE7}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MovieComponent.Domain", "src\MovieComponent.Domain\MovieComponent.Domain.csproj", "{A08241ED-8F29-4DF7-9BCF-2E310A9C8187}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MovieComponent.Infrastructure.MongoDb", "src\MovieComponent.Infrastructure.MongoDb\MovieComponent.Infrastructure.MongoDb.csproj", "{E7BCC402-E50E-4AA1-AA7C-1A9C95DBDB50}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -31,10 +45,10 @@ Global {6A10979B-FBAE-488A-B5BC-0E45716DFE88}.Debug|Any CPU.Build.0 = Debug|Any CPU {6A10979B-FBAE-488A-B5BC-0E45716DFE88}.Release|Any CPU.ActiveCfg = Release|Any CPU {6A10979B-FBAE-488A-B5BC-0E45716DFE88}.Release|Any CPU.Build.0 = Release|Any CPU - {09DBB876-546F-40EF-B1D8-D0FDAE02E80D}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {09DBB876-546F-40EF-B1D8-D0FDAE02E80D}.Debug|Any CPU.Build.0 = Debug|Any CPU - {09DBB876-546F-40EF-B1D8-D0FDAE02E80D}.Release|Any CPU.ActiveCfg = Release|Any CPU - {09DBB876-546F-40EF-B1D8-D0FDAE02E80D}.Release|Any CPU.Build.0 = Release|Any CPU + {3C4E6452-A0A7-4860-83F1-4AF7A0CF1E57}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {3C4E6452-A0A7-4860-83F1-4AF7A0CF1E57}.Debug|Any CPU.Build.0 = Debug|Any CPU + {3C4E6452-A0A7-4860-83F1-4AF7A0CF1E57}.Release|Any CPU.ActiveCfg = Release|Any CPU + {3C4E6452-A0A7-4860-83F1-4AF7A0CF1E57}.Release|Any CPU.Build.0 = Release|Any CPU {A2193B19-8191-4A28-9E64-23D9083A0531}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {A2193B19-8191-4A28-9E64-23D9083A0531}.Debug|Any CPU.Build.0 = Debug|Any CPU {A2193B19-8191-4A28-9E64-23D9083A0531}.Release|Any CPU.ActiveCfg = Release|Any CPU @@ -43,6 +57,22 @@ Global {F7250DAA-718B-44FB-990B-AD7DFC488AA7}.Debug|Any CPU.Build.0 = Debug|Any CPU {F7250DAA-718B-44FB-990B-AD7DFC488AA7}.Release|Any CPU.ActiveCfg = Release|Any CPU {F7250DAA-718B-44FB-990B-AD7DFC488AA7}.Release|Any CPU.Build.0 = Release|Any CPU + {5B65EF04-3F7A-4042-8D8B-DFB3AD7CC560}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {5B65EF04-3F7A-4042-8D8B-DFB3AD7CC560}.Debug|Any CPU.Build.0 = Debug|Any CPU + {5B65EF04-3F7A-4042-8D8B-DFB3AD7CC560}.Release|Any CPU.ActiveCfg = Release|Any CPU + {5B65EF04-3F7A-4042-8D8B-DFB3AD7CC560}.Release|Any CPU.Build.0 = Release|Any CPU + {2079D00A-8489-4A43-BC2E-0B7CFB772CE7}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {2079D00A-8489-4A43-BC2E-0B7CFB772CE7}.Debug|Any CPU.Build.0 = Debug|Any CPU + {2079D00A-8489-4A43-BC2E-0B7CFB772CE7}.Release|Any CPU.ActiveCfg = Release|Any CPU + {2079D00A-8489-4A43-BC2E-0B7CFB772CE7}.Release|Any CPU.Build.0 = Release|Any CPU + {A08241ED-8F29-4DF7-9BCF-2E310A9C8187}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {A08241ED-8F29-4DF7-9BCF-2E310A9C8187}.Debug|Any CPU.Build.0 = Debug|Any CPU + {A08241ED-8F29-4DF7-9BCF-2E310A9C8187}.Release|Any CPU.ActiveCfg = Release|Any CPU + {A08241ED-8F29-4DF7-9BCF-2E310A9C8187}.Release|Any CPU.Build.0 = Release|Any CPU + {E7BCC402-E50E-4AA1-AA7C-1A9C95DBDB50}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {E7BCC402-E50E-4AA1-AA7C-1A9C95DBDB50}.Debug|Any CPU.Build.0 = Debug|Any CPU + {E7BCC402-E50E-4AA1-AA7C-1A9C95DBDB50}.Release|Any CPU.ActiveCfg = Release|Any CPU + {E7BCC402-E50E-4AA1-AA7C-1A9C95DBDB50}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -51,9 +81,13 @@ Global {6A10979B-FBAE-488A-B5BC-0E45716DFE88} = {215584EC-8DFC-4DAD-B3A0-D419CEC24260} {A52EF787-A7DD-4F2F-99B1-9A62CBE1DE5B} = {59B2BDBC-F7DE-47F8-880E-248202630B1A} {A6A8275A-6360-43E7-AB5A-D8A1E96BED06} = {59B2BDBC-F7DE-47F8-880E-248202630B1A} - {09DBB876-546F-40EF-B1D8-D0FDAE02E80D} = {61C528CD-816E-4FC5-ADFA-C5C25A3C4052} + {3C4E6452-A0A7-4860-83F1-4AF7A0CF1E57} = {61C528CD-816E-4FC5-ADFA-C5C25A3C4052} {A2193B19-8191-4A28-9E64-23D9083A0531} = {A52EF787-A7DD-4F2F-99B1-9A62CBE1DE5B} {F7250DAA-718B-44FB-990B-AD7DFC488AA7} = {A52EF787-A7DD-4F2F-99B1-9A62CBE1DE5B} + {5B65EF04-3F7A-4042-8D8B-DFB3AD7CC560} = {61C528CD-816E-4FC5-ADFA-C5C25A3C4052} + {2079D00A-8489-4A43-BC2E-0B7CFB772CE7} = {A52EF787-A7DD-4F2F-99B1-9A62CBE1DE5B} + {A08241ED-8F29-4DF7-9BCF-2E310A9C8187} = {A6A8275A-6360-43E7-AB5A-D8A1E96BED06} + {E7BCC402-E50E-4AA1-AA7C-1A9C95DBDB50} = {A6A8275A-6360-43E7-AB5A-D8A1E96BED06} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {876333E7-72D4-4F43-B6F9-9432CA2B7425} diff --git a/dotnet/README.md b/dotnet/README.md new file mode 100644 index 0000000..f159eca --- /dev/null +++ b/dotnet/README.md @@ -0,0 +1,21 @@ +# Keep track .NET solution + +[![Build Status](https://dev.azure.com/devprofr/open-source/_apis/build/status/keeptrack-CI?branchName=master)](https://dev.azure.com/devprofr/open-source/_build/latest?definitionId=18&branchName=master) +[![Quality Gate Status](https://sonarcloud.io/api/project_badges/measure?project=devpro.keep-track&metric=alert_status)](https://sonarcloud.io/dashboard?id=devpro.keep-track) +[![Coverage](https://sonarcloud.io/api/project_badges/measure?project=devpro.keep-track&metric=coverage)](https://sonarcloud.io/dashboard?id=devpro.keep-track) + +## Dependencies + +- SDK: .NET Core 3.0 +- DB: MongoDB 4.2 + +## Configuration + +- Value for key `KeepTrack_MongoDbConnectionString`: .NET connection string to access MongoDB cluster, ideally set as an environment variable. + +## Local run + +- Clone the solution: `git clone ...` +- Build the solution: `dotnet build` +- Run the console: `dotnet dotnet src\ConsoleApp\bin\Debug\netcoreapp3.0\KeepTrack.ConsoleApp.dll ...` +- Run the web api: `dotnet run --project src\Api` diff --git a/dotnet/src/Api/Api.csproj b/dotnet/src/Api/Api.csproj new file mode 100644 index 0000000..f475738 --- /dev/null +++ b/dotnet/src/Api/Api.csproj @@ -0,0 +1,35 @@ + + + + netcoreapp3.0 + KeepTrack.Api + KeepTrack.Api + {651432D5-8481-4807-A422-552EEB4DE8C2} + true + + + + full + true + + + + + + + + + + + + + + + + + + + + + + diff --git a/dotnet/src/Api/AppConfiguration.cs b/dotnet/src/Api/AppConfiguration.cs new file mode 100644 index 0000000..8fb6923 --- /dev/null +++ b/dotnet/src/Api/AppConfiguration.cs @@ -0,0 +1,75 @@ +using System.Collections.Generic; +using Microsoft.Extensions.Configuration; +using Microsoft.OpenApi.Models; +using Withywoods.Configuration; +using Withywoods.Dal.MongoDb; +using Withywoods.Dal.MongoDb.Serialization; + +namespace KeepTrack.Api +{ + /// + /// Web application configuration. + /// This class implements the interface from the libraries that are used in the application. + /// + public class AppConfiguration : IMongoDbConfiguration + { + #region Constructor & private fields + + /// + /// Create a new instance of + /// + /// + public AppConfiguration(IConfiguration configurationRoot) + { + ConfigurationRoot = configurationRoot; + } + + /// + /// Configuration root. + /// + public IConfiguration ConfigurationRoot { get; set; } + + #endregion + + #region IMongoDbConfiguration properties + + /// + /// MongoDB connection string => secret! + /// This is really a sensitive information so better defined as an environment variable. + /// + public string ConnectionString => ConfigurationRoot.TryGetSection("KeepTrack_MongoDbConnectionString").Value; + + /// + /// MongoDB collection name. + /// + public string DatabaseName => ConfigurationRoot.TryGetSection("Infrastructure:MongoDB:DatabaseName").Value; + + /// + /// MongoDB serialization conventions. + /// + public List SerializationConventions => + new List + { + ConventionValues.CamelCaseElementName, + ConventionValues.EnumAsString, + ConventionValues.IgnoreExtraElements, + ConventionValues.IgnoreNullValues + }; + + #endregion + + #region General properties + + /// + /// Open API information. + /// + public OpenApiInfo OpenApiInfo => + new OpenApiInfo + { + Title = "Keep Track API", + Version = "1.0" + }; + + #endregion + } +} diff --git a/dotnet/src/Api/Controllers/CarHistoryController.cs b/dotnet/src/Api/Controllers/CarHistoryController.cs new file mode 100644 index 0000000..9bd8765 --- /dev/null +++ b/dotnet/src/Api/Controllers/CarHistoryController.cs @@ -0,0 +1,135 @@ +using System.Collections.Generic; +using System.Threading.Tasks; +using AutoMapper; +using KeepTrack.Api.Dto; +using KeepTrack.CarComponent.Domain; +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Mvc; + +namespace Api.Controllers +{ + /// + /// Car history controller. + /// + [ApiController] + [Authorize] + [Route("api/car-history")] + public class CarHistoryController : KeepTrack.Api.Controllers.ControllerBase + { + private readonly IMapper _mapper; + private readonly ICarHistoryRepository _carHistoryRepository; + + /// + /// Creates a new instance of . + /// + /// + /// + public CarHistoryController(IMapper mapper, ICarHistoryRepository carHistoryRepository) + { + _mapper = mapper; + _carHistoryRepository = carHistoryRepository; + } + + /// + /// Gets all the history for a given car. + /// + /// Car ID + /// + [HttpGet] + [ProducesResponseType(200, Type = typeof(List))] + [ProducesResponseType(400)] + [ProducesResponseType(500)] + public async Task Get(string carId) + { + if (string.IsNullOrEmpty(carId)) + { + return BadRequest(); + } + + var models = await _carHistoryRepository.FindAllAsync(carId, GetUserId()); + return Ok(_mapper.Map>(models)); + } + + /// + /// Gets information from a single car history. + /// + /// + /// + [HttpGet("{id}", Name = "GetCarHistoryById")] + [ProducesResponseType(200, Type = typeof(CarHistoryDto))] + [ProducesResponseType(400)] + [ProducesResponseType(404)] + [ProducesResponseType(500)] + public async Task GetById(string id) + { + if (string.IsNullOrEmpty(id)) + { + return BadRequest(); + } + + var model = await _carHistoryRepository.FindOneAsync(id, GetUserId()); + if (model == null) + { + return NotFound(); + } + + return Ok(_mapper.Map(model)); + } + + /// + /// Creates a new car history. + /// + /// + [HttpPost] + [ProducesResponseType(201)] + public async Task Post([FromBody] CarHistoryDto dto) + { + var input = _mapper.Map(dto); + input.OwnerId = GetUserId(); + var model = await _carHistoryRepository.CreateAsync(input); + return CreatedAtRoute("GetCarHistoryById", new { id = model.Id }, _mapper.Map(model)); + } + + /// + /// Updates a car history. + /// + /// + /// + [HttpPut("{id}")] + [ProducesResponseType(204)] + [ProducesResponseType(400)] + [ProducesResponseType(500)] + public async Task Put(string id, [FromBody] CarHistoryDto dto) + { + if (string.IsNullOrEmpty(id)) + { + return BadRequest(); + } + + var input = _mapper.Map(dto); + input.OwnerId = GetUserId(); + await _carHistoryRepository.UpdateAsync(id, input, GetUserId()); + return NoContent(); + } + + /// + /// Deletes a car history. + /// + /// + /// + [HttpDelete("{id}")] + [ProducesResponseType(204)] + [ProducesResponseType(400)] + [ProducesResponseType(500)] + public async Task Delete(string id) + { + if (string.IsNullOrEmpty(id)) + { + return BadRequest(); + } + + await _carHistoryRepository.DeleteAsync(id, GetUserId()); + return NoContent(); + } + } +} diff --git a/dotnet/src/Api/Controllers/ControllerBase.cs b/dotnet/src/Api/Controllers/ControllerBase.cs new file mode 100644 index 0000000..4a8db0f --- /dev/null +++ b/dotnet/src/Api/Controllers/ControllerBase.cs @@ -0,0 +1,26 @@ +using System; +using System.Linq; + +namespace KeepTrack.Api.Controllers +{ + /// + /// Base controller for the web application. + /// + public abstract class ControllerBase : Microsoft.AspNetCore.Mvc.ControllerBase + { + /// + /// Get authenticated user id. + /// + /// + protected string GetUserId() + { + var userId = User.Claims.FirstOrDefault(x => x.Type == "user_id")?.Value; + if (string.IsNullOrEmpty(userId)) + { + throw new UnauthorizedAccessException(); + } + + return userId; + } + } +} diff --git a/dotnet/src/Api/Controllers/MovieController.cs b/dotnet/src/Api/Controllers/MovieController.cs new file mode 100644 index 0000000..0d2a310 --- /dev/null +++ b/dotnet/src/Api/Controllers/MovieController.cs @@ -0,0 +1,129 @@ +using System.Collections.Generic; +using System.Threading.Tasks; +using AutoMapper; +using KeepTrack.Api.Dto; +using KeepTrack.MovieComponent.Domain; +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Mvc; + +namespace KeepTrack.Api.Controllers +{ + /// + /// Movie controller. + /// + [ApiController] + [Authorize] + [Route("api/movies")] + public class MovieController : KeepTrack.Api.Controllers.ControllerBase + { + private readonly IMapper _mapper; + private readonly IMovieRepository _movieRepository; + + /// + /// Creates a new instance of . + /// + /// + /// + public MovieController(IMapper mapper, IMovieRepository movieRepository) + { + _mapper = mapper; + _movieRepository = movieRepository; + } + + /// + /// Gets all the movies. + /// + /// + [HttpGet] + [ProducesResponseType(200, Type = typeof(List))] + [ProducesResponseType(400)] + [ProducesResponseType(500)] + public async Task Get() + { + var models = await _movieRepository.FindAllAsync(GetUserId()); + return Ok(_mapper.Map>(models)); + } + + /// + /// Gets information from a single movie. + /// + /// + /// + [HttpGet("{id}", Name = "GetMovieById")] + [ProducesResponseType(200, Type = typeof(MovieDto))] + [ProducesResponseType(400)] + [ProducesResponseType(404)] + [ProducesResponseType(500)] + public async Task GetById(string id) + { + if (string.IsNullOrEmpty(id)) + { + return BadRequest(); + } + + var model = await _movieRepository.FindOneAsync(id, GetUserId()); + if (model == null) + { + return NotFound(); + } + + return Ok(_mapper.Map(model)); + } + + /// + /// Creates a new car history. + /// + /// + [HttpPost] + [ProducesResponseType(201)] + public async Task Post([FromBody] MovieDto dto) + { + var input = _mapper.Map(dto); + input.OwnerId = GetUserId(); + var model = await _movieRepository.CreateAsync(input); + return CreatedAtRoute("GetMovieById", new { id = model.Id }, _mapper.Map(model)); + } + + /// + /// Updates a movie. + /// + /// + /// + [HttpPut("{id}")] + [ProducesResponseType(204)] + [ProducesResponseType(400)] + [ProducesResponseType(500)] + public async Task Put(string id, [FromBody] MovieDto dto) + { + if (string.IsNullOrEmpty(id)) + { + return BadRequest(); + } + + var input = _mapper.Map(dto); + input.OwnerId = GetUserId(); + await _movieRepository.UpdateAsync(id, input, GetUserId()); + return NoContent(); + } + + /// + /// Deletes a movie. + /// + /// + /// + [HttpDelete("{id}")] + [ProducesResponseType(204)] + [ProducesResponseType(400)] + [ProducesResponseType(500)] + public async Task Delete(string id) + { + if (string.IsNullOrEmpty(id)) + { + return BadRequest(); + } + + await _movieRepository.DeleteAsync(id, GetUserId()); + return NoContent(); + } + } +} diff --git a/dotnet/src/Api/Dto/CarHistoryDto.cs b/dotnet/src/Api/Dto/CarHistoryDto.cs new file mode 100644 index 0000000..c045e05 --- /dev/null +++ b/dotnet/src/Api/Dto/CarHistoryDto.cs @@ -0,0 +1,90 @@ +using System; + +namespace KeepTrack.Api.Dto +{ + /// + /// Car history data transfer object. + /// + public class CarHistoryDto + { + /// + /// History ID. + /// + public string Id { get; set; } + + /// + /// Car ID. + /// + public string CarId { get; set; } + + /// + /// History date. + /// + public DateTime HistoryDate { get; set; } + + /// + /// Mileage indicated on the car. + /// + public int Mileage { get; set; } + + /// + /// Action made on the car. + /// + public string Action { get; set; } + + /// + /// City. + /// + public string City { get; set; } + + /// + /// Longitude. + /// + public double? Longitude { get; set; } + + /// + /// Latitude. + /// + public double? Latitude { get; set; } + + /// + /// Fuel category. + /// + public string FuelCategory { get; set; } + + /// + /// Fuel volme (L). + /// + public double? FuelVolume { get; set; } + + /// + /// Fuel unit price. + /// + public double? FuelUnitPrice { get; set; } + + /// + /// Amount. + /// + public double? Amount { get; set; } + + /// + /// Is full tank? + /// + public bool? IsFullTank { get; set; } + + /// + /// Delta mileage since last refuel. + /// + public double? DeltaMileage { get; set; } + + /// + /// Last refuel history id. + /// + public string LastRefuelHistoryId { get; set; } + + /// + /// Station brand name. + /// + public string StationBrandName { get; set; } + } +} diff --git a/dotnet/src/Api/Dto/MovieDto.cs b/dotnet/src/Api/Dto/MovieDto.cs new file mode 100644 index 0000000..4677d7c --- /dev/null +++ b/dotnet/src/Api/Dto/MovieDto.cs @@ -0,0 +1,18 @@ +namespace KeepTrack.Api.Dto +{ + /// + /// Movie data transfer object. + /// + public class MovieDto + { + /// + /// Movie ID. + /// + public string Id { get; set; } + + /// + /// Title. + /// + public string Title { get; set; } + } +} diff --git a/dotnet/src/Api/Filters/CustomExceptionFilterAttribute.cs b/dotnet/src/Api/Filters/CustomExceptionFilterAttribute.cs new file mode 100644 index 0000000..6bfe61a --- /dev/null +++ b/dotnet/src/Api/Filters/CustomExceptionFilterAttribute.cs @@ -0,0 +1,43 @@ +using System; +using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Mvc.Filters; + +namespace KeepTrack.Api.Filters +{ + /// + /// Exception filter to make sure the + /// + public sealed class CustomExceptionFilterAttribute : ExceptionFilterAttribute + { + /// + /// Create a new instance of . + /// + public CustomExceptionFilterAttribute() + { + } + + /// + /// Review when an exception is raised. + /// + /// + public override void OnException(ExceptionContext context) + { + switch (context.Exception) + { + case ArgumentNullException argumentNullException: + context.Result = new JsonResult(argumentNullException.Message); + context.HttpContext.Response.StatusCode = 400; + break; + case ArgumentException argumentException: + context.Result = new JsonResult(argumentException.Message); + context.HttpContext.Response.StatusCode = 400; + break; + default: + context.Result = new JsonResult(context.Exception.Message); + context.HttpContext.Response.StatusCode = 500; + break; + } + base.OnException(context); + } + } +} diff --git a/dotnet/src/Api/MappingProfiles/CarMappingProfile.cs b/dotnet/src/Api/MappingProfiles/CarMappingProfile.cs new file mode 100644 index 0000000..6dedefc --- /dev/null +++ b/dotnet/src/Api/MappingProfiles/CarMappingProfile.cs @@ -0,0 +1,28 @@ +using AutoMapper; + +namespace KeepTrack.Api.MappingProfiles +{ + /// + /// Car mapping profile. + /// + public class CarMappingProfile : Profile + { + /// + /// Profile name. + /// + public override string ProfileName + { + get { return "KeepTrackApiCarMappingProfile"; } + } + + /// + /// Create a new instance of . + /// + public CarMappingProfile() + { + CreateMap() + .ForMember(x => x.OwnerId, opt => opt.Ignore()); + CreateMap(); + } + } +} diff --git a/dotnet/src/Api/MappingProfiles/MovieMappingProfile.cs b/dotnet/src/Api/MappingProfiles/MovieMappingProfile.cs new file mode 100644 index 0000000..5db0d3e --- /dev/null +++ b/dotnet/src/Api/MappingProfiles/MovieMappingProfile.cs @@ -0,0 +1,28 @@ +using AutoMapper; + +namespace KeepTrack.Api.MappingProfiles +{ + /// + /// Movie mapping profile. + /// + public class MovieMappingProfile : Profile + { + /// + /// Profile name. + /// + public override string ProfileName + { + get { return "KeepTrackApiMovieMappingProfile"; } + } + + /// + /// Create a new instance of . + /// + public MovieMappingProfile() + { + CreateMap() + .ForMember(x => x.OwnerId, opt => opt.Ignore()); + CreateMap(); + } + } +} diff --git a/dotnet/src/Api/Program.cs b/dotnet/src/Api/Program.cs new file mode 100644 index 0000000..60cb7b1 --- /dev/null +++ b/dotnet/src/Api/Program.cs @@ -0,0 +1,34 @@ +using Microsoft.AspNetCore.Hosting; +using Microsoft.Extensions.Hosting; + +namespace KeepTrack.Api +{ + /// + /// Application program. + /// + public static class Program + { + /// + /// Starting point. + /// + /// + public static void Main(string[] args) + { + CreateHostBuilder(args).Build().Run(); + } + + /// + /// Create web application web host builder. + /// + /// + /// + public static IHostBuilder CreateHostBuilder(string[] args) + { + return Host.CreateDefaultBuilder(args) + .ConfigureWebHostDefaults(webBuilder => + { + webBuilder.UseStartup(); + }); + } + } +} diff --git a/dotnet/src/Api/Properties/launchSettings.json b/dotnet/src/Api/Properties/launchSettings.json new file mode 100644 index 0000000..70dcaad --- /dev/null +++ b/dotnet/src/Api/Properties/launchSettings.json @@ -0,0 +1,30 @@ +{ + "$schema": "http://json.schemastore.org/launchsettings.json", + "iisSettings": { + "windowsAuthentication": false, + "anonymousAuthentication": true, + "iisExpress": { + "applicationUrl": "http://localhost:61193", + "sslPort": 44368 + } + }, + "profiles": { + "IIS Express": { + "commandName": "IISExpress", + "launchBrowser": true, + "launchUrl": "swagger", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + }, + "Api": { + "commandName": "Project", + "launchBrowser": true, + "launchUrl": "swagger", + "applicationUrl": "https://localhost:5001;http://localhost:5000", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + } + } +} diff --git a/dotnet/src/Api/Startup.cs b/dotnet/src/Api/Startup.cs new file mode 100644 index 0000000..c8f01ac --- /dev/null +++ b/dotnet/src/Api/Startup.cs @@ -0,0 +1,163 @@ +using System; +using System.IO; +using System.Reflection; +using AutoMapper; +using KeepTrack.CarComponent.Infrastructure.MongoDb.DependencyInjection; +using KeepTrack.MovieComponent.Infrastructure.MongoDb.DependencyInjection; +using Microsoft.AspNetCore.Authentication.JwtBearer; +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Hosting; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Hosting; +using Microsoft.IdentityModel.Tokens; +using Microsoft.OpenApi.Models; +using MongoDB.Bson; +using Withywoods.Dal.MongoDb.DependencyInjection; +using Withywoods.Dal.MongoDb.MappingConverters; + +namespace KeepTrack.Api +{ + /// + /// Application startup. + /// + public class Startup + { + private readonly AppConfiguration _configuration; + + /// + /// Create a new instance of . + /// + /// + public Startup(IConfiguration configuration) + { + _configuration = new AppConfiguration(configuration); + } + + /// + /// Configure services. + /// + /// + public void ConfigureServices(IServiceCollection services) + { + services.AddSingleton(_configuration.ConfigurationRoot) + .AddCarInfrastructureMongoDb() + .AddMovieInfrastructureMongoDb() + .AddMongoDbContext(); + + ConfigureAutoMapper(services); + + ConfigureAuthentication(services, _configuration.ConfigurationRoot); + + services.AddControllers(opts => + { + opts.Filters.Add(); + }); + + ConfigureSwagger(services, _configuration.OpenApiInfo); + } + + /// + /// Configure the application pipeline. + /// + /// + /// + public void Configure(IApplicationBuilder app, IWebHostEnvironment env) + { + if (env.IsDevelopment()) + { + app.UseDeveloperExceptionPage(); + } + + app.UseSwagger(); + + app.UseSwaggerUI(c => + { + c.SwaggerEndpoint($"/swagger/{_configuration.OpenApiInfo.Version}/swagger.json", _configuration.OpenApiInfo.Title); + }); + + app.UseHttpsRedirection(); + + app.UseRouting(); + + app.UseAuthentication(); + + app.UseAuthorization(); + + app.UseEndpoints(endpoints => + { + endpoints.MapControllers(); + }); + } + + private static void ConfigureAutoMapper(IServiceCollection serviceCollection) + { + var mappingConfig = new MapperConfiguration(x => + { + // Infrastructure MongoDB + x.AddProfile(new CarComponent.Infrastructure.MongoDb.MappingProfiles.CarMappingProfile()); + x.AddProfile(new MovieComponent.Infrastructure.MongoDb.MappingProfiles.MovieMappingProfile()); + x.CreateMap().ConvertUsing(); + x.CreateMap().ConvertUsing(); + // Api + x.AddProfile(new MappingProfiles.CarMappingProfile()); + x.AddProfile(new MappingProfiles.MovieMappingProfile()); + // General + x.AllowNullCollections = true; + }); + var mapper = mappingConfig.CreateMapper(); + mapper.ConfigurationProvider.AssertConfigurationIsValid(); + serviceCollection.AddSingleton(mapper); + } + + private static void ConfigureAuthentication(IServiceCollection serviceCollection, IConfiguration configuration) + { + serviceCollection + .AddAuthentication(JwtBearerDefaults.AuthenticationScheme) + .AddJwtBearer(options => + { + options.Authority = configuration["Authentication:JwtBearer:Authority"]; + options.TokenValidationParameters = new TokenValidationParameters + { + ValidateIssuer = true, + ValidIssuer = configuration["Authentication:JwtBearer:TokenValidation:Issuer"], + ValidateAudience = true, + ValidAudience = configuration["Authentication:JwtBearer:TokenValidation:Audience"], + ValidateLifetime = true + }; + }); + } + + private static void ConfigureSwagger(IServiceCollection serviceCollection, OpenApiInfo openApiInfo) + { + serviceCollection.AddSwaggerGen(c => + { + c.SwaggerDoc(openApiInfo.Version, + new OpenApiInfo { Title = openApiInfo.Title, Version = openApiInfo.Version }); + + c.AddSecurityDefinition("Bearer", new OpenApiSecurityScheme + { + Description = "JWT Authorization header using the Bearer scheme. Example: \"Authorization: Bearer {token}\"", + Name = "Authorization", + In = ParameterLocation.Header, + Type = SecuritySchemeType.ApiKey + }); + + c.AddSecurityRequirement(new OpenApiSecurityRequirement + { + { + new OpenApiSecurityScheme + { + Reference = new OpenApiReference { Type = ReferenceType.SecurityScheme, Id = "Bearer" } + }, + new[] { "readAccess", "writeAccess" } + } + }); + + var xmlFile = $"{Assembly.GetExecutingAssembly().GetName().Name}.xml"; + var xmlPath = Path.Combine(AppContext.BaseDirectory, xmlFile); + c.IncludeXmlComments(xmlPath); + }); + } + } +} diff --git a/dotnet/src/Api/appsettings.json b/dotnet/src/Api/appsettings.json new file mode 100644 index 0000000..e32f0c5 --- /dev/null +++ b/dotnet/src/Api/appsettings.json @@ -0,0 +1,24 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft": "Warning", + "Microsoft.Hosting.Lifetime": "Information" + } + }, + "Infrastructure": { + "MongoDB": { + "DatabaseName": "inventory" + } + }, + "AllowedHosts": "*", + "Authentication": { + "JwtBearer": { + "Authority": "", + "TokenValidation": { + "Issuer": "", + "Audience": "" + } + } + } +} diff --git a/dotnet/src/CarComponent.Domain/CarHistoryModel.cs b/dotnet/src/CarComponent.Domain/CarHistoryModel.cs index bdf5aad..10b9cde 100644 --- a/dotnet/src/CarComponent.Domain/CarHistoryModel.cs +++ b/dotnet/src/CarComponent.Domain/CarHistoryModel.cs @@ -5,7 +5,37 @@ namespace KeepTrack.CarComponent.Domain public class CarHistoryModel { public string Id { get; set; } + + public string OwnerId { get; set; } + public string CarId { get; set; } + public DateTime HistoryDate { get; set; } + + public int Mileage { get; set; } + + public string Action { get; set; } + + public string City { get; set; } + + public double? Longitude { get; set; } + + public double? Latitude { get; set; } + + public string FuelCategory { get; set; } + + public double? FuelVolume { get; set; } + + public double? FuelUnitPrice { get; set; } + + public double? Amount { get; set; } + + public bool? IsFullTank { get; set; } + + public double? DeltaMileage { get; set; } + + public string LastRefuelHistoryId { get; set; } + + public string StationBrandName { get; set; } } } diff --git a/dotnet/src/CarComponent.Domain/CarModel.cs b/dotnet/src/CarComponent.Domain/CarModel.cs index 8fa7ad5..e21ff7a 100644 --- a/dotnet/src/CarComponent.Domain/CarModel.cs +++ b/dotnet/src/CarComponent.Domain/CarModel.cs @@ -3,6 +3,9 @@ public class CarModel { public string Id { get; set; } + + public string OwnerId { get; set; } + public string Name { get; set; } public override string ToString() diff --git a/dotnet/src/CarComponent.Domain/ICarHistoryRepository.cs b/dotnet/src/CarComponent.Domain/ICarHistoryRepository.cs index b97cf8b..9d5d4a1 100644 --- a/dotnet/src/CarComponent.Domain/ICarHistoryRepository.cs +++ b/dotnet/src/CarComponent.Domain/ICarHistoryRepository.cs @@ -5,7 +5,14 @@ namespace KeepTrack.CarComponent.Domain { public interface ICarHistoryRepository { - Task FindOneAsync(string id); - Task> FindAllAsync(string carId); + Task FindOneAsync(string id, string ownerId); + + Task> FindAllAsync(string carId, string ownerId); + + Task CreateAsync(CarHistoryModel model); + + Task UpdateAsync(string id, CarHistoryModel model, string ownerId); + + Task DeleteAsync(string id, string ownerId); } } diff --git a/dotnet/src/CarComponent.Infrastructure.MongoDb/CarComponent.Infrastructure.MongoDb.csproj b/dotnet/src/CarComponent.Infrastructure.MongoDb/CarComponent.Infrastructure.MongoDb.csproj index 59209cd..ab1454e 100644 --- a/dotnet/src/CarComponent.Infrastructure.MongoDb/CarComponent.Infrastructure.MongoDb.csproj +++ b/dotnet/src/CarComponent.Infrastructure.MongoDb/CarComponent.Infrastructure.MongoDb.csproj @@ -12,12 +12,9 @@ true - - - - + diff --git a/dotnet/src/CarComponent.Infrastructure.MongoDb/Entities/Car.cs b/dotnet/src/CarComponent.Infrastructure.MongoDb/Entities/Car.cs index 2818c94..d9fc176 100644 --- a/dotnet/src/CarComponent.Infrastructure.MongoDb/Entities/Car.cs +++ b/dotnet/src/CarComponent.Infrastructure.MongoDb/Entities/Car.cs @@ -1,5 +1,4 @@ -using MongoDB.Bson; -using MongoDB.Bson.Serialization.Attributes; +using MongoDB.Bson.Serialization.Attributes; namespace KeepTrack.CarComponent.Infrastructure.MongoDb.Entities { @@ -7,6 +6,10 @@ public class Car { [BsonId] public string Id { get; set; } + + [BsonElement("owner_id")] + public string OwnerId { get; set; } + [BsonElement("commercial_name")] public string Name { get; set; } } diff --git a/dotnet/src/CarComponent.Infrastructure.MongoDb/Entities/CarHistory.cs b/dotnet/src/CarComponent.Infrastructure.MongoDb/Entities/CarHistory.cs index 34326fc..a27cb2e 100644 --- a/dotnet/src/CarComponent.Infrastructure.MongoDb/Entities/CarHistory.cs +++ b/dotnet/src/CarComponent.Infrastructure.MongoDb/Entities/CarHistory.cs @@ -1,16 +1,41 @@ using System; +using System.Collections.Generic; +using KeepTrack.Dal.MongoDb.Entities; using MongoDB.Bson; using MongoDB.Bson.Serialization.Attributes; namespace KeepTrack.CarComponent.Infrastructure.MongoDb.Entities { - public class CarHistory + public partial class CarHistory : IEntity { [BsonId] public ObjectId Id { get; set; } + + [BsonElement("owner_id")] + public string OwnerId { get; set; } + [BsonElement("car_id")] public string CarId { get; set; } + [BsonElement("history_date")] public DateTime HistoryDate { get; set; } + + [BsonElement("mileage")] + public double Mileage { get; set; } + + [BsonElement("action")] + public string Action { get; set; } + + [BsonElement("location")] + public CarHistoryLocation Location { get; set; } + + [BsonElement("coordinates")] + public List Coordinates { get; set; } + + [BsonElement("fuel")] + public CarHistoryFuel Fuel { get; set; } + + [BsonElement("station")] + public CarHistoryStation Station { get; set; } } } diff --git a/dotnet/src/CarComponent.Infrastructure.MongoDb/Entities/CarHistoryFuel.cs b/dotnet/src/CarComponent.Infrastructure.MongoDb/Entities/CarHistoryFuel.cs new file mode 100644 index 0000000..911d620 --- /dev/null +++ b/dotnet/src/CarComponent.Infrastructure.MongoDb/Entities/CarHistoryFuel.cs @@ -0,0 +1,29 @@ +using MongoDB.Bson; +using MongoDB.Bson.Serialization.Attributes; + +namespace KeepTrack.CarComponent.Infrastructure.MongoDb.Entities +{ + public class CarHistoryFuel + { + [BsonElement("category")] + public string Category { get; set; } + + [BsonElement("volume")] + public double? Volume { get; set; } + + [BsonElement("unit_price")] + public double? UnitPrice { get; set; } + + [BsonElement("amount")] + public double? Amount { get; set; } + + [BsonElement("is_full_tank")] + public bool? IsFullTank { get; set; } + + [BsonElement("delta_mileage")] + public double? DeltaMileage { get; set; } + + [BsonElement("last_refuel_history_id")] + public ObjectId? LastRefuelHistoryId { get; set; } + } +} diff --git a/dotnet/src/CarComponent.Infrastructure.MongoDb/Entities/CarHistoryLocation.cs b/dotnet/src/CarComponent.Infrastructure.MongoDb/Entities/CarHistoryLocation.cs new file mode 100644 index 0000000..d91a423 --- /dev/null +++ b/dotnet/src/CarComponent.Infrastructure.MongoDb/Entities/CarHistoryLocation.cs @@ -0,0 +1,10 @@ +using MongoDB.Bson.Serialization.Attributes; + +namespace KeepTrack.CarComponent.Infrastructure.MongoDb.Entities +{ + public partial class CarHistoryLocation + { + [BsonElement("city")] + public string City { get; set; } + } +} diff --git a/dotnet/src/CarComponent.Infrastructure.MongoDb/Entities/CarHistoryStation.cs b/dotnet/src/CarComponent.Infrastructure.MongoDb/Entities/CarHistoryStation.cs new file mode 100644 index 0000000..18f9995 --- /dev/null +++ b/dotnet/src/CarComponent.Infrastructure.MongoDb/Entities/CarHistoryStation.cs @@ -0,0 +1,10 @@ +using MongoDB.Bson.Serialization.Attributes; + +namespace KeepTrack.CarComponent.Infrastructure.MongoDb.Entities +{ + public partial class CarHistoryStation + { + [BsonElement("brand_name")] + public string BrandName { get; set; } + } +} diff --git a/dotnet/src/CarComponent.Infrastructure.MongoDb/MappingProfiles/CarMappingProfile.cs b/dotnet/src/CarComponent.Infrastructure.MongoDb/MappingProfiles/CarMappingProfile.cs index f150d65..5d4f96b 100644 --- a/dotnet/src/CarComponent.Infrastructure.MongoDb/MappingProfiles/CarMappingProfile.cs +++ b/dotnet/src/CarComponent.Infrastructure.MongoDb/MappingProfiles/CarMappingProfile.cs @@ -1,4 +1,5 @@ -using AutoMapper; +using System.Collections.Generic; +using AutoMapper; namespace KeepTrack.CarComponent.Infrastructure.MongoDb.MappingProfiles { @@ -12,7 +13,38 @@ public override string ProfileName public CarMappingProfile() { CreateMap(); - CreateMap(); + CreateMap(); + + MapCarHistoryModel(); + MapCarHistory(); + } + + private void MapCarHistoryModel() + { + CreateMap() + .ForMember(x => x.City, opt => opt.MapFrom(x => x.Location != null ? x.Location.City : null)) + .ForMember(x => x.Longitude, opt => opt.MapFrom(x => x.Coordinates != null ? x.Coordinates[0] : (double?)null)) + .ForMember(x => x.Latitude, opt => opt.MapFrom(x => x.Coordinates != null ? x.Coordinates[1] : (double?)null)) + .ForMember(x => x.Amount, opt => opt.MapFrom(x => x.Fuel != null ? x.Fuel.Amount : null)) + .ForMember(x => x.IsFullTank, opt => opt.MapFrom(x => x.Fuel != null ? x.Fuel.IsFullTank : null)) + .ForMember(x => x.DeltaMileage, opt => opt.MapFrom(x => x.Fuel != null ? x.Fuel.DeltaMileage : null)) + .ForMember(x => x.LastRefuelHistoryId, opt => opt.MapFrom(x => x.Fuel != null ? x.Fuel.LastRefuelHistoryId : null)); + } + + private void MapCarHistory() + { + CreateMap() + .ForMember(x => x.Location, opt => opt.MapFrom(x => x)) + .ForMember(x => x.Coordinates, opt => opt.MapFrom(x => (x.Longitude.HasValue && x.Latitude.HasValue) ? new List { x.Longitude.Value, x.Latitude.Value } : null)) + .ForMember(x => x.Fuel, opt => opt.MapFrom(x => x)) + .ForMember(x => x.Station, opt => opt.MapFrom(x => x)); + CreateMap(); + CreateMap() + .ForMember(x => x.Category, opt => opt.MapFrom(x => x.FuelCategory)) + .ForMember(x => x.Volume, opt => opt.MapFrom(x => x.FuelVolume)) + .ForMember(x => x.UnitPrice, opt => opt.MapFrom(x => x.FuelUnitPrice)); + CreateMap() + .ForMember(x => x.BrandName, opt => opt.MapFrom(x => x.StationBrandName)); } } } diff --git a/dotnet/src/CarComponent.Infrastructure.MongoDb/Repositories/CarHistoryRepository.cs b/dotnet/src/CarComponent.Infrastructure.MongoDb/Repositories/CarHistoryRepository.cs index 4077c6b..ff44301 100644 --- a/dotnet/src/CarComponent.Infrastructure.MongoDb/Repositories/CarHistoryRepository.cs +++ b/dotnet/src/CarComponent.Infrastructure.MongoDb/Repositories/CarHistoryRepository.cs @@ -3,15 +3,14 @@ using AutoMapper; using KeepTrack.CarComponent.Domain; using KeepTrack.CarComponent.Infrastructure.MongoDb.Entities; +using KeepTrack.Dal.MongoDb.Repositories; using Microsoft.Extensions.Logging; -using MongoDB.Bson; using MongoDB.Driver; using Withywoods.Dal.MongoDb; -using Withywoods.Dal.MongoDb.Repositories; namespace KeepTrack.CarComponent.Infrastructure.Repositories { - public class CarHistoryRepository : RepositoryBase, ICarHistoryRepository + public class CarHistoryRepository : RepositoryBase, ICarHistoryRepository { public CarHistoryRepository(IMongoDbContext mongoDbContext, ILogger logger, IMapper mapper) : base(mongoDbContext, logger, mapper) @@ -20,22 +19,10 @@ public CarHistoryRepository(IMongoDbContext mongoDbContext, ILogger "car_history"; - public async Task FindOneAsync(string id) - { - if (!ObjectId.TryParse(id, out var objectId)) - { - throw new System.Exception($"Cannot find the car history. \"{id}\" is not a valid id."); - } - - var collection = GetCollection(); - var dbEntries = await collection.FindAsync(x => x.Id == objectId); - return Mapper.Map(dbEntries.FirstOrDefault()); - } - - public async Task> FindAllAsync(string carId) + public async Task> FindAllAsync(string carId, string ownerId) { var collection = GetCollection(); - var dbEntries = await collection.FindAsync(x => x.CarId == carId); + var dbEntries = await collection.FindAsync(x => x.CarId == carId && x.OwnerId == ownerId); return Mapper.Map>(dbEntries.ToList()); } } diff --git a/dotnet/src/CarComponent.Infrastructure.MongoDb/Repositories/CarRepository.cs b/dotnet/src/CarComponent.Infrastructure.MongoDb/Repositories/CarRepository.cs index 98c4591..c085bed 100644 --- a/dotnet/src/CarComponent.Infrastructure.MongoDb/Repositories/CarRepository.cs +++ b/dotnet/src/CarComponent.Infrastructure.MongoDb/Repositories/CarRepository.cs @@ -1,9 +1,9 @@ -using System.Threading.Tasks; +using System; +using System.Threading.Tasks; using AutoMapper; using KeepTrack.CarComponent.Domain; using KeepTrack.CarComponent.Infrastructure.MongoDb.Entities; using Microsoft.Extensions.Logging; -using MongoDB.Bson; using MongoDB.Driver; using Withywoods.Dal.MongoDb; using Withywoods.Dal.MongoDb.Repositories; @@ -23,7 +23,7 @@ public async Task FindOneAsync(string id) { if (string.IsNullOrEmpty(id)) { - throw new System.Exception($"Cannot find a car. \"{id}\" is not a valid id."); + throw new ArgumentNullException(nameof(id), $"Cannot find a car. \"{id}\" is not a valid id."); } var collection = GetCollection(); diff --git a/dotnet/src/ConsoleApp/Program.cs b/dotnet/src/ConsoleApp/Program.cs index e11c917..e3432b1 100644 --- a/dotnet/src/ConsoleApp/Program.cs +++ b/dotnet/src/ConsoleApp/Program.cs @@ -41,7 +41,7 @@ private async static Task Main(string[] args) await Parser.Default.ParseArguments(args) .MapResult( (CommandLineOptions opts) => RunOptionsAndReturnExitCode(opts), - errs => Task.FromResult(HandleParseError(errs)) + errs => Task.FromResult(HandleParseError()) ); } @@ -53,30 +53,35 @@ private async static Task RunOptionsAndReturnExitCode(CommandLineOptions op using (var serviceProvider = CreateServiceProvider(configuration)) { - if (opts.Action == "CarDemo") + switch (opts.Action) { - var id = opts.Id; + case "CarDemo": + var id = opts.Id; - LogVerbose(opts, "Query the car collection"); + LogVerbose(opts, "Query the car collection"); - var carRepository = serviceProvider.GetService(); - var car = await carRepository.FindOneAsync(id); + var carRepository = serviceProvider.GetService(); + var car = await carRepository.FindOneAsync(id); - Console.WriteLine($"Car found: {car}"); + Console.WriteLine($"Car found: {car}"); - LogVerbose(opts, "Query the car history collection"); + LogVerbose(opts, "Query the car history collection"); - var carHistoryRepository = serviceProvider.GetService(); - var history = await carHistoryRepository.FindAllAsync(id); + var carHistoryRepository = serviceProvider.GetService(); + var history = await carHistoryRepository.FindAllAsync(id, "xxxx"); - Console.WriteLine($"Car history found: {history.Count}"); + Console.WriteLine($"Car history found: {history.Count}"); + break; + default: + Console.WriteLine($"Unknown action \"{opts.Action}\""); + return -1; } return 0; } } - private static int HandleParseError(IEnumerable errs) + private static int HandleParseError() { return -2; } diff --git a/dotnet/src/Dal.MongoDb/Dal.MongoDb.csproj b/dotnet/src/Dal.MongoDb/Dal.MongoDb.csproj index c458d65..c885981 100644 --- a/dotnet/src/Dal.MongoDb/Dal.MongoDb.csproj +++ b/dotnet/src/Dal.MongoDb/Dal.MongoDb.csproj @@ -13,8 +13,7 @@ - - + diff --git a/dotnet/src/Dal.MongoDb/Entities/IEntity.cs b/dotnet/src/Dal.MongoDb/Entities/IEntity.cs new file mode 100644 index 0000000..5b82df1 --- /dev/null +++ b/dotnet/src/Dal.MongoDb/Entities/IEntity.cs @@ -0,0 +1,10 @@ +using MongoDB.Bson; + +namespace KeepTrack.Dal.MongoDb.Entities +{ + public interface IEntity + { + ObjectId Id { get; set; } + string OwnerId { get; set; } + } +} diff --git a/dotnet/src/Dal.MongoDb/Repositories/RepositoryBase.cs b/dotnet/src/Dal.MongoDb/Repositories/RepositoryBase.cs new file mode 100644 index 0000000..5ce011f --- /dev/null +++ b/dotnet/src/Dal.MongoDb/Repositories/RepositoryBase.cs @@ -0,0 +1,61 @@ +using System; +using System.Threading.Tasks; +using AutoMapper; +using KeepTrack.Dal.MongoDb.Entities; +using Microsoft.Extensions.Logging; +using MongoDB.Bson; +using MongoDB.Driver; +using Withywoods.Dal.MongoDb; + +namespace KeepTrack.Dal.MongoDb.Repositories +{ + public abstract class RepositoryBase : Withywoods.Dal.MongoDb.Repositories.RepositoryBase where U : IEntity + { + protected RepositoryBase(IMongoDbContext mongoDbContext, ILogger> logger, IMapper mapper) + : base(mongoDbContext, logger, mapper) + { + } + + public async Task FindOneAsync(string id, string ownerId) + { + var objectId = ParseObjectId(id); + var collection = GetCollection(); + var dbEntries = await collection.FindAsync(x => x.Id == objectId && x.OwnerId == ownerId); + return Mapper.Map(dbEntries.FirstOrDefault()); + } + + public async Task CreateAsync(T model) + { + var collection = GetCollection(); + var entity = Mapper.Map(model); + await collection.InsertOneAsync(entity); + return Mapper.Map(entity); + } + + public async Task UpdateAsync(string id, T model, string ownerId) + { + var objectId = ParseObjectId(id); + var collection = GetCollection(); + var entity = Mapper.Map(model); + var result = await collection.ReplaceOneAsync(x => x.Id == objectId && x.OwnerId == ownerId, entity); + return result.ModifiedCount; + } + + public async Task DeleteAsync(string id, string ownerId) + { + var objectId = ParseObjectId(id); + var collection = GetCollection(); + var result = await collection.DeleteOneAsync(x => x.Id == objectId && x.OwnerId == ownerId); + return result.DeletedCount; + } + + protected static ObjectId ParseObjectId(string id, string message = null) + { + if (string.IsNullOrEmpty(id) || !ObjectId.TryParse(id, out var objectId)) + { + throw new ArgumentException($"{message}{id} is not a valid id.", nameof(id)); + } + return objectId; + } + } +} diff --git a/dotnet/src/MovieComponent.Domain/IMovieRepository.cs b/dotnet/src/MovieComponent.Domain/IMovieRepository.cs new file mode 100644 index 0000000..f3e0468 --- /dev/null +++ b/dotnet/src/MovieComponent.Domain/IMovieRepository.cs @@ -0,0 +1,18 @@ +using System.Collections.Generic; +using System.Threading.Tasks; + +namespace KeepTrack.MovieComponent.Domain +{ + public interface IMovieRepository + { + Task FindOneAsync(string id, string ownerId); + + Task> FindAllAsync(string ownerId); + + Task CreateAsync(MovieModel model); + + Task UpdateAsync(string id, MovieModel model, string ownerId); + + Task DeleteAsync(string id, string ownerId); + } +} diff --git a/dotnet/src/MovieComponent.Domain/MovieComponent.Domain.csproj b/dotnet/src/MovieComponent.Domain/MovieComponent.Domain.csproj new file mode 100644 index 0000000..753689c --- /dev/null +++ b/dotnet/src/MovieComponent.Domain/MovieComponent.Domain.csproj @@ -0,0 +1,15 @@ + + + + netstandard2.1 + KeepTrack.MovieComponent.Domain + KeepTrack.MovieComponent.Domain + {FAD8CE20-6164-4986-B3C1-6DF1EFF3C58B} + + + + full + true + + + diff --git a/dotnet/src/MovieComponent.Domain/MovieModel.cs b/dotnet/src/MovieComponent.Domain/MovieModel.cs new file mode 100644 index 0000000..3926abc --- /dev/null +++ b/dotnet/src/MovieComponent.Domain/MovieModel.cs @@ -0,0 +1,11 @@ +namespace KeepTrack.MovieComponent.Domain +{ + public class MovieModel + { + public string Id { get; set; } + + public string OwnerId { get; set; } + + public string Title { get; set; } + } +} diff --git a/dotnet/src/MovieComponent.Infrastructure.MongoDb/DependencyInjection/ServiceCollectionExtensions.cs b/dotnet/src/MovieComponent.Infrastructure.MongoDb/DependencyInjection/ServiceCollectionExtensions.cs new file mode 100644 index 0000000..37dcefa --- /dev/null +++ b/dotnet/src/MovieComponent.Infrastructure.MongoDb/DependencyInjection/ServiceCollectionExtensions.cs @@ -0,0 +1,21 @@ +using System; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.DependencyInjection.Extensions; + +namespace KeepTrack.MovieComponent.Infrastructure.MongoDb.DependencyInjection +{ + public static class ServiceCollectionExtensions + { + public static IServiceCollection AddMovieInfrastructureMongoDb(this IServiceCollection services) + { + if (services == null) + { + throw new ArgumentNullException(nameof(services)); + } + + services.TryAddTransient(); + + return services; + } + } +} diff --git a/dotnet/src/MovieComponent.Infrastructure.MongoDb/Entities/Movie.cs b/dotnet/src/MovieComponent.Infrastructure.MongoDb/Entities/Movie.cs new file mode 100644 index 0000000..e62b702 --- /dev/null +++ b/dotnet/src/MovieComponent.Infrastructure.MongoDb/Entities/Movie.cs @@ -0,0 +1,18 @@ +using KeepTrack.Dal.MongoDb.Entities; +using MongoDB.Bson; +using MongoDB.Bson.Serialization.Attributes; + +namespace KeepTrack.MovieComponent.Infrastructure.MongoDb.Entities +{ + public class Movie : IEntity + { + [BsonId] + public ObjectId Id { get; set; } + + [BsonElement("owner_id")] + public string OwnerId { get; set; } + + [BsonElement("title")] + public string Title { get; set; } + } +} diff --git a/dotnet/src/MovieComponent.Infrastructure.MongoDb/MappingProfiles/MovieMappingProfile.cs b/dotnet/src/MovieComponent.Infrastructure.MongoDb/MappingProfiles/MovieMappingProfile.cs new file mode 100644 index 0000000..cf0a82a --- /dev/null +++ b/dotnet/src/MovieComponent.Infrastructure.MongoDb/MappingProfiles/MovieMappingProfile.cs @@ -0,0 +1,18 @@ +using AutoMapper; + +namespace KeepTrack.MovieComponent.Infrastructure.MongoDb.MappingProfiles +{ + public class MovieMappingProfile : Profile + { + public override string ProfileName + { + get { return "KeepTrackMovieInfrastructureMongoDbMappingProfile"; } + } + + public MovieMappingProfile() + { + CreateMap(); + CreateMap(); + } + } +} diff --git a/dotnet/src/MovieComponent.Infrastructure.MongoDb/MovieComponent.Infrastructure.MongoDb.csproj b/dotnet/src/MovieComponent.Infrastructure.MongoDb/MovieComponent.Infrastructure.MongoDb.csproj new file mode 100644 index 0000000..0a144e5 --- /dev/null +++ b/dotnet/src/MovieComponent.Infrastructure.MongoDb/MovieComponent.Infrastructure.MongoDb.csproj @@ -0,0 +1,20 @@ + + + + netstandard2.1 + KeepTrack.MovieComponent.Infrastructure.MongoDb + KeepTrack.MovieComponent.Infrastructure.MongoDb + {02479358-D65A-4EF1-BA14-BD21706FBA20} + + + + full + true + + + + + + + + diff --git a/dotnet/src/MovieComponent.Infrastructure.MongoDb/Repositories/MovieRepository.cs b/dotnet/src/MovieComponent.Infrastructure.MongoDb/Repositories/MovieRepository.cs new file mode 100644 index 0000000..5bb360b --- /dev/null +++ b/dotnet/src/MovieComponent.Infrastructure.MongoDb/Repositories/MovieRepository.cs @@ -0,0 +1,29 @@ +using System.Collections.Generic; +using System.Threading.Tasks; +using AutoMapper; +using KeepTrack.Dal.MongoDb.Repositories; +using KeepTrack.MovieComponent.Domain; +using KeepTrack.MovieComponent.Infrastructure.MongoDb.Entities; +using Microsoft.Extensions.Logging; +using MongoDB.Driver; +using Withywoods.Dal.MongoDb; + +namespace KeepTrack.MovieComponent.Infrastructure.MongoDb.Repositories +{ + public class MovieRepository : RepositoryBase, IMovieRepository + { + public MovieRepository(IMongoDbContext mongoDbContext, ILogger logger, IMapper mapper) + : base(mongoDbContext, logger, mapper) + { + } + + protected override string CollectionName => "movie"; + + public async Task> FindAllAsync(string ownerId) + { + var collection = GetCollection(); + var dbEntries = await collection.FindAsync(x => x.OwnerId == ownerId); + return Mapper.Map>(dbEntries.ToList()); + } + } +} diff --git a/dotnet/test/CarComponent.Domain.UnitTests/CarComponent.Domain.UnitTests.csproj b/dotnet/test/CarComponent.Domain.UnitTests/CarComponent.Domain.UnitTests.csproj new file mode 100644 index 0000000..781da62 --- /dev/null +++ b/dotnet/test/CarComponent.Domain.UnitTests/CarComponent.Domain.UnitTests.csproj @@ -0,0 +1,26 @@ + + + + netcoreapp3.0 + + false + + + + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + + + + +