add route entity management

This commit is contained in:
cuqmbr 2025-05-01 20:50:22 +03:00
parent d500d1f84c
commit fdf147fe83
Signed by: cuqmbr
GPG Key ID: 0AA446880C766199
47 changed files with 1978 additions and 29 deletions

View File

@ -10,9 +10,24 @@ public class ValidationException : Exception
Errors = new Dictionary<string, string[]>(); Errors = new Dictionary<string, string[]>();
} }
public ValidationException(string message) : base(message)
{
Errors = new Dictionary<string, string[]>();
}
public ValidationException(IEnumerable<ValidationFailure> failures) public ValidationException(IEnumerable<ValidationFailure> failures)
: this() : this()
{ {
// TODO: Make serialized dictionary look more like this
// "errors": {
// "viewModel": [
// "The viewModel field is required."
// ],
// "$.addresses[0].order": [
// "The JSON value could not be converted to System.Int16. Path: $.addresses[0].order | LineNumber: 5 | BytePositionInLine: 26."
// ]
// },
Errors = failures Errors = failures
.GroupBy(f => f.PropertyName, f => f.ErrorMessage) .GroupBy(f => f.PropertyName, f => f.ErrorMessage)
.ToDictionary(fg => fg.Key, fg => fg.ToArray()); .ToDictionary(fg => fg.Key, fg => fg.ToArray());

View File

@ -0,0 +1,6 @@
using cuqmbr.TravelGuide.Domain.Entities;
namespace cuqmbr.TravelGuide.Application.Common.Interfaces
.Persistence.Repositories;
public interface RouteRepository : BaseRepository<Route> { }

View File

@ -12,6 +12,8 @@ public interface UnitOfWork : IDisposable
AddressRepository AddressRepository { get; } AddressRepository AddressRepository { get; }
RouteRepository RouteRepository { get; }
int Save(); int Save();
Task<int> SaveAsync(CancellationToken cancellationToken); Task<int> SaveAsync(CancellationToken cancellationToken);

View File

@ -6,6 +6,10 @@
"LessThanOrEqualTo": "Must be less than or equal to {0:G}.", "LessThanOrEqualTo": "Must be less than or equal to {0:G}.",
"MustBeInEnum": "Must be one of the following: {0}." "MustBeInEnum": "Must be one of the following: {0}."
}, },
"Validation": {
"DistinctOrder": "Must have distinct order values.",
"SameVehicleType": "Must have the same vehicle type."
},
"ExceptionHandling": { "ExceptionHandling": {
"ValidationException": { "ValidationException": {
"Title": "One or more validation errors occurred.", "Title": "One or more validation errors occurred.",

View File

@ -0,0 +1,14 @@
using cuqmbr.TravelGuide.Domain.Enums;
using MediatR;
using cuqmbr.TravelGuide.Application.Routes.Models;
namespace cuqmbr.TravelGuide.Application.Routes.Commands.AddRoute;
public record AddRouteCommand : IRequest<RouteDto>
{
public string Name { get; set; }
public VehicleType VehicleType { get; set; }
public ICollection<RouteAddressModel> Addresses { get; set; }
}

View File

@ -0,0 +1,31 @@
using cuqmbr.TravelGuide.Application.Common.Authorization;
using cuqmbr.TravelGuide.Application.Common.Interfaces.Services;
using cuqmbr.TravelGuide.Application.Common.Models;
using MediatR.Behaviors.Authorization;
namespace cuqmbr.TravelGuide.Application.Routes.Commands.AddRoute;
public class AddRouteCommandAuthorizer :
AbstractRequestAuthorizer<AddRouteCommand>
{
private readonly SessionUserService _sessionUserService;
public AddRouteCommandAuthorizer(SessionUserService sessionUserService)
{
_sessionUserService = sessionUserService;
}
public override void BuildPolicy(AddRouteCommand request)
{
UseRequirement(new MustBeAuthenticatedRequirement
{
IsAuthenticated= _sessionUserService.IsAuthenticated
});
UseRequirement(new MustBeInRolesRequirement
{
RequiredRoles = [IdentityRole.Administrator],
UserRoles = _sessionUserService.Roles
});
}
}

View File

@ -0,0 +1,84 @@
using MediatR;
using cuqmbr.TravelGuide.Application.Common.Interfaces.Persistence;
using cuqmbr.TravelGuide.Domain.Entities;
using AutoMapper;
using cuqmbr.TravelGuide.Application.Common.Exceptions;
using Microsoft.Extensions.Localization;
using FluentValidation.Results;
namespace cuqmbr.TravelGuide.Application.Routes.Commands.AddRoute;
public class AddRouteCommandHandler :
IRequestHandler<AddRouteCommand, RouteDto>
{
private readonly UnitOfWork _unitOfWork;
private readonly IMapper _mapper;
private readonly IStringLocalizer _localizer;
public AddRouteCommandHandler(
UnitOfWork unitOfWork,
IMapper mapper,
IStringLocalizer localizer)
{
_unitOfWork = unitOfWork;
_mapper = mapper;
_localizer = localizer;
}
public async Task<RouteDto> Handle(
AddRouteCommand request,
CancellationToken cancellationToken)
{
var page = await _unitOfWork.AddressRepository.GetPageAsync(
e => request.Addresses.Select(a => a.Guid).Contains(e.Guid),
e => e.City.Region.Country,
1, request.Addresses.Count, cancellationToken);
var invalidVehicleTypeAddress =
page.Items.FirstOrDefault(a => a.VehicleType != request.VehicleType);
if (invalidVehicleTypeAddress != null)
{
throw new ValidationException(
new List<ValidationFailure>
{
new()
{
PropertyName = nameof(request.Addresses),
ErrorMessage = _localizer["Validation.SameVehicleType"]
}
});
}
var pageContainsAllRequestedAddresses =
request.Addresses.Select(e => e.Guid)
.All(e => page.Items.Select(e => e.Guid).Contains(e));
if (!pageContainsAllRequestedAddresses)
{
var notFoundCount = request.Addresses.Count - page.TotalCount;
throw new NotFoundException(
$"{notFoundCount} addresses was not found.");
}
var entity = new Route()
{
Name = request.Name,
VehicleType = request.VehicleType,
RouteAddresses = request.Addresses.Select(
e => new RouteAddress()
{
Order = e.Order,
AddressId = page.Items.Single(i => i.Guid == e.Guid).Id
})
.OrderBy(e => e.Order)
.ToArray()
};
entity = await _unitOfWork.RouteRepository.AddOneAsync(
entity, cancellationToken);
await _unitOfWork.SaveAsync(cancellationToken);
_unitOfWork.Dispose();
return _mapper.Map<RouteDto>(entity);
}
}

View File

@ -0,0 +1,45 @@
using cuqmbr.TravelGuide.Application.Common.Interfaces.Services;
using cuqmbr.TravelGuide.Domain.Enums;
using FluentValidation;
using Microsoft.Extensions.Localization;
namespace cuqmbr.TravelGuide.Application.Routes.Commands.AddRoute;
public class AddRouteCommandValidator : AbstractValidator<AddRouteCommand>
{
public AddRouteCommandValidator(
IStringLocalizer localizer,
CultureService cultureService)
{
RuleFor(v => v.Name)
.NotEmpty()
.WithMessage(localizer["FluentValidation.NotEmpty"])
.MaximumLength(64)
.WithMessage(
String.Format(
cultureService.Culture,
localizer["FluentValidation.MaximumLength"],
64));
RuleFor(v => v.VehicleType)
.Must((v, vt) => VehicleType.Enumerations.ContainsValue(vt))
.WithMessage(
String.Format(
localizer["FluentValidation.MustBeInEnum"],
String.Join(
", ",
VehicleType.Enumerations.Values.Select(e => e.Name))));
RuleFor(v => v.Addresses.Count)
.GreaterThanOrEqualTo(2)
.WithMessage(
String.Format(
cultureService.Culture,
localizer["FluentValidation.GreaterThanOrEqualTo"],
2));
RuleFor(v => v.Addresses)
.Must((v, a) => a.DistinctBy(e => e.Order).Count() == a.Count())
.WithMessage(localizer["Validation.DistinctOrder"]);
}
}

View File

@ -0,0 +1,8 @@
using MediatR;
namespace cuqmbr.TravelGuide.Application.Routes.Commands.DeleteRoute;
public record DeleteRouteCommand : IRequest
{
public Guid Guid { get; set; }
}

View File

@ -0,0 +1,31 @@
using cuqmbr.TravelGuide.Application.Common.Authorization;
using cuqmbr.TravelGuide.Application.Common.Interfaces.Services;
using cuqmbr.TravelGuide.Application.Common.Models;
using MediatR.Behaviors.Authorization;
namespace cuqmbr.TravelGuide.Application.Routes.Commands.DeleteRoute;
public class DeleteRouteCommandAuthorizer :
AbstractRequestAuthorizer<DeleteRouteCommand>
{
private readonly SessionUserService _sessionUserService;
public DeleteRouteCommandAuthorizer(SessionUserService sessionUserService)
{
_sessionUserService = sessionUserService;
}
public override void BuildPolicy(DeleteRouteCommand request)
{
UseRequirement(new MustBeAuthenticatedRequirement
{
IsAuthenticated= _sessionUserService.IsAuthenticated
});
UseRequirement(new MustBeInRolesRequirement
{
RequiredRoles = [IdentityRole.Administrator],
UserRoles = _sessionUserService.Roles
});
}
}

View File

@ -0,0 +1,37 @@
using MediatR;
using cuqmbr.TravelGuide.Application.Common.Interfaces.Persistence;
using cuqmbr.TravelGuide.Application.Common.Exceptions;
namespace cuqmbr.TravelGuide.Application.Routes.Commands.DeleteRoute;
public class DeleteRouteCommandHandler : IRequestHandler<DeleteRouteCommand>
{
private readonly UnitOfWork _unitOfWork;
public DeleteRouteCommandHandler(UnitOfWork unitOfWork)
{
_unitOfWork = unitOfWork;
}
public async Task Handle(
DeleteRouteCommand request,
CancellationToken cancellationToken)
{
var entity = await _unitOfWork.RouteRepository.GetOneAsync(
e => e.Guid == request.Guid, cancellationToken);
if (entity == null)
{
throw new NotFoundException();
}
// TODO: Check for Vehicles that using this route in Enrollments
// Delete if there are no such Vehicles
await _unitOfWork.RouteRepository.DeleteOneAsync(
entity, cancellationToken);
await _unitOfWork.SaveAsync(cancellationToken);
_unitOfWork.Dispose();
}
}

View File

@ -0,0 +1,14 @@
using FluentValidation;
using Microsoft.Extensions.Localization;
namespace cuqmbr.TravelGuide.Application.Routes.Commands.DeleteRoute;
public class DeleteRouteCommandValidator : AbstractValidator<DeleteRouteCommand>
{
public DeleteRouteCommandValidator(IStringLocalizer localizer)
{
RuleFor(v => v.Guid)
.NotEmpty()
.WithMessage(localizer["FluentValidation.NotEmpty"]);
}
}

View File

@ -0,0 +1,16 @@
using MediatR;
using cuqmbr.TravelGuide.Domain.Enums;
using cuqmbr.TravelGuide.Application.Routes.Models;
namespace cuqmbr.TravelGuide.Application.Routes.Commands.UpdateRoute;
public record UpdateRouteCommand : IRequest<RouteDto>
{
public Guid Guid { get; set; }
public string Name { get; set; }
public VehicleType VehicleType { get; set; }
public ICollection<RouteAddressModel> Addresses { get; set; }
}

View File

@ -0,0 +1,31 @@
using cuqmbr.TravelGuide.Application.Common.Authorization;
using cuqmbr.TravelGuide.Application.Common.Interfaces.Services;
using cuqmbr.TravelGuide.Application.Common.Models;
using MediatR.Behaviors.Authorization;
namespace cuqmbr.TravelGuide.Application.Routes.Commands.UpdateRoute;
public class UpdateRouteCommandAuthorizer :
AbstractRequestAuthorizer<UpdateRouteCommand>
{
private readonly SessionUserService _sessionUserService;
public UpdateRouteCommandAuthorizer(SessionUserService sessionUserService)
{
_sessionUserService = sessionUserService;
}
public override void BuildPolicy(UpdateRouteCommand request)
{
UseRequirement(new MustBeAuthenticatedRequirement
{
IsAuthenticated= _sessionUserService.IsAuthenticated
});
UseRequirement(new MustBeInRolesRequirement
{
RequiredRoles = [IdentityRole.Administrator],
UserRoles = _sessionUserService.Roles
});
}
}

View File

@ -0,0 +1,108 @@
using MediatR;
using cuqmbr.TravelGuide.Application.Common.Interfaces.Persistence;
using AutoMapper;
using cuqmbr.TravelGuide.Application.Common.Exceptions;
using FluentValidation.Results;
using Microsoft.Extensions.Localization;
using cuqmbr.TravelGuide.Domain.Entities;
namespace cuqmbr.TravelGuide.Application.Routes.Commands.UpdateRoute;
public class UpdateRouteCommandHandler :
IRequestHandler<UpdateRouteCommand, RouteDto>
{
private readonly UnitOfWork _unitOfWork;
private readonly IMapper _mapper;
public IStringLocalizer _localizer { get; set; }
public UpdateRouteCommandHandler(
UnitOfWork unitOfWork,
IMapper mapper,
IStringLocalizer localizer)
{
_unitOfWork = unitOfWork;
_mapper = mapper;
_localizer = localizer;
}
public async Task<RouteDto> Handle(
UpdateRouteCommand request,
CancellationToken cancellationToken)
{
var entity = await _unitOfWork.RouteRepository.GetOneAsync(
e => e.Guid == request.Guid,
e => e.RouteAddresses,
cancellationToken);
if (entity == null)
{
throw new NotFoundException();
}
var page = await _unitOfWork.AddressRepository.GetPageAsync(
e => request.Addresses.Select(a => a.Guid).Contains(e.Guid),
e => e.City.Region.Country,
1, request.Addresses.Count, cancellationToken);
var invalidVehicleTypeAddress =
page.Items.FirstOrDefault(a => a.VehicleType != request.VehicleType);
if (invalidVehicleTypeAddress != null)
{
throw new ValidationException(
new List<ValidationFailure>
{
new()
{
PropertyName = nameof(request.Addresses),
ErrorMessage = _localizer["Validation.SameVehicleType"]
}
});
}
var pageContainsAllRequestedAddresses =
request.Addresses.Select(e => e.Guid)
.All(e => page.Items.Select(e => e.Guid).Contains(e));
if (!pageContainsAllRequestedAddresses)
{
var notFoundCount = request.Addresses.Count - page.TotalCount;
throw new NotFoundException(
$"{notFoundCount} addresses was not found.");
}
entity.Guid = request.Guid;
entity.Name = request.Name;
entity.VehicleType = request.VehicleType;
var requestRouteAddresses = request.Addresses.Select(
e => new RouteAddress()
{
Order = e.Order,
AddressId = page.Items.Single(i => i.Guid == e.Guid).Id
});
var commonRouteAddresses = entity.RouteAddresses.IntersectBy(
requestRouteAddresses.Select(ra => (ra.Order, ra.AddressId)),
ra => (ra.Order, ra.AddressId));
var newRouteAddresses = requestRouteAddresses.ExceptBy(
entity.RouteAddresses.Select(ra => (ra.Order, ra.AddressId)),
ra => (ra.Order, ra.AddressId));
var combinedRouteAddresses = commonRouteAddresses.UnionBy(
newRouteAddresses, ra => (ra.Order, ra.AddressId));
entity.RouteAddresses = combinedRouteAddresses
.OrderBy(e => e.Order).ToList();
entity = await _unitOfWork.RouteRepository.UpdateOneAsync(
entity, cancellationToken);
await _unitOfWork.SaveAsync(cancellationToken);
_unitOfWork.Dispose();
return _mapper.Map<RouteDto>(entity);
}
}

View File

@ -0,0 +1,49 @@
using cuqmbr.TravelGuide.Application.Common.Interfaces.Services;
using cuqmbr.TravelGuide.Domain.Enums;
using FluentValidation;
using Microsoft.Extensions.Localization;
namespace cuqmbr.TravelGuide.Application.Routes.Commands.UpdateRoute;
public class UpdateRouteCommandValidator : AbstractValidator<UpdateRouteCommand>
{
public UpdateRouteCommandValidator(
IStringLocalizer localizer,
CultureService cultureService)
{
RuleFor(v => v.Guid)
.NotEmpty()
.WithMessage(localizer["FluentValidation.NotEmpty"]);
RuleFor(v => v.Name)
.NotEmpty()
.WithMessage(localizer["FluentValidation.NotEmpty"])
.MaximumLength(64)
.WithMessage(
String.Format(
cultureService.Culture,
localizer["FluentValidation.MaximumLength"],
64));
RuleFor(v => v.VehicleType)
.Must((v, vt) => VehicleType.Enumerations.ContainsValue(vt))
.WithMessage(
String.Format(
localizer["FluentValidation.MustBeInEnum"],
String.Join(
", ",
VehicleType.Enumerations.Values.Select(e => e.Name))));
RuleFor(v => v.Addresses.Count)
.GreaterThanOrEqualTo(2)
.WithMessage(
String.Format(
cultureService.Culture,
localizer["FluentValidation.GreaterThanOrEqualTo"],
2));
RuleFor(v => v.Addresses)
.Must((v, a) => a.DistinctBy(e => e.Order).Count() == a.Count())
.WithMessage(localizer["Validation.DistinctOrder"]);
}
}

View File

@ -0,0 +1,8 @@
namespace cuqmbr.TravelGuide.Application.Routes.Models;
public sealed class RouteAddressModel
{
public short Order { get; set; }
public Guid Guid { get; set; }
}

View File

@ -0,0 +1,8 @@
using MediatR;
namespace cuqmbr.TravelGuide.Application.Routes.Queries.GetRoute;
public record GetRouteQuery : IRequest<RouteDto>
{
public Guid Guid { get; set; }
}

View File

@ -0,0 +1,31 @@
using cuqmbr.TravelGuide.Application.Common.Authorization;
using cuqmbr.TravelGuide.Application.Common.Interfaces.Services;
using cuqmbr.TravelGuide.Application.Common.Models;
using MediatR.Behaviors.Authorization;
namespace cuqmbr.TravelGuide.Application.Routes.Queries.GetRoute;
public class GetRouteQueryAuthorizer :
AbstractRequestAuthorizer<GetRouteQuery>
{
private readonly SessionUserService _sessionUserService;
public GetRouteQueryAuthorizer(SessionUserService sessionUserService)
{
_sessionUserService = sessionUserService;
}
public override void BuildPolicy(GetRouteQuery request)
{
UseRequirement(new MustBeAuthenticatedRequirement
{
IsAuthenticated= _sessionUserService.IsAuthenticated
});
UseRequirement(new MustBeInRolesRequirement
{
RequiredRoles = [IdentityRole.Administrator],
UserRoles = _sessionUserService.Roles
});
}
}

View File

@ -0,0 +1,60 @@
using MediatR;
using cuqmbr.TravelGuide.Application.Common.Interfaces.Persistence;
using cuqmbr.TravelGuide.Application.Common.Exceptions;
using AutoMapper;
using cuqmbr.TravelGuide.Domain.Entities;
namespace cuqmbr.TravelGuide.Application.Routes.Queries.GetRoute;
public class GetRouteQueryHandler :
IRequestHandler<GetRouteQuery, RouteDto>
{
private readonly UnitOfWork _unitOfWork;
private readonly IMapper _mapper;
public GetRouteQueryHandler(
UnitOfWork unitOfWork,
IMapper mapper)
{
_unitOfWork = unitOfWork;
_mapper = mapper;
}
public async Task<RouteDto> Handle(
GetRouteQuery request,
CancellationToken cancellationToken)
{
var entity = await _unitOfWork.RouteRepository.GetOneAsync(
e => e.Guid == request.Guid, e => e.RouteAddresses,
cancellationToken);
if (entity == null)
{
throw new NotFoundException();
}
// TODO: Find a way to include through lists
var addresses = await _unitOfWork.AddressRepository.GetPageAsync(
e => entity.RouteAddresses.Select(ra => ra.AddressId).Contains(e.Id),
e => e.City.Region.Country,
1, entity.RouteAddresses.Count, cancellationToken);
entity.RouteAddresses = entity.RouteAddresses.Select(
e => new RouteAddress()
{
Id = e.Id,
Guid = e.Guid,
Order = e.Order,
RouteId = e.RouteId,
Route = e.Route,
AddressId = e.AddressId,
Address = addresses.Items.First(a => a.Id == e.AddressId)
})
.OrderBy(e => e.Order)
.ToArray();
_unitOfWork.Dispose();
return _mapper.Map<RouteDto>(entity);
}
}

View File

@ -0,0 +1,14 @@
using FluentValidation;
using Microsoft.Extensions.Localization;
namespace cuqmbr.TravelGuide.Application.Routes.Queries.GetRoute;
public class GetRouteQueryValidator : AbstractValidator<GetRouteQuery>
{
public GetRouteQueryValidator(IStringLocalizer localizer)
{
RuleFor(v => v.Guid)
.NotEmpty()
.WithMessage(localizer["FluentValidation.NotEmpty"]);
}
}

View File

@ -0,0 +1,18 @@
using cuqmbr.TravelGuide.Application.Common.Models;
using cuqmbr.TravelGuide.Domain.Enums;
using MediatR;
namespace cuqmbr.TravelGuide.Application.Routes.Queries.GetRoutesPage;
public record GetRoutesPageQuery : IRequest<PaginatedList<RouteDto>>
{
public int PageNumber { get; set; } = 1;
public int PageSize { get; set; } = 10;
public string Search { get; set; } = String.Empty;
public string Sort { get; set; } = String.Empty;
public VehicleType? VehicleType { get; set; }
}

View File

@ -0,0 +1,31 @@
using cuqmbr.TravelGuide.Application.Common.Authorization;
using cuqmbr.TravelGuide.Application.Common.Interfaces.Services;
using cuqmbr.TravelGuide.Application.Common.Models;
using MediatR.Behaviors.Authorization;
namespace cuqmbr.TravelGuide.Application.Routes.Queries.GetRoutesPage;
public class GetRoutesPageQueryAuthorizer :
AbstractRequestAuthorizer<GetRoutesPageQuery>
{
private readonly SessionUserService _sessionUserService;
public GetRoutesPageQueryAuthorizer(SessionUserService sessionUserService)
{
_sessionUserService = sessionUserService;
}
public override void BuildPolicy(GetRoutesPageQuery request)
{
UseRequirement(new MustBeAuthenticatedRequirement
{
IsAuthenticated= _sessionUserService.IsAuthenticated
});
UseRequirement(new MustBeInRolesRequirement
{
RequiredRoles = [IdentityRole.Administrator],
UserRoles = _sessionUserService.Roles
});
}
}

View File

@ -0,0 +1,74 @@
using MediatR;
using cuqmbr.TravelGuide.Application.Common.Interfaces.Persistence;
using AutoMapper;
using cuqmbr.TravelGuide.Application.Common.Models;
using cuqmbr.TravelGuide.Application.Common.Extensions;
using cuqmbr.TravelGuide.Domain.Entities;
namespace cuqmbr.TravelGuide.Application.Routes.Queries.GetRoutesPage;
public class GetRoutesPageQueryHandler :
IRequestHandler<GetRoutesPageQuery, PaginatedList<RouteDto>>
{
private readonly UnitOfWork _unitOfWork;
private readonly IMapper _mapper;
public GetRoutesPageQueryHandler(
UnitOfWork unitOfWork,
IMapper mapper)
{
_unitOfWork = unitOfWork;
_mapper = mapper;
}
public async Task<PaginatedList<RouteDto>> Handle(
GetRoutesPageQuery request,
CancellationToken cancellationToken)
{
var paginatedList = await _unitOfWork.RouteRepository.GetPageAsync(
e =>
e.Name.ToLower().Contains(request.Search.ToLower()) &&
(request.VehicleType != null
? e.VehicleType == request.VehicleType
: true),
e => e.RouteAddresses,
request.PageNumber, request.PageSize,
cancellationToken);
foreach (var route in paginatedList.Items)
{
// TODO: Find a way to include through lists
var addresses = await _unitOfWork.AddressRepository.GetPageAsync(
e => route.RouteAddresses.Select(ra => ra.AddressId).Contains(e.Id),
e => e.City.Region.Country,
1, route.RouteAddresses.Count, cancellationToken);
route.RouteAddresses = route.RouteAddresses.Select(
e => new RouteAddress()
{
Id = e.Id,
Guid = e.Guid,
Order = e.Order,
RouteId = e.RouteId,
Route = e.Route,
AddressId = e.AddressId,
Address = addresses.Items.First(a => a.Id == e.AddressId)
})
.OrderBy(e => e.Order)
.ToArray();
}
var mappedItems = _mapper
.ProjectTo<RouteDto>(paginatedList.Items.AsQueryable());
mappedItems = QueryableExtension<RouteDto>
.ApplySort(mappedItems, request.Sort);
_unitOfWork.Dispose();
return new PaginatedList<RouteDto>(
mappedItems.ToList(),
paginatedList.TotalCount, request.PageNumber,
request.PageSize);
}
}

View File

@ -0,0 +1,44 @@
using cuqmbr.TravelGuide.Application.Common.Interfaces.Services;
using cuqmbr.TravelGuide.Domain.Enums;
using FluentValidation;
using Microsoft.Extensions.Localization;
namespace cuqmbr.TravelGuide.Application.Routes.Queries.GetRoutesPage;
public class GetRoutesPageQueryValidator : AbstractValidator<GetRoutesPageQuery>
{
public GetRoutesPageQueryValidator(
IStringLocalizer localizer,
CultureService cultureService)
{
RuleFor(v => v.PageNumber)
.GreaterThanOrEqualTo(1)
.WithMessage(
String.Format(
cultureService.Culture,
localizer["FluentValidation.GreaterThanOrEqualTo"],
1));
RuleFor(v => v.PageSize)
.GreaterThanOrEqualTo(1)
.WithMessage(
String.Format(
cultureService.Culture,
localizer["FluentValidation.GreaterThanOrEqualTo"],
1))
.LessThanOrEqualTo(50)
.WithMessage(
String.Format(
cultureService.Culture,
localizer["FluentValidation.LessThanOrEqualTo"],
50));
RuleFor(v => v.Search)
.MaximumLength(64)
.WithMessage(
String.Format(
cultureService.Culture,
localizer["FluentValidation.MaximumLength"],
64));
}
}

View File

@ -0,0 +1,70 @@
using cuqmbr.TravelGuide.Application.Common.Mappings;
using cuqmbr.TravelGuide.Domain.Entities;
namespace cuqmbr.TravelGuide.Application.Routes;
public sealed class RouteAddressDto : IMapFrom<RouteAddress>
{
public short Order { get; set; }
public Guid Uuid { get; set; }
public string Name { get; set; }
public double Longitude { get; set; }
public double Latitude { get; set; }
public string VehicleType { get; set; }
public Guid CountryUuid { get; set; }
public string CountryName { get; set; }
public Guid RegionUuid { get; set; }
public string RegionName { get; set; }
public Guid CityUuid { get; set; }
public string CityName { get; set; }
public void Mapping(MappingProfile profile)
{
profile.CreateMap<RouteAddress, RouteAddressDto>()
.ForMember(
d => d.Uuid,
opt => opt.MapFrom(s => s.Address.Guid))
.ForMember(
d => d.Name,
opt => opt.MapFrom(s => s.Address.Name))
.ForMember(
d => d.Longitude,
opt => opt.MapFrom(s => s.Address.Longitude))
.ForMember(
d => d.Latitude,
opt => opt.MapFrom(s => s.Address.Latitude))
.ForMember(
d => d.VehicleType,
opt => opt.MapFrom(s => s.Address.VehicleType.Name))
.ForMember(
d => d.CityUuid,
opt => opt.MapFrom(s => s.Address.City.Guid))
.ForMember(
d => d.CityName,
opt => opt.MapFrom(s => s.Address.City.Name))
.ForMember(
d => d.RegionUuid,
opt => opt.MapFrom(s => s.Address.City.Region.Guid))
.ForMember(
d => d.RegionName,
opt => opt.MapFrom(s => s.Address.City.Region.Name))
.ForMember(
d => d.CountryUuid,
opt => opt.MapFrom(s => s.Address.City.Region.Country.Guid))
.ForMember(
d => d.CountryName,
opt => opt.MapFrom(s => s.Address.City.Region.Country.Name));
}
}

View File

@ -0,0 +1,29 @@
using cuqmbr.TravelGuide.Application.Common.Mappings;
using cuqmbr.TravelGuide.Domain.Entities;
namespace cuqmbr.TravelGuide.Application.Routes;
public sealed class RouteDto : IMapFrom<Route>
{
public Guid Uuid { get; set; }
public string Name { get; set; }
public string VehicleType { get; set; }
public ICollection<RouteAddressDto> Addresses { get; set; }
public void Mapping(MappingProfile profile)
{
profile.CreateMap<Route, RouteDto>()
.ForMember(
d => d.Uuid,
opt => opt.MapFrom(s => s.Guid))
.ForMember(
d => d.VehicleType,
opt => opt.MapFrom(s => s.VehicleType.Name))
.ForMember(
d => d.Addresses,
opt => opt.MapFrom(s => s.RouteAddresses));
}
}

View File

@ -0,0 +1,10 @@
namespace cuqmbr.TravelGuide.Application.Routes.ViewModels;
public sealed class AddRouteViewModel
{
public string Name { get; set; }
public string VehicleType { get; set; }
public ICollection<RouteAddressViewModel> Addresses { get; set; }
}

View File

@ -0,0 +1,6 @@
namespace cuqmbr.TravelGuide.Application.Routes.ViewModels;
public sealed class GetRoutesPageFilterViewModel
{
public string? VehicleType { get; set; }
}

View File

@ -0,0 +1,8 @@
namespace cuqmbr.TravelGuide.Application.Routes.ViewModels;
public sealed class RouteAddressViewModel
{
public short Order { get; set; }
public Guid Uuid { get; set; }
}

View File

@ -0,0 +1,10 @@
namespace cuqmbr.TravelGuide.Application.Routes.ViewModels;
public sealed class UpdateRouteViewModel
{
public string Name { get; set; }
public string VehicleType { get; set; }
public ICollection<RouteAddressViewModel> Addresses { get; set; }
}

View File

@ -19,5 +19,5 @@ public sealed class Address : EntityBase
public City City { get; set; } public City City { get; set; }
// public ICollection<RouteAddress> AddressRoutes { get; set; } public ICollection<RouteAddress> AddressRoutes { get; set; }
} }

View File

@ -6,9 +6,8 @@ public sealed class Route : EntityBase
{ {
public string Name { get; set; } public string Name { get; set; }
// public VehicleType VehicleType { get; set; } public VehicleType VehicleType { get; set; }
public ICollection<RouteAddress> RouteAddresses { get; set; } public ICollection<RouteAddress> RouteAddresses { get; set; }
} }

View File

@ -0,0 +1,198 @@
using Microsoft.AspNetCore.Mvc;
using Swashbuckle.AspNetCore.Annotations;
using cuqmbr.TravelGuide.Application.Common.Models;
using cuqmbr.TravelGuide.Application.Common.ViewModels;
using cuqmbr.TravelGuide.Domain.Enums;
using cuqmbr.TravelGuide.Application.Routes;
using cuqmbr.TravelGuide.Application.Routes.Commands.AddRoute;
using cuqmbr.TravelGuide.Application.Routes.Queries.GetRoutesPage;
using cuqmbr.TravelGuide.Application.Routes.Queries.GetRoute;
using cuqmbr.TravelGuide.Application.Routes.Commands.UpdateRoute;
using cuqmbr.TravelGuide.Application.Routes.Commands.DeleteRoute;
using cuqmbr.TravelGuide.Application.Routes.ViewModels;
using cuqmbr.TravelGuide.Application.Routes.Models;
namespace cuqmbr.TravelGuide.HttpApi.Controllers;
[Route("routes")]
public class RoutesController : ControllerBase
{
[HttpPost]
[SwaggerOperation("Add a route")]
[SwaggerResponse(
StatusCodes.Status201Created, "Object successfuly created",
typeof(RouteDto))]
[SwaggerResponse(
StatusCodes.Status400BadRequest, "Input data validation error",
typeof(HttpValidationProblemDetails))]
[SwaggerResponse(
StatusCodes.Status401Unauthorized, "Unauthorized to perform an action",
typeof(ProblemDetails))]
[SwaggerResponse(
StatusCodes.Status403Forbidden,
"Not enough privileges to perform an action",
typeof(ProblemDetails))]
[SwaggerResponse(
StatusCodes.Status404NotFound, "One or more addresses was not found",
typeof(ProblemDetails))]
[SwaggerResponse(
StatusCodes.Status500InternalServerError, "Internal server error",
typeof(ProblemDetails))]
public async Task<ActionResult<RouteDto>> Add(
[FromBody] AddRouteViewModel viewModel,
CancellationToken cancellationToken)
{
return StatusCode(
StatusCodes.Status201Created,
await Mediator.Send(
new AddRouteCommand()
{
Name = viewModel.Name,
VehicleType = VehicleType.FromName(viewModel.VehicleType),
Addresses = viewModel.Addresses.Select(
e => new RouteAddressModel()
{
Order = e.Order,
Guid = e.Uuid
}).ToArray()
},
cancellationToken));
}
[HttpGet]
[SwaggerOperation("Get a list of all routes")]
[SwaggerResponse(
StatusCodes.Status200OK, "Request successful",
typeof(PaginatedList<RouteDto>))]
[SwaggerResponse(
StatusCodes.Status400BadRequest, "Input data validation error",
typeof(HttpValidationProblemDetails))]
[SwaggerResponse(
StatusCodes.Status401Unauthorized, "Unauthorized to perform an action",
typeof(ProblemDetails))]
[SwaggerResponse(
StatusCodes.Status403Forbidden,
"Not enough privileges to perform an action",
typeof(ProblemDetails))]
[SwaggerResponse(
StatusCodes.Status500InternalServerError, "Internal server error",
typeof(ProblemDetails))]
public async Task<PaginatedList<RouteDto>> GetPage(
[FromQuery] PageQuery pageQuery, [FromQuery] SearchQuery searchQuery,
[FromQuery] SortQuery sortQuery,
[FromQuery] GetRoutesPageFilterViewModel filterQuery,
CancellationToken cancellationToken)
{
return await Mediator.Send(
new GetRoutesPageQuery()
{
PageNumber = pageQuery.PageNumber,
PageSize = pageQuery.PageSize,
Search = searchQuery.Search,
Sort = sortQuery.Sort,
VehicleType = VehicleType.FromName(filterQuery.VehicleType)
},
cancellationToken);
}
[HttpGet("{uuid:guid}")]
[SwaggerOperation("Get a route by uuid")]
[SwaggerResponse(
StatusCodes.Status200OK, "Request successful", typeof(RouteDto))]
[SwaggerResponse(
StatusCodes.Status400BadRequest, "Input data validation error",
typeof(HttpValidationProblemDetails))]
[SwaggerResponse(
StatusCodes.Status401Unauthorized, "Unauthorized to perform an action",
typeof(ProblemDetails))]
[SwaggerResponse(
StatusCodes.Status403Forbidden,
"Not enough privileges to perform an action",
typeof(ProblemDetails))]
[SwaggerResponse(
StatusCodes.Status404NotFound, "Object not found", typeof(RouteDto))]
[SwaggerResponse(
StatusCodes.Status500InternalServerError, "Internal server error",
typeof(ProblemDetails))]
public async Task<RouteDto> Get(
[FromRoute] Guid uuid,
CancellationToken cancellationToken)
{
return await Mediator.Send(new GetRouteQuery() { Guid = uuid },
cancellationToken);
}
[HttpPut("{uuid:guid}")]
[SwaggerOperation("Update a route")]
[SwaggerResponse(
StatusCodes.Status400BadRequest, "Object already exists",
typeof(ProblemDetails))]
[SwaggerResponse(
StatusCodes.Status400BadRequest, "Input data validation error",
typeof(HttpValidationProblemDetails))]
[SwaggerResponse(
StatusCodes.Status401Unauthorized, "Unauthorized to perform an action",
typeof(ProblemDetails))]
[SwaggerResponse(
StatusCodes.Status403Forbidden,
"Not enough privileges to perform an action",
typeof(ProblemDetails))]
[SwaggerResponse(
StatusCodes.Status404NotFound, "Object not found", typeof(RouteDto))]
[SwaggerResponse(
StatusCodes.Status404NotFound, "One or more addresses was not found",
typeof(ProblemDetails))]
[SwaggerResponse(
StatusCodes.Status500InternalServerError, "Internal server error",
typeof(ProblemDetails))]
public async Task<RouteDto> Update(
[FromRoute] Guid uuid,
[FromBody] UpdateRouteViewModel viewModel,
CancellationToken cancellationToken)
{
return await Mediator.Send(
new UpdateRouteCommand()
{
Guid = uuid,
Name = viewModel.Name,
VehicleType = VehicleType.FromName(viewModel.VehicleType),
Addresses = viewModel.Addresses.Select(
e => new RouteAddressModel()
{
Order = e.Order,
Guid = e.Uuid
}).ToArray()
},
cancellationToken);
}
[HttpDelete("{uuid:guid}")]
[SwaggerOperation("Delete a route")]
[SwaggerResponse(StatusCodes.Status204NoContent, "Request successful")]
[SwaggerResponse(
StatusCodes.Status400BadRequest, "Input data validation error",
typeof(HttpValidationProblemDetails))]
[SwaggerResponse(
StatusCodes.Status401Unauthorized, "Unauthorized to perform an action",
typeof(ProblemDetails))]
[SwaggerResponse(
StatusCodes.Status403Forbidden,
"Not enough privileges to perform an action",
typeof(ProblemDetails))]
[SwaggerResponse(
StatusCodes.Status404NotFound, "Object not found", typeof(RouteDto))]
[SwaggerResponse(
StatusCodes.Status500InternalServerError, "Internal server error",
typeof(ProblemDetails))]
public async Task<IActionResult> Delete(
[FromRoute] Guid uuid,
CancellationToken cancellationToken)
{
await Mediator.Send(
new DeleteRouteCommand() { Guid = uuid },
cancellationToken);
return StatusCode(StatusCodes.Status204NoContent);
}
}

View File

@ -1,7 +1,7 @@
using cuqmbr.TravelGuide.Application.Common.Exceptions; using cuqmbr.TravelGuide.Application.Common.Exceptions;
using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Localization; using Microsoft.Extensions.Localization;
using System.Reflection; using System.Diagnostics;
namespace cuqmbr.TravelGuide.HttpApi.Middlewares; namespace cuqmbr.TravelGuide.HttpApi.Middlewares;
@ -11,6 +11,8 @@ public class GlobalExceptionHandlerMiddleware : IMiddleware
private readonly ILogger _logger; private readonly ILogger _logger;
private readonly IStringLocalizer _localizer; private readonly IStringLocalizer _localizer;
public GlobalExceptionHandlerMiddleware( public GlobalExceptionHandlerMiddleware(
ILogger<GlobalExceptionHandlerMiddleware> logger, ILogger<GlobalExceptionHandlerMiddleware> logger,
IStringLocalizer localizer) IStringLocalizer localizer)
@ -75,7 +77,7 @@ public class GlobalExceptionHandlerMiddleware : IMiddleware
context.Response.StatusCode = StatusCodes.Status400BadRequest; context.Response.StatusCode = StatusCodes.Status400BadRequest;
await context.Response.WriteAsJsonAsync(new HttpValidationProblemDetails(ex.Errors) await context.Response.WriteAsJsonAsync(new HttpValidationProblemDetailsWithTraceId(ex.Errors)
{ {
Status = StatusCodes.Status400BadRequest, Status = StatusCodes.Status400BadRequest,
Type = "https://datatracker.ietf.org/doc/html/rfc7231#section-6.5.1", Type = "https://datatracker.ietf.org/doc/html/rfc7231#section-6.5.1",
@ -90,7 +92,7 @@ public class GlobalExceptionHandlerMiddleware : IMiddleware
{ {
context.Response.StatusCode = StatusCodes.Status401Unauthorized; context.Response.StatusCode = StatusCodes.Status401Unauthorized;
await context.Response.WriteAsJsonAsync(new ProblemDetails() await context.Response.WriteAsJsonAsync(new ProblemDetailsWithTraceId()
{ {
Status = StatusCodes.Status401Unauthorized, Status = StatusCodes.Status401Unauthorized,
Type = "https://datatracker.ietf.org/doc/html/rfc7235#section-3.1", Type = "https://datatracker.ietf.org/doc/html/rfc7235#section-3.1",
@ -105,7 +107,7 @@ public class GlobalExceptionHandlerMiddleware : IMiddleware
{ {
context.Response.StatusCode = StatusCodes.Status400BadRequest; context.Response.StatusCode = StatusCodes.Status400BadRequest;
await context.Response.WriteAsJsonAsync(new ProblemDetails() await context.Response.WriteAsJsonAsync(new ProblemDetailsWithTraceId()
{ {
Status = StatusCodes.Status400BadRequest, Status = StatusCodes.Status400BadRequest,
Type = "https://datatracker.ietf.org/doc/html/rfc7231#section-6.5.1", Type = "https://datatracker.ietf.org/doc/html/rfc7231#section-6.5.1",
@ -120,7 +122,7 @@ public class GlobalExceptionHandlerMiddleware : IMiddleware
{ {
context.Response.StatusCode = StatusCodes.Status400BadRequest; context.Response.StatusCode = StatusCodes.Status400BadRequest;
await context.Response.WriteAsJsonAsync(new ProblemDetails() await context.Response.WriteAsJsonAsync(new ProblemDetailsWithTraceId()
{ {
Status = StatusCodes.Status400BadRequest, Status = StatusCodes.Status400BadRequest,
Type = "https://datatracker.ietf.org/doc/html/rfc7231#section-6.5.1", Type = "https://datatracker.ietf.org/doc/html/rfc7231#section-6.5.1",
@ -135,7 +137,7 @@ public class GlobalExceptionHandlerMiddleware : IMiddleware
{ {
context.Response.StatusCode = StatusCodes.Status400BadRequest; context.Response.StatusCode = StatusCodes.Status400BadRequest;
await context.Response.WriteAsJsonAsync(new ProblemDetails() await context.Response.WriteAsJsonAsync(new ProblemDetailsWithTraceId()
{ {
Status = StatusCodes.Status400BadRequest, Status = StatusCodes.Status400BadRequest,
Type = "https://datatracker.ietf.org/doc/html/rfc7231#section-6.5.1", Type = "https://datatracker.ietf.org/doc/html/rfc7231#section-6.5.1",
@ -150,7 +152,7 @@ public class GlobalExceptionHandlerMiddleware : IMiddleware
{ {
context.Response.StatusCode = StatusCodes.Status403Forbidden; context.Response.StatusCode = StatusCodes.Status403Forbidden;
await context.Response.WriteAsJsonAsync(new ProblemDetails() await context.Response.WriteAsJsonAsync(new ProblemDetailsWithTraceId()
{ {
Status = StatusCodes.Status403Forbidden, Status = StatusCodes.Status403Forbidden,
Type = "https://datatracker.ietf.org/doc/html/rfc7231#section-6.5.3", Type = "https://datatracker.ietf.org/doc/html/rfc7231#section-6.5.3",
@ -165,7 +167,7 @@ public class GlobalExceptionHandlerMiddleware : IMiddleware
{ {
context.Response.StatusCode = StatusCodes.Status400BadRequest; context.Response.StatusCode = StatusCodes.Status400BadRequest;
await context.Response.WriteAsJsonAsync(new ProblemDetails() await context.Response.WriteAsJsonAsync(new ProblemDetailsWithTraceId()
{ {
Status = StatusCodes.Status400BadRequest, Status = StatusCodes.Status400BadRequest,
Type = "https://datatracker.ietf.org/doc/html/rfc7231#section-6.5.1", Type = "https://datatracker.ietf.org/doc/html/rfc7231#section-6.5.1",
@ -180,7 +182,7 @@ public class GlobalExceptionHandlerMiddleware : IMiddleware
{ {
context.Response.StatusCode = StatusCodes.Status404NotFound; context.Response.StatusCode = StatusCodes.Status404NotFound;
await context.Response.WriteAsJsonAsync(new ProblemDetails() await context.Response.WriteAsJsonAsync(new ProblemDetailsWithTraceId()
{ {
Status = StatusCodes.Status404NotFound, Status = StatusCodes.Status404NotFound,
Type = "https://datatracker.ietf.org/doc/html/rfc7231#section-6.5.4", Type = "https://datatracker.ietf.org/doc/html/rfc7231#section-6.5.4",
@ -193,7 +195,7 @@ public class GlobalExceptionHandlerMiddleware : IMiddleware
{ {
context.Response.StatusCode = StatusCodes.Status500InternalServerError; context.Response.StatusCode = StatusCodes.Status500InternalServerError;
await context.Response.WriteAsJsonAsync(new ProblemDetails() await context.Response.WriteAsJsonAsync(new ProblemDetailsWithTraceId()
{ {
Status = StatusCodes.Status500InternalServerError, Status = StatusCodes.Status500InternalServerError,
Type = "https://datatracker.ietf.org/doc/html/rfc7231#section-6.6.1", Type = "https://datatracker.ietf.org/doc/html/rfc7231#section-6.6.1",
@ -202,9 +204,26 @@ public class GlobalExceptionHandlerMiddleware : IMiddleware
}); });
} }
// class ProblemDetailsWithTraceId : ProblemDetails class ProblemDetailsWithTraceId : ProblemDetails
// { {
// public string TraceId { get; init; } = Activity.Current?.TraceId.ToString(); public ProblemDetailsWithTraceId()
// } {
} Extensions = new Dictionary<string, object?>()
{
["traceId"] = Activity.Current.Id
};
}
}
class HttpValidationProblemDetailsWithTraceId : HttpValidationProblemDetails
{
public HttpValidationProblemDetailsWithTraceId(
IDictionary<string, string[]> errors) : base(errors)
{
Extensions = new Dictionary<string, object?>()
{
["traceId"] = Activity.Current.Id
};
}
}
}

View File

@ -16,6 +16,8 @@ public sealed class InMemoryUnitOfWork : UnitOfWork
CountryRepository = new InMemoryCountryRepository(_dbContext); CountryRepository = new InMemoryCountryRepository(_dbContext);
RegionRepository = new InMemoryRegionRepository(_dbContext); RegionRepository = new InMemoryRegionRepository(_dbContext);
CityRepository = new InMemoryCityRepository(_dbContext); CityRepository = new InMemoryCityRepository(_dbContext);
AddressRepository = new InMemoryAddressRepository(_dbContext);
RouteRepository = new InMemoryRouteRepository(_dbContext);
} }
public CountryRepository CountryRepository { get; init; } public CountryRepository CountryRepository { get; init; }
@ -26,6 +28,8 @@ public sealed class InMemoryUnitOfWork : UnitOfWork
public AddressRepository AddressRepository { get; init; } public AddressRepository AddressRepository { get; init; }
public RouteRepository RouteRepository { get; init; }
public int Save() public int Save()
{ {
return _dbContext.SaveChanges(); return _dbContext.SaveChanges();

View File

@ -0,0 +1,11 @@
using cuqmbr.TravelGuide.Application.Common.Interfaces.Persistence.Repositories;
using cuqmbr.TravelGuide.Domain.Entities;
namespace cuqmbr.TravelGuide.Persistence.InMemory.Repositories;
public sealed class InMemoryRouteRepository :
InMemoryBaseRepository<Route>, RouteRepository
{
public InMemoryRouteRepository(InMemoryDbContext dbContext)
: base(dbContext) { }
}

View File

@ -18,7 +18,7 @@ public class AddressConfiguration : BaseConfiguration<Address>
builder builder
.ToTable( .ToTable(
"addresses", "addresses",
b => b.HasCheckConstraint( a => a.HasCheckConstraint(
"ck_" + "ck_" +
$"{builder.Metadata.GetTableName()}_" + $"{builder.Metadata.GetTableName()}_" +
$"{builder.Property(a => a.VehicleType) $"{builder.Property(a => a.VehicleType)

View File

@ -0,0 +1,81 @@
using cuqmbr.TravelGuide.Domain.Entities;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Metadata.Builders;
namespace cuqmbr.TravelGuide.Persistence.PostgreSql.Configurations;
public class RouteAddressConfiguration : BaseConfiguration<RouteAddress>
{
public override void Configure(EntityTypeBuilder<RouteAddress> builder)
{
builder
.ToTable("route_addresses");
base.Configure(builder);
builder
.Property(ra => ra.Order)
.HasColumnName("order")
.HasColumnType("smallint")
.IsRequired(true);
builder
.Property(ra => ra.AddressId)
.HasColumnName("address_id")
.HasColumnType("bigint")
.IsRequired(true);
builder
.HasOne(ra => ra.Address)
.WithMany(a => a.AddressRoutes)
.HasForeignKey(ra => ra.AddressId)
.HasConstraintName(
"fk_" +
$"{builder.Metadata.GetTableName()}_" +
$"{builder.Property(ra => ra.AddressId).Metadata.GetColumnName()}")
.OnDelete(DeleteBehavior.Cascade);
builder
.HasIndex(ra => ra.AddressId)
.HasDatabaseName(
"ix_" +
$"{builder.Metadata.GetTableName()}_" +
$"{builder.Property(ra => ra.AddressId).Metadata.GetColumnName()}");
builder
.Property(ra => ra.RouteId)
.HasColumnName("route_id")
.HasColumnType("bigint")
.IsRequired(true);
builder
.HasOne(ra => ra.Route)
.WithMany(a => a.RouteAddresses)
.HasForeignKey(ra => ra.RouteId)
.HasConstraintName(
"fk_" +
$"{builder.Metadata.GetTableName()}_" +
$"{builder.Property(ra => ra.RouteId).Metadata.GetColumnName()}")
.OnDelete(DeleteBehavior.Cascade);
builder
.HasIndex(ra => ra.RouteId)
.HasDatabaseName(
"ix_" +
$"{builder.Metadata.GetTableName()}_" +
$"{builder.Property(ra => ra.RouteId).Metadata.GetColumnName()}");
builder
.HasAlternateKey(ra => new { ra.AddressId, ra.RouteId, ra.Order })
.HasName(
"altk_" +
$"{builder.Metadata.GetTableName()}_" +
$"{builder.Property(ra => ra.AddressId).Metadata.GetColumnName()}_" +
$"{builder.Property(ra => ra.RouteId).Metadata.GetColumnName()}_" +
$"{builder.Property(ra => ra.Order).Metadata.GetColumnName()}");
}
}

View File

@ -0,0 +1,40 @@
using cuqmbr.TravelGuide.Domain.Entities;
using cuqmbr.TravelGuide.Domain.Enums;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Metadata.Builders;
namespace cuqmbr.TravelGuide.Persistence.PostgreSql.Configurations;
public class RouteConfiguration : BaseConfiguration<Route>
{
public override void Configure(EntityTypeBuilder<Route> builder)
{
builder
.Property(r => r.VehicleType)
.HasColumnName("vehicle_type")
.HasColumnType("varchar(16)")
.IsRequired(true);
builder
.ToTable(
"routes",
r => r.HasCheckConstraint(
"ck_" +
$"{builder.Metadata.GetTableName()}_" +
$"{builder.Property(a => a.VehicleType)
.Metadata.GetColumnName()}",
$"{builder.Property(a => a.VehicleType)
.Metadata.GetColumnName()} IN ('{String
.Join("', '", VehicleType.Enumerations
.Values.Select(v => v.Name))}')"));
base.Configure(builder);
builder
.Property(r => r.Name)
.HasColumnName("name")
.HasColumnType("varchar(64)")
.IsRequired(true);
}
}

View File

@ -0,0 +1,400 @@
// <auto-generated />
using System;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Infrastructure;
using Microsoft.EntityFrameworkCore.Migrations;
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
using cuqmbr.TravelGuide.Persistence.PostgreSql;
#nullable disable
namespace Persistence.PostgreSql.Migrations
{
[DbContext(typeof(PostgreSqlDbContext))]
[Migration("20250501112816_Add_Route_and_RouteAddresses")]
partial class Add_Route_and_RouteAddresses
{
/// <inheritdoc />
protected override void BuildTargetModel(ModelBuilder modelBuilder)
{
#pragma warning disable 612, 618
modelBuilder
.HasDefaultSchema("application")
.HasAnnotation("ProductVersion", "9.0.4")
.HasAnnotation("Relational:MaxIdentifierLength", 63);
NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder);
modelBuilder.HasSequence("addresses_id_sequence");
modelBuilder.HasSequence("cities_id_sequence");
modelBuilder.HasSequence("countries_id_sequence");
modelBuilder.HasSequence("regions_id_sequence");
modelBuilder.HasSequence("route_addresses_id_sequence");
modelBuilder.HasSequence("routes_id_sequence");
modelBuilder.Entity("cuqmbr.TravelGuide.Domain.Entities.Address", b =>
{
b.Property<long>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("bigint")
.HasColumnName("id")
.HasDefaultValueSql("nextval('application.addresses_id_sequence')");
NpgsqlPropertyBuilderExtensions.UseSequence(b.Property<long>("Id"), "addresses_id_sequence");
b.Property<long>("CityId")
.HasColumnType("bigint")
.HasColumnName("city_id");
b.Property<Guid>("Guid")
.HasColumnType("uuid")
.HasColumnName("uuid");
b.Property<double>("Latitude")
.HasColumnType("double precision");
b.Property<double>("Longitude")
.HasColumnType("double precision");
b.Property<string>("Name")
.IsRequired()
.HasColumnType("varchar(128)")
.HasColumnName("name");
b.Property<string>("VehicleType")
.IsRequired()
.HasColumnType("varchar(16)")
.HasColumnName("vehicle_type");
b.HasKey("Id")
.HasName("pk_addresses");
b.HasAlternateKey("Guid")
.HasName("altk_addresses_Guid");
b.HasIndex("CityId")
.HasDatabaseName("ix_addresses_city_id");
b.HasIndex("Guid")
.IsUnique()
.HasDatabaseName("ix_addresses_uuid");
b.HasIndex("Id")
.IsUnique()
.HasDatabaseName("ix_addresses_id");
b.ToTable("addresses", "application", t =>
{
t.HasCheckConstraint("ck_addresses_vehicle_type", "vehicle_type IN ('bus', 'train', 'aircraft')");
});
});
modelBuilder.Entity("cuqmbr.TravelGuide.Domain.Entities.City", b =>
{
b.Property<long>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("bigint")
.HasColumnName("id")
.HasDefaultValueSql("nextval('application.cities_id_sequence')");
NpgsqlPropertyBuilderExtensions.UseSequence(b.Property<long>("Id"), "cities_id_sequence");
b.Property<Guid>("Guid")
.HasColumnType("uuid")
.HasColumnName("uuid");
b.Property<string>("Name")
.IsRequired()
.HasColumnType("varchar(64)")
.HasColumnName("name");
b.Property<long>("RegionId")
.HasColumnType("bigint")
.HasColumnName("region_id");
b.HasKey("Id")
.HasName("pk_cities");
b.HasAlternateKey("Guid")
.HasName("altk_cities_Guid");
b.HasIndex("Guid")
.IsUnique()
.HasDatabaseName("ix_cities_uuid");
b.HasIndex("Id")
.IsUnique()
.HasDatabaseName("ix_cities_id");
b.HasIndex("RegionId")
.HasDatabaseName("ix_cities_region_id");
b.ToTable("cities", "application");
});
modelBuilder.Entity("cuqmbr.TravelGuide.Domain.Entities.Country", b =>
{
b.Property<long>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("bigint")
.HasColumnName("id")
.HasDefaultValueSql("nextval('application.countries_id_sequence')");
NpgsqlPropertyBuilderExtensions.UseSequence(b.Property<long>("Id"), "countries_id_sequence");
b.Property<Guid>("Guid")
.HasColumnType("uuid")
.HasColumnName("uuid");
b.Property<string>("Name")
.IsRequired()
.HasColumnType("varchar(64)")
.HasColumnName("name");
b.HasKey("Id")
.HasName("pk_countries");
b.HasAlternateKey("Guid")
.HasName("altk_countries_Guid");
b.HasIndex("Guid")
.IsUnique()
.HasDatabaseName("ix_countries_uuid");
b.HasIndex("Id")
.IsUnique()
.HasDatabaseName("ix_countries_id");
b.ToTable("countries", "application");
});
modelBuilder.Entity("cuqmbr.TravelGuide.Domain.Entities.Region", b =>
{
b.Property<long>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("bigint")
.HasColumnName("id")
.HasDefaultValueSql("nextval('application.regions_id_sequence')");
NpgsqlPropertyBuilderExtensions.UseSequence(b.Property<long>("Id"), "regions_id_sequence");
b.Property<long>("CountryId")
.HasColumnType("bigint")
.HasColumnName("country_id");
b.Property<Guid>("Guid")
.HasColumnType("uuid")
.HasColumnName("uuid");
b.Property<string>("Name")
.IsRequired()
.HasColumnType("varchar(64)")
.HasColumnName("name");
b.HasKey("Id")
.HasName("pk_regions");
b.HasAlternateKey("Guid")
.HasName("altk_regions_Guid");
b.HasIndex("CountryId")
.HasDatabaseName("ix_regions_country_id");
b.HasIndex("Guid")
.IsUnique()
.HasDatabaseName("ix_regions_uuid");
b.HasIndex("Id")
.IsUnique()
.HasDatabaseName("ix_regions_id");
b.ToTable("regions", "application");
});
modelBuilder.Entity("cuqmbr.TravelGuide.Domain.Entities.Route", b =>
{
b.Property<long>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("bigint")
.HasColumnName("id")
.HasDefaultValueSql("nextval('application.routes_id_sequence')");
NpgsqlPropertyBuilderExtensions.UseSequence(b.Property<long>("Id"), "routes_id_sequence");
b.Property<Guid>("Guid")
.HasColumnType("uuid")
.HasColumnName("uuid");
b.Property<string>("Name")
.IsRequired()
.HasColumnType("varchar(64)")
.HasColumnName("name");
b.Property<string>("VehicleType")
.IsRequired()
.HasColumnType("varchar(16)")
.HasColumnName("vehicle_type");
b.HasKey("Id")
.HasName("pk_routes");
b.HasAlternateKey("Guid")
.HasName("altk_routes_Guid");
b.HasIndex("Guid")
.IsUnique()
.HasDatabaseName("ix_routes_uuid");
b.HasIndex("Id")
.IsUnique()
.HasDatabaseName("ix_routes_id");
b.ToTable("routes", "application", t =>
{
t.HasCheckConstraint("ck_routes_vehicle_type", "vehicle_type IN ('bus', 'train', 'aircraft')");
});
});
modelBuilder.Entity("cuqmbr.TravelGuide.Domain.Entities.RouteAddress", b =>
{
b.Property<long>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("bigint")
.HasColumnName("id")
.HasDefaultValueSql("nextval('application.route_addresses_id_sequence')");
NpgsqlPropertyBuilderExtensions.UseSequence(b.Property<long>("Id"), "route_addresses_id_sequence");
b.Property<long>("AddressId")
.HasColumnType("bigint")
.HasColumnName("address_id");
b.Property<Guid>("Guid")
.HasColumnType("uuid")
.HasColumnName("uuid");
b.Property<short>("Order")
.HasColumnType("smallint")
.HasColumnName("order");
b.Property<long>("RouteId")
.HasColumnType("bigint")
.HasColumnName("route_id");
b.HasKey("Id")
.HasName("pk_route_addresses");
b.HasAlternateKey("Guid")
.HasName("altk_route_addresses_Guid");
b.HasAlternateKey("AddressId", "RouteId", "Order")
.HasName("altk_route_addresses_address_id_route_id_order");
b.HasIndex("AddressId")
.HasDatabaseName("ix_route_addresses_address_id");
b.HasIndex("Guid")
.IsUnique()
.HasDatabaseName("ix_route_addresses_uuid");
b.HasIndex("Id")
.IsUnique()
.HasDatabaseName("ix_route_addresses_id");
b.HasIndex("RouteId")
.HasDatabaseName("ix_route_addresses_route_id");
b.ToTable("route_addresses", "application");
});
modelBuilder.Entity("cuqmbr.TravelGuide.Domain.Entities.Address", b =>
{
b.HasOne("cuqmbr.TravelGuide.Domain.Entities.City", "City")
.WithMany("Addresses")
.HasForeignKey("CityId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired()
.HasConstraintName("fk_addresses_city_id");
b.Navigation("City");
});
modelBuilder.Entity("cuqmbr.TravelGuide.Domain.Entities.City", b =>
{
b.HasOne("cuqmbr.TravelGuide.Domain.Entities.Region", "Region")
.WithMany("Cities")
.HasForeignKey("RegionId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired()
.HasConstraintName("fk_cities_region_id");
b.Navigation("Region");
});
modelBuilder.Entity("cuqmbr.TravelGuide.Domain.Entities.Region", b =>
{
b.HasOne("cuqmbr.TravelGuide.Domain.Entities.Country", "Country")
.WithMany("Regions")
.HasForeignKey("CountryId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired()
.HasConstraintName("fk_regions_country_id");
b.Navigation("Country");
});
modelBuilder.Entity("cuqmbr.TravelGuide.Domain.Entities.RouteAddress", b =>
{
b.HasOne("cuqmbr.TravelGuide.Domain.Entities.Address", "Address")
.WithMany("AddressRoutes")
.HasForeignKey("AddressId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired()
.HasConstraintName("fk_route_addresses_address_id");
b.HasOne("cuqmbr.TravelGuide.Domain.Entities.Route", "Route")
.WithMany("RouteAddresses")
.HasForeignKey("RouteId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired()
.HasConstraintName("fk_route_addresses_route_id");
b.Navigation("Address");
b.Navigation("Route");
});
modelBuilder.Entity("cuqmbr.TravelGuide.Domain.Entities.Address", b =>
{
b.Navigation("AddressRoutes");
});
modelBuilder.Entity("cuqmbr.TravelGuide.Domain.Entities.City", b =>
{
b.Navigation("Addresses");
});
modelBuilder.Entity("cuqmbr.TravelGuide.Domain.Entities.Country", b =>
{
b.Navigation("Regions");
});
modelBuilder.Entity("cuqmbr.TravelGuide.Domain.Entities.Region", b =>
{
b.Navigation("Cities");
});
modelBuilder.Entity("cuqmbr.TravelGuide.Domain.Entities.Route", b =>
{
b.Navigation("RouteAddresses");
});
#pragma warning restore 612, 618
}
}
}

View File

@ -0,0 +1,132 @@
using System;
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace Persistence.PostgreSql.Migrations
{
/// <inheritdoc />
public partial class Add_Route_and_RouteAddresses : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.CreateSequence(
name: "route_addresses_id_sequence",
schema: "application");
migrationBuilder.CreateSequence(
name: "routes_id_sequence",
schema: "application");
migrationBuilder.CreateTable(
name: "routes",
schema: "application",
columns: table => new
{
id = table.Column<long>(type: "bigint", nullable: false, defaultValueSql: "nextval('application.routes_id_sequence')"),
name = table.Column<string>(type: "varchar(64)", nullable: false),
vehicle_type = table.Column<string>(type: "varchar(16)", nullable: false),
uuid = table.Column<Guid>(type: "uuid", nullable: false)
},
constraints: table =>
{
table.PrimaryKey("pk_routes", x => x.id);
table.UniqueConstraint("altk_routes_Guid", x => x.uuid);
table.CheckConstraint("ck_routes_vehicle_type", "vehicle_type IN ('bus', 'train', 'aircraft')");
});
migrationBuilder.CreateTable(
name: "route_addresses",
schema: "application",
columns: table => new
{
id = table.Column<long>(type: "bigint", nullable: false, defaultValueSql: "nextval('application.route_addresses_id_sequence')"),
order = table.Column<short>(type: "smallint", nullable: false),
address_id = table.Column<long>(type: "bigint", nullable: false),
route_id = table.Column<long>(type: "bigint", nullable: false),
uuid = table.Column<Guid>(type: "uuid", nullable: false)
},
constraints: table =>
{
table.PrimaryKey("pk_route_addresses", x => x.id);
table.UniqueConstraint("altk_route_addresses_address_id_route_id_order", x => new { x.address_id, x.route_id, x.order });
table.UniqueConstraint("altk_route_addresses_Guid", x => x.uuid);
table.ForeignKey(
name: "fk_route_addresses_address_id",
column: x => x.address_id,
principalSchema: "application",
principalTable: "addresses",
principalColumn: "id",
onDelete: ReferentialAction.Cascade);
table.ForeignKey(
name: "fk_route_addresses_route_id",
column: x => x.route_id,
principalSchema: "application",
principalTable: "routes",
principalColumn: "id",
onDelete: ReferentialAction.Cascade);
});
migrationBuilder.CreateIndex(
name: "ix_route_addresses_address_id",
schema: "application",
table: "route_addresses",
column: "address_id");
migrationBuilder.CreateIndex(
name: "ix_route_addresses_id",
schema: "application",
table: "route_addresses",
column: "id",
unique: true);
migrationBuilder.CreateIndex(
name: "ix_route_addresses_route_id",
schema: "application",
table: "route_addresses",
column: "route_id");
migrationBuilder.CreateIndex(
name: "ix_route_addresses_uuid",
schema: "application",
table: "route_addresses",
column: "uuid",
unique: true);
migrationBuilder.CreateIndex(
name: "ix_routes_id",
schema: "application",
table: "routes",
column: "id",
unique: true);
migrationBuilder.CreateIndex(
name: "ix_routes_uuid",
schema: "application",
table: "routes",
column: "uuid",
unique: true);
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropTable(
name: "route_addresses",
schema: "application");
migrationBuilder.DropTable(
name: "routes",
schema: "application");
migrationBuilder.DropSequence(
name: "route_addresses_id_sequence",
schema: "application");
migrationBuilder.DropSequence(
name: "routes_id_sequence",
schema: "application");
}
}
}

View File

@ -31,6 +31,10 @@ namespace Persistence.PostgreSql.Migrations
modelBuilder.HasSequence("regions_id_sequence"); modelBuilder.HasSequence("regions_id_sequence");
modelBuilder.HasSequence("route_addresses_id_sequence");
modelBuilder.HasSequence("routes_id_sequence");
modelBuilder.Entity("cuqmbr.TravelGuide.Domain.Entities.Address", b => modelBuilder.Entity("cuqmbr.TravelGuide.Domain.Entities.Address", b =>
{ {
b.Property<long>("Id") b.Property<long>("Id")
@ -210,6 +214,102 @@ namespace Persistence.PostgreSql.Migrations
b.ToTable("regions", "application"); b.ToTable("regions", "application");
}); });
modelBuilder.Entity("cuqmbr.TravelGuide.Domain.Entities.Route", b =>
{
b.Property<long>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("bigint")
.HasColumnName("id")
.HasDefaultValueSql("nextval('application.routes_id_sequence')");
NpgsqlPropertyBuilderExtensions.UseSequence(b.Property<long>("Id"), "routes_id_sequence");
b.Property<Guid>("Guid")
.HasColumnType("uuid")
.HasColumnName("uuid");
b.Property<string>("Name")
.IsRequired()
.HasColumnType("varchar(64)")
.HasColumnName("name");
b.Property<string>("VehicleType")
.IsRequired()
.HasColumnType("varchar(16)")
.HasColumnName("vehicle_type");
b.HasKey("Id")
.HasName("pk_routes");
b.HasAlternateKey("Guid")
.HasName("altk_routes_Guid");
b.HasIndex("Guid")
.IsUnique()
.HasDatabaseName("ix_routes_uuid");
b.HasIndex("Id")
.IsUnique()
.HasDatabaseName("ix_routes_id");
b.ToTable("routes", "application", t =>
{
t.HasCheckConstraint("ck_routes_vehicle_type", "vehicle_type IN ('bus', 'train', 'aircraft')");
});
});
modelBuilder.Entity("cuqmbr.TravelGuide.Domain.Entities.RouteAddress", b =>
{
b.Property<long>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("bigint")
.HasColumnName("id")
.HasDefaultValueSql("nextval('application.route_addresses_id_sequence')");
NpgsqlPropertyBuilderExtensions.UseSequence(b.Property<long>("Id"), "route_addresses_id_sequence");
b.Property<long>("AddressId")
.HasColumnType("bigint")
.HasColumnName("address_id");
b.Property<Guid>("Guid")
.HasColumnType("uuid")
.HasColumnName("uuid");
b.Property<short>("Order")
.HasColumnType("smallint")
.HasColumnName("order");
b.Property<long>("RouteId")
.HasColumnType("bigint")
.HasColumnName("route_id");
b.HasKey("Id")
.HasName("pk_route_addresses");
b.HasAlternateKey("Guid")
.HasName("altk_route_addresses_Guid");
b.HasAlternateKey("AddressId", "RouteId", "Order")
.HasName("altk_route_addresses_address_id_route_id_order");
b.HasIndex("AddressId")
.HasDatabaseName("ix_route_addresses_address_id");
b.HasIndex("Guid")
.IsUnique()
.HasDatabaseName("ix_route_addresses_uuid");
b.HasIndex("Id")
.IsUnique()
.HasDatabaseName("ix_route_addresses_id");
b.HasIndex("RouteId")
.HasDatabaseName("ix_route_addresses_route_id");
b.ToTable("route_addresses", "application");
});
modelBuilder.Entity("cuqmbr.TravelGuide.Domain.Entities.Address", b => modelBuilder.Entity("cuqmbr.TravelGuide.Domain.Entities.Address", b =>
{ {
b.HasOne("cuqmbr.TravelGuide.Domain.Entities.City", "City") b.HasOne("cuqmbr.TravelGuide.Domain.Entities.City", "City")
@ -246,6 +346,32 @@ namespace Persistence.PostgreSql.Migrations
b.Navigation("Country"); b.Navigation("Country");
}); });
modelBuilder.Entity("cuqmbr.TravelGuide.Domain.Entities.RouteAddress", b =>
{
b.HasOne("cuqmbr.TravelGuide.Domain.Entities.Address", "Address")
.WithMany("AddressRoutes")
.HasForeignKey("AddressId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired()
.HasConstraintName("fk_route_addresses_address_id");
b.HasOne("cuqmbr.TravelGuide.Domain.Entities.Route", "Route")
.WithMany("RouteAddresses")
.HasForeignKey("RouteId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired()
.HasConstraintName("fk_route_addresses_route_id");
b.Navigation("Address");
b.Navigation("Route");
});
modelBuilder.Entity("cuqmbr.TravelGuide.Domain.Entities.Address", b =>
{
b.Navigation("AddressRoutes");
});
modelBuilder.Entity("cuqmbr.TravelGuide.Domain.Entities.City", b => modelBuilder.Entity("cuqmbr.TravelGuide.Domain.Entities.City", b =>
{ {
b.Navigation("Addresses"); b.Navigation("Addresses");
@ -260,6 +386,11 @@ namespace Persistence.PostgreSql.Migrations
{ {
b.Navigation("Cities"); b.Navigation("Cities");
}); });
modelBuilder.Entity("cuqmbr.TravelGuide.Domain.Entities.Route", b =>
{
b.Navigation("RouteAddresses");
});
#pragma warning restore 612, 618 #pragma warning restore 612, 618
} }
} }

View File

@ -1,8 +1,8 @@
using System.Reflection; using System.Reflection;
using cuqmbr.TravelGuide.Domain.Enums; using cuqmbr.TravelGuide.Domain.Enums;
using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
using Microsoft.Extensions.Options; using Microsoft.Extensions.Options;
using cuqmbr.TravelGuide.Persistence.PostgreSql.TypeConverters;
namespace cuqmbr.TravelGuide.Persistence.PostgreSql; namespace cuqmbr.TravelGuide.Persistence.PostgreSql;
@ -41,12 +41,3 @@ public class PostgreSqlDbContext : DbContext
.HaveConversion<VehicleTypeConverter>(); .HaveConversion<VehicleTypeConverter>();
} }
} }
public class VehicleTypeConverter : ValueConverter<VehicleType, string>
{
public VehicleTypeConverter()
: base(
v => v.Name,
v => VehicleType.FromName(v))
{ }
}

View File

@ -17,6 +17,7 @@ public sealed class PostgreSqlUnitOfWork : UnitOfWork
RegionRepository = new PostgreSqlRegionRepository(_dbContext); RegionRepository = new PostgreSqlRegionRepository(_dbContext);
CityRepository = new PostgreSqlCityRepository(_dbContext); CityRepository = new PostgreSqlCityRepository(_dbContext);
AddressRepository = new PostgreSqlAddressRepository(_dbContext); AddressRepository = new PostgreSqlAddressRepository(_dbContext);
RouteRepository = new PostgreSqlRouteRepository(_dbContext);
} }
public CountryRepository CountryRepository { get; init; } public CountryRepository CountryRepository { get; init; }
@ -27,6 +28,8 @@ public sealed class PostgreSqlUnitOfWork : UnitOfWork
public AddressRepository AddressRepository { get; init; } public AddressRepository AddressRepository { get; init; }
public RouteRepository RouteRepository { get; init; }
public int Save() public int Save()
{ {
return _dbContext.SaveChanges(); return _dbContext.SaveChanges();

View File

@ -0,0 +1,11 @@
using cuqmbr.TravelGuide.Application.Common.Interfaces.Persistence.Repositories;
using cuqmbr.TravelGuide.Domain.Entities;
namespace cuqmbr.TravelGuide.Persistence.PostgreSql.Repositories;
public sealed class PostgreSqlRouteRepository :
PostgreSqlBaseRepository<Route>, RouteRepository
{
public PostgreSqlRouteRepository(PostgreSqlDbContext dbContext)
: base(dbContext) { }
}

View File

@ -0,0 +1,13 @@
using cuqmbr.TravelGuide.Domain.Enums;
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
namespace cuqmbr.TravelGuide.Persistence.PostgreSql.TypeConverters;
public class VehicleTypeConverter : ValueConverter<VehicleType, string>
{
public VehicleTypeConverter()
: base(
v => v.Name,
v => VehicleType.FromName(v))
{ }
}