feat: add complex route addition and retrieval

This commit is contained in:
cuqmbr 2022-11-30 11:06:57 +02:00
parent a91af2f42a
commit 280d883213
29 changed files with 1523 additions and 112 deletions

View File

@ -31,16 +31,20 @@ public class MapperInitializer : Profile
CreateMap<Address, CreateAddressDto>().ReverseMap();
CreateMap<Address, UpdateAddressDto>().ReverseMap();
CreateMap<Address, InCityAddressDto>().ReverseMap();
CreateMap<Address, CreateAddressInRouteAddress>().ReverseMap();
CreateMap<Address, AddressInRouteAddress>().ReverseMap();
CreateMap<RouteAddress, RouteAddressDto>().ReverseMap();
CreateMap<RouteAddress, CreateRouteAddressDto>().ReverseMap();
CreateMap<RouteAddress, UpdateRouteAddressDto>().ReverseMap();
CreateMap<RouteAddress, CreateRouteAddressWithAddressDto>().ReverseMap();
CreateMap<RouteAddress, RouteAddressWithAddressDto>().ReverseMap();
CreateMap<Route, RouteDto>().ReverseMap();
CreateMap<Route, CreateRouteDto>().ReverseMap();
CreateMap<Route, UpdateRouteDto>().ReverseMap();
CreateMap<Route, CreateRouteWithAddressesDto>().ReverseMap();
CreateMap<Route, RouteWithAddressesDto>().ReverseMap();

View File

@ -30,6 +30,19 @@ public class RouteController : ControllerBase
return CreatedAtAction(nameof(GetRoute), new {id = result.route.Id}, result.route);
}
[HttpPost("withAddresses")]
public async Task<IActionResult> AddRouteWithAddresses(CreateRouteWithAddressesDto route)
{
var result = await _routeManagementService.AddRouteWithAddresses(route);
if (!result.isSucceed)
{
return result.actionResult;
}
return CreatedAtAction(nameof(GetRoute), new {id = result.route.Id}, result.route);
}
[HttpGet]
public async Task<IActionResult> GetRoutes([FromQuery] RouteParameters parameters)
{
@ -45,6 +58,21 @@ public class RouteController : ControllerBase
return Ok(result.routes);
}
[HttpGet("withAddresses")]
public async Task<IActionResult> GetRouteWithAddresses([FromQuery] RouteWithAddressesParameters parameters)
{
var result = await _routeManagementService.GetRoutesWithAddresses(parameters);
if (!result.isSucceed)
{
return result.actionResult;
}
Response.Headers.Add("X-Pagination", JsonConvert.SerializeObject(result.pagingMetadata));
return Ok(result.routes);
}
[HttpGet("{id}")]
public async Task<IActionResult> GetRoute(int id, [FromQuery] string? fields)
{
@ -62,6 +90,24 @@ public class RouteController : ControllerBase
return Ok(result.route);
}
[HttpGet("withAddresses/{id}")]
public async Task<IActionResult> GetRouteWithAddresses(int id, [FromQuery] string? fields)
{
if (!await _routeManagementService.IsRouteExists(id))
{
return NotFound();
}
var result = await _routeManagementService.GetRouteWithAddresses(id, fields);
if (!result.isSucceed)
{
return BadRequest(result.message);
}
return Ok(result.route);
}
[HttpPut("{id}")]
public async Task<IActionResult> UpdateRoute(int id, UpdateRouteDto route)

View File

@ -18,6 +18,8 @@ public class ApplicationDbContext : IdentityDbContext<User>
public DbSet<Route> Routes { get; set; } = null!;
public DbSet<RouteAddress> RouteAddresses { get; set; } = null!;
public DbSet<Address> Addresses { get; set; } = null!;
public DbSet<RouteAddressDetails> RouteAddressDetails { get; set; } = null!;
public DbSet<City> Cities { get; set; } = null!;
public DbSet<State> States { get; set; } = null!;
public DbSet<Country> Countries { get; set; } = null!;

8
Server/Helpers/IPager.cs Normal file
View File

@ -0,0 +1,8 @@
using SharedModels.QueryParameters;
namespace Server.Helpers;
public interface IPager<T>
{
PagingMetadata<T> ApplyPaging(ref IQueryable<T> obj, int pageNumber, int pageSize);
}

19
Server/Helpers/Pager.cs Normal file
View File

@ -0,0 +1,19 @@
using SharedModels.QueryParameters;
namespace Server.Helpers;
public class Pager<T> : IPager<T>
{
public PagingMetadata<T> ApplyPaging(ref IQueryable<T> obj,
int pageNumber, int pageSize)
{
var metadata = new PagingMetadata<T>(obj,
pageNumber, pageSize);
obj = obj
.Skip((pageNumber - 1) * pageSize)
.Take(pageSize);
return metadata;
}
}

View File

@ -1,3 +1,4 @@
using System.Dynamic;
using System.Linq.Dynamic.Core;
using System.Reflection;
using System.Text;
@ -14,8 +15,9 @@ public class SortHelper<T> : ISortHelper<T>
}
var orderParams = orderByQueryString.Trim().Split(",");
var propertyInfos =
typeof(T).GetProperties(BindingFlags.Public | BindingFlags.Instance);
var propertyStrings = typeof(T) == typeof(ExpandoObject) ?
(entities.First() as ExpandoObject).ToDictionary(o => o.Key, o => o.Value).Keys.ToList() :
typeof(T).GetProperties(BindingFlags.Public | BindingFlags.Instance).ToList().ConvertAll(o => o.GetType().ToString());
var orderQueryBuilder = new StringBuilder();
foreach (var param in orderParams)
@ -26,8 +28,8 @@ public class SortHelper<T> : ISortHelper<T>
}
var propertyFromQueryName = param[0] == '-' || param[0] == '+' ? param.Substring(1) : param;
var objectProperty = propertyInfos.FirstOrDefault(pi =>
pi.Name.Equals(propertyFromQueryName, StringComparison.InvariantCultureIgnoreCase));
var objectProperty = propertyStrings.FirstOrDefault(ps =>
ps.Equals(propertyFromQueryName, StringComparison.InvariantCultureIgnoreCase));
if (objectProperty == null)
{
@ -36,11 +38,13 @@ public class SortHelper<T> : ISortHelper<T>
var sortingOrder = param[0] == '-' ? "descending" : "ascending";
orderQueryBuilder.Append($"{objectProperty.Name} {sortingOrder}, ");
orderQueryBuilder.Append($"{objectProperty} {sortingOrder}, ");
}
var orderQuery = orderQueryBuilder.ToString().TrimEnd(',', ' ');
return entities.OrderBy(orderQuery);
return typeof(T) == typeof(ExpandoObject) ?
entities.Cast<dynamic>().OrderBy(orderQuery).Cast<T>() :
entities.OrderBy(orderQuery);
}
}

View File

