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
+
+
+
+
+
+
+
+