add vehicles hierarchy management
All checks were successful
/ build (push) Successful in 6m13s
/ tests (push) Successful in 47s
/ build-docker (push) Successful in 6m48s

This commit is contained in:
cuqmbr 2025-05-03 10:09:52 +03:00
parent 201e6e7dfc
commit 3ebd0c3a2c
Signed by: cuqmbr
GPG Key ID: 0AA446880C766199
106 changed files with 3599 additions and 16 deletions

View File

@ -0,0 +1,23 @@
using cuqmbr.TravelGuide.Application.Common.Mappings;
using cuqmbr.TravelGuide.Domain.Entities;
namespace cuqmbr.TravelGuide.Application.Aircrafts;
public sealed class AircraftDto : IMapFrom<Aircraft>
{
public Guid Uuid { get; set; }
public string Number { get; set; }
public string Model { get; set; }
public short Capacity { get; set; }
public void Mapping(MappingProfile profile)
{
profile.CreateMap<Aircraft, AircraftDto>()
.ForMember(
d => d.Uuid,
opt => opt.MapFrom(s => s.Guid));
}
}

View File

@ -0,0 +1,12 @@
using MediatR;
namespace cuqmbr.TravelGuide.Application.Aircrafts.Commands.AddAircraft;
public record AddAircraftCommand : IRequest<AircraftDto>
{
public string Number { get; set; }
public string Model { get; set; }
public short Capacity { 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.Aircrafts.Commands.AddAircraft;
public class AddAircraftCommandAuthorizer :
AbstractRequestAuthorizer<AddAircraftCommand>
{
private readonly SessionUserService _sessionUserService;
public AddAircraftCommandAuthorizer(SessionUserService sessionUserService)
{
_sessionUserService = sessionUserService;
}
public override void BuildPolicy(AddAircraftCommand request)
{
UseRequirement(new MustBeAuthenticatedRequirement
{
IsAuthenticated= _sessionUserService.IsAuthenticated
});
UseRequirement(new MustBeInRolesRequirement
{
RequiredRoles = [IdentityRole.Administrator],
UserRoles = _sessionUserService.Roles
});
}
}

View File

@ -0,0 +1,51 @@
using MediatR;
using cuqmbr.TravelGuide.Application.Common.Interfaces.Persistence;
using cuqmbr.TravelGuide.Domain.Entities;
using AutoMapper;
using cuqmbr.TravelGuide.Application.Common.Exceptions;
namespace cuqmbr.TravelGuide.Application.Aircrafts.Commands.AddAircraft;
public class AddAircraftCommandHandler :
IRequestHandler<AddAircraftCommand, AircraftDto>
{
private readonly UnitOfWork _unitOfWork;
private readonly IMapper _mapper;
public AddAircraftCommandHandler(
UnitOfWork unitOfWork,
IMapper mapper)
{
_unitOfWork = unitOfWork;
_mapper = mapper;
}
public async Task<AircraftDto> Handle(
AddAircraftCommand request,
CancellationToken cancellationToken)
{
var entity = await _unitOfWork.AircraftRepository.GetOneAsync(
e => e.Number == request.Number, cancellationToken);
if (entity != null)
{
throw new DuplicateEntityException(
"Aircraft with given number already exists.");
}
entity = new Aircraft()
{
Number = request.Number,
Model = request.Model,
Capacity = request.Capacity
};
entity = await _unitOfWork.AircraftRepository.AddOneAsync(
entity, cancellationToken);
await _unitOfWork.SaveAsync(cancellationToken);
_unitOfWork.Dispose();
return _mapper.Map<AircraftDto>(entity);
}
}

View File

@ -0,0 +1,37 @@
using cuqmbr.TravelGuide.Application.Common.Interfaces.Services;
using FluentValidation;
using Microsoft.Extensions.Localization;
namespace cuqmbr.TravelGuide.Application.Aircrafts.Commands.AddAircraft;
public class AddAircraftCommandValidator : AbstractValidator<AddAircraftCommand>
{
public AddAircraftCommandValidator(
IStringLocalizer localizer,
CultureService cultureService)
{
RuleFor(v => v.Number)
.NotEmpty()
.WithMessage(localizer["FluentValidation.NotEmpty"])
.MaximumLength(32)
.WithMessage(
String.Format(
cultureService.Culture,
localizer["FluentValidation.MaximumLength"],
32));
RuleFor(v => v.Model)
.NotEmpty()
.WithMessage(localizer["FluentValidation.NotEmpty"])
.MaximumLength(64)
.WithMessage(
String.Format(
cultureService.Culture,
localizer["FluentValidation.MaximumLength"],
64));
RuleFor(v => v.Capacity)
.NotEmpty()
.WithMessage(localizer["FluentValidation.NotEmpty"]);
}
}

View File

@ -0,0 +1,8 @@
using MediatR;
namespace cuqmbr.TravelGuide.Application.Aircrafts.Commands.DeleteAircraft;
public record DeleteAircraftCommand : 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.Aircrafts.Commands.DeleteAircraft;
public class DeleteAircraftCommandAuthorizer :
AbstractRequestAuthorizer<DeleteAircraftCommand>
{
private readonly SessionUserService _sessionUserService;
public DeleteAircraftCommandAuthorizer(SessionUserService sessionUserService)
{
_sessionUserService = sessionUserService;
}
public override void BuildPolicy(DeleteAircraftCommand request)
{
UseRequirement(new MustBeAuthenticatedRequirement
{
IsAuthenticated= _sessionUserService.IsAuthenticated
});
UseRequirement(new MustBeInRolesRequirement
{
RequiredRoles = [IdentityRole.Administrator],
UserRoles = _sessionUserService.Roles
});
}
}

View File

@ -0,0 +1,34 @@
using MediatR;
using cuqmbr.TravelGuide.Application.Common.Interfaces.Persistence;
using cuqmbr.TravelGuide.Application.Common.Exceptions;
namespace cuqmbr.TravelGuide.Application.Aircrafts.Commands.DeleteAircraft;
public class DeleteAircraftCommandHandler : IRequestHandler<DeleteAircraftCommand>
{
private readonly UnitOfWork _unitOfWork;
public DeleteAircraftCommandHandler(UnitOfWork unitOfWork)
{
_unitOfWork = unitOfWork;
}
public async Task Handle(
DeleteAircraftCommand request,
CancellationToken cancellationToken)
{
var entity = await _unitOfWork.AircraftRepository.GetOneAsync(
e => e.Guid == request.Guid, cancellationToken);
if (entity == null)
{
throw new NotFoundException();
}
await _unitOfWork.AircraftRepository.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.Aircrafts.Commands.DeleteAircraft;
public class DeleteAircraftCommandValidator : AbstractValidator<DeleteAircraftCommand>
{
public DeleteAircraftCommandValidator(IStringLocalizer localizer)
{
RuleFor(v => v.Guid)
.NotEmpty()
.WithMessage(localizer["FluentValidation.NotEmpty"]);
}
}

View File

@ -0,0 +1,14 @@
using MediatR;
namespace cuqmbr.TravelGuide.Application.Aircrafts.Commands.UpdateAircraft;
public record UpdateAircraftCommand : IRequest<AircraftDto>
{
public Guid Guid { get; set; }
public string Number { get; set; }
public string Model { get; set; }
public short Capacity { 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.Aircrafts.Commands.UpdateAircraft;
public class UpdateAircraftCommandAuthorizer :
AbstractRequestAuthorizer<UpdateAircraftCommand>
{
private readonly SessionUserService _sessionUserService;
public UpdateAircraftCommandAuthorizer(SessionUserService sessionUserService)
{
_sessionUserService = sessionUserService;
}
public override void BuildPolicy(UpdateAircraftCommand request)
{
UseRequirement(new MustBeAuthenticatedRequirement
{
IsAuthenticated= _sessionUserService.IsAuthenticated
});
UseRequirement(new MustBeInRolesRequirement
{
RequiredRoles = [IdentityRole.Administrator],
UserRoles = _sessionUserService.Roles
});
}
}

View File

@ -0,0 +1,56 @@
using MediatR;
using cuqmbr.TravelGuide.Application.Common.Interfaces.Persistence;
using AutoMapper;
using cuqmbr.TravelGuide.Application.Common.Exceptions;
namespace cuqmbr.TravelGuide.Application.Aircrafts.Commands.UpdateAircraft;
public class UpdateAircraftCommandHandler :
IRequestHandler<UpdateAircraftCommand, AircraftDto>
{
private readonly UnitOfWork _unitOfWork;
private readonly IMapper _mapper;
public UpdateAircraftCommandHandler(
UnitOfWork unitOfWork,
IMapper mapper)
{
_unitOfWork = unitOfWork;
_mapper = mapper;
}
public async Task<AircraftDto> Handle(
UpdateAircraftCommand request,
CancellationToken cancellationToken)
{
var entity = await _unitOfWork.AircraftRepository.GetOneAsync(
e => e.Guid == request.Guid, cancellationToken);
if (entity == null)
{
throw new NotFoundException();
}
var duplicateEntity = await _unitOfWork.AircraftRepository.GetOneAsync(
e => e.Number == request.Number,
cancellationToken);
if (duplicateEntity != null)
{
throw new DuplicateEntityException(
"Aircraft with given number already exists.");
}
entity.Number = request.Number;
entity.Model = request.Model;
entity.Capacity = request.Capacity;
entity = await _unitOfWork.AircraftRepository.UpdateOneAsync(
entity, cancellationToken);
await _unitOfWork.SaveAsync(cancellationToken);
_unitOfWork.Dispose();
return _mapper.Map<AircraftDto>(entity);
}
}

View File

@ -0,0 +1,41 @@
using cuqmbr.TravelGuide.Application.Common.Interfaces.Services;
using FluentValidation;
using Microsoft.Extensions.Localization;
namespace cuqmbr.TravelGuide.Application.Aircrafts.Commands.UpdateAircraft;
public class UpdateAircraftCommandValidator : AbstractValidator<UpdateAircraftCommand>
{
public UpdateAircraftCommandValidator(
IStringLocalizer localizer,
CultureService cultureService)
{
RuleFor(v => v.Guid)
.NotEmpty()
.WithMessage(localizer["FluentValidation.NotEmpty"]);
RuleFor(v => v.Number)
.NotEmpty()
.WithMessage(localizer["FluentValidation.NotEmpty"])
.MaximumLength(32)
.WithMessage(
String.Format(
cultureService.Culture,
localizer["FluentValidation.MaximumLength"],
32));
RuleFor(v => v.Model)
.NotEmpty()
.WithMessage(localizer["FluentValidation.NotEmpty"])
.MaximumLength(64)
.WithMessage(
String.Format(
cultureService.Culture,
localizer["FluentValidation.MaximumLength"],
64));
RuleFor(v => v.Capacity)
.NotEmpty()
.WithMessage(localizer["FluentValidation.NotEmpty"]);
}
}

View File

@ -0,0 +1,8 @@
using MediatR;
namespace cuqmbr.TravelGuide.Application.Aircrafts.Queries.GetAircraft;
public record GetAircraftQuery : IRequest<AircraftDto>
{
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.Aircrafts.Queries.GetAircraft;
public class GetAircraftQueryAuthorizer :
AbstractRequestAuthorizer<GetAircraftQuery>
{
private readonly SessionUserService _sessionUserService;
public GetAircraftQueryAuthorizer(SessionUserService sessionUserService)
{
_sessionUserService = sessionUserService;
}
public override void BuildPolicy(GetAircraftQuery request)
{
UseRequirement(new MustBeAuthenticatedRequirement
{
IsAuthenticated= _sessionUserService.IsAuthenticated
});
UseRequirement(new MustBeInRolesRequirement
{
RequiredRoles = [IdentityRole.Administrator],
UserRoles = _sessionUserService.Roles
});
}
}

View File

@ -0,0 +1,38 @@
using MediatR;
using cuqmbr.TravelGuide.Application.Common.Interfaces.Persistence;
using cuqmbr.TravelGuide.Application.Common.Exceptions;
using AutoMapper;
namespace cuqmbr.TravelGuide.Application.Aircrafts.Queries.GetAircraft;
public class GetAircraftQueryHandler :
IRequestHandler<GetAircraftQuery, AircraftDto>
{
private readonly UnitOfWork _unitOfWork;
private readonly IMapper _mapper;
public GetAircraftQueryHandler(
UnitOfWork unitOfWork,
IMapper mapper)
{
_unitOfWork = unitOfWork;
_mapper = mapper;
}
public async Task<AircraftDto> Handle(
GetAircraftQuery request,
CancellationToken cancellationToken)
{
var entity = await _unitOfWork.AircraftRepository.GetOneAsync(
e => e.Guid == request.Guid, cancellationToken);
_unitOfWork.Dispose();
if (entity == null)
{
throw new NotFoundException();
}
return _mapper.Map<AircraftDto>(entity);
}
}

View File

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

View File

@ -0,0 +1,23 @@
using cuqmbr.TravelGuide.Application.Common.Models;
using MediatR;
namespace cuqmbr.TravelGuide.Application.Aircrafts.Queries.GetAircraftsPage;
public record GetAircraftsPageQuery : IRequest<PaginatedList<AircraftDto>>
{
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 string? Number { get; set; }
public string? Model { get; set; }
public short? CapacityGreaterOrEqualThan { get; set; }
public short? CapacityLessOrEqualThan { 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.Aircrafts.Queries.GetAircraftsPage;
public class GetAircraftsPageQueryAuthorizer :
AbstractRequestAuthorizer<GetAircraftsPageQuery>
{
private readonly SessionUserService _sessionUserService;
public GetAircraftsPageQueryAuthorizer(SessionUserService sessionUserService)
{
_sessionUserService = sessionUserService;
}
public override void BuildPolicy(GetAircraftsPageQuery request)
{
UseRequirement(new MustBeAuthenticatedRequirement
{
IsAuthenticated= _sessionUserService.IsAuthenticated
});
UseRequirement(new MustBeInRolesRequirement
{
RequiredRoles = [IdentityRole.Administrator],
UserRoles = _sessionUserService.Roles
});
}
}

View File

@ -0,0 +1,53 @@
using MediatR;
using cuqmbr.TravelGuide.Application.Common.Interfaces.Persistence;
using AutoMapper;
using cuqmbr.TravelGuide.Application.Common.Models;
using cuqmbr.TravelGuide.Application.Common.Extensions;
namespace cuqmbr.TravelGuide.Application.Aircrafts.Queries.GetAircraftsPage;
public class GetAircraftsPageQueryHandler :
IRequestHandler<GetAircraftsPageQuery, PaginatedList<AircraftDto>>
{
private readonly UnitOfWork _unitOfWork;
private readonly IMapper _mapper;
public GetAircraftsPageQueryHandler(
UnitOfWork unitOfWork,
IMapper mapper)
{
_unitOfWork = unitOfWork;
_mapper = mapper;
}
public async Task<PaginatedList<AircraftDto>> Handle(
GetAircraftsPageQuery request,
CancellationToken cancellationToken)
{
var paginatedList = await _unitOfWork.AircraftRepository.GetPageAsync(
e =>
(e.Number.ToLower().Contains(request.Search.ToLower()) ||
e.Model.ToLower().Contains(request.Search.ToLower())) &&
(request.CapacityGreaterOrEqualThan != null
? e.Capacity >= request.CapacityGreaterOrEqualThan
: true) &&
(request.CapacityLessOrEqualThan != null
? e.Capacity <= request.CapacityLessOrEqualThan
: true),
request.PageNumber, request.PageSize,
cancellationToken);
var mappedItems = _mapper
.ProjectTo<AircraftDto>(paginatedList.Items.AsQueryable());
mappedItems = QueryableExtension<AircraftDto>
.ApplySort(mappedItems, request.Sort);
_unitOfWork.Dispose();
return new PaginatedList<AircraftDto>(
mappedItems.ToList(),
paginatedList.TotalCount, request.PageNumber,
request.PageSize);
}
}

View File

@ -0,0 +1,43 @@
using cuqmbr.TravelGuide.Application.Common.Interfaces.Services;
using FluentValidation;
using Microsoft.Extensions.Localization;
namespace cuqmbr.TravelGuide.Application.Aircrafts.Queries.GetAircraftsPage;
public class GetAircraftsPageQueryValidator : AbstractValidator<GetAircraftsPageQuery>
{
public GetAircraftsPageQueryValidator(
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,10 @@
namespace cuqmbr.TravelGuide.Application.Aircrafts.ViewModels;
public sealed class AddAircraftViewModel
{
public string Number { get; set; }
public string Model { get; set; }
public short Capacity { get; set; }
}

View File

@ -0,0 +1,15 @@
namespace cuqmbr.TravelGuide.Application.Aircrafts.ViewModels;
public sealed class GetAircraftsPageFilterViewModel
{
public string? Number { get; set; }
public string? Model { get; set; }
// TODO: Consider adding strict equals rule although it is not
// necessarily needed to filter with exact capacity
public short? CapacityGreaterOrEqualThan { get; set; }
public short? CapacityLessOrEqualThan { get; set; }
}

View File

@ -0,0 +1,10 @@
namespace cuqmbr.TravelGuide.Application.Aircrafts.ViewModels;
public sealed class UpdateAircraftViewModel
{
public string Number { get; set; }
public string Model { get; set; }
public short Capacity { get; set; }
}

View File

@ -0,0 +1,23 @@
using cuqmbr.TravelGuide.Application.Common.Mappings;
using cuqmbr.TravelGuide.Domain.Entities;
namespace cuqmbr.TravelGuide.Application.Buses;
public sealed class BusDto : IMapFrom<Bus>
{
public Guid Uuid { get; set; }
public string Number { get; set; }
public string Model { get; set; }
public short Capacity { get; set; }
public void Mapping(MappingProfile profile)
{
profile.CreateMap<Bus, BusDto>()
.ForMember(
d => d.Uuid,
opt => opt.MapFrom(s => s.Guid));
}
}

View File

@ -0,0 +1,12 @@
using MediatR;
namespace cuqmbr.TravelGuide.Application.Buses.Commands.AddBus;
public record AddBusCommand : IRequest<BusDto>
{
public string Number { get; set; }
public string Model { get; set; }
public short Capacity { 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.Buses.Commands.AddBus;
public class AddBusCommandAuthorizer :
AbstractRequestAuthorizer<AddBusCommand>
{
private readonly SessionUserService _sessionUserService;
public AddBusCommandAuthorizer(SessionUserService sessionUserService)
{
_sessionUserService = sessionUserService;
}
public override void BuildPolicy(AddBusCommand request)
{
UseRequirement(new MustBeAuthenticatedRequirement
{
IsAuthenticated= _sessionUserService.IsAuthenticated
});
UseRequirement(new MustBeInRolesRequirement
{
RequiredRoles = [IdentityRole.Administrator],
UserRoles = _sessionUserService.Roles
});
}
}

View File

@ -0,0 +1,51 @@
using MediatR;
using cuqmbr.TravelGuide.Application.Common.Interfaces.Persistence;
using cuqmbr.TravelGuide.Domain.Entities;
using AutoMapper;
using cuqmbr.TravelGuide.Application.Common.Exceptions;
namespace cuqmbr.TravelGuide.Application.Buses.Commands.AddBus;
public class AddBusCommandHandler :
IRequestHandler<AddBusCommand, BusDto>
{
private readonly UnitOfWork _unitOfWork;
private readonly IMapper _mapper;
public AddBusCommandHandler(
UnitOfWork unitOfWork,
IMapper mapper)
{
_unitOfWork = unitOfWork;
_mapper = mapper;
}
public async Task<BusDto> Handle(
AddBusCommand request,
CancellationToken cancellationToken)
{
var entity = await _unitOfWork.BusRepository.GetOneAsync(
e => e.Number == request.Number, cancellationToken);
if (entity != null)
{
throw new DuplicateEntityException(
"Bus with given number already exists.");
}
entity = new Bus()
{
Number = request.Number,
Model = request.Model,
Capacity = request.Capacity
};
entity = await _unitOfWork.BusRepository.AddOneAsync(
entity, cancellationToken);
await _unitOfWork.SaveAsync(cancellationToken);
_unitOfWork.Dispose();
return _mapper.Map<BusDto>(entity);
}
}

View File

@ -0,0 +1,37 @@
using cuqmbr.TravelGuide.Application.Common.Interfaces.Services;
using FluentValidation;
using Microsoft.Extensions.Localization;
namespace cuqmbr.TravelGuide.Application.Buses.Commands.AddBus;
public class AddBusCommandValidator : AbstractValidator<AddBusCommand>
{
public AddBusCommandValidator(
IStringLocalizer localizer,
CultureService cultureService)
{
RuleFor(v => v.Number)
.NotEmpty()
.WithMessage(localizer["FluentValidation.NotEmpty"])
.MaximumLength(32)
.WithMessage(
String.Format(
cultureService.Culture,
localizer["FluentValidation.MaximumLength"],
32));
RuleFor(v => v.Model)
.NotEmpty()
.WithMessage(localizer["FluentValidation.NotEmpty"])
.MaximumLength(64)
.WithMessage(
String.Format(
cultureService.Culture,
localizer["FluentValidation.MaximumLength"],
64));
RuleFor(v => v.Capacity)
.NotEmpty()
.WithMessage(localizer["FluentValidation.NotEmpty"]);
}
}

View File

@ -0,0 +1,8 @@
using MediatR;
namespace cuqmbr.TravelGuide.Application.Buses.Commands.DeleteBus;
public record DeleteBusCommand : 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.Buses.Commands.DeleteBus;
public class DeleteBusCommandAuthorizer :
AbstractRequestAuthorizer<DeleteBusCommand>
{
private readonly SessionUserService _sessionUserService;
public DeleteBusCommandAuthorizer(SessionUserService sessionUserService)
{
_sessionUserService = sessionUserService;
}
public override void BuildPolicy(DeleteBusCommand request)
{
UseRequirement(new MustBeAuthenticatedRequirement
{
IsAuthenticated= _sessionUserService.IsAuthenticated
});
UseRequirement(new MustBeInRolesRequirement
{
RequiredRoles = [IdentityRole.Administrator],
UserRoles = _sessionUserService.Roles
});
}
}

View File

@ -0,0 +1,34 @@
using MediatR;
using cuqmbr.TravelGuide.Application.Common.Interfaces.Persistence;
using cuqmbr.TravelGuide.Application.Common.Exceptions;
namespace cuqmbr.TravelGuide.Application.Buses.Commands.DeleteBus;
public class DeleteBusCommandHandler : IRequestHandler<DeleteBusCommand>
{
private readonly UnitOfWork _unitOfWork;
public DeleteBusCommandHandler(UnitOfWork unitOfWork)
{
_unitOfWork = unitOfWork;
}
public async Task Handle(
DeleteBusCommand request,
CancellationToken cancellationToken)
{
var entity = await _unitOfWork.BusRepository.GetOneAsync(
e => e.Guid == request.Guid, cancellationToken);
if (entity == null)
{
throw new NotFoundException();
}
await _unitOfWork.BusRepository.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.Buses.Commands.DeleteBus;
public class DeleteBusCommandValidator : AbstractValidator<DeleteBusCommand>
{
public DeleteBusCommandValidator(IStringLocalizer localizer)
{
RuleFor(v => v.Guid)
.NotEmpty()
.WithMessage(localizer["FluentValidation.NotEmpty"]);
}
}

View File

@ -0,0 +1,14 @@
using MediatR;
namespace cuqmbr.TravelGuide.Application.Buses.Commands.UpdateBus;
public record UpdateBusCommand : IRequest<BusDto>
{
public Guid Guid { get; set; }
public string Number { get; set; }
public string Model { get; set; }
public short Capacity { 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.Buses.Commands.UpdateBus;
public class UpdateBusCommandAuthorizer :
AbstractRequestAuthorizer<UpdateBusCommand>
{
private readonly SessionUserService _sessionUserService;
public UpdateBusCommandAuthorizer(SessionUserService sessionUserService)
{
_sessionUserService = sessionUserService;
}
public override void BuildPolicy(UpdateBusCommand request)
{
UseRequirement(new MustBeAuthenticatedRequirement
{
IsAuthenticated= _sessionUserService.IsAuthenticated
});
UseRequirement(new MustBeInRolesRequirement
{
RequiredRoles = [IdentityRole.Administrator],
UserRoles = _sessionUserService.Roles
});
}
}

View File

@ -0,0 +1,56 @@
using MediatR;
using cuqmbr.TravelGuide.Application.Common.Interfaces.Persistence;
using AutoMapper;
using cuqmbr.TravelGuide.Application.Common.Exceptions;
namespace cuqmbr.TravelGuide.Application.Buses.Commands.UpdateBus;
public class UpdateBusCommandHandler :
IRequestHandler<UpdateBusCommand, BusDto>
{
private readonly UnitOfWork _unitOfWork;
private readonly IMapper _mapper;
public UpdateBusCommandHandler(
UnitOfWork unitOfWork,
IMapper mapper)
{
_unitOfWork = unitOfWork;
_mapper = mapper;
}
public async Task<BusDto> Handle(
UpdateBusCommand request,
CancellationToken cancellationToken)
{
var entity = await _unitOfWork.BusRepository.GetOneAsync(
e => e.Guid == request.Guid, cancellationToken);
if (entity == null)
{
throw new NotFoundException();
}
var duplicateEntity = await _unitOfWork.BusRepository.GetOneAsync(
e => e.Number == request.Number,
cancellationToken);
if (duplicateEntity != null)
{
throw new DuplicateEntityException(
"Bus with given number already exists.");
}
entity.Number = request.Number;
entity.Model = request.Model;
entity.Capacity = request.Capacity;
entity = await _unitOfWork.BusRepository.UpdateOneAsync(
entity, cancellationToken);
await _unitOfWork.SaveAsync(cancellationToken);
_unitOfWork.Dispose();
return _mapper.Map<BusDto>(entity);
}
}

View File

@ -0,0 +1,41 @@
using cuqmbr.TravelGuide.Application.Common.Interfaces.Services;
using FluentValidation;
using Microsoft.Extensions.Localization;
namespace cuqmbr.TravelGuide.Application.Buses.Commands.UpdateBus;
public class UpdateBusCommandValidator : AbstractValidator<UpdateBusCommand>
{
public UpdateBusCommandValidator(
IStringLocalizer localizer,
CultureService cultureService)
{
RuleFor(v => v.Guid)
.NotEmpty()
.WithMessage(localizer["FluentValidation.NotEmpty"]);
RuleFor(v => v.Number)
.NotEmpty()
.WithMessage(localizer["FluentValidation.NotEmpty"])
.MaximumLength(32)
.WithMessage(
String.Format(
cultureService.Culture,
localizer["FluentValidation.MaximumLength"],
32));
RuleFor(v => v.Model)
.NotEmpty()
.WithMessage(localizer["FluentValidation.NotEmpty"])
.MaximumLength(64)
.WithMessage(
String.Format(
cultureService.Culture,
localizer["FluentValidation.MaximumLength"],
64));
RuleFor(v => v.Capacity)
.NotEmpty()
.WithMessage(localizer["FluentValidation.NotEmpty"]);
}
}

View File

@ -0,0 +1,8 @@
using MediatR;
namespace cuqmbr.TravelGuide.Application.Buses.Queries.GetBus;
public record GetBusQuery : IRequest<BusDto>
{
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.Buses.Queries.GetBus;
public class GetBusQueryAuthorizer :
AbstractRequestAuthorizer<GetBusQuery>
{
private readonly SessionUserService _sessionUserService;
public GetBusQueryAuthorizer(SessionUserService sessionUserService)
{
_sessionUserService = sessionUserService;
}
public override void BuildPolicy(GetBusQuery request)
{
UseRequirement(new MustBeAuthenticatedRequirement
{
IsAuthenticated= _sessionUserService.IsAuthenticated
});
UseRequirement(new MustBeInRolesRequirement
{
RequiredRoles = [IdentityRole.Administrator],
UserRoles = _sessionUserService.Roles
});
}
}

View File

@ -0,0 +1,38 @@
using MediatR;
using cuqmbr.TravelGuide.Application.Common.Interfaces.Persistence;
using cuqmbr.TravelGuide.Application.Common.Exceptions;
using AutoMapper;
namespace cuqmbr.TravelGuide.Application.Buses.Queries.GetBus;
public class GetBusQueryHandler :
IRequestHandler<GetBusQuery, BusDto>
{
private readonly UnitOfWork _unitOfWork;
private readonly IMapper _mapper;
public GetBusQueryHandler(
UnitOfWork unitOfWork,
IMapper mapper)
{
_unitOfWork = unitOfWork;
_mapper = mapper;
}
public async Task<BusDto> Handle(
GetBusQuery request,
CancellationToken cancellationToken)
{
var entity = await _unitOfWork.BusRepository.GetOneAsync(
e => e.Guid == request.Guid, cancellationToken);
_unitOfWork.Dispose();
if (entity == null)
{
throw new NotFoundException();
}
return _mapper.Map<BusDto>(entity);
}
}

View File

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

View File

@ -0,0 +1,23 @@
using cuqmbr.TravelGuide.Application.Common.Models;
using MediatR;
namespace cuqmbr.TravelGuide.Application.Buses.Queries.GetBusesPage;
public record GetBusesPageQuery : IRequest<PaginatedList<BusDto>>
{
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 string? Number { get; set; }
public string? Model { get; set; }
public short? CapacityGreaterOrEqualThan { get; set; }
public short? CapacityLessOrEqualThan { 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.Buses.Queries.GetBusesPage;
public class GetBusesPageQueryAuthorizer :
AbstractRequestAuthorizer<GetBusesPageQuery>
{
private readonly SessionUserService _sessionUserService;
public GetBusesPageQueryAuthorizer(SessionUserService sessionUserService)
{
_sessionUserService = sessionUserService;
}
public override void BuildPolicy(GetBusesPageQuery request)
{
UseRequirement(new MustBeAuthenticatedRequirement
{
IsAuthenticated= _sessionUserService.IsAuthenticated
});
UseRequirement(new MustBeInRolesRequirement
{
RequiredRoles = [IdentityRole.Administrator],
UserRoles = _sessionUserService.Roles
});
}
}

View File

@ -0,0 +1,53 @@
using MediatR;
using cuqmbr.TravelGuide.Application.Common.Interfaces.Persistence;
using AutoMapper;
using cuqmbr.TravelGuide.Application.Common.Models;
using cuqmbr.TravelGuide.Application.Common.Extensions;
namespace cuqmbr.TravelGuide.Application.Buses.Queries.GetBusesPage;
public class GetBusesPageQueryHandler :
IRequestHandler<GetBusesPageQuery, PaginatedList<BusDto>>
{
private readonly UnitOfWork _unitOfWork;
private readonly IMapper _mapper;
public GetBusesPageQueryHandler(
UnitOfWork unitOfWork,
IMapper mapper)
{
_unitOfWork = unitOfWork;
_mapper = mapper;
}
public async Task<PaginatedList<BusDto>> Handle(
GetBusesPageQuery request,
CancellationToken cancellationToken)
{
var paginatedList = await _unitOfWork.BusRepository.GetPageAsync(
e =>
(e.Number.ToLower().Contains(request.Search.ToLower()) ||
e.Model.ToLower().Contains(request.Search.ToLower())) &&
(request.CapacityGreaterOrEqualThan != null
? e.Capacity >= request.CapacityGreaterOrEqualThan
: true) &&
(request.CapacityLessOrEqualThan != null
? e.Capacity <= request.CapacityLessOrEqualThan
: true),
request.PageNumber, request.PageSize,
cancellationToken);
var mappedItems = _mapper
.ProjectTo<BusDto>(paginatedList.Items.AsQueryable());
mappedItems = QueryableExtension<BusDto>
.ApplySort(mappedItems, request.Sort);
_unitOfWork.Dispose();
return new PaginatedList<BusDto>(
mappedItems.ToList(),
paginatedList.TotalCount, request.PageNumber,
request.PageSize);
}
}

View File

@ -0,0 +1,43 @@
using cuqmbr.TravelGuide.Application.Common.Interfaces.Services;
using FluentValidation;
using Microsoft.Extensions.Localization;
namespace cuqmbr.TravelGuide.Application.Buses.Queries.GetBusesPage;
public class GetBusesPageQueryValidator : AbstractValidator<GetBusesPageQuery>
{
public GetBusesPageQueryValidator(
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,10 @@
namespace cuqmbr.TravelGuide.Application.Buses.ViewModels;
public sealed class AddBusViewModel
{
public string Number { get; set; }
public string Model { get; set; }
public short Capacity { get; set; }
}

View File

@ -0,0 +1,15 @@
namespace cuqmbr.TravelGuide.Application.Buses.ViewModels;
public sealed class GetBusesPageFilterViewModel
{
public string? Number { get; set; }
public string? Model { get; set; }
// TODO: Consider adding strict equals rule although it is not
// necessarily needed to filter with exact capacity
public short? CapacityGreaterOrEqualThan { get; set; }
public short? CapacityLessOrEqualThan { get; set; }
}

View File

@ -0,0 +1,10 @@
namespace cuqmbr.TravelGuide.Application.Buses.ViewModels;
public sealed class UpdateBusViewModel
{
public string Number { get; set; }
public string Model { get; set; }
public short Capacity { get; set; }
}

View File

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

View File

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

View File

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

View File

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

View File

@ -14,6 +14,14 @@ public interface UnitOfWork : IDisposable
RouteRepository RouteRepository { get; }
VehicleRepository VehicleRepository { get; }
BusRepository BusRepository { get; }
AircraftRepository AircraftRepository { get; }
TrainRepository TrainRepository { get; }
int Save();
Task<int> SaveAsync(CancellationToken cancellationToken);

View File

@ -0,0 +1,12 @@
using MediatR;
namespace cuqmbr.TravelGuide.Application.Trains.Commands.AddTrain;
public record AddTrainCommand : IRequest<TrainDto>
{
public string Number { get; set; }
public string Model { get; set; }
public short Capacity { 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.Trains.Commands.AddTrain;
public class AddTrainCommandAuthorizer :
AbstractRequestAuthorizer<AddTrainCommand>
{
private readonly SessionUserService _sessionUserService;
public AddTrainCommandAuthorizer(SessionUserService sessionUserService)
{
_sessionUserService = sessionUserService;
}
public override void BuildPolicy(AddTrainCommand request)
{
UseRequirement(new MustBeAuthenticatedRequirement
{
IsAuthenticated= _sessionUserService.IsAuthenticated
});
UseRequirement(new MustBeInRolesRequirement
{
RequiredRoles = [IdentityRole.Administrator],
UserRoles = _sessionUserService.Roles
});
}
}

View File

@ -0,0 +1,51 @@
using MediatR;
using cuqmbr.TravelGuide.Application.Common.Interfaces.Persistence;
using cuqmbr.TravelGuide.Domain.Entities;
using AutoMapper;
using cuqmbr.TravelGuide.Application.Common.Exceptions;
namespace cuqmbr.TravelGuide.Application.Trains.Commands.AddTrain;
public class AddTrainCommandHandler :
IRequestHandler<AddTrainCommand, TrainDto>
{
private readonly UnitOfWork _unitOfWork;
private readonly IMapper _mapper;
public AddTrainCommandHandler(
UnitOfWork unitOfWork,
IMapper mapper)
{
_unitOfWork = unitOfWork;
_mapper = mapper;
}
public async Task<TrainDto> Handle(
AddTrainCommand request,
CancellationToken cancellationToken)
{
var entity = await _unitOfWork.TrainRepository.GetOneAsync(
e => e.Number == request.Number, cancellationToken);
if (entity != null)
{
throw new DuplicateEntityException(
"Train with given number already exists.");
}
entity = new Train()
{
Number = request.Number,
Model = request.Model,
Capacity = request.Capacity
};
entity = await _unitOfWork.TrainRepository.AddOneAsync(
entity, cancellationToken);
await _unitOfWork.SaveAsync(cancellationToken);
_unitOfWork.Dispose();
return _mapper.Map<TrainDto>(entity);
}
}

View File

@ -0,0 +1,37 @@
using cuqmbr.TravelGuide.Application.Common.Interfaces.Services;
using FluentValidation;
using Microsoft.Extensions.Localization;
namespace cuqmbr.TravelGuide.Application.Trains.Commands.AddTrain;
public class AddTrainCommandValidator : AbstractValidator<AddTrainCommand>
{
public AddTrainCommandValidator(
IStringLocalizer localizer,
CultureService cultureService)
{
RuleFor(v => v.Number)
.NotEmpty()
.WithMessage(localizer["FluentValidation.NotEmpty"])
.MaximumLength(32)
.WithMessage(
String.Format(
cultureService.Culture,
localizer["FluentValidation.MaximumLength"],
32));
RuleFor(v => v.Model)
.NotEmpty()
.WithMessage(localizer["FluentValidation.NotEmpty"])
.MaximumLength(64)
.WithMessage(
String.Format(
cultureService.Culture,
localizer["FluentValidation.MaximumLength"],
64));
RuleFor(v => v.Capacity)
.NotEmpty()
.WithMessage(localizer["FluentValidation.NotEmpty"]);
}
}

View File

@ -0,0 +1,8 @@
using MediatR;
namespace cuqmbr.TravelGuide.Application.Trains.Commands.DeleteTrain;
public record DeleteTrainCommand : 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.Trains.Commands.DeleteTrain;
public class DeleteTrainCommandAuthorizer :
AbstractRequestAuthorizer<DeleteTrainCommand>
{
private readonly SessionUserService _sessionUserService;
public DeleteTrainCommandAuthorizer(SessionUserService sessionUserService)
{
_sessionUserService = sessionUserService;
}
public override void BuildPolicy(DeleteTrainCommand request)
{
UseRequirement(new MustBeAuthenticatedRequirement
{
IsAuthenticated= _sessionUserService.IsAuthenticated
});
UseRequirement(new MustBeInRolesRequirement
{
RequiredRoles = [IdentityRole.Administrator],
UserRoles = _sessionUserService.Roles
});
}
}

View File

@ -0,0 +1,34 @@
using MediatR;
using cuqmbr.TravelGuide.Application.Common.Interfaces.Persistence;
using cuqmbr.TravelGuide.Application.Common.Exceptions;
namespace cuqmbr.TravelGuide.Application.Trains.Commands.DeleteTrain;
public class DeleteTrainCommandHandler : IRequestHandler<DeleteTrainCommand>
{
private readonly UnitOfWork _unitOfWork;
public DeleteTrainCommandHandler(UnitOfWork unitOfWork)
{
_unitOfWork = unitOfWork;
}
public async Task Handle(
DeleteTrainCommand request,
CancellationToken cancellationToken)
{
var entity = await _unitOfWork.TrainRepository.GetOneAsync(
e => e.Guid == request.Guid, cancellationToken);
if (entity == null)
{
throw new NotFoundException();
}
await _unitOfWork.TrainRepository.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.Trains.Commands.DeleteTrain;
public class DeleteTrainCommandValidator : AbstractValidator<DeleteTrainCommand>
{
public DeleteTrainCommandValidator(IStringLocalizer localizer)
{
RuleFor(v => v.Guid)
.NotEmpty()
.WithMessage(localizer["FluentValidation.NotEmpty"]);
}
}

View File

@ -0,0 +1,14 @@
using MediatR;
namespace cuqmbr.TravelGuide.Application.Trains.Commands.UpdateTrain;
public record UpdateTrainCommand : IRequest<TrainDto>
{
public Guid Guid { get; set; }
public string Number { get; set; }
public string Model { get; set; }
public short Capacity { 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.Trains.Commands.UpdateTrain;
public class UpdateTrainCommandAuthorizer :
AbstractRequestAuthorizer<UpdateTrainCommand>
{
private readonly SessionUserService _sessionUserService;
public UpdateTrainCommandAuthorizer(SessionUserService sessionUserService)
{
_sessionUserService = sessionUserService;
}
public override void BuildPolicy(UpdateTrainCommand request)
{
UseRequirement(new MustBeAuthenticatedRequirement
{
IsAuthenticated= _sessionUserService.IsAuthenticated
});
UseRequirement(new MustBeInRolesRequirement
{
RequiredRoles = [IdentityRole.Administrator],
UserRoles = _sessionUserService.Roles
});
}
}

View File

@ -0,0 +1,56 @@
using MediatR;
using cuqmbr.TravelGuide.Application.Common.Interfaces.Persistence;
using AutoMapper;
using cuqmbr.TravelGuide.Application.Common.Exceptions;
namespace cuqmbr.TravelGuide.Application.Trains.Commands.UpdateTrain;
public class UpdateTrainCommandHandler :
IRequestHandler<UpdateTrainCommand, TrainDto>
{
private readonly UnitOfWork _unitOfWork;
private readonly IMapper _mapper;
public UpdateTrainCommandHandler(
UnitOfWork unitOfWork,
IMapper mapper)
{
_unitOfWork = unitOfWork;
_mapper = mapper;
}
public async Task<TrainDto> Handle(
UpdateTrainCommand request,
CancellationToken cancellationToken)
{
var entity = await _unitOfWork.TrainRepository.GetOneAsync(
e => e.Guid == request.Guid, cancellationToken);
if (entity == null)
{
throw new NotFoundException();
}
var duplicateEntity = await _unitOfWork.TrainRepository.GetOneAsync(
e => e.Number == request.Number,
cancellationToken);
if (duplicateEntity != null)
{
throw new DuplicateEntityException(
"Train with given number already exists.");
}
entity.Number = request.Number;
entity.Model = request.Model;
entity.Capacity = request.Capacity;
entity = await _unitOfWork.TrainRepository.UpdateOneAsync(
entity, cancellationToken);
await _unitOfWork.SaveAsync(cancellationToken);
_unitOfWork.Dispose();
return _mapper.Map<TrainDto>(entity);
}
}

View File

@ -0,0 +1,41 @@
using cuqmbr.TravelGuide.Application.Common.Interfaces.Services;
using FluentValidation;
using Microsoft.Extensions.Localization;
namespace cuqmbr.TravelGuide.Application.Trains.Commands.UpdateTrain;
public class UpdateTrainCommandValidator : AbstractValidator<UpdateTrainCommand>
{
public UpdateTrainCommandValidator(
IStringLocalizer localizer,
CultureService cultureService)
{
RuleFor(v => v.Guid)
.NotEmpty()
.WithMessage(localizer["FluentValidation.NotEmpty"]);
RuleFor(v => v.Number)
.NotEmpty()
.WithMessage(localizer["FluentValidation.NotEmpty"])
.MaximumLength(32)
.WithMessage(
String.Format(
cultureService.Culture,
localizer["FluentValidation.MaximumLength"],
32));
RuleFor(v => v.Model)
.NotEmpty()
.WithMessage(localizer["FluentValidation.NotEmpty"])
.MaximumLength(64)
.WithMessage(
String.Format(
cultureService.Culture,
localizer["FluentValidation.MaximumLength"],
64));
RuleFor(v => v.Capacity)
.NotEmpty()
.WithMessage(localizer["FluentValidation.NotEmpty"]);
}
}

View File

@ -0,0 +1,8 @@
using MediatR;
namespace cuqmbr.TravelGuide.Application.Trains.Queries.GetTrain;
public record GetTrainQuery : IRequest<TrainDto>
{
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.Trains.Queries.GetTrain;
public class GetTrainQueryAuthorizer :
AbstractRequestAuthorizer<GetTrainQuery>
{
private readonly SessionUserService _sessionUserService;
public GetTrainQueryAuthorizer(SessionUserService sessionUserService)
{
_sessionUserService = sessionUserService;
}
public override void BuildPolicy(GetTrainQuery request)
{
UseRequirement(new MustBeAuthenticatedRequirement
{
IsAuthenticated= _sessionUserService.IsAuthenticated
});
UseRequirement(new MustBeInRolesRequirement
{
RequiredRoles = [IdentityRole.Administrator],
UserRoles = _sessionUserService.Roles
});
}
}

View File

@ -0,0 +1,38 @@
using MediatR;
using cuqmbr.TravelGuide.Application.Common.Interfaces.Persistence;
using cuqmbr.TravelGuide.Application.Common.Exceptions;
using AutoMapper;
namespace cuqmbr.TravelGuide.Application.Trains.Queries.GetTrain;
public class GetTrainQueryHandler :
IRequestHandler<GetTrainQuery, TrainDto>
{
private readonly UnitOfWork _unitOfWork;
private readonly IMapper _mapper;
public GetTrainQueryHandler(
UnitOfWork unitOfWork,
IMapper mapper)
{
_unitOfWork = unitOfWork;
_mapper = mapper;
}
public async Task<TrainDto> Handle(
GetTrainQuery request,
CancellationToken cancellationToken)
{
var entity = await _unitOfWork.TrainRepository.GetOneAsync(
e => e.Guid == request.Guid, cancellationToken);
_unitOfWork.Dispose();
if (entity == null)
{
throw new NotFoundException();
}
return _mapper.Map<TrainDto>(entity);
}
}

View File

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

View File

@ -0,0 +1,23 @@
using cuqmbr.TravelGuide.Application.Common.Models;
using MediatR;
namespace cuqmbr.TravelGuide.Application.Trains.Queries.GetTrainsPage;
public record GetTrainsPageQuery : IRequest<PaginatedList<TrainDto>>
{
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 string? Number { get; set; }
public string? Model { get; set; }
public short? CapacityGreaterOrEqualThan { get; set; }
public short? CapacityLessOrEqualThan { 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.Trains.Queries.GetTrainsPage;
public class GetTrainsPageQueryAuthorizer :
AbstractRequestAuthorizer<GetTrainsPageQuery>
{
private readonly SessionUserService _sessionUserService;
public GetTrainsPageQueryAuthorizer(SessionUserService sessionUserService)
{
_sessionUserService = sessionUserService;
}
public override void BuildPolicy(GetTrainsPageQuery request)
{
UseRequirement(new MustBeAuthenticatedRequirement
{
IsAuthenticated= _sessionUserService.IsAuthenticated
});
UseRequirement(new MustBeInRolesRequirement
{
RequiredRoles = [IdentityRole.Administrator],
UserRoles = _sessionUserService.Roles
});
}
}

View File

@ -0,0 +1,53 @@
using MediatR;
using cuqmbr.TravelGuide.Application.Common.Interfaces.Persistence;
using AutoMapper;
using cuqmbr.TravelGuide.Application.Common.Models;
using cuqmbr.TravelGuide.Application.Common.Extensions;
namespace cuqmbr.TravelGuide.Application.Trains.Queries.GetTrainsPage;
public class GetTrainsPageQueryHandler :
IRequestHandler<GetTrainsPageQuery, PaginatedList<TrainDto>>
{
private readonly UnitOfWork _unitOfWork;
private readonly IMapper _mapper;
public GetTrainsPageQueryHandler(
UnitOfWork unitOfWork,
IMapper mapper)
{
_unitOfWork = unitOfWork;
_mapper = mapper;
}
public async Task<PaginatedList<TrainDto>> Handle(
GetTrainsPageQuery request,
CancellationToken cancellationToken)
{
var paginatedList = await _unitOfWork.TrainRepository.GetPageAsync(
e =>
(e.Number.ToLower().Contains(request.Search.ToLower()) ||
e.Model.ToLower().Contains(request.Search.ToLower())) &&
(request.CapacityGreaterOrEqualThan != null
? e.Capacity >= request.CapacityGreaterOrEqualThan
: true) &&
(request.CapacityLessOrEqualThan != null
? e.Capacity <= request.CapacityLessOrEqualThan
: true),
request.PageNumber, request.PageSize,
cancellationToken);
var mappedItems = _mapper
.ProjectTo<TrainDto>(paginatedList.Items.AsQueryable());
mappedItems = QueryableExtension<TrainDto>
.ApplySort(mappedItems, request.Sort);
_unitOfWork.Dispose();
return new PaginatedList<TrainDto>(
mappedItems.ToList(),
paginatedList.TotalCount, request.PageNumber,
request.PageSize);
}
}

View File

@ -0,0 +1,43 @@
using cuqmbr.TravelGuide.Application.Common.Interfaces.Services;
using FluentValidation;
using Microsoft.Extensions.Localization;
namespace cuqmbr.TravelGuide.Application.Trains.Queries.GetTrainsPage;
public class GetTrainsPageQueryValidator : AbstractValidator<GetTrainsPageQuery>
{
public GetTrainsPageQueryValidator(
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,23 @@
using cuqmbr.TravelGuide.Application.Common.Mappings;
using cuqmbr.TravelGuide.Domain.Entities;
namespace cuqmbr.TravelGuide.Application.Trains;
public sealed class TrainDto : IMapFrom<Train>
{
public Guid Uuid { get; set; }
public string Number { get; set; }
public string Model { get; set; }
public short Capacity { get; set; }
public void Mapping(MappingProfile profile)
{
profile.CreateMap<Train, TrainDto>()
.ForMember(
d => d.Uuid,
opt => opt.MapFrom(s => s.Guid));
}
}

View File

@ -0,0 +1,10 @@
namespace cuqmbr.TravelGuide.Application.Trains.ViewModels;
public sealed class AddTrainViewModel
{
public string Number { get; set; }
public string Model { get; set; }
public short Capacity { get; set; }
}

View File

@ -0,0 +1,15 @@
namespace cuqmbr.TravelGuide.Application.Trains.ViewModels;
public sealed class GetTrainsPageFilterViewModel
{
public string? Number { get; set; }
public string? Model { get; set; }
// TODO: Consider adding strict equals rule although it is not
// necessarily needed to filter with exact capacity
public short? CapacityGreaterOrEqualThan { get; set; }
public short? CapacityLessOrEqualThan { get; set; }
}

View File

@ -0,0 +1,10 @@
namespace cuqmbr.TravelGuide.Application.Trains.ViewModels;
public sealed class UpdateTrainViewModel
{
public string Number { get; set; }
public string Model { get; set; }
public short Capacity { get; set; }
}

View File

@ -29,6 +29,7 @@ public static class Configuration
configuration.ConnectionString,
options =>
{
// TODO: Move to persistence project
options.MigrationsHistoryTable(
"ef_migrations_history",
configuration.PartitionName);

View File

@ -0,0 +1,12 @@
namespace cuqmbr.TravelGuide.Domain.Entities;
public class Aircraft : Vehicle
{
public string Number { get; set; }
public string Model { get; set; }
public short Capacity { get; set; }
// TODO: Add more properties to describe aircraft's capabilities
}

View File

@ -0,0 +1,12 @@
namespace cuqmbr.TravelGuide.Domain.Entities;
public class Bus : Vehicle
{
public string Number { get; set; }
public string Model { get; set; }
public short Capacity { get; set; }
// TODO: Add more properties to describe bus' capabilities
}

View File

@ -0,0 +1,12 @@
namespace cuqmbr.TravelGuide.Domain.Entities;
public class Train : Vehicle
{
public string Number { get; set; }
public string Model { get; set; }
public short Capacity { get; set; }
// TODO: Add more properties to describe train's capabilities
}

View File

@ -0,0 +1,8 @@
using cuqmbr.TravelGuide.Domain.Enums;
namespace cuqmbr.TravelGuide.Domain.Entities;
public abstract class Vehicle : EntityBase
{
public VehicleType VehicleType { get; set; }
}

View File

@ -0,0 +1,193 @@
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.Aircrafts;
using cuqmbr.TravelGuide.Application.Aircrafts.Commands.AddAircraft;
using cuqmbr.TravelGuide.Application.Aircrafts.Queries.GetAircraftsPage;
using cuqmbr.TravelGuide.Application.Aircrafts.Queries.GetAircraft;
using cuqmbr.TravelGuide.Application.Aircrafts.Commands.UpdateAircraft;
using cuqmbr.TravelGuide.Application.Aircrafts.Commands.DeleteAircraft;
using cuqmbr.TravelGuide.Application.Aircrafts.ViewModels;
namespace cuqmbr.TravelGuide.HttpApi.Controllers;
[Route("aircrafts")]
public class AircraftsController : ControllerBase
{
[HttpPost]
[SwaggerOperation("Add a aircraft")]
[SwaggerResponse(
StatusCodes.Status201Created, "Object successfuly created",
typeof(AircraftDto))]
[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, "Parent object not found",
typeof(ProblemDetails))]
[SwaggerResponse(
StatusCodes.Status500InternalServerError, "Internal server error",
typeof(ProblemDetails))]
public async Task<ActionResult<AircraftDto>> Add(
[FromBody] AddAircraftViewModel viewModel,
CancellationToken cancellationToken)
{
return StatusCode(
StatusCodes.Status201Created,
await Mediator.Send(
new AddAircraftCommand()
{
Number = viewModel.Number,
Model = viewModel.Model,
Capacity = viewModel.Capacity
},
cancellationToken));
}
[HttpGet]
[SwaggerOperation("Get a list of all aircrafts")]
[SwaggerResponse(
StatusCodes.Status200OK, "Request successful",
typeof(PaginatedList<AircraftDto>))]
[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<AircraftDto>> GetPage(
[FromQuery] PageQuery pageQuery, [FromQuery] SearchQuery searchQuery,
[FromQuery] SortQuery sortQuery,
[FromQuery] GetAircraftsPageFilterViewModel filterQuery,
CancellationToken cancellationToken)
{
return await Mediator.Send(
new GetAircraftsPageQuery()
{
PageNumber = pageQuery.PageNumber,
PageSize = pageQuery.PageSize,
Search = searchQuery.Search,
Sort = sortQuery.Sort,
CapacityGreaterOrEqualThan =
filterQuery.CapacityGreaterOrEqualThan,
CapacityLessOrEqualThan =
filterQuery.CapacityLessOrEqualThan
},
cancellationToken);
}
[HttpGet("{uuid:guid}")]
[SwaggerOperation("Get a aircraft by uuid")]
[SwaggerResponse(
StatusCodes.Status200OK, "Request successful", typeof(AircraftDto))]
[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(AircraftDto))]
[SwaggerResponse(
StatusCodes.Status500InternalServerError, "Internal server error",
typeof(ProblemDetails))]
public async Task<AircraftDto> Get(
[FromRoute] Guid uuid,
CancellationToken cancellationToken)
{
return await Mediator.Send(new GetAircraftQuery() { Guid = uuid },
cancellationToken);
}
[HttpPut("{uuid:guid}")]
[SwaggerOperation("Update a aircraft")]
[SwaggerResponse(
StatusCodes.Status200OK, "Request successful", typeof(AircraftDto))]
[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(AircraftDto))]
[SwaggerResponse(
StatusCodes.Status404NotFound, "Parent object not found",
typeof(ProblemDetails))]
[SwaggerResponse(
StatusCodes.Status500InternalServerError, "Internal server error",
typeof(ProblemDetails))]
public async Task<AircraftDto> Update(
[FromRoute] Guid uuid,
[FromBody] UpdateAircraftViewModel viewModel,
CancellationToken cancellationToken)
{
return await Mediator.Send(
new UpdateAircraftCommand()
{
Guid = uuid,
Number = viewModel.Number,
Model = viewModel.Model,
Capacity = viewModel.Capacity
},
cancellationToken);
}
[HttpDelete("{uuid:guid}")]
[SwaggerOperation("Delete a aircraft")]
[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(AircraftDto))]
[SwaggerResponse(
StatusCodes.Status500InternalServerError, "Internal server error",
typeof(ProblemDetails))]
public async Task<IActionResult> Delete(
[FromRoute] Guid uuid,
CancellationToken cancellationToken)
{
await Mediator.Send(
new DeleteAircraftCommand() { Guid = uuid },
cancellationToken);
return StatusCode(StatusCodes.Status204NoContent);
}
}

View File

@ -0,0 +1,193 @@
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.Buses;
using cuqmbr.TravelGuide.Application.Buses.Commands.AddBus;
using cuqmbr.TravelGuide.Application.Buses.Queries.GetBusesPage;
using cuqmbr.TravelGuide.Application.Buses.Queries.GetBus;
using cuqmbr.TravelGuide.Application.Buses.Commands.UpdateBus;
using cuqmbr.TravelGuide.Application.Buses.Commands.DeleteBus;
using cuqmbr.TravelGuide.Application.Buses.ViewModels;
namespace cuqmbr.TravelGuide.HttpApi.Controllers;
[Route("buses")]
public class BusesController : ControllerBase
{
[HttpPost]
[SwaggerOperation("Add a bus")]
[SwaggerResponse(
StatusCodes.Status201Created, "Object successfuly created",
typeof(BusDto))]
[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, "Parent object not found",
typeof(ProblemDetails))]
[SwaggerResponse(
StatusCodes.Status500InternalServerError, "Internal server error",
typeof(ProblemDetails))]
public async Task<ActionResult<BusDto>> Add(
[FromBody] AddBusViewModel viewModel,
CancellationToken cancellationToken)
{
return StatusCode(
StatusCodes.Status201Created,
await Mediator.Send(
new AddBusCommand()
{
Number = viewModel.Number,
Model = viewModel.Model,
Capacity = viewModel.Capacity
},
cancellationToken));
}
[HttpGet]
[SwaggerOperation("Get a list of all buses")]
[SwaggerResponse(
StatusCodes.Status200OK, "Request successful",
typeof(PaginatedList<BusDto>))]
[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<BusDto>> GetPage(
[FromQuery] PageQuery pageQuery, [FromQuery] SearchQuery searchQuery,
[FromQuery] SortQuery sortQuery,
[FromQuery] GetBusesPageFilterViewModel filterQuery,
CancellationToken cancellationToken)
{
return await Mediator.Send(
new GetBusesPageQuery()
{
PageNumber = pageQuery.PageNumber,
PageSize = pageQuery.PageSize,
Search = searchQuery.Search,
Sort = sortQuery.Sort,
CapacityGreaterOrEqualThan =
filterQuery.CapacityGreaterOrEqualThan,
CapacityLessOrEqualThan =
filterQuery.CapacityLessOrEqualThan
},
cancellationToken);
}
[HttpGet("{uuid:guid}")]
[SwaggerOperation("Get a bus by uuid")]
[SwaggerResponse(
StatusCodes.Status200OK, "Request successful", typeof(BusDto))]
[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(BusDto))]
[SwaggerResponse(
StatusCodes.Status500InternalServerError, "Internal server error",
typeof(ProblemDetails))]
public async Task<BusDto> Get(
[FromRoute] Guid uuid,
CancellationToken cancellationToken)
{
return await Mediator.Send(new GetBusQuery() { Guid = uuid },
cancellationToken);
}
[HttpPut("{uuid:guid}")]
[SwaggerOperation("Update a bus")]
[SwaggerResponse(
StatusCodes.Status200OK, "Request successful", typeof(BusDto))]
[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(BusDto))]
[SwaggerResponse(
StatusCodes.Status404NotFound, "Parent object not found",
typeof(ProblemDetails))]
[SwaggerResponse(
StatusCodes.Status500InternalServerError, "Internal server error",
typeof(ProblemDetails))]
public async Task<BusDto> Update(
[FromRoute] Guid uuid,
[FromBody] UpdateBusViewModel viewModel,
CancellationToken cancellationToken)
{
return await Mediator.Send(
new UpdateBusCommand()
{
Guid = uuid,
Number = viewModel.Number,
Model = viewModel.Model,
Capacity = viewModel.Capacity
},
cancellationToken);
}
[HttpDelete("{uuid:guid}")]
[SwaggerOperation("Delete a bus")]
[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(BusDto))]
[SwaggerResponse(
StatusCodes.Status500InternalServerError, "Internal server error",
typeof(ProblemDetails))]
public async Task<IActionResult> Delete(
[FromRoute] Guid uuid,
CancellationToken cancellationToken)
{
await Mediator.Send(
new DeleteBusCommand() { Guid = uuid },
cancellationToken);
return StatusCode(StatusCodes.Status204NoContent);
}
}

View File

@ -1,6 +1,7 @@
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Localization;
using cuqmbr.TravelGuide.Application.Common.Interfaces.Services;
using cuqmbr.TravelGuide.Application.Common.Interfaces.Persistence;
namespace cuqmbr.TravelGuide.HttpApi.Controllers;
@ -8,19 +9,41 @@ namespace cuqmbr.TravelGuide.HttpApi.Controllers;
public class TestsController : ControllerBase
{
private readonly IStringLocalizer _localizer;
private readonly UnitOfWork _unitOfWork;
public TestsController(
CultureService cultureService,
IStringLocalizer localizer)
IStringLocalizer localizer,
UnitOfWork unitOfWork)
{
_localizer = localizer;
_unitOfWork = unitOfWork;
}
[HttpGet("getLocalizedString/{inputString}")]
public Task<string> getLocalizedString(
public Task<string> GetLocalizedString(
[FromRoute] string inputString,
CancellationToken cancellationToken)
{
return Task.FromResult<string>(_localizer[inputString]);
}
[HttpGet("trigger")]
public async Task Trigger(CancellationToken cancellationToken)
{
// await _unitOfWork.BusRepository.AddOneAsync(
// new Domain.Entities.Bus()
// {
// Number = "AB1234MK",
// Model = "This is a fancy bus model",
// Capacity = 40
// },
// cancellationToken);
//
// await _unitOfWork.SaveAsync(cancellationToken);
// _unitOfWork.Dispose();
var vehicles = await _unitOfWork.VehicleRepository
.GetPageAsync(1, 10, cancellationToken);
}
}

View File

@ -0,0 +1,193 @@
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.Trains;
using cuqmbr.TravelGuide.Application.Trains.Commands.AddTrain;
using cuqmbr.TravelGuide.Application.Trains.Queries.GetTrainsPage;
using cuqmbr.TravelGuide.Application.Trains.Queries.GetTrain;
using cuqmbr.TravelGuide.Application.Trains.Commands.UpdateTrain;
using cuqmbr.TravelGuide.Application.Trains.Commands.DeleteTrain;
using cuqmbr.TravelGuide.Application.Trains.ViewModels;
namespace cuqmbr.TravelGuide.HttpApi.Controllers;
[Route("trains")]
public class TrainsController : ControllerBase
{
[HttpPost]
[SwaggerOperation("Add a train")]
[SwaggerResponse(
StatusCodes.Status201Created, "Object successfuly created",
typeof(TrainDto))]
[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, "Parent object not found",
typeof(ProblemDetails))]
[SwaggerResponse(
StatusCodes.Status500InternalServerError, "Internal server error",
typeof(ProblemDetails))]
public async Task<ActionResult<TrainDto>> Add(
[FromBody] AddTrainViewModel viewModel,
CancellationToken cancellationToken)
{
return StatusCode(
StatusCodes.Status201Created,
await Mediator.Send(
new AddTrainCommand()
{
Number = viewModel.Number,
Model = viewModel.Model,
Capacity = viewModel.Capacity
},
cancellationToken));
}
[HttpGet]
[SwaggerOperation("Get a list of all trains")]
[SwaggerResponse(
StatusCodes.Status200OK, "Request successful",
typeof(PaginatedList<TrainDto>))]
[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<TrainDto>> GetPage(
[FromQuery] PageQuery pageQuery, [FromQuery] SearchQuery searchQuery,
[FromQuery] SortQuery sortQuery,
[FromQuery] GetTrainsPageFilterViewModel filterQuery,
CancellationToken cancellationToken)
{
return await Mediator.Send(
new GetTrainsPageQuery()
{
PageNumber = pageQuery.PageNumber,
PageSize = pageQuery.PageSize,
Search = searchQuery.Search,
Sort = sortQuery.Sort,
CapacityGreaterOrEqualThan =
filterQuery.CapacityGreaterOrEqualThan,
CapacityLessOrEqualThan =
filterQuery.CapacityLessOrEqualThan
},
cancellationToken);
}
[HttpGet("{uuid:guid}")]
[SwaggerOperation("Get a train by uuid")]
[SwaggerResponse(
StatusCodes.Status200OK, "Request successful", typeof(TrainDto))]
[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(TrainDto))]
[SwaggerResponse(
StatusCodes.Status500InternalServerError, "Internal server error",
typeof(ProblemDetails))]
public async Task<TrainDto> Get(
[FromRoute] Guid uuid,
CancellationToken cancellationToken)
{
return await Mediator.Send(new GetTrainQuery() { Guid = uuid },
cancellationToken);
}
[HttpPut("{uuid:guid}")]
[SwaggerOperation("Update a train")]
[SwaggerResponse(
StatusCodes.Status200OK, "Request successful", typeof(TrainDto))]
[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(TrainDto))]
[SwaggerResponse(
StatusCodes.Status404NotFound, "Parent object not found",
typeof(ProblemDetails))]
[SwaggerResponse(
StatusCodes.Status500InternalServerError, "Internal server error",
typeof(ProblemDetails))]
public async Task<TrainDto> Update(
[FromRoute] Guid uuid,
[FromBody] UpdateTrainViewModel viewModel,
CancellationToken cancellationToken)
{
return await Mediator.Send(
new UpdateTrainCommand()
{
Guid = uuid,
Number = viewModel.Number,
Model = viewModel.Model,
Capacity = viewModel.Capacity
},
cancellationToken);
}
[HttpDelete("{uuid:guid}")]
[SwaggerOperation("Delete a train")]
[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(TrainDto))]
[SwaggerResponse(
StatusCodes.Status500InternalServerError, "Internal server error",
typeof(ProblemDetails))]
public async Task<IActionResult> Delete(
[FromRoute] Guid uuid,
CancellationToken cancellationToken)
{
await Mediator.Send(
new DeleteTrainCommand() { Guid = uuid },
cancellationToken);
return StatusCode(StatusCodes.Status204NoContent);
}
}

View File

@ -0,0 +1,31 @@
{
"Application": {
"Logging": {
"Type": "SimpleConsole",
"LogLevel": "Information",
"TimestampFormat": "yyyy-MM-ddTHH:mm:ss.fffK",
"UseUtcTimestamp": true
},
"Datastore": {
"Type": "postgresql",
"ConnectionString": "Host=127.0.0.1:5432;Database=travel_guide;Username=postgres;Password=0000;Include Error Detail=true"
},
"Localization": {
"DefaultCultureName": "en-US",
"CacheDuration": "00:30:00"
}
},
"Identity": {
"Datastore": {
"Type": "postgresql",
"ConnectionString": "Host=127.0.0.1:5432;Database=travel_guide;Username=postgres;Password=0000"
},
"JsonWebToken": {
"Issuer": "https://api.travel-guide.cuqmbr.xyz",
"Audience": "https://travel-guide.cuqmbr.xyz",
"IssuerSigningKey": "a2c98dec80787a4e85ffb5bcbc24f7e4cc014d8a4fe43e9520480a50759164bc",
"AccessTokenValidity": "24:00:00",
"RefreshTokenValidity": "72:00:00"
}
}
}

View File

@ -18,6 +18,10 @@ public sealed class InMemoryUnitOfWork : UnitOfWork
CityRepository = new InMemoryCityRepository(_dbContext);
AddressRepository = new InMemoryAddressRepository(_dbContext);
RouteRepository = new InMemoryRouteRepository(_dbContext);
VehicleRepository = new InMemoryVehicleRepository(_dbContext);
BusRepository = new InMemoryBusRepository(_dbContext);
AircraftRepository = new InMemoryAircraftRepository(_dbContext);
TrainRepository = new InMemoryTrainRepository(_dbContext);
}
public CountryRepository CountryRepository { get; init; }
@ -30,6 +34,14 @@ public sealed class InMemoryUnitOfWork : UnitOfWork
public RouteRepository RouteRepository { get; init; }
public VehicleRepository VehicleRepository { get; init; }
public BusRepository BusRepository { get; init; }
public AircraftRepository AircraftRepository { get; init; }
public TrainRepository TrainRepository { get; init; }
public int Save()
{
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 InMemoryAircraftRepository :
InMemoryBaseRepository<Aircraft>, AircraftRepository
{
public InMemoryAircraftRepository(InMemoryDbContext dbContext)
: base(dbContext) { }
}

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 InMemoryBusRepository :
InMemoryBaseRepository<Bus>, BusRepository
{
public InMemoryBusRepository(InMemoryDbContext dbContext)
: base(dbContext) { }
}

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 InMemoryTrainRepository :
InMemoryBaseRepository<Train>, TrainRepository
{
public InMemoryTrainRepository(InMemoryDbContext dbContext)
: base(dbContext) { }
}

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 InMemoryVehicleRepository :
InMemoryBaseRepository<Vehicle>, VehicleRepository
{
public InMemoryVehicleRepository(InMemoryDbContext dbContext)
: base(dbContext) { }
}

View File

@ -0,0 +1,33 @@
using cuqmbr.TravelGuide.Domain.Entities;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Metadata.Builders;
namespace cuqmbr.TravelGuide.Persistence.PostgreSql.Configurations;
public class AircraftConfiguration : IEntityTypeConfiguration<Aircraft>
{
public void Configure(EntityTypeBuilder<Aircraft> builder)
{
builder
.HasBaseType<Vehicle>();
builder
.Property(b => b.Number)
.HasColumnName("number")
.HasColumnType("varchar(32)")
.IsRequired(true);
builder
.Property(b => b.Model)
.HasColumnName("model")
.HasColumnType("varchar(64)")
.IsRequired(true);
builder
.Property(b => b.Capacity)
.HasColumnName("capacity")
.HasColumnType("smallint")
.IsRequired(true);
}
}

View File

@ -9,23 +9,21 @@ public class BaseConfiguration<TEntity> : IEntityTypeConfiguration<TEntity>
{
public virtual void Configure(EntityTypeBuilder<TEntity> builder)
{
// Set table name for inherited types using type name
// instead of mapped table name
var tableName = builder.Metadata.GetTableName();
builder
.HasKey(b => b.Id)
.HasName($"pk_{builder.Metadata.GetTableName() ??
// Set primary key for inherited types using type name
// instead of mapped table name
builder.Metadata.ShortName().ToLower()}");
.HasName($"pk_{tableName}");
builder
.Property(b => b.Id)
.HasColumnName("id")
.HasColumnType("bigint")
.UseSequence(
$"{builder.Metadata.GetTableName() ??
// Set sequence for inherited types using type name
// instead of mapped table name
builder.Metadata.ShortName().ToLower()}_" +
$"{builder.Property(b => b.Id).Metadata.GetColumnName()}_" +
$"{tableName}_" +
$"{builder.Property(b => b.Id).Metadata.GetColumnName()}_" +
"sequence");
@ -39,10 +37,7 @@ public class BaseConfiguration<TEntity> : IEntityTypeConfiguration<TEntity>
.HasAlternateKey(b => b.Guid)
.HasName(
"altk_" +
$"{builder.Metadata.GetTableName() ??
// Set alternate key for inherited types using type name
// instead of mapped table name
builder.Metadata.ShortName().ToLower()}_" +
$"{tableName}_" +
$"{builder.Property(b => b.Guid).Metadata.GetColumnName()}");
}
}

View File

@ -0,0 +1,33 @@
using cuqmbr.TravelGuide.Domain.Entities;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Metadata.Builders;
namespace cuqmbr.TravelGuide.Persistence.PostgreSql.Configurations;
public class BusConfiguration : IEntityTypeConfiguration<Bus>
{
public void Configure(EntityTypeBuilder<Bus> builder)
{
builder
.HasBaseType<Vehicle>();
builder
.Property(b => b.Number)
.HasColumnName("number")
.HasColumnType("varchar(32)")
.IsRequired(true);
builder
.Property(b => b.Model)
.HasColumnName("model")
.HasColumnType("varchar(64)")
.IsRequired(true);
builder
.Property(b => b.Capacity)
.HasColumnName("capacity")
.HasColumnType("smallint")
.IsRequired(true);
}
}

View File

@ -0,0 +1,33 @@
using cuqmbr.TravelGuide.Domain.Entities;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Metadata.Builders;
namespace cuqmbr.TravelGuide.Persistence.PostgreSql.Configurations;
public class TrainConfiguration : IEntityTypeConfiguration<Train>
{
public void Configure(EntityTypeBuilder<Train> builder)
{
builder
.HasBaseType<Vehicle>();
builder
.Property(b => b.Number)
.HasColumnName("number")
.HasColumnType("varchar(32)")
.IsRequired(true);
builder
.Property(b => b.Model)
.HasColumnName("model")
.HasColumnType("varchar(64)")
.IsRequired(true);
builder
.Property(b => b.Capacity)
.HasColumnName("capacity")
.HasColumnType("smallint")
.IsRequired(true);
}
}

View File

@ -0,0 +1,41 @@
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 VehicleConfiguration : BaseConfiguration<Vehicle>
{
public override void Configure(EntityTypeBuilder<Vehicle> builder)
{
builder
.Property(a => a.VehicleType)
.HasColumnName("vehicle_type")
.HasColumnType("varchar(16)")
.IsRequired(true);
builder
.ToTable(
"vehicles",
v => v.HasCheckConstraint(
"ck_" +
$"{builder.Metadata.GetTableName()}_" +
$"{builder.Property(v => v.VehicleType)
.Metadata.GetColumnName()}",
$"{builder.Property(v => v.VehicleType)
.Metadata.GetColumnName()} IN ('{String
.Join("', '", VehicleType.Enumerations
.Values.Select(v => v.Name))}')"));
builder
.ToTable("vehicles")
.UseTphMappingStrategy()
.HasDiscriminator(v => v.VehicleType)
.HasValue<Bus>(VehicleType.Bus)
.HasValue<Aircraft>(VehicleType.Aircraft)
.HasValue<Train>(VehicleType.Train);
base.Configure(builder);
}
}

View File

@ -0,0 +1,476 @@
// <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("20250503053607_Add_Bus_Aircraft_Train_with_basic_properties")]
partial class Add_Bus_Aircraft_Train_with_basic_properties
{
/// <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.HasSequence("vehicles_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_uuid");
b.HasIndex("CityId")
.HasDatabaseName("ix_addresses_city_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_uuid");
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_uuid");
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_uuid");
b.HasIndex("CountryId")
.HasDatabaseName("ix_regions_country_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_uuid");
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_uuid");
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("RouteId")
.HasDatabaseName("ix_route_addresses_route_id");
b.ToTable("route_addresses", "application");
});
modelBuilder.Entity("cuqmbr.TravelGuide.Domain.Entities.Vehicle", b =>
{
b.Property<long>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("bigint")
.HasColumnName("id")
.HasDefaultValueSql("nextval('application.vehicles_id_sequence')");
NpgsqlPropertyBuilderExtensions.UseSequence(b.Property<long>("Id"), "vehicles_id_sequence");
b.Property<Guid>("Guid")
.HasColumnType("uuid")
.HasColumnName("uuid");
b.Property<string>("VehicleType")
.IsRequired()
.HasColumnType("varchar(16)")
.HasColumnName("vehicle_type");
b.HasKey("Id")
.HasName("pk_vehicles");
b.HasAlternateKey("Guid")
.HasName("altk_vehicles_uuid");
b.ToTable("vehicles", "application", t =>
{
t.HasCheckConstraint("ck_vehicles_vehicle_type", "vehicle_type IN ('bus', 'train', 'aircraft')");
});
b.HasDiscriminator<string>("VehicleType");
b.UseTphMappingStrategy();
});
modelBuilder.Entity("cuqmbr.TravelGuide.Domain.Entities.Aircraft", b =>
{
b.HasBaseType("cuqmbr.TravelGuide.Domain.Entities.Vehicle");
b.Property<short>("Capacity")
.ValueGeneratedOnUpdateSometimes()
.HasColumnType("smallint")
.HasColumnName("capacity");
b.Property<string>("Model")
.IsRequired()
.ValueGeneratedOnUpdateSometimes()
.HasColumnType("varchar(64)")
.HasColumnName("model");
b.Property<string>("Number")
.IsRequired()
.ValueGeneratedOnUpdateSometimes()
.HasColumnType("varchar(32)")
.HasColumnName("number");
b.ToTable(t =>
{
t.HasCheckConstraint("ck_vehicles_vehicle_type", "vehicle_type IN ('bus', 'train', 'aircraft')");
});
b.HasDiscriminator().HasValue("aircraft");
});
modelBuilder.Entity("cuqmbr.TravelGuide.Domain.Entities.Bus", b =>
{
b.HasBaseType("cuqmbr.TravelGuide.Domain.Entities.Vehicle");
b.Property<short>("Capacity")
.ValueGeneratedOnUpdateSometimes()
.HasColumnType("smallint")
.HasColumnName("capacity");
b.Property<string>("Model")
.IsRequired()
.ValueGeneratedOnUpdateSometimes()
.HasColumnType("varchar(64)")
.HasColumnName("model");
b.Property<string>("Number")
.IsRequired()
.ValueGeneratedOnUpdateSometimes()
.HasColumnType("varchar(32)")
.HasColumnName("number");
b.ToTable(t =>
{
t.HasCheckConstraint("ck_vehicles_vehicle_type", "vehicle_type IN ('bus', 'train', 'aircraft')");
});
b.HasDiscriminator().HasValue("bus");
});
modelBuilder.Entity("cuqmbr.TravelGuide.Domain.Entities.Train", b =>
{
b.HasBaseType("cuqmbr.TravelGuide.Domain.Entities.Vehicle");
b.Property<short>("Capacity")
.ValueGeneratedOnUpdateSometimes()
.HasColumnType("smallint")
.HasColumnName("capacity");
b.Property<string>("Model")
.IsRequired()
.ValueGeneratedOnUpdateSometimes()
.HasColumnType("varchar(64)")
.HasColumnName("model");
b.Property<string>("Number")
.IsRequired()
.ValueGeneratedOnUpdateSometimes()
.HasColumnType("varchar(32)")
.HasColumnName("number");
b.ToTable(t =>
{
t.HasCheckConstraint("ck_vehicles_vehicle_type", "vehicle_type IN ('bus', 'train', 'aircraft')");
});
b.HasDiscriminator().HasValue("train");
});
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,50 @@
using System;
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace Persistence.PostgreSql.Migrations
{
/// <inheritdoc />
public partial class Add_Bus_Aircraft_Train_with_basic_properties : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.CreateSequence(
name: "vehicles_id_sequence",
schema: "application");
migrationBuilder.CreateTable(
name: "vehicles",
schema: "application",
columns: table => new
{
id = table.Column<long>(type: "bigint", nullable: false, defaultValueSql: "nextval('application.vehicles_id_sequence')"),
vehicle_type = table.Column<string>(type: "varchar(16)", nullable: false),
number = table.Column<string>(type: "varchar(32)", nullable: true),
model = table.Column<string>(type: "varchar(64)", nullable: true),
capacity = table.Column<short>(type: "smallint", nullable: true),
uuid = table.Column<Guid>(type: "uuid", nullable: false)
},
constraints: table =>
{
table.PrimaryKey("pk_vehicles", x => x.id);
table.UniqueConstraint("altk_vehicles_uuid", x => x.uuid);
table.CheckConstraint("ck_vehicles_vehicle_type", "vehicle_type IN ('bus', 'train', 'aircraft')");
});
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropTable(
name: "vehicles",
schema: "application");
migrationBuilder.DropSequence(
name: "vehicles_id_sequence",
schema: "application");
}
}
}

View File

@ -35,6 +35,8 @@ namespace Persistence.PostgreSql.Migrations
modelBuilder.HasSequence("routes_id_sequence");
modelBuilder.HasSequence("vehicles_id_sequence");
modelBuilder.Entity("cuqmbr.TravelGuide.Domain.Entities.Address", b =>
{
b.Property<long>("Id")
@ -262,6 +264,128 @@ namespace Persistence.PostgreSql.Migrations
b.ToTable("route_addresses", "application");
});
modelBuilder.Entity("cuqmbr.TravelGuide.Domain.Entities.Vehicle", b =>
{
b.Property<long>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("bigint")
.HasColumnName("id")
.HasDefaultValueSql("nextval('application.vehicles_id_sequence')");
NpgsqlPropertyBuilderExtensions.UseSequence(b.Property<long>("Id"), "vehicles_id_sequence");
b.Property<Guid>("Guid")
.HasColumnType("uuid")
.HasColumnName("uuid");
b.Property<string>("VehicleType")
.IsRequired()
.HasColumnType("varchar(16)")
.HasColumnName("vehicle_type");
b.HasKey("Id")
.HasName("pk_vehicles");
b.HasAlternateKey("Guid")
.HasName("altk_vehicles_uuid");
b.ToTable("vehicles", "application", t =>
{
t.HasCheckConstraint("ck_vehicles_vehicle_type", "vehicle_type IN ('bus', 'train', 'aircraft')");
});
b.HasDiscriminator<string>("VehicleType");
b.UseTphMappingStrategy();
});
modelBuilder.Entity("cuqmbr.TravelGuide.Domain.Entities.Aircraft", b =>
{
b.HasBaseType("cuqmbr.TravelGuide.Domain.Entities.Vehicle");
b.Property<short>("Capacity")
.ValueGeneratedOnUpdateSometimes()
.HasColumnType("smallint")
.HasColumnName("capacity");
b.Property<string>("Model")
.IsRequired()
.ValueGeneratedOnUpdateSometimes()
.HasColumnType("varchar(64)")
.HasColumnName("model");
b.Property<string>("Number")
.IsRequired()
.ValueGeneratedOnUpdateSometimes()
.HasColumnType("varchar(32)")
.HasColumnName("number");
b.ToTable(t =>
{
t.HasCheckConstraint("ck_vehicles_vehicle_type", "vehicle_type IN ('bus', 'train', 'aircraft')");
});
b.HasDiscriminator().HasValue("aircraft");
});
modelBuilder.Entity("cuqmbr.TravelGuide.Domain.Entities.Bus", b =>
{
b.HasBaseType("cuqmbr.TravelGuide.Domain.Entities.Vehicle");
b.Property<short>("Capacity")
.ValueGeneratedOnUpdateSometimes()
.HasColumnType("smallint")
.HasColumnName("capacity");
b.Property<string>("Model")
.IsRequired()
.ValueGeneratedOnUpdateSometimes()
.HasColumnType("varchar(64)")
.HasColumnName("model");
b.Property<string>("Number")
.IsRequired()
.ValueGeneratedOnUpdateSometimes()
.HasColumnType("varchar(32)")
.HasColumnName("number");
b.ToTable(t =>
{
t.HasCheckConstraint("ck_vehicles_vehicle_type", "vehicle_type IN ('bus', 'train', 'aircraft')");
});
b.HasDiscriminator().HasValue("bus");
});
modelBuilder.Entity("cuqmbr.TravelGuide.Domain.Entities.Train", b =>
{
b.HasBaseType("cuqmbr.TravelGuide.Domain.Entities.Vehicle");
b.Property<short>("Capacity")
.ValueGeneratedOnUpdateSometimes()
.HasColumnType("smallint")
.HasColumnName("capacity");
b.Property<string>("Model")
.IsRequired()
.ValueGeneratedOnUpdateSometimes()
.HasColumnType("varchar(64)")
.HasColumnName("model");
b.Property<string>("Number")
.IsRequired()
.ValueGeneratedOnUpdateSometimes()
.HasColumnType("varchar(32)")
.HasColumnName("number");
b.ToTable(t =>
{
t.HasCheckConstraint("ck_vehicles_vehicle_type", "vehicle_type IN ('bus', 'train', 'aircraft')");
});
b.HasDiscriminator().HasValue("train");
});
modelBuilder.Entity("cuqmbr.TravelGuide.Domain.Entities.Address", b =>
{
b.HasOne("cuqmbr.TravelGuide.Domain.Entities.City", "City")

Some files were not shown because too many files have changed in this diff Show More