@ -0,0 +1,915 @@
// <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 Server.Data;
#nullable disable
namespace Server.Migrations
{
[DbContext(typeof(ApplicationDbContext))]
[Migration("20221129160345_Decouple_RouteAddress_and_RouteAddressDetails")]
partial class Decouple_RouteAddress_and_RouteAddressDetails
{
protected override void BuildTargetModel(ModelBuilder modelBuilder)
{
#pragma warning disable 612, 618
modelBuilder
.HasAnnotation("ProductVersion", "6.0.9")
.HasAnnotation("Relational:MaxIdentifierLength", 63);
NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder);
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRole", b =>
{
b.Property<string>("Id")
.HasColumnType("text");
b.Property<string>("ConcurrencyStamp")
.IsConcurrencyToken()
.HasColumnType("text");
b.Property<string>("Name")
.HasMaxLength(256)
.HasColumnType("character varying(256)");
b.Property<string>("NormalizedName")
.HasMaxLength(256)
.HasColumnType("character varying(256)");
b.HasKey("Id");
b.HasIndex("NormalizedName")
.IsUnique()
.HasDatabaseName("RoleNameIndex");
b.ToTable("AspNetRoles", (string)null);
});
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim<string>", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("integer");
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id"));
b.Property<string>("ClaimType")
.HasColumnType("text");
b.Property<string>("ClaimValue")
.HasColumnType("text");
b.Property<string>("RoleId")
.IsRequired()
.HasColumnType("text");
b.HasKey("Id");
b.HasIndex("RoleId");
b.ToTable("AspNetRoleClaims", (string)null);
});
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim<string>", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("integer");
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id"));
b.Property<string>("ClaimType")
.HasColumnType("text");
b.Property<string>("ClaimValue")
.HasColumnType("text");
b.Property<string>("UserId")
.IsRequired()
.HasColumnType("text");
b.HasKey("Id");
b.HasIndex("UserId");
b.ToTable("AspNetUserClaims", (string)null);
});
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin<string>", b =>
{
b.Property<string>("LoginProvider")
.HasColumnType("text");
b.Property<string>("ProviderKey")
.HasColumnType("text");
b.Property<string>("ProviderDisplayName")
.HasColumnType("text");
b.Property<string>("UserId")
.IsRequired()
.HasColumnType("text");
b.HasKey("LoginProvider", "ProviderKey");
b.HasIndex("UserId");
b.ToTable("AspNetUserLogins", (string)null);
});
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserRole<string>", b =>
{
b.Property<string>("UserId")
.HasColumnType("text");
b.Property<string>("RoleId")
.HasColumnType("text");
b.HasKey("UserId", "RoleId");
b.HasIndex("RoleId");
b.ToTable("AspNetUserRoles", (string)null);
});
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken<string>", b =>
{
b.Property<string>("UserId")
.HasColumnType("text");
b.Property<string>("LoginProvider")
.HasColumnType("text");
b.Property<string>("Name")
.HasColumnType("text");
b.Property<string>("Value")
.HasColumnType("text");
b.HasKey("UserId", "LoginProvider", "Name");
b.ToTable("AspNetUserTokens", (string)null);
});
modelBuilder.Entity("Server.Models.Address", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("integer");
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id"));
b.Property<int>("CityId")
.HasColumnType("integer");
b.Property<double>("Latitude")
.HasColumnType("double precision");
b.Property<double>("Longitude")
.HasColumnType("double precision");
b.Property<string>("Name")
.IsRequired()
.HasColumnType("text");
b.HasKey("Id");
b.HasIndex("CityId");
b.ToTable("Addresses");
});
modelBuilder.Entity("Server.Models.City", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("integer");
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id"));
b.Property<string>("Name")
.IsRequired()
.HasColumnType("text");
b.Property<int>("StateId")
.HasColumnType("integer");
b.HasKey("Id");
b.HasIndex("StateId");
b.ToTable("Cities");
});
modelBuilder.Entity("Server.Models.Company", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("integer");
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id"));
b.Property<string>("Name")
.IsRequired()
.HasColumnType("text");
b.Property<string>("OwnerId")
.IsRequired()
.HasColumnType("text");
b.HasKey("Id");
b.HasIndex("OwnerId");
b.ToTable("Companies");
});
modelBuilder.Entity("Server.Models.Country", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("integer");
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id"));
b.Property<string>("Code")
.IsRequired()
.HasColumnType("text");
b.Property<string>("Name")
.IsRequired()
.HasColumnType("text");
b.HasKey("Id");
b.ToTable("Countries");
});
modelBuilder.Entity("Server.Models.Review", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("integer");
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id"));
b.Property<string>("Comment")
.HasColumnType("text");
b.Property<DateTime>("PostDateTimeUtc")
.HasColumnType("timestamp with time zone");
b.Property<int>("Rating")
.HasColumnType("integer");
b.Property<string>("UserId")
.IsRequired()
.HasColumnType("text");
b.Property<int>("VehicleEnrollmentId")
.HasColumnType("integer");
b.HasKey("Id");
b.HasIndex("UserId");
b.HasIndex("VehicleEnrollmentId");
b.ToTable("Reviews");
});
modelBuilder.Entity("Server.Models.Route", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("integer");
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id"));
b.Property<string>("Type")
.IsRequired()
.HasColumnType("text");
b.HasKey("Id");
b.ToTable("Routes");
});
modelBuilder.Entity("Server.Models.RouteAddress", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("integer");
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id"));
b.Property<int>("AddressId")
.HasColumnType("integer");
b.Property<int>("Order")
.HasColumnType("integer");
b.Property<int>("RouteAddressDetailsId")
.HasColumnType("integer");
b.Property<int>("RouteId")
.HasColumnType("integer");
b.HasKey("Id");
b.HasIndex("AddressId");
b.HasIndex("RouteId");
b.ToTable("RouteAddresses");
});
modelBuilder.Entity("Server.Models.RouteAddressDetails", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("integer");
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id"));
b.Property<double>("CostToNextCity")
.HasColumnType("double precision");
b.Property<int>("RouteAddressId")
.HasColumnType("integer");
b.Property<TimeSpan>("TimeSpanToNextCity")
.HasColumnType("interval");
b.Property<int>("VehicleEnrollmentId")
.HasColumnType("integer");
b.Property<TimeSpan>("WaitTimeSpan")
.HasColumnType("interval");
b.HasKey("Id");
b.HasIndex("RouteAddressId");
b.HasIndex("VehicleEnrollmentId");
b.ToTable("RouteAddressDetails");
});
modelBuilder.Entity("Server.Models.State", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("integer");
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id"));
b.Property<int>("CountryId")
.HasColumnType("integer");
b.Property<string>("Name")
.IsRequired()
.HasColumnType("text");
b.HasKey("Id");
b.HasIndex("CountryId");
b.ToTable("States");
});
modelBuilder.Entity("Server.Models.Ticket", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("integer");
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id"));
b.Property<int>("FirstRouteAddressId")
.HasColumnType("integer");
b.Property<bool>("IsMissed")
.HasColumnType("boolean");
b.Property<bool>("IsReturned")
.HasColumnType("boolean");
b.Property<int>("LastRouteAddressId")
.HasColumnType("integer");
b.Property<DateTime>("PurchaseDateTimeUtc")
.HasColumnType("timestamp with time zone");
b.Property<int>("TicketGroupId")
.HasColumnType("integer");
b.Property<int>("VehicleEnrollmentId")
.HasColumnType("integer");
b.HasKey("Id");
b.HasIndex("TicketGroupId");
b.HasIndex("VehicleEnrollmentId");
b.ToTable("Tickets");
});
modelBuilder.Entity("Server.Models.TicketGroup", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("integer");
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id"));
b.Property<string>("UserId")
.IsRequired()
.HasColumnType("text");
b.HasKey("Id");
b.HasIndex("UserId");
b.ToTable("TicketGroups");
});
modelBuilder.Entity("Server.Models.User", b =>
{
b.Property<string>("Id")
.HasColumnType("text");
b.Property<int>("AccessFailedCount")
.HasColumnType("integer");
b.Property<string>("ConcurrencyStamp")
.IsConcurrencyToken()
.HasColumnType("text");
b.Property<string>("Email")
.HasMaxLength(256)
.HasColumnType("character varying(256)");
b.Property<bool>("EmailConfirmed")
.HasColumnType("boolean");
b.Property<string>("FirstName")
.HasColumnType("text");
b.Property<string>("LastName")
.HasColumnType("text");
b.Property<bool>("LockoutEnabled")
.HasColumnType("boolean");
b.Property<DateTimeOffset?>("LockoutEnd")
.HasColumnType("timestamp with time zone");
b.Property<string>("NormalizedEmail")
.HasMaxLength(256)
.HasColumnType("character varying(256)");
b.Property<string>("NormalizedUserName")
.HasMaxLength(256)
.HasColumnType("character varying(256)");
b.Property<string>("PasswordHash")
.HasColumnType("text");
b.Property<string>("PhoneNumber")
.HasColumnType("text");
b.Property<bool>("PhoneNumberConfirmed")
.HasColumnType("boolean");
b.Property<string>("SecurityStamp")
.HasColumnType("text");
b.Property<bool>("TwoFactorEnabled")
.HasColumnType("boolean");
b.Property<string>("UserName")
.HasMaxLength(256)
.HasColumnType("character varying(256)");
b.HasKey("Id");
b.HasIndex("NormalizedEmail")
.HasDatabaseName("EmailIndex");
b.HasIndex("NormalizedUserName")
.IsUnique()
.HasDatabaseName("UserNameIndex");
b.ToTable("AspNetUsers", (string)null);
});
modelBuilder.Entity("Server.Models.Vehicle", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("integer");
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id"));
b.Property<int>("Capacity")
.HasColumnType("integer");
b.Property<int>("CompanyId")
.HasColumnType("integer");
b.Property<bool>("HasBelts")
.HasColumnType("boolean");
b.Property<bool>("HasClimateControl")
.HasColumnType("boolean");
b.Property<bool>("HasOutlet")
.HasColumnType("boolean");
b.Property<bool>("HasStewardess")
.HasColumnType("boolean");
b.Property<bool>("HasTV")
.HasColumnType("boolean");
b.Property<bool>("HasWC")
.HasColumnType("boolean");
b.Property<bool>("HasWiFi")
.HasColumnType("boolean");
b.Property<string>("Number")
.IsRequired()
.HasColumnType("text");
b.Property<string>("Type")
.IsRequired()
.HasColumnType("text");
b.HasKey("Id");
b.HasIndex("CompanyId");
b.ToTable("Vehicles");
});
modelBuilder.Entity("Server.Models.VehicleEnrollment", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("integer");
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id"));
b.Property<string>("CancelationComment")
.HasColumnType("text");
b.Property<TimeSpan?>("DelayTimeSpan")
.HasColumnType("interval");
b.Property<DateTime>("DepartureDateTimeUtc")
.HasColumnType("timestamp with time zone");
b.Property<bool>("IsCanceled")
.HasColumnType("boolean");
b.Property<int>("RouteAddressDetailsId")
.HasColumnType("integer");
b.Property<int>("RouteId")
.HasColumnType("integer");
b.Property<int>("VehicleId")
.HasColumnType("integer");
b.HasKey("Id");
b.HasIndex("RouteId");
b.HasIndex("VehicleId");
b.ToTable("VehicleEnrollments");
});
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim<string>", b =>
{
b.HasOne("Microsoft.AspNetCore.Identity.IdentityRole", null)
.WithMany()
.HasForeignKey("RoleId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
});
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim<string>", b =>
{
b.HasOne("Server.Models.User", null)
.WithMany()
.HasForeignKey("UserId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
});
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin<string>", b =>
{
b.HasOne("Server.Models.User", null)
.WithMany()
.HasForeignKey("UserId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
});
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserRole<string>", b =>
{
b.HasOne("Microsoft.AspNetCore.Identity.IdentityRole", null)
.WithMany()
.HasForeignKey("RoleId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.HasOne("Server.Models.User", null)
.WithMany()
.HasForeignKey("UserId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
});
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken<string>", b =>
{
b.HasOne("Server.Models.User", null)
.WithMany()
.HasForeignKey("UserId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
});
modelBuilder.Entity("Server.Models.Address", b =>
{
b.HasOne("Server.Models.City", "City")
.WithMany("Addresses")
.HasForeignKey("CityId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("City");
});
modelBuilder.Entity("Server.Models.City", b =>
{
b.HasOne("Server.Models.State", "State")
.WithMany("Cities")
.HasForeignKey("StateId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("State");
});
modelBuilder.Entity("Server.Models.Company", b =>
{
b.HasOne("Server.Models.User", "Owner")
.WithMany()
.HasForeignKey("OwnerId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("Owner");
});
modelBuilder.Entity("Server.Models.Review", b =>
{
b.HasOne("Server.Models.User", "User")
.WithMany("Reviews")
.HasForeignKey("UserId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.HasOne("Server.Models.VehicleEnrollment", "VehicleEnrollment")
.WithMany("Reviews")
.HasForeignKey("VehicleEnrollmentId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("User");
b.Navigation("VehicleEnrollment");
});
modelBuilder.Entity("Server.Models.RouteAddress", b =>
{
b.HasOne("Server.Models.Address", "Address")
.WithMany("RouteAddresses")
.HasForeignKey("AddressId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.HasOne("Server.Models.Route", "Route")
.WithMany("RouteAddresses")
.HasForeignKey("RouteId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("Address");
b.Navigation("Route");
});
modelBuilder.Entity("Server.Models.RouteAddressDetails", b =>
{
b.HasOne("Server.Models.RouteAddress", "RouteAddress")
.WithMany("RouteAddressDetails")
.HasForeignKey("RouteAddressId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.HasOne("Server.Models.VehicleEnrollment", "VehicleEnrollment")
.WithMany("RouteAddressDetails")
.HasForeignKey("VehicleEnrollmentId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("RouteAddress");
b.Navigation("VehicleEnrollment");
});
modelBuilder.Entity("Server.Models.State", b =>
{
b.HasOne("Server.Models.Country", "Country")
.WithMany("States")
.HasForeignKey("CountryId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("Country");
});
modelBuilder.Entity("Server.Models.Ticket", b =>
{
b.HasOne("Server.Models.TicketGroup", "TicketGroup")
.WithMany("Tickets")
.HasForeignKey("TicketGroupId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.HasOne("Server.Models.VehicleEnrollment", "VehicleEnrollment")
.WithMany("Tickets")
.HasForeignKey("VehicleEnrollmentId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("TicketGroup");
b.Navigation("VehicleEnrollment");
});
modelBuilder.Entity("Server.Models.TicketGroup", b =>
{
b.HasOne("Server.Models.User", "User")
.WithMany("TicketGroups")
.HasForeignKey("UserId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("User");
});
modelBuilder.Entity("Server.Models.User", b =>
{
b.OwnsMany("Server.Models.RefreshToken", "RefreshTokens", b1 =>
{
b1.Property<string>("UserId")
.HasColumnType("text");
b1.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("integer");
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b1.Property<int>("Id"));
b1.Property<DateTime>("CreationDateTime")
.HasColumnType("timestamp with time zone");
b1.Property<DateTime>("ExpiryDateTime")
.HasColumnType("timestamp with time zone");
b1.Property<DateTime?>("Revoked")
.HasColumnType("timestamp with time zone");
b1.Property<string>("Token")
.IsRequired()
.HasColumnType("text");
b1.HasKey("UserId", "Id");
b1.ToTable("RefreshToken");
b1.WithOwner()
.HasForeignKey("UserId");
});
b.Navigation("RefreshTokens");
});
modelBuilder.Entity("Server.Models.Vehicle", b =>
{
b.HasOne("Server.Models.Company", "Company")
.WithMany("Vehicles")
.HasForeignKey("CompanyId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("Company");
});
modelBuilder.Entity("Server.Models.VehicleEnrollment", b =>
{
b.HasOne("Server.Models.Route", "Route")
.WithMany("VehicleEnrollments")
.HasForeignKey("RouteId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.HasOne("Server.Models.Vehicle", "Vehicle")
.WithMany("VehicleEnrollments")
.HasForeignKey("VehicleId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("Route");
b.Navigation("Vehicle");
});
modelBuilder.Entity("Server.Models.Address", b =>
{
b.Navigation("RouteAddresses");
});
modelBuilder.Entity("Server.Models.City", b =>
{
b.Navigation("Addresses");
});
modelBuilder.Entity("Server.Models.Company", b =>
{
b.Navigation("Vehicles");
});
modelBuilder.Entity("Server.Models.Country", b =>
{
b.Navigation("States");
});
modelBuilder.Entity("Server.Models.Route", b =>
{
b.Navigation("RouteAddresses");
b.Navigation("VehicleEnrollments");
});
modelBuilder.Entity("Server.Models.RouteAddress", b =>
{
b.Navigation("RouteAddressDetails");
});
modelBuilder.Entity("Server.Models.State", b =>
{
b.Navigation("Cities");
});
modelBuilder.Entity("Server.Models.TicketGroup", b =>
{
b.Navigation("Tickets");
});
modelBuilder.Entity("Server.Models.User", b =>
{
b.Navigation("Reviews");
b.Navigation("TicketGroups");
});
modelBuilder.Entity("Server.Models.Vehicle", b =>
{
b.Navigation("VehicleEnrollments");
});
modelBuilder.Entity("Server.Models.VehicleEnrollment", b =>
{
b.Navigation("Reviews");
b.Navigation("RouteAddressDetails");
b.Navigation("Tickets");
});
#pragma warning restore 612, 618
}
}
}

View File

@ -0,0 +1,114 @@
using System;
using Microsoft.EntityFrameworkCore.Migrations;
using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
#nullable disable
namespace Server.Migrations
{
public partial class Decouple_RouteAddress_and_RouteAddressDetails : Migration
{
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropColumn(
name: "CostToNextCity",
table: "RouteAddresses");
migrationBuilder.DropColumn(
name: "TimeSpanToNextCity",
table: "RouteAddresses");
migrationBuilder.DropColumn(
name: "WaitTimeSpan",
table: "RouteAddresses");
migrationBuilder.AddColumn<int>(
name: "RouteAddressDetailsId",
table: "VehicleEnrollments",
type: "integer",
nullable: false,
defaultValue: 0);
migrationBuilder.AddColumn<int>(
name: "RouteAddressDetailsId",
table: "RouteAddresses",
type: "integer",
nullable: false,
defaultValue: 0);
migrationBuilder.CreateTable(
name: "RouteAddressDetails",
columns: table => new
{
Id = table.Column<int>(type: "integer", nullable: false)
.Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn),
VehicleEnrollmentId = table.Column<int>(type: "integer", nullable: false),
RouteAddressId = table.Column<int>(type: "integer", nullable: false),
TimeSpanToNextCity = table.Column<TimeSpan>(type: "interval", nullable: false),
WaitTimeSpan = table.Column<TimeSpan>(type: "interval", nullable: false),
CostToNextCity = table.Column<double>(type: "double precision", nullable: false)
},
constraints: table =>
{
table.PrimaryKey("PK_RouteAddressDetails", x => x.Id);
table.ForeignKey(
name: "FK_RouteAddressDetails_RouteAddresses_RouteAddressId",
column: x => x.RouteAddressId,
principalTable: "RouteAddresses",
principalColumn: "Id",
onDelete: ReferentialAction.Cascade);
table.ForeignKey(
name: "FK_RouteAddressDetails_VehicleEnrollments_VehicleEnrollmentId",
column: x => x.VehicleEnrollmentId,
principalTable: "VehicleEnrollments",
principalColumn: "Id",
onDelete: ReferentialAction.Cascade);
});
migrationBuilder.CreateIndex(
name: "IX_RouteAddressDetails_RouteAddressId",
table: "RouteAddressDetails",
column: "RouteAddressId");
migrationBuilder.CreateIndex(
name: "IX_RouteAddressDetails_VehicleEnrollmentId",
table: "RouteAddressDetails",
column: "VehicleEnrollmentId");
}
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropTable(
name: "RouteAddressDetails");
migrationBuilder.DropColumn(
name: "RouteAddressDetailsId",
table: "VehicleEnrollments");
migrationBuilder.DropColumn(
name: "RouteAddressDetailsId",
table: "RouteAddresses");
migrationBuilder.AddColumn<double>(
name: "CostToNextCity",
table: "RouteAddresses",
type: "double precision",
nullable: false,
defaultValue: 0.0);
migrationBuilder.AddColumn<TimeSpan>(
name: "TimeSpanToNextCity",
table: "RouteAddresses",
type: "interval",
nullable: false,
defaultValue: new TimeSpan(0, 0, 0, 0, 0));
migrationBuilder.AddColumn<TimeSpan>(
name: "WaitTimeSpan",
table: "RouteAddresses",
type: "interval",
nullable: false,
defaultValue: new TimeSpan(0, 0, 0, 0, 0));
}
}
}

View File

@ -309,21 +309,15 @@ namespace Server.Migrations
b.Property<int>("AddressId")
.HasColumnType("integer");
b.Property<double>("CostToNextCity")
.HasColumnType("double precision");
b.Property<int>("Order")
.HasColumnType("integer");
b.Property<int>("RouteAddressDetailsId")
.HasColumnType("integer");
b.Property<int>("RouteId")
.HasColumnType("integer");
b.Property<TimeSpan>("TimeSpanToNextCity")
.HasColumnType("interval");
b.Property<TimeSpan>("WaitTimeSpan")
.HasColumnType("interval");
b.HasKey("Id");
b.HasIndex("AddressId");
@ -333,6 +327,38 @@ namespace Server.Migrations
b.ToTable("RouteAddresses");
});
modelBuilder.Entity("Server.Models.RouteAddressDetails", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("integer");
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id"));
b.Property<double>("CostToNextCity")
.HasColumnType("double precision");
b.Property<int>("RouteAddressId")
.HasColumnType("integer");
b.Property<TimeSpan>("TimeSpanToNextCity")
.HasColumnType("interval");
b.Property<int>("VehicleEnrollmentId")
.HasColumnType("integer");
b.Property<TimeSpan>("WaitTimeSpan")
.HasColumnType("interval");
b.HasKey("Id");
b.HasIndex("RouteAddressId");
b.HasIndex("VehicleEnrollmentId");
b.ToTable("RouteAddressDetails");
});
modelBuilder.Entity("Server.Models.State", b =>
{
b.Property<int>("Id")
@ -552,6 +578,9 @@ namespace Server.Migrations
b.Property<bool>("IsCanceled")
.HasColumnType("boolean");
b.Property<int>("RouteAddressDetailsId")
.HasColumnType("integer");
b.Property<int>("RouteId")
.HasColumnType("integer");
@ -689,6 +718,25 @@ namespace Server.Migrations
b.Navigation("Route");
});
modelBuilder.Entity("Server.Models.RouteAddressDetails", b =>
{
b.HasOne("Server.Models.RouteAddress", "RouteAddress")
.WithMany("RouteAddressDetails")
.HasForeignKey("RouteAddressId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.HasOne("Server.Models.VehicleEnrollment", "VehicleEnrollment")
.WithMany("RouteAddressDetails")
.HasForeignKey("VehicleEnrollmentId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("RouteAddress");
b.Navigation("VehicleEnrollment");
});
modelBuilder.Entity("Server.Models.State", b =>
{
b.HasOne("Server.Models.Country", "Country")
@ -824,6 +872,11 @@ namespace Server.Migrations
b.Navigation("VehicleEnrollments");
});
modelBuilder.Entity("Server.Models.RouteAddress", b =>
{
b.Navigation("RouteAddressDetails");
});
modelBuilder.Entity("Server.Models.State", b =>
{
b.Navigation("Cities");
@ -850,6 +903,8 @@ namespace Server.Migrations
{
b.Navigation("Reviews");
b.Navigation("RouteAddressDetails");
b.Navigation("Tickets");
});
#pragma warning restore 612, 618

View File

@ -10,18 +10,25 @@ public class Address
public int Id { get; set; }
public string Name { get; set; } = null!;
public double Latitude { get; set; }
public double Longitude { get; set; }
[ForeignKey("CityId")]
public int CityId { get; set; }
public City? City { get; set; }
public City City { get; set; } = null!;
public virtual IList<RouteAddress> RouteAddresses { get; set; } = null!;
public override string ToString()
public string GetFullName()
{
return $"{City.State.Country.Name}, {City.State.Name}, " +
$"{City.Name}, {this.Name}";
if (City == null || City.State == null || City.State.Country == null)
{
throw new NullReferenceException(
$"Properties {nameof(City)}, {nameof(City.State)}, " +
$"{nameof(City.State.Country)} must not be null");
}
return $"{City.GetFullName()}, {Name}";
}
}

View File

@ -10,10 +10,23 @@ public class City
public int Id { get; set; }
public string Name { get; set; } = null!;
public virtual IList<Address>? Addresses { get; set; }
[ForeignKey("StateId")]
public int StateId { get; set; }
public State? State { get; set; }
public State State { get; set; } = null!;
public string GetFullName()
{
if (State == null || State.Country == null)
{
throw new NullReferenceException(
$"Properties {nameof(State)}, " +
$"{nameof(State.Country)} must not be null");
}
return $"{State.GetFullName()}, {Name}";
}
}

View File

@ -1,4 +1,5 @@
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;
using SharedModels.DataTransferObjects;
namespace Server.Models;
@ -12,4 +13,9 @@ public class Country
public string Name { get; set; } = null!;
public virtual IList<State> States { get; set; } = null!;
public string GetFullName()
{
return $"{Name}";
}
}

View File

@ -16,8 +16,9 @@ public class RouteAddress
public int AddressId { get; set; }
public Address Address { get; set; } = null!;
[ForeignKey("RouteAddressDetailsId")]
public int RouteAddressDetailsId { get; set; }
public virtual IList<RouteAddressDetails> RouteAddressDetails { get; set; } = null!;
public int Order { get; set; }
public TimeSpan TimeSpanToNextCity { get; set; }
public TimeSpan WaitTimeSpan { get; set; }
public double CostToNextCity { get; set; }
}

View File

@ -0,0 +1,22 @@
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;
namespace Server.Models;
public class RouteAddressDetails
{
[Key]
public int Id { get; set; }
[ForeignKey("VehicleEnrollmentId")]
public int VehicleEnrollmentId { get; set; }
public VehicleEnrollment VehicleEnrollment { get; set; } = null!;
[ForeignKey("RouteAddressId")]
public int RouteAddressId { get; set; }
public RouteAddress RouteAddress { get; set; } = null!;
public TimeSpan TimeSpanToNextCity { get; set; }
public TimeSpan WaitTimeSpan { get; set; }
public double CostToNextCity { get; set; }
}

View File

@ -10,10 +10,20 @@ public class State
public int Id { get; set; }
public string Name { get; set; } = null!;
public virtual IList<City> Cities { get; set; } = null!;
[ForeignKey("CountryId")]
public int CountryId { get; set; }
public Country? Country { get; set; } = null!;
public Country Country { get; set; } = null!;
public string GetFullName()
{
if (Country == null)
{
throw new NullReferenceException($"Property {nameof(Country)} must not be null");
}
return $"{Country.GetFullName()}, {Name}";
}
}

View File

@ -16,6 +16,10 @@ public class VehicleEnrollment
public int RouteId { get; set; }
public Route Route { get; set; } = null!;
[ForeignKey("RouteAddressDetailsId")]
public int RouteAddressDetailsId { get; set; }
public virtual IList<RouteAddressDetails> RouteAddressDetails { get; set; } = null!;
public DateTime DepartureDateTimeUtc { get; set; }
public TimeSpan? DelayTimeSpan { get; set; }

View File

@ -1,3 +1,4 @@
using System.Dynamic;
using System.Text;
using Microsoft.AspNetCore.Authentication.JwtBearer;
using Microsoft.AspNetCore.Identity;
@ -105,7 +106,7 @@ builder.Services.AddScoped<IRouteManagementService, RouteManagementService>();
builder.Services.AddScoped<IRouteAddressManagementService, RouteAddressManagementService>();
builder.Services.AddScoped<IStatisticsService, StatisticsService>();
builder.Services.AddScoped<IReportService, ReportService>();
builder.Services.AddScoped<ISortHelper<Country>, SortHelper<Country>>();
builder.Services.AddScoped<ISortHelper<State>, SortHelper<State>>();
@ -132,9 +133,15 @@ builder.Services.AddScoped<IDataShaper<VehicleEnrollment>, DataShaper<VehicleEnr
builder.Services.AddScoped<IDataShaper<Route>, DataShaper<Route>>();
builder.Services.AddScoped<IDataShaper<RouteAddress>, DataShaper<RouteAddress>>();
builder.Services.AddScoped<ISortHelper<ExpandoObject>, SortHelper<ExpandoObject>>();
builder.Services.AddScoped<IDataShaper<UserDto>, DataShaper<UserDto>>();
builder.Services.AddScoped<IDataShaper<CompanyDto>, DataShaper<CompanyDto>>();
builder.Services.AddScoped<IDataShaper<AddressDto>, DataShaper<AddressDto>>();
builder.Services.AddScoped<IDataShaper<RouteDto>, DataShaper<RouteDto>>();
builder.Services.AddScoped<IDataShaper<RouteWithAddressesDto>, DataShaper<RouteWithAddressesDto>>();
builder.Services.AddScoped<IPager<ExpandoObject>, Pager<ExpandoObject>>();
// Adding DB Context with PostgreSQL
var connectionString = builder.Configuration.GetConnectionString("DefaultConnection");

View File

@ -22,8 +22,12 @@
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
<PackageReference Include="Microsoft.VisualStudio.Web.CodeGeneration.Design" Version="6.0.10" />
<PackageReference Include="MigraDocCore.DocumentObjectModel" Version="1.3.41" />
<PackageReference Include="MigraDocCore.Rendering" Version="1.3.41" />
<PackageReference Include="Npgsql.EntityFrameworkCore.PostgreSQL" Version="6.0.7" />
<PackageReference Include="PdfSharpCore" Version="1.3.41" />
<PackageReference Include="Swashbuckle.AspNetCore" Version="6.4.0" />
<PackageReference Include="System.Drawing.Common" Version="6.0.0" />
<PackageReference Include="System.IdentityModel.Tokens.Jwt" Version="6.23.1" />
<PackageReference Include="System.Linq.Dynamic.Core" Version="1.2.20" />
</ItemGroup>

View File

@ -1,3 +1,5 @@
using System.Dynamic;
using Microsoft.AspNetCore.Mvc;
using SharedModels.DataTransferObjects;
using SharedModels.QueryParameters;
using SharedModels.QueryParameters.Objects;
@ -8,9 +10,13 @@ namespace Server.Services;
public interface IRouteManagementService
{
Task<(bool isSucceed, string message, RouteDto route)> AddRoute(CreateRouteDto createRouteDto);
Task<(bool isSucceed, string message, IEnumerable<RouteDto> routes,
PagingMetadata<Route> pagingMetadata)> GetRoutes(RouteParameters parameters);
Task<(bool isSucceed, string message, RouteDto route)> GetRoute(int id, string? fields);
Task<(bool isSucceed, IActionResult? actionResult, RouteWithAddressesDto route)> AddRouteWithAddresses(CreateRouteWithAddressesDto createRouteWithAddressesDto);
Task<(bool isSucceed, string message, IEnumerable<ExpandoObject> routes,
PagingMetadata<ExpandoObject> pagingMetadata)> GetRoutes(RouteParameters parameters);
Task<(bool isSucceed, IActionResult? actionResult, IEnumerable<ExpandoObject> routes,
PagingMetadata<ExpandoObject> pagingMetadata)> GetRoutesWithAddresses(RouteWithAddressesParameters parameters);
Task<(bool isSucceed, string message, ExpandoObject route)> GetRoute(int id, string? fields);
Task<(bool isSucceed, string message, ExpandoObject route)> GetRouteWithAddresses(int id, string? fields);
Task<(bool isSucceed, string message, UpdateRouteDto route)> UpdateRoute(UpdateRouteDto updateRouteDto);
Task<(bool isSucceed, string message)> DeleteRoute(int id);
Task<bool> IsRouteExists(int id);

View File

@ -26,6 +26,7 @@ public class ReportService : IReportService
.ThenInclude(t => t.VehicleEnrollment)
.ThenInclude(ve => ve.Vehicle)
.ThenInclude(v => v.Company)
.Include(tg => tg.User)
.Include(tg => tg.Tickets)
.ThenInclude(t => t.VehicleEnrollment)
@ -35,6 +36,12 @@ public class ReportService : IReportService
.ThenInclude(a => a.City)
.ThenInclude(c => c.State)
.ThenInclude(s => s.Country)
.Include(tg => tg.User)
.Include(tg => tg.Tickets)
.ThenInclude(t => t.VehicleEnrollment)
.ThenInclude(ve => ve.RouteAddressDetails)
.FirstOrDefaultAsync(tg => tg.Id == ticketGroupId);
// Define document
@ -374,13 +381,16 @@ public class ReportService : IReportService
foreach (var routeAddress in routeAddresses)
{
var details = routeAddress.RouteAddressDetails
.First(rad => rad.RouteAddressId == routeAddress.Id);
if (routeAddress.AddressId == ticket.FirstRouteAddressId)
{
break;
}
departureDateTimeUtc += routeAddress.TimeSpanToNextCity;
departureDateTimeUtc += routeAddress.WaitTimeSpan;
departureDateTimeUtc += details.TimeSpanToNextCity;
departureDateTimeUtc += details.WaitTimeSpan;
}
return departureDateTimeUtc;
@ -395,12 +405,15 @@ public class ReportService : IReportService
foreach (var routeAddress in routeAddresses)
{
var details = routeAddress.RouteAddressDetails
.First(rad => rad.RouteAddressId == routeAddress.Id);
if (routeAddress.AddressId == ticket.LastRouteAddressId)
{
break;
}
arrivalDateTimeUtc += routeAddress.TimeSpanToNextCity;
arrivalDateTimeUtc += details.TimeSpanToNextCity;
}
return arrivalDateTimeUtc;
@ -432,7 +445,10 @@ public class ReportService : IReportService
foreach (var routeAddress in routeAddresses)
{
cost += routeAddress.CostToNextCity;
var details = routeAddress.RouteAddressDetails
.First(rad => rad.RouteAddressId == routeAddress.Id);
cost += details.CostToNextCity;
}
return cost;

View File

@ -1,4 +1,7 @@
using System.Dynamic;
using AutoMapper;
using AutoMapper.QueryableExtensions;
using Microsoft.AspNetCore.Mvc;
using Microsoft.EntityFrameworkCore;
using Server.Data;
using Server.Helpers;
@ -13,17 +16,23 @@ public class RouteManagementService : IRouteManagementService
{
private readonly ApplicationDbContext _dbContext;
private readonly IMapper _mapper;
private readonly ISortHelper<Route> _routeSortHelper;
private readonly IDataShaper<Route> _routeDataShaper;
private readonly ISortHelper<ExpandoObject> _routeSortHelper;
private readonly IDataShaper<RouteDto> _routeDataShaper;
private readonly IDataShaper<RouteWithAddressesDto> _routeWithAddressesDataShaper;
private readonly IPager<ExpandoObject> _pager;
public RouteManagementService(ApplicationDbContext dbContext,
IMapper mapper, ISortHelper<Route> routeSortHelper,
IDataShaper<Route> routeDataShaper)
IMapper mapper, ISortHelper<ExpandoObject> routeSortHelper,
IDataShaper<RouteDto> routeDataShaper,
IDataShaper<RouteWithAddressesDto> routeWithAddressesDataShaper,
IPager<ExpandoObject> pager)
{
_dbContext = dbContext;
_mapper = mapper;
_routeSortHelper = routeSortHelper;
_routeDataShaper = routeDataShaper;
_routeWithAddressesDataShaper = routeWithAddressesDataShaper;
_pager = pager;
}
public async Task<(bool isSucceed, string message, RouteDto route)> AddRoute(CreateRouteDto createRouteDto)
@ -36,8 +45,31 @@ public class RouteManagementService : IRouteManagementService
return (true, String.Empty, _mapper.Map<RouteDto>(route));
}
public async Task<(bool isSucceed, string message, IEnumerable<RouteDto> routes,
PagingMetadata<Route> pagingMetadata)> GetRoutes(RouteParameters parameters)
public async Task<(bool isSucceed, IActionResult? actionResult, RouteWithAddressesDto route)> AddRouteWithAddresses(CreateRouteWithAddressesDto createRouteWithAddressesDto)
{
var route = _mapper.Map<Route>(createRouteWithAddressesDto);
foreach (var routeAddress in route.RouteAddresses)
{
var dbAddress = await _dbContext.Addresses
.FirstOrDefaultAsync(a => a.Id == routeAddress.AddressId);
if (dbAddress == null)
{
return (false, new BadRequestObjectResult($"Address with Id = {routeAddress.AddressId} doesn't exist"), null!);
}
routeAddress.Address = dbAddress;
}
await _dbContext.Routes.AddAsync(route);
await _dbContext.SaveChangesAsync();
return (true, null, _mapper.Map<RouteWithAddressesDto>(route));
}
public async Task<(bool isSucceed, string message, IEnumerable<ExpandoObject> routes,
PagingMetadata<ExpandoObject> pagingMetadata)> GetRoutes(RouteParameters parameters)
{
var dbRoutes = _dbContext.Routes
.AsQueryable();
@ -45,9 +77,12 @@ public class RouteManagementService : IRouteManagementService
SearchByAllRouteFields(ref dbRoutes, parameters.Search);
FilterByRouteType(ref dbRoutes, parameters.Type);
var routeDtos = _mapper.ProjectTo<RouteDto>(dbRoutes);
var shapedData = _routeDataShaper.ShapeData(routeDtos, parameters.Fields).AsQueryable();
try
{
dbRoutes = _routeSortHelper.ApplySort(dbRoutes, parameters.Sort);
shapedData = _routeSortHelper.ApplySort(shapedData, parameters.Sort);
// By calling Any() we will check if LINQ to Entities Query will be
// executed. If not it will throw an InvalidOperationException exception
@ -57,14 +92,11 @@ public class RouteManagementService : IRouteManagementService
{
return (false, "Invalid sorting string", null, null)!;
}
var pagingMetadata = ApplyPaging(ref dbRoutes, parameters.PageNumber,
parameters.PageSize);
var shapedRoutesData = _routeDataShaper.ShapeData(dbRoutes, parameters.Fields);
var routeDtos = shapedRoutesData.ToList().ConvertAll(r => _mapper.Map<RouteDto>(r));
return (true, "", routeDtos, pagingMetadata);
var pagingMetadata = _pager.ApplyPaging(ref shapedData, parameters.PageNumber,
parameters.PageSize);
return (true, "", shapedData, pagingMetadata);
void SearchByAllRouteFields(ref IQueryable<Route> route,
string? search)
@ -88,23 +120,100 @@ public class RouteManagementService : IRouteManagementService
routes = routes.Where(r => r.Type == type);
}
}
public async Task<(bool isSucceed, IActionResult? actionResult, IEnumerable<ExpandoObject> routes,
PagingMetadata<ExpandoObject> pagingMetadata)> GetRoutesWithAddresses(RouteWithAddressesParameters parameters)
{
var dbRoutes = _dbContext.Routes
.Include(r => r.RouteAddresses.OrderBy(ra => ra.Order))
.ThenInclude(ra => ra.Address).ThenInclude(a => a.City)
.ThenInclude(c => c.State).ThenInclude(s => s.Country)
.AsQueryable();
SearchByAllRouteFields(ref dbRoutes, parameters.Search);
FilterByRouteType(ref dbRoutes, parameters.Type);
FilterByFromAddressName(ref dbRoutes, parameters.FromAddressName);
FilterByToAddressName(ref dbRoutes, parameters.ToAddressName);
var routeDtos = _mapper.ProjectTo<RouteWithAddressesDto>(dbRoutes);
var shapedData = _routeWithAddressesDataShaper.ShapeData(routeDtos, parameters.Fields).AsQueryable();
PagingMetadata<Route> ApplyPaging(ref IQueryable<Route> routes,
int pageNumber, int pageSize)
try
{
var metadata = new PagingMetadata<Route>(routes,
pageNumber, pageSize);
routes = routes
.Skip((pageNumber - 1) * pageSize)
.Take(pageSize);
shapedData = _routeSortHelper.ApplySort(shapedData, parameters.Sort);
}
catch (Exception e)
{
return (false, new BadRequestObjectResult("Invalid sorting string"), null, null)!;
}
var pagingMetadata = _pager.ApplyPaging(ref shapedData, parameters.PageNumber,
parameters.PageSize);
return metadata;
return (true, null, shapedData, pagingMetadata);
void SearchByAllRouteFields(ref IQueryable<Route> route,
string? search)
{
if (!route.Any() || String.IsNullOrWhiteSpace(search))
{
return;
}
// TODO Optimize (remove client evaluation)
route = route.ToArray().Where(r =>
r.Type.ToLower().Contains(search.ToLower()) ||
r.RouteAddresses.OrderBy(ra => ra.Order).First().Address
.GetFullName().ToLower().Contains(search.ToLower()) ||
r.RouteAddresses.OrderBy(ra => ra.Order).Last().Address
.GetFullName().ToLower().Contains(search.ToLower()))
.AsQueryable();
}
void FilterByRouteType(ref IQueryable<Route> routes,
string? type)
{
if (!routes.Any() || String.IsNullOrWhiteSpace(type))
{
return;
}
routes = routes.Where(r => r.Type.ToLower().Contains(type.ToLower()));
}
void FilterByFromAddressName(ref IQueryable<Route> routes,
string? addressName)
{
if (!routes.Any() || String.IsNullOrWhiteSpace(addressName))
{
return;
}
// TODO Optimize (remove client evaluation)
routes = routes.ToArray().Where(r =>
r.RouteAddresses.First().Address
.GetFullName().ToLower().Contains(addressName.ToLower()))
.AsQueryable();
}
void FilterByToAddressName(ref IQueryable<Route> routes,
string? addressName)
{
if (!routes.Any() || String.IsNullOrWhiteSpace(addressName))
{
return;
}
// TODO Optimize (remove client evaluation)
routes = routes.ToArray().Where(r =>
r.RouteAddresses.Last().Address.
GetFullName().ToLower().Contains(addressName.ToLower()))
.AsQueryable();
}
}
public async Task<(bool isSucceed, string message, RouteDto route)> GetRoute(int id, string? fields)
public async Task<(bool isSucceed, string message, ExpandoObject route)> GetRoute(int id, string? fields)
{
var dbRoute = await _dbContext.Routes.Where(r => r.Id == id)
.FirstOrDefaultAsync();
@ -119,10 +228,33 @@ public class RouteManagementService : IRouteManagementService
fields = RouteParameters.DefaultFields;
}
var shapedRouteData = _routeDataShaper.ShapeData(dbRoute, fields);
var routeDto = _mapper.Map<RouteDto>(shapedRouteData);
var routeDto = _mapper.Map<RouteDto>(dbRoute);
var shapedRouteData = _routeDataShaper.ShapeData(routeDto, fields);
return (true, "", routeDto);
return (true, "", shapedRouteData);
}
public async Task<(bool isSucceed, string message, ExpandoObject route)> GetRouteWithAddresses(int id, string? fields)
{
var dbRoute = await _dbContext.Routes.Where(r => r.Id == id)
.Include(r => r.RouteAddresses).ThenInclude(ra => ra.Address)
.ThenInclude(a => a.City).ThenInclude(c => c.State)
.ThenInclude(s => s.Country).FirstOrDefaultAsync();
if (dbRoute == null)
{
return (false, $"Route doesn't exist", null)!;
}
if (String.IsNullOrWhiteSpace(fields))
{
fields = RouteWithAddressesParameters.DefaultFields;
}
var routeDto = _mapper.Map<RouteWithAddressesDto>(dbRoute);
var shapedRouteData = _routeDataShaper.ShapeData(routeDto, fields);
return (true, "", shapedRouteData);
}
public async Task<(bool isSucceed, string message, UpdateRouteDto route)> UpdateRoute(UpdateRouteDto updateRouteDto)

View File

@ -44,6 +44,8 @@ public class StateManagementService : IStateManagementService
.Include(s => s.Cities)
.ThenInclude(c => c.Addresses).AsQueryable();
var s = dbStates.ToList().ConvertAll(s => _mapper.Map<StateDto>(s));
SearchByAllStateFields(ref dbStates, parameters.Search);
FilterByStateName(ref dbStates, parameters.Name);
FilterByCountryId(ref dbStates, parameters.CountryId);

View File

@ -17,16 +17,18 @@ public class StatisticsService : IStatisticsService
private readonly IDataShaper<UserDto> _userDataShaper;
private readonly IDataShaper<CompanyDto> _companyDataShaper;
private readonly IDataShaper<AddressDto> _addressDataShaper;
private readonly IPager<ExpandoObject> _pager;
public StatisticsService(ApplicationDbContext dbContext, IMapper mapper,
IDataShaper<UserDto> userDataShaper, IDataShaper<CompanyDto> companyDataShaper,
IDataShaper<AddressDto> addressDataShaper)
IDataShaper<AddressDto> addressDataShaper, IPager<ExpandoObject> pager)
{
_dbContext = dbContext;
_mapper = mapper;
_userDataShaper = userDataShaper;
_companyDataShaper = companyDataShaper;
_addressDataShaper = addressDataShaper;
_pager = pager;
}
// Popularity is measured in number of purchased tickets
@ -76,8 +78,8 @@ public class StatisticsService : IStatisticsService
}
var shapedData = shapedDataArray.AsQueryable();
var pagingMetadata = ApplyPaging(ref shapedData, parameters.PageNumber,
parameters.PageSize);
var pagingMetadata = _pager.ApplyPaging(ref shapedData,
parameters.PageNumber, parameters.PageSize);
shapedDataArray = shapedData.ToArray();
return (true, null, shapedDataArray, pagingMetadata);
@ -166,8 +168,8 @@ public class StatisticsService : IStatisticsService
}
var shapedData = shapedDataArray.AsQueryable();
var pagingMetadata = ApplyPaging(ref shapedData, parameters.PageNumber,
parameters.PageSize);
var pagingMetadata = _pager.ApplyPaging(ref shapedData,
parameters.PageNumber, parameters.PageSize);
shapedDataArray = shapedData.ToArray();
return (true, null, shapedDataArray, pagingMetadata);
@ -265,23 +267,10 @@ public class StatisticsService : IStatisticsService
}
var shapedData = shapedDataArray.AsQueryable();
var pagingMetadata = ApplyPaging(ref shapedData, parameters.PageNumber,
parameters.PageSize);
var pagingMetadata = _pager.ApplyPaging(ref shapedData,
parameters.PageNumber, parameters.PageSize);
shapedDataArray = shapedData.ToArray();
return (true, null, shapedDataArray, pagingMetadata);
}
PagingMetadata<T> ApplyPaging<T>(ref IQueryable<T> obj,
int pageNumber, int pageSize)
{
var metadata = new PagingMetadata<T>(obj,
pageNumber, pageSize);
obj = obj
.Skip((pageNumber - 1) * pageSize)
.Take(pageSize);
return metadata;
}
}

View File

@ -5,8 +5,7 @@ namespace SharedModels.DataTransferObjects;
public class AddressDto : CreateAddressDto
{
public int Id { get; set; }
public InAddressCityDto City { get; set; } = null!;
public string FullName = null!;
}
public class CreateAddressDto
@ -37,4 +36,25 @@ public class InCityAddressDto
public string Name { get; set; } = null!;
public double Latitude { get; set; }
public double Longitude { get; set; }
}
public class CreateAddressInRouteAddress
{
public int? Id { get; set; }
[StringLength(maximumLength: 250, ErrorMessage = "Address name is too long")]
public string? Name { get; set; } = null!;
[Range(-90, 90, ErrorMessage = "Latitude must be in range(-90, 90)")]
public double? Latitude { get; set; }
[Range(-180, 180, ErrorMessage = "Longitude must be in range(-180, 180)")]
public double? Longitude { get; set; }
public int? CityId { get; set; }
}
public class AddressInRouteAddress : CreateAddressInRouteAddress
{
public string FullName = null!;
}

View File

@ -6,7 +6,7 @@ public class CityDto : CreateCityDto
{
public int Id { get; set; }
public InCityStateDto State { get; set; } = null!;
public string FullName = null!;
public virtual IList<InCityAddressDto>? Addresses { get; set; }
}

View File

@ -16,19 +16,8 @@ public class CreateRouteAddressDto
public int AddressId { get; set; }
[Required]
[Range(0, Int32.MaxValue)]
public int Order { get; set; }
[Required]
[DataType(DataType.Duration)]
public TimeSpan TimeSpanToNextCity { get; set; }
[Required]
[DataType(DataType.Duration)]
public TimeSpan WaitTimeSpan { get; set; }
[Required]
[DataType(DataType.Currency)]
public double CostToNextCity { get; set; }
}
public class UpdateRouteAddressDto : CreateRouteAddressDto
@ -39,21 +28,16 @@ public class UpdateRouteAddressDto : CreateRouteAddressDto
public class CreateRouteAddressWithAddressDto
{
[Range(0, Int32.MaxValue)]
public int Order { get; set; }
[Required]
public CreateAddressInRouteAddress Address { get; set; } = null!;
}
public class RouteAddressWithAddressDto
{
public int Order { get; set; }
[Required]
[DataType(DataType.Duration)]
public TimeSpan TimeSpanToNextCity { get; set; }
[Required]
[DataType(DataType.Duration)]
public TimeSpan WaitTimeSpan { get; set; }
[Required]
[DataType(DataType.Currency)]
public double CostToNextCity { get; set; }
[Required]
public CreateAddressDto Address { get; set; } = null!;
public AddressInRouteAddress Address { get; set; } = null!;
}

View File

@ -24,4 +24,9 @@ public class CreateRouteWithAddressesDto : CreateRouteDto
[Required]
[MinLength(2)]
public IList<CreateRouteAddressWithAddressDto> RouteAddresses { get; set; } = null!;
}
public class RouteWithAddressesDto : RouteDto
{
public IList<RouteAddressWithAddressDto> RouteAddresses { get; set; } = null!;
}

View File

@ -5,7 +5,8 @@ namespace SharedModels.DataTransferObjects;
public class StateDto : CreateStateDto
{
public int Id { get; set; }
public InStateCountryDto Country { get; set; } = null!;
public string FullName = null!;
public virtual IList<InStateCityDto> Cities { get; set; } = null!;
}

View File

@ -0,0 +1,15 @@
namespace SharedModels.QueryParameters.Objects;
public class RouteWithAddressesParameters : ParametersBase
{
public const string DefaultFields = "id,type,routeAddresses";
public RouteWithAddressesParameters()
{
Fields = DefaultFields;
}
public string? Type { get; set; }
public string? FromAddressName { get; set; }
public string? ToAddressName { get; set; }
}