Merge pull request #51 from meysamhadeli/develop

change grpc service from magic-onion to grpc-dotnet
This commit is contained in:
Meysam Hadeli 2022-10-15 01:46:49 +03:30 committed by GitHub
commit dacd6cbeed
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
74 changed files with 544 additions and 422 deletions

View File

@ -73,7 +73,7 @@ High-level plan is represented in the table
- ✔️ **[`IdGen`](https://github.com/RobThree/IdGen)** - Twitter Snowflake-alike ID generator for .Net
- ✔️ **[`Yarp`](https://github.com/microsoft/reverse-proxy)** - Reverse proxy toolkit for building fast proxy servers in .NET
- ✔️ **[`Tye`](https://github.com/dotnet/tye)** - Developer tool that makes developing, testing, and deploying microservices and distributed applications easier
- ✔️ **[`MagicOnion`](https://github.com/Cysharp/MagicOnion)** - gRPC based HTTP/2 RPC Streaming Framework for .NET, .NET Core and Unity.
- ✔️ **[`gRPC-dotnet`](https://github.com/grpc/grpc-dotnet)** - gRPC functionality for .NET.
- ✔️ **[`EventStore`](https://github.com/EventStore/EventStore)** - The open-source, functional database with Complex Event Processing.
- ✔️ **[`MongoDB.Driver`](https://github.com/mongodb/mongo-csharp-driver)** - .NET Driver for MongoDB.
- ✔️ **[`xUnit.net`](https://github.com/xunit/xunit)** - A free, open source, community-focused unit testing tool for the .NET Framework.

View File

@ -251,7 +251,7 @@ Content-Type: application/json
authorization: bearer {{Authenticate.response.body.access_token}}
{
"passengerId": 4776722699124736,
"passengerId": 4779536520052736,
"flightId": 1,
"description": "I want to fly to iran"
}

View File

@ -21,10 +21,7 @@
<PackageReference Include="Figgle" Version="0.4.0" />
<PackageReference Include="FluentValidation" Version="10.3.6" />
<PackageReference Include="FluentValidation.AspNetCore" Version="10.3.6" />
<PackageReference Include="MagicOnion" Version="4.4.0" />
<PackageReference Include="MagicOnion.Abstractions" Version="4.4.0" />
<PackageReference Include="MagicOnion.Client" Version="4.4.0" />
<PackageReference Include="MagicOnion.Server" Version="4.4.0" />
<PackageReference Include="Grpc.Core.Testing" Version="2.46.5" />
<PackageReference Include="Mongo2Go" Version="3.1.3" />
<PackageReference Include="NSubstitute" Version="4.3.0" />
<PackageReference Include="Polly" Version="7.2.3" />
@ -124,6 +121,10 @@
<PackageReference Include="Moq" Version="4.16.1" />
<PackageReference Include="Microsoft.Extensions.Hosting" Version="6.0.0" />
<PackageReference Include="Google.Protobuf" Version="3.21.7" />
<PackageReference Include="Grpc.Net.Client" Version="2.49.0" />
<PackageReference Include="Grpc.Net.ClientFactory" Version="2.49.0" />
</ItemGroup>

View File

@ -1,89 +0,0 @@
using MagicOnion;
using MessagePack;
namespace BuildingBlocks.Contracts.Grpc;
public interface IFlightGrpcService : IService<IFlightGrpcService>
{
UnaryResult<FlightResponseDto> GetById(long id);
UnaryResult<IEnumerable<SeatResponseDto>> GetAvailableSeats(long flightId);
UnaryResult<SeatResponseDto> ReserveSeat(ReserveSeatRequestDto request);
}
[MessagePackObject]
public class ReserveSeatRequestDto
{
[Key(0)]
public long FlightId { get; set; }
[Key(1)]
public string SeatNumber { get; set; }
}
[MessagePackObject]
public record SeatResponseDto
{
[Key(0)]
public long Id { get; set; }
[Key(1)]
public string SeatNumber { get; init; }
[Key(2)]
public SeatType Type { get; init; }
[Key(3)]
public SeatClass Class { get; init; }
[Key(4)]
public long FlightId { get; init; }
}
[MessagePackObject]
public record FlightResponseDto
{
[Key(0)]
public long Id { get; init; }
[Key(1)]
public string FlightNumber { get; init; }
[Key(2)]
public long AircraftId { get; init; }
[Key(3)]
public long DepartureAirportId { get; init; }
[Key(4)]
public DateTime DepartureDate { get; init; }
[Key(5)]
public DateTime ArriveDate { get; init; }
[Key(6)]
public long ArriveAirportId { get; init; }
[Key(7)]
public decimal DurationMinutes { get; init; }
[Key(8)]
public DateTime FlightDate { get; init; }
[Key(9)]
public FlightStatus Status { get; init; }
[Key(10)]
public decimal Price { get; init; }
[Key(11)]
public long FlightId { get; init; }
}
public enum FlightStatus
{
Flying = 1,
Delay = 2,
Canceled = 3,
Completed = 4
}
public enum SeatType
{
Window,
Middle,
Aisle
}
public enum SeatClass
{
FirstClass,
Business,
Economy
}

View File

@ -1,35 +0,0 @@
using MagicOnion;
using MessagePack;
namespace BuildingBlocks.Contracts.Grpc;
public interface IPassengerGrpcService : IService<IPassengerGrpcService>
{
UnaryResult<PassengerResponseDto> GetById(long id);
}
[MessagePackObject]
public class PassengerResponseDto
{
[Key(0)]
public long Id { get; init; }
[Key(1)]
public string Name { get; init; }
[Key(2)]
public string PassportNumber { get; init; }
[Key(3)]
public PassengerType PassengerType { get; init; }
[Key(4)]
public int Age { get; init; }
[Key(5)]
public string Email { get; init; }
}
public enum PassengerType
{
Male,
Female,
Baby,
Unknown
}

View File

@ -1,6 +1,4 @@
using BuildingBlocks.Contracts.Grpc;
using Microsoft.Extensions.DependencyInjection;
using Moq;
using Microsoft.Extensions.DependencyInjection;
using NSubstitute;
namespace BuildingBlocks.Web;

View File

@ -15,9 +15,11 @@ using BuildingBlocks.PersistMessageProcessor;
using BuildingBlocks.Swagger;
using BuildingBlocks.Web;
using Figgle;
using Flight;
using FluentValidation;
using Hellang.Middleware.ProblemDetails;
using Microsoft.AspNetCore.Mvc.ApiExplorer;
using Passenger;
using Prometheus;
using Serilog;
@ -26,7 +28,6 @@ var configuration = builder.Configuration;
var env = builder.Environment;
var appOptions = builder.Services.GetOptions<AppOptions>("AppOptions");
builder.Services.Configure<GrpcOptions>(options => configuration.GetSection("Grpc").Bind(options));
Console.WriteLine(FiggleFonts.Standard.Render(appOptions.Name));
@ -49,14 +50,14 @@ builder.Services.AddCustomMassTransit(typeof(BookingRoot).Assembly, env);
builder.Services.AddCustomOpenTelemetry();
builder.Services.AddTransient<AuthHeaderHandler>();
builder.Services.AddMagicOnionClients();
SnowFlakIdGenerator.Configure(3);
// ref: https://github.com/oskardudycz/EventSourcing.NetCore/tree/main/Sample/EventStoreDB/ECommerce
builder.Services.AddEventStore(configuration, typeof(BookingRoot).Assembly)
.AddEventStoreDBSubscriptionToAll();
builder.Services.AddGrpcClients();
var app = builder.Build();
if (app.Environment.IsDevelopment())

View File

@ -6,15 +6,30 @@
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Grpc.Tools" Version="2.49.1">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
<PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="6.0.1">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\..\..\..\BuildingBlocks\BuildingBlocks.csproj" />
</ItemGroup>
<ItemGroup>
<Protobuf Include="GrpcClient\Protos\flight.proto" GrpcServices="Both" />
<Protobuf Include="GrpcClient\Protos\passenger.proto" GrpcServices="Both" />
</ItemGroup>
<ItemGroup>
<Folder Include="GrpcClient\Protos" />
</ItemGroup>
</Project>

View File

@ -1,49 +1,54 @@
using Ardalis.GuardClauses;
using Booking.Booking.Events.Domain;
using Booking.Booking.Exceptions;
using Booking.Booking.Models.ValueObjects;
using BuildingBlocks.Contracts.Grpc;
using Booking.Configuration;
using BuildingBlocks.Core;
using BuildingBlocks.Core.CQRS;
using BuildingBlocks.EventStoreDB.Repository;
using BuildingBlocks.Utils;
using Flight;
using Grpc.Net.Client;
using Microsoft.Extensions.Options;
using Passenger;
namespace Booking.Booking.Features.CreateBooking;
public class CreateBookingCommandHandler : ICommandHandler<CreateBookingCommand, ulong>
{
private readonly IEventStoreDBRepository<Models.Booking> _eventStoreDbRepository;
private readonly IFlightGrpcService _flightGrpcService;
private readonly ICurrentUserProvider _currentUserProvider;
private readonly IEventDispatcher _eventDispatcher;
private readonly IPassengerGrpcService _passengerGrpcService;
private readonly FlightGrpcService.FlightGrpcServiceClient _flightGrpcServiceClient;
private readonly PassengerGrpcService.PassengerGrpcServiceClient _passengerGrpcServiceClient;
public CreateBookingCommandHandler(IEventStoreDBRepository<Models.Booking> eventStoreDbRepository,
IPassengerGrpcService passengerGrpcService,
IFlightGrpcService flightGrpcService,
ICurrentUserProvider currentUserProvider,
IEventDispatcher eventDispatcher)
IEventDispatcher eventDispatcher,
FlightGrpcService.FlightGrpcServiceClient flightGrpcServiceClient,
PassengerGrpcService.PassengerGrpcServiceClient passengerGrpcServiceClient)
{
_eventStoreDbRepository = eventStoreDbRepository;
_passengerGrpcService = passengerGrpcService;
_flightGrpcService = flightGrpcService;
_currentUserProvider = currentUserProvider;
_eventDispatcher = eventDispatcher;
_flightGrpcServiceClient = flightGrpcServiceClient;
_passengerGrpcServiceClient = passengerGrpcServiceClient;
}
public async Task<ulong> Handle(CreateBookingCommand command,
CancellationToken cancellationToken)
public async Task<ulong> Handle(CreateBookingCommand command, CancellationToken cancellationToken)
{
Guard.Against.Null(command, nameof(command));
var flight = await _flightGrpcService.GetById(command.FlightId);
var flight = await _flightGrpcServiceClient.GetByIdAsync(new Flight.GetByIdRequest {Id = command.FlightId});
if (flight is null)
throw new FlightNotFoundException();
var passenger = await _passengerGrpcService.GetById(command.PassengerId);
var passenger =
await _passengerGrpcServiceClient.GetByIdAsync(new Passenger.GetByIdRequest {Id = command.PassengerId});
var emptySeat = (await _flightGrpcService.GetAvailableSeats(command.FlightId))?.First();
var emptySeat = (await _flightGrpcServiceClient
.GetAvailableSeatsAsync(new GetAvailableSeatsRequest {FlightId = command.FlightId}).ResponseAsync)
?.Items?.FirstOrDefault();
var reservation = await _eventStoreDbRepository.Find(command.Id, cancellationToken);
@ -52,12 +57,13 @@ public class CreateBookingCommandHandler : ICommandHandler<CreateBookingCommand,
var aggrigate = Models.Booking.Create(command.Id, new PassengerInfo(passenger.Name), new Trip(
flight.FlightNumber, flight.AircraftId, flight.DepartureAirportId,
flight.ArriveAirportId, flight.FlightDate, flight.Price, command.Description, emptySeat?.SeatNumber),
flight.ArriveAirportId, flight.FlightDate.ToDateTime(), (decimal)flight.Price, command.Description,
emptySeat?.SeatNumber),
false, _currentUserProvider.GetCurrentUserId());
await _eventDispatcher.SendAsync(aggrigate.DomainEvents, cancellationToken: cancellationToken);
await _flightGrpcService.ReserveSeat(new ReserveSeatRequestDto
await _flightGrpcServiceClient.ReserveSeatAsync(new ReserveSeatRequest
{
FlightId = flight.FlightId, SeatNumber = emptySeat?.SeatNumber
});

View File

@ -0,0 +1,27 @@
using Booking.Configuration;
using BuildingBlocks.Web;
using Flight;
using Microsoft.Extensions.DependencyInjection;
using Passenger;
namespace Booking.Extensions;
public static class GrpcClientExtensions
{
public static IServiceCollection AddGrpcClients(this IServiceCollection services)
{
var grpcOptions = services.GetOptions<GrpcOptions>("Grpc");
services.AddGrpcClient<FlightGrpcService.FlightGrpcServiceClient>(o =>
{
o.Address = new Uri(grpcOptions.FlightAddress);
});
services.AddGrpcClient<PassengerGrpcService.PassengerGrpcServiceClient>(o =>
{
o.Address = new Uri(grpcOptions.PassengerAddress);
});
return services;
}
}

View File

@ -1,21 +0,0 @@
using Booking.Configuration;
using BuildingBlocks.Contracts.Grpc;
using BuildingBlocks.Web;
using Grpc.Net.Client;
using MagicOnion.Client;
using Microsoft.Extensions.DependencyInjection;
namespace Booking.Extensions;
public static class MagicOnionClientExtensions
{
public static IServiceCollection AddMagicOnionClients(this IServiceCollection services)
{
var grpcOptions = services.GetOptions<GrpcOptions>("Grpc");
services.AddSingleton(x => MagicOnionClient.Create<IPassengerGrpcService>(GrpcChannel.ForAddress(grpcOptions.PassengerAddress)));
services.AddSingleton(x => MagicOnionClient.Create<IFlightGrpcService>(GrpcChannel.ForAddress(grpcOptions.FlightAddress)));
return services;
}
}

View File

@ -0,0 +1,76 @@
syntax = "proto3";
package flight;
import "google/protobuf/timestamp.proto";
service FlightGrpcService {
rpc GetById (GetByIdRequest) returns (FlightResponse);
rpc GetAvailableSeats (GetAvailableSeatsRequest) returns (ListSeatsResponse);
rpc ReserveSeat (ReserveSeatRequest) returns (SeatsResponse);
}
message GetByIdRequest {
int64 Id = 1;
}
message FlightResponse {
int64 Id = 1;
string FlightNumber = 2;
int64 AircraftId = 3;
int64 DepartureAirportId = 4;
google.protobuf.Timestamp DepartureDate = 5;
google.protobuf.Timestamp ArriveDate = 6;
int64 ArriveAirportId = 7;
double DurationMinutes = 8;
google.protobuf.Timestamp FlightDate = 9;
FlightStatus Status = 10;
double Price = 11;
int64 FlightId = 12;
}
message GetAvailableSeatsRequest {
int64 FlightId = 1;
}
message SeatsResponse {
int64 Id = 1;
string SeatNumber = 2;
SeatType Type = 3;
SeatClass Class = 4;
int64 FlightId = 5;
}
message ReserveSeatRequest {
int64 FlightId = 1;
string SeatNumber = 2;
}
message ListSeatsResponse {
repeated SeatsResponse items = 1;
}
enum FlightStatus {
FLIGHT_STATUS_UNKNOWN = 0;
FLIGHT_STATUS_FLYING = 1;
FLIGHT_STATUS_DELAY = 2;
FLIGHT_STATUS_CANCELED = 3;
FLIGHT_STATUS_COMPLETED = 4;
}
enum SeatType {
SEAT_TYPE_UNKNOWN = 0;
SEAT_TYPE_WINDOW = 1;
SEAT_TYPE_MIDDLE = 2;
SEAT_TYPE_AISLE = 3;
}
enum SeatClass {
SEAT_CLASS_UNKNOWN = 0;
SEAT_CLASS_FIRST_CLASS = 1;
SEAT_CLASS_BUSINESS = 2;
SEAT_CLASS_ECONOMY = 3;
}

View File

@ -0,0 +1,30 @@
syntax = "proto3";
package passenger;
service PassengerGrpcService {
rpc GetById (GetByIdRequest) returns (PassengerResponse);
}
message GetByIdRequest {
int64 Id = 1;
}
message PassengerResponse {
int64 Id = 1;
string Name = 2;
string PassportNumber = 3;
PassengerType PassengerType = 4;
int32 Age = 5;
string Email = 6;
}
enum PassengerType {
PASSENGER_TYPE_UNKNOWN = 0;
PASSENGER_TYPE_MALE = 1;
PASSENGER_TYPE_FEMALE = 2;
PASSENGER_TYPE_BABY = 3;
}

View File

@ -1,27 +1,30 @@
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Booking.Booking.Models.Reads;
using Booking.Data;
using BuildingBlocks.Contracts.EventBus.Messages;
using BuildingBlocks.Contracts.Grpc;
using BuildingBlocks.PersistMessageProcessor.Data;
using BuildingBlocks.TestBase;
using Flight;
using FluentAssertions;
using Grpc.Core;
using Grpc.Core.Testing;
using Integration.Test.Fakes;
using MagicOnion;
using MassTransit;
using MassTransit.Testing;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.DependencyInjection.Extensions;
using NSubstitute;
using Passenger;
using Xunit;
using GetByIdRequest = Flight.GetByIdRequest;
namespace Integration.Test.Booking.Features;
public class CreateBookingTests : IntegrationTestBase<Program, PersistMessageDbContext, BookingReadDbContext>
{
private readonly ITestHarness _testHarness;
public CreateBookingTests(
IntegrationTestFixture<Program, PersistMessageDbContext, BookingReadDbContext> integrationTestFixture) : base(
integrationTestFixture)
@ -56,11 +59,13 @@ public class CreateBookingTests : IntegrationTestBase<Program, PersistMessageDbC
{
services.Replace(ServiceDescriptor.Singleton(x =>
{
var mock = Substitute.For<IPassengerGrpcService>();
mock.GetById(Arg.Any<long>())
.Returns(new UnaryResult<PassengerResponseDto>(new FakePassengerResponseDto().Generate()));
var mockPassenger = Substitute.For<PassengerGrpcService.PassengerGrpcServiceClient>();
return mock;
mockPassenger.GetByIdAsync(Arg.Any<Passenger.GetByIdRequest>())
.Returns(TestCalls.AsyncUnaryCall(Task.FromResult(new FakePassengerResponse().Generate()),
Task.FromResult(new Metadata()), () => Status.DefaultSuccess, () => new Metadata(), () => { }));
return mockPassenger;
}));
}
@ -68,19 +73,21 @@ public class CreateBookingTests : IntegrationTestBase<Program, PersistMessageDbC
{
services.Replace(ServiceDescriptor.Singleton(x =>
{
var mock = Substitute.For<IFlightGrpcService>();
var mockFlight = Substitute.For<FlightGrpcService.FlightGrpcServiceClient>();
mock.GetById(Arg.Any<long>())
.Returns(new UnaryResult<FlightResponseDto>(Task.FromResult(new FakeFlightResponseDto().Generate())));
mockFlight.GetByIdAsync(Arg.Any<GetByIdRequest>())
.Returns(TestCalls.AsyncUnaryCall(Task.FromResult(new FakeFlightResponse().Generate()),
Task.FromResult(new Metadata()), () => Status.DefaultSuccess, () => new Metadata(), () => { }));
mock.GetAvailableSeats(Arg.Any<long>())
.Returns(
new UnaryResult<IEnumerable<SeatResponseDto>>(Task.FromResult(FakeSeatsResponseDto.Generate())));
mockFlight.GetAvailableSeatsAsync(Arg.Any<GetAvailableSeatsRequest>())
.Returns(TestCalls.AsyncUnaryCall(Task.FromResult(FakeSeatsResponse.Generate()),
Task.FromResult(new Metadata()), () => Status.DefaultSuccess, () => new Metadata(), () => { }));
mock.ReserveSeat(new FakeReserveSeatRequestDto().Generate())
.Returns(new UnaryResult<SeatResponseDto>(Task.FromResult(FakeSeatsResponseDto.Generate().First())));
mockFlight.ReserveSeatAsync(Arg.Any<ReserveSeatRequest>())
.Returns(TestCalls.AsyncUnaryCall(Task.FromResult(FakeSeatsResponse.Generate()?.Items?.First()),
Task.FromResult(new Metadata()), () => Status.DefaultSuccess, () => new Metadata(), () => { }));
return mock;
return mockFlight;
}));
}
}

View File

@ -0,0 +1,23 @@
using System;
using AutoBogus;
using Flight;
using Google.Protobuf.WellKnownTypes;
namespace Integration.Test.Fakes;
public class FakeFlightResponse : AutoFaker<FlightResponse>
{
public FakeFlightResponse()
{
RuleFor(r => r.Id, _ => 1);
RuleFor(r => r.Price, _ => 100);
RuleFor(r => r.Status, _ => FlightStatus.Completed);
RuleFor(r => r.AircraftId, _ => 1);
RuleFor(r => r.ArriveAirportId, _ => 1);
RuleFor(r => r.ArriveDate, _ => DateTime.SpecifyKind(DateTime.Now, DateTimeKind.Utc).ToTimestamp());
RuleFor(r => r.DepartureDate, _ => DateTime.SpecifyKind(DateTime.Now, DateTimeKind.Utc).ToTimestamp());
RuleFor(r => r.FlightDate, _ => DateTime.SpecifyKind(DateTime.Now, DateTimeKind.Utc).ToTimestamp());
RuleFor(r => r.FlightNumber, _ => "121LP");
RuleFor(r => r.DepartureAirportId, _ => 2);
}
}

View File

@ -1,22 +0,0 @@
using System;
using AutoBogus;
using BuildingBlocks.Contracts.Grpc;
namespace Integration.Test.Fakes;
public class FakeFlightResponseDto : AutoFaker<FlightResponseDto>
{
public FakeFlightResponseDto()
{
RuleFor(r => r.Id, _ => 1);
RuleFor(r => r.Price, _ => 100);
RuleFor(r => r.Status, _ => FlightStatus.Completed);
RuleFor(r => r.AircraftId, _ => 1);
RuleFor(r => r.ArriveAirportId, _ => 1);
RuleFor(r => r.ArriveDate, _ => DateTime.Now);
RuleFor(r => r.DepartureDate, _ => DateTime.Now);
RuleFor(r => r.FlightDate, _ => DateTime.Now);
RuleFor(r => r.FlightNumber, _ => "121LP");
RuleFor(r => r.DepartureAirportId, _ => 2);
}
}

View File

@ -1,12 +1,12 @@
using AutoBogus;
using BuildingBlocks.Contracts.Grpc;
using BuildingBlocks.IdsGenerator;
using Passenger;
namespace Integration.Test.Fakes;
public class FakePassengerResponseDto : AutoFaker<PassengerResponseDto>
public class FakePassengerResponse : AutoFaker<PassengerResponse>
{
public FakePassengerResponseDto()
public FakePassengerResponse()
{
RuleFor(r => r.Id, _ => SnowFlakIdGenerator.NewId());
}

View File

@ -1,13 +0,0 @@
using AutoBogus;
using BuildingBlocks.Contracts.Grpc;
namespace Integration.Test.Fakes;
public class FakeReserveSeatRequestDto : AutoFaker<ReserveSeatRequestDto>
{
public FakeReserveSeatRequestDto()
{
RuleFor(r => r.FlightId, _ => 1);
RuleFor(r => r.SeatNumber, _ => "33F");
}
}

View File

@ -1,15 +1,16 @@
using System.Collections.Generic;
using BuildingBlocks.Contracts.Grpc;
using Flight;
namespace Integration.Test.Fakes;
public static class FakeSeatsResponseDto
public static class FakeSeatsResponse
{
public static IEnumerable<SeatResponseDto> Generate()
public static ListSeatsResponse Generate()
{
return new List<SeatResponseDto>()
var result = new ListSeatsResponse();
result.Items.AddRange(new List<SeatsResponse>
{
new SeatResponseDto()
new SeatsResponse()
{
FlightId = 1,
Class = SeatClass.Economy,
@ -17,7 +18,7 @@ public static class FakeSeatsResponseDto
SeatNumber = "33F",
Id = 1
},
new SeatResponseDto()
new SeatsResponse()
{
FlightId = 1,
Class = SeatClass.Economy,
@ -25,6 +26,8 @@ public static class FakeSeatsResponseDto
SeatNumber = "22D",
Id = 2
}
};
});
return result;
}
}

View File

@ -1,3 +1,4 @@
using System.Net;
using System.Reflection;
using BuildingBlocks.Caching;
using BuildingBlocks.Core;
@ -19,9 +20,11 @@ using Flight;
using Flight.Data;
using Flight.Data.Seed;
using Flight.Extensions;
using Flight.GrpcServer.Services;
using FluentValidation;
using Hellang.Middleware.ProblemDetails;
using Microsoft.AspNetCore.Mvc.ApiExplorer;
using Microsoft.AspNetCore.Server.Kestrel.Core;
using Prometheus;
using Serilog;
@ -60,8 +63,6 @@ builder.Services.AddGrpc(options =>
options.Interceptors.Add<GrpcExceptionInterceptor>();
});
builder.Services.AddMagicOnion();
SnowFlakIdGenerator.Configure(1);
builder.Services.AddCachingRequest(new List<Assembly> {typeof(FlightRoot).Assembly});
@ -91,7 +92,7 @@ app.UseEndpoints(endpoints =>
{
endpoints.MapControllers();
endpoints.MapMetrics();
endpoints.MapMagicOnionService();
endpoints.MapGrpcService<FlightGrpcServices>();
});
app.MapGet("/", x => x.Response.WriteAsync(appOptions.Name));

View File

@ -79,12 +79,12 @@ public class FlightDataSeeder : IDataSeeder
{
var seats = new List<Seat>
{
Seat.Create(1 ,"12A", SeatType.Window, SeatClass.Economy, 1),
Seat.Create(2, "12B", SeatType.Window, SeatClass.Economy, 1),
Seat.Create(3, "12C", SeatType.Middle, SeatClass.Economy, 1),
Seat.Create(4, "12D", SeatType.Middle, SeatClass.Economy, 1),
Seat.Create(5, "12E", SeatType.Aisle, SeatClass.Economy, 1),
Seat.Create(6, "12F", SeatType.Aisle, SeatClass.Economy, 1)
Seat.Create(1 ,"12A", Seats.Enums.SeatType.Window, Seats.Enums.SeatClass.Economy, 1),
Seat.Create(2, "12B", Seats.Enums.SeatType.Window, Seats.Enums.SeatClass.Economy, 1),
Seat.Create(3, "12C", Seats.Enums.SeatType.Middle, Seats.Enums.SeatClass.Economy, 1),
Seat.Create(4, "12D", Seats.Enums.SeatType.Middle, Seats.Enums.SeatClass.Economy, 1),
Seat.Create(5, "12E", Seats.Enums.SeatType.Aisle, Seats.Enums.SeatClass.Economy, 1),
Seat.Create(6, "12F", Seats.Enums.SeatType.Aisle, Seats.Enums.SeatClass.Economy, 1)
};
await _flightDbContext.Seats.AddRangeAsync(seats);
@ -102,7 +102,7 @@ public class FlightDataSeeder : IDataSeeder
Flights.Models.Flight.Create(1, "BD467", 1, 1, new DateTime(2022, 1, 31, 12, 0, 0),
new DateTime(2022, 1, 31, 14, 0, 0),
2, 120m,
new DateTime(2022, 1, 31), FlightStatus.Completed,
new DateTime(2022, 1, 31), Flights.Enums.FlightStatus.Completed,
8000)
};
await _flightDbContext.Flights.AddRangeAsync(flights);

View File

@ -5,6 +5,12 @@
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Grpc.AspNetCore" Version="2.49.0" />
<PackageReference Include="Grpc.Net.Client" Version="2.49.0" />
<PackageReference Include="Grpc.Tools" Version="2.49.1">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
<PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="6.0.1">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
@ -15,7 +21,10 @@
<Folder Include="Aircrafts\Exceptions" />
<Folder Include="Airports\Exceptions" />
<Folder Include="Data\Migrations" />
<Folder Include="Enum" />
</ItemGroup>
<ItemGroup>
<Protobuf Include="GrpcServer\Protos\flight.proto" GrpcServices="Both" />
</ItemGroup>
<ItemGroup>

View File

@ -1,7 +1,8 @@
namespace Flight.Flights.Models;
namespace Flight.Flights.Enums;
public enum FlightStatus
{
Unknown = 0,
Flying = 1,
Delay = 2,
Canceled = 3,

View File

@ -6,4 +6,4 @@ namespace Flight.Flights.Events.Domain;
public record FlightCreatedDomainEvent(long Id, string FlightNumber, long AircraftId, DateTime DepartureDate,
long DepartureAirportId, DateTime ArriveDate, long ArriveAirportId, decimal DurationMinutes,
DateTime FlightDate, FlightStatus Status, decimal Price, bool IsDeleted) : IDomainEvent;
DateTime FlightDate, Enums.FlightStatus Status, decimal Price, bool IsDeleted) : IDomainEvent;

View File

@ -6,4 +6,4 @@ namespace Flight.Flights.Events.Domain;
public record FlightDeletedDomainEvent(long Id, string FlightNumber, long AircraftId, DateTime DepartureDate,
long DepartureAirportId, DateTime ArriveDate, long ArriveAirportId, decimal DurationMinutes,
DateTime FlightDate, FlightStatus Status, decimal Price, bool IsDeleted) : IDomainEvent;
DateTime FlightDate, Enums.FlightStatus Status, decimal Price, bool IsDeleted) : IDomainEvent;

View File

@ -5,4 +5,4 @@ using Flight.Flights.Models;
namespace Flight.Flights.Events.Domain;
public record FlightUpdatedDomainEvent(long Id, string FlightNumber, long AircraftId, DateTime DepartureDate,
long DepartureAirportId, DateTime ArriveDate, long ArriveAirportId, decimal DurationMinutes,
DateTime FlightDate, FlightStatus Status, decimal Price, bool IsDeleted) : IDomainEvent;
DateTime FlightDate, Enums.FlightStatus Status, decimal Price, bool IsDeleted) : IDomainEvent;

View File

@ -9,7 +9,7 @@ namespace Flight.Flights.Features.CreateFlight;
public record CreateFlightCommand(string FlightNumber, long AircraftId, long DepartureAirportId,
DateTime DepartureDate, DateTime ArriveDate, long ArriveAirportId,
decimal DurationMinutes, DateTime FlightDate, FlightStatus Status, decimal Price) : ICommand<FlightResponseDto>, IInternalCommand
decimal DurationMinutes, DateTime FlightDate, Enums.FlightStatus Status, decimal Price) : ICommand<FlightResponseDto>, IInternalCommand
{
public long Id { get; init; } = SnowFlakIdGenerator.NewId();
}

View File

@ -10,10 +10,10 @@ public class CreateFlightCommandValidator : AbstractValidator<CreateFlightComman
RuleFor(x => x.Price).GreaterThan(0).WithMessage("Price must be greater than 0");
RuleFor(x => x.Status).Must(p => (p.GetType().IsEnum &&
p == FlightStatus.Flying) ||
p == FlightStatus.Canceled ||
p == FlightStatus.Delay ||
p == FlightStatus.Completed)
p == Enums.FlightStatus.Flying) ||
p == Enums.FlightStatus.Canceled ||
p == Enums.FlightStatus.Delay ||
p == Enums.FlightStatus.Completed)
.WithMessage("Status must be Flying, Delay, Canceled or Completed");
RuleFor(x => x.AircraftId).NotEmpty().WithMessage("AircraftId must be not empty");

View File

@ -8,7 +8,7 @@ public class CreateFlightMongoCommand : InternalCommand
{
public CreateFlightMongoCommand(long id, string flightNumber, long aircraftId, DateTime departureDate,
long departureAirportId,
DateTime arriveDate, long arriveAirportId, decimal durationMinutes, DateTime flightDate, FlightStatus status,
DateTime arriveDate, long arriveAirportId, decimal durationMinutes, DateTime flightDate, Enums.FlightStatus status,
decimal price, bool isDeleted)
{
Id = id;
@ -33,7 +33,7 @@ public class CreateFlightMongoCommand : InternalCommand
public long ArriveAirportId { get; }
public decimal DurationMinutes { get; }
public DateTime FlightDate { get; }
public FlightStatus Status { get; }
public Enums.FlightStatus Status { get; }
public decimal Price { get; }
public bool IsDeleted { get; }
}

View File

@ -8,7 +8,7 @@ public class DeleteFlightMongoCommand : InternalCommand
{
public DeleteFlightMongoCommand(long id, string flightNumber, long aircraftId, DateTime departureDate,
long departureAirportId,
DateTime arriveDate, long arriveAirportId, decimal durationMinutes, DateTime flightDate, FlightStatus status,
DateTime arriveDate, long arriveAirportId, decimal durationMinutes, DateTime flightDate, Enums.FlightStatus status,
decimal price, bool isDeleted)
{
Id = id;
@ -33,7 +33,7 @@ public class DeleteFlightMongoCommand : InternalCommand
public long ArriveAirportId { get; }
public decimal DurationMinutes { get; }
public DateTime FlightDate { get; }
public FlightStatus Status { get; }
public Enums.FlightStatus Status { get; }
public decimal Price { get; }
public bool IsDeleted { get; }
}

View File

@ -8,7 +8,7 @@ public class UpdateFlightMongoCommand : InternalCommand
{
public UpdateFlightMongoCommand(long Id, string FlightNumber, long AircraftId, DateTime DepartureDate,
long DepartureAirportId,
DateTime ArriveDate, long ArriveAirportId, decimal DurationMinutes, DateTime FlightDate, FlightStatus Status,
DateTime ArriveDate, long ArriveAirportId, decimal DurationMinutes, DateTime FlightDate, Enums.FlightStatus Status,
decimal Price, bool IsDeleted)
{
this.Id = Id;
@ -33,7 +33,7 @@ public class UpdateFlightMongoCommand : InternalCommand
public long ArriveAirportId { get; }
public decimal DurationMinutes { get; }
public DateTime FlightDate { get; }
public FlightStatus Status { get; }
public Enums.FlightStatus Status { get; }
public decimal Price { get; }
public bool IsDeleted { get; }
}

View File

@ -20,7 +20,7 @@ public record UpdateFlightCommand : ICommand<FlightResponseDto>, IInvalidateCach
public decimal DurationMinutes { get; init; }
public DateTime FlightDate { get; init; }
public FlightStatus Status { get; init; }
public Enums.FlightStatus Status { get; init; }
public bool IsDeleted { get; init; } = false;
public decimal Price { get; init; }

View File

@ -13,10 +13,10 @@ public class UpdateFlightCommandValidator : AbstractValidator<CreateFlightComman
RuleFor(x => x.Price).GreaterThan(0).WithMessage("Price must be greater than 0");
RuleFor(x => x.Status).Must(p => (p.GetType().IsEnum &&
p == FlightStatus.Flying) ||
p == FlightStatus.Canceled ||
p == FlightStatus.Delay ||
p == FlightStatus.Completed)
p == Enums.FlightStatus.Flying) ||
p == Enums.FlightStatus.Canceled ||
p == Enums.FlightStatus.Delay ||
p == Enums.FlightStatus.Completed)
.WithMessage("Status must be Flying, Delay, Canceled or Completed");
RuleFor(x => x.AircraftId).NotEmpty().WithMessage("AircraftId must be not empty");

View File

@ -14,12 +14,12 @@ public record Flight : Aggregate<long>
public long ArriveAirportId { get; private set; }
public decimal DurationMinutes { get; private set; }
public DateTime FlightDate { get; private set; }
public FlightStatus Status { get; private set; }
public Enums.FlightStatus Status { get; private set; }
public decimal Price { get; private set; }
public static Flight Create(long id, string flightNumber, long aircraftId,
long departureAirportId, DateTime departureDate, DateTime arriveDate,
long arriveAirportId, decimal durationMinutes, DateTime flightDate, FlightStatus status,
long arriveAirportId, decimal durationMinutes, DateTime flightDate, Enums.FlightStatus status,
decimal price, bool isDeleted = false)
{
var flight = new Flight
@ -51,7 +51,7 @@ public record Flight : Aggregate<long>
public void Update(long id, string flightNumber, long aircraftId,
long departureAirportId, DateTime departureDate, DateTime arriveDate,
long arriveAirportId, decimal durationMinutes, DateTime flightDate, FlightStatus status,
long arriveAirportId, decimal durationMinutes, DateTime flightDate, Enums.FlightStatus status,
decimal price, bool isDeleted = false)
{
FlightNumber = flightNumber;
@ -74,7 +74,7 @@ public record Flight : Aggregate<long>
public void Delete(long id, string flightNumber, long aircraftId,
long departureAirportId, DateTime departureDate, DateTime arriveDate,
long arriveAirportId, decimal durationMinutes, DateTime flightDate, FlightStatus status,
long arriveAirportId, decimal durationMinutes, DateTime flightDate, Enums.FlightStatus status,
decimal price, bool isDeleted = true)
{
FlightNumber = flightNumber;

View File

@ -16,7 +16,7 @@ public class FlightReadModel
public long ArriveAirportId { get; init; }
public decimal DurationMinutes { get; init; }
public DateTime FlightDate { get; init; }
public FlightStatus Status { get; init; }
public Enums.FlightStatus Status { get; init; }
public decimal Price { get; init; }
public bool IsDeleted { get; init; }
}

View File

@ -1,40 +0,0 @@
using System.Collections.Generic;
using BuildingBlocks.Contracts.Grpc;
using Flight.Flights.Features.GetFlightById;
using Flight.Seats.Features.GetAvailableSeats;
using Flight.Seats.Features.ReserveSeat;
using MagicOnion;
using MagicOnion.Server;
using Mapster;
using MediatR;
using SeatResponseDto = BuildingBlocks.Contracts.Grpc.SeatResponseDto;
namespace Flight.GrpcServer;
public class FlightGrpcService : ServiceBase<IFlightGrpcService>, IFlightGrpcService
{
private readonly IMediator _mediator;
public FlightGrpcService(IMediator mediator)
{
_mediator = mediator;
}
public async UnaryResult<FlightResponseDto> GetById(long id)
{
var result = await _mediator.Send(new GetFlightByIdQuery(id));
return result.Adapt<FlightResponseDto>();
}
public async UnaryResult<IEnumerable<SeatResponseDto>> GetAvailableSeats(long flightId)
{
var result = await _mediator.Send(new GetAvailableSeatsQuery(flightId));
return result.Adapt<IEnumerable<SeatResponseDto>>();
}
public async UnaryResult<SeatResponseDto> ReserveSeat(ReserveSeatRequestDto request)
{
var result = await _mediator.Send(new ReserveSeatCommand(request.FlightId, request.SeatNumber));
return result.Adapt<SeatResponseDto>();
}
}

View File

@ -0,0 +1,76 @@
syntax = "proto3";
package flight;
import "google/protobuf/timestamp.proto";
service FlightGrpcService {
rpc GetById (GetByIdRequest) returns (FlightResponse);
rpc GetAvailableSeats (GetAvailableSeatsRequest) returns (ListSeatsResponse);
rpc ReserveSeat (ReserveSeatRequest) returns (SeatsResponse);
}
message GetByIdRequest {
int64 Id = 1;
}
message FlightResponse {
int64 Id = 1;
string FlightNumber = 2;
int64 AircraftId = 3;
int64 DepartureAirportId = 4;
google.protobuf.Timestamp DepartureDate = 5;
google.protobuf.Timestamp ArriveDate = 6;
int64 ArriveAirportId = 7;
double DurationMinutes = 8;
google.protobuf.Timestamp FlightDate = 9;
FlightStatus Status = 10;
double Price = 11;
int64 FlightId = 12;
}
message GetAvailableSeatsRequest {
int64 FlightId = 1;
}
message SeatsResponse {
int64 Id = 1;
string SeatNumber = 2;
SeatType Type = 3;
SeatClass Class = 4;
int64 FlightId = 5;
}
message ReserveSeatRequest {
int64 FlightId = 1;
string SeatNumber = 2;
}
message ListSeatsResponse {
repeated SeatsResponse items = 1;
}
enum FlightStatus {
FLIGHT_STATUS_UNKNOWN = 0;
FLIGHT_STATUS_FLYING = 1;
FLIGHT_STATUS_DELAY = 2;
FLIGHT_STATUS_CANCELED = 3;
FLIGHT_STATUS_COMPLETED = 4;
}
enum SeatType {
SEAT_TYPE_UNKNOWN = 0;
SEAT_TYPE_WINDOW = 1;
SEAT_TYPE_MIDDLE = 2;
SEAT_TYPE_AISLE = 3;
}
enum SeatClass {
SEAT_CLASS_UNKNOWN = 0;
SEAT_CLASS_FIRST_CLASS = 1;
SEAT_CLASS_BUSINESS = 2;
SEAT_CLASS_ECONOMY = 3;
}

View File

@ -0,0 +1,45 @@
using System.Threading.Tasks;
using Flight.Flights.Features.GetFlightById;
using Flight.Seats.Features.GetAvailableSeats;
using Flight.Seats.Features.ReserveSeat;
using Grpc.Core;
using Mapster;
using MediatR;
namespace Flight.GrpcServer.Services;
public class FlightGrpcServices : FlightGrpcService.FlightGrpcServiceBase
{
private readonly IMediator _mediator;
public FlightGrpcServices(IMediator mediator)
{
_mediator = mediator;
}
public override async Task<FlightResponse> GetById(GetByIdRequest request, ServerCallContext context)
{
var result = await _mediator.Send(new GetFlightByIdQuery(request.Id));
return result.Adapt<FlightResponse>();
}
public override async Task<SeatsResponse> ReserveSeat(ReserveSeatRequest request, ServerCallContext context)
{
var result = await _mediator.Send(new ReserveSeatCommand(request.FlightId, request.SeatNumber));
return result.Adapt<SeatsResponse>();
}
public override async Task<ListSeatsResponse> GetAvailableSeats(GetAvailableSeatsRequest request, ServerCallContext context)
{
var result = new ListSeatsResponse();
var availableSeats = await _mediator.Send(new GetAvailableSeatsQuery(request.FlightId));
foreach (var availableSeat in availableSeats)
{
result.Items.Add(availableSeat.Adapt<SeatsResponse>());
}
return result;
}
}

View File

@ -1,7 +1,8 @@
namespace Flight.Seats.Models;
namespace Flight.Seats.Enums;
public enum SeatClass
{
Unknown = 0,
FirstClass,
Business,
Economy

View File

@ -1,7 +1,8 @@
namespace Flight.Seats.Models;
namespace Flight.Seats.Enums;
public enum SeatType
{
Unknown = 0,
Window,
Middle,
Aisle

View File

@ -3,4 +3,4 @@ using Flight.Seats.Models;
namespace Flight.Seats.Events;
public record SeatCreatedDomainEvent(long Id, string SeatNumber, SeatType Type, SeatClass Class, long FlightId, bool IsDeleted) : IDomainEvent;
public record SeatCreatedDomainEvent(long Id, string SeatNumber, Enums.SeatType Type, Enums.SeatClass Class, long FlightId, bool IsDeleted) : IDomainEvent;

View File

@ -3,4 +3,4 @@ using Flight.Seats.Models;
namespace Flight.Seats.Events;
public record SeatReservedDomainEvent(long Id, string SeatNumber, SeatType Type, SeatClass Class, long FlightId, bool IsDeleted) : IDomainEvent;
public record SeatReservedDomainEvent(long Id, string SeatNumber, Enums.SeatType Type, Enums.SeatClass Class, long FlightId, bool IsDeleted) : IDomainEvent;

View File

@ -6,7 +6,7 @@ using Flight.Seats.Models;
namespace Flight.Seats.Features.CreateSeat;
public record CreateSeatCommand(string SeatNumber, SeatType Type, SeatClass Class, long FlightId) : ICommand<SeatResponseDto>, IInternalCommand
public record CreateSeatCommand(string SeatNumber, Enums.SeatType Type, Enums.SeatClass Class, long FlightId) : ICommand<SeatResponseDto>, IInternalCommand
{
public long Id { get; init; } = SnowFlakIdGenerator.NewId();
}

View File

@ -11,9 +11,9 @@ public class CreateSeatCommandValidator : AbstractValidator<CreateSeatCommand>
RuleFor(x => x.SeatNumber).NotEmpty().WithMessage("SeatNumber is required");
RuleFor(x => x.FlightId).NotEmpty().WithMessage("FlightId is required");
RuleFor(x => x.Class).Must(p => (p.GetType().IsEnum &&
p == SeatClass.FirstClass) ||
p == SeatClass.Business ||
p == SeatClass.Economy)
p == Enums.SeatClass.FirstClass) ||
p == Enums.SeatClass.Business ||
p == Enums.SeatClass.Economy)
.WithMessage("Status must be FirstClass, Business or Economy");
}
}

View File

@ -5,7 +5,7 @@ namespace Flight.Seats.Features.CreateSeat.Reads;
public class CreateSeatMongoCommand : InternalCommand
{
public CreateSeatMongoCommand(long id, string seatNumber, SeatType type, SeatClass @class,
public CreateSeatMongoCommand(long id, string seatNumber, Enums.SeatType type, Enums.SeatClass @class,
long flightId, bool isDeleted)
{
Id = id;
@ -18,8 +18,8 @@ public class CreateSeatMongoCommand : InternalCommand
public long Id { get; }
public string SeatNumber { get; }
public SeatType Type { get; }
public SeatClass Class { get; }
public Enums.SeatType Type { get; }
public Enums.SeatClass Class { get; }
public long FlightId { get; }
public bool IsDeleted { get; }
}

View File

@ -5,7 +5,7 @@ namespace Flight.Seats.Features.ReserveSeat.Reads;
public class ReserveSeatMongoCommand : InternalCommand
{
public ReserveSeatMongoCommand(long id, string seatNumber, SeatType type, SeatClass @class, long flightId,
public ReserveSeatMongoCommand(long id, string seatNumber, Enums.SeatType type, Enums.SeatClass @class, long flightId,
bool isDeleted)
{
Id = id;
@ -17,8 +17,8 @@ public class ReserveSeatMongoCommand : InternalCommand
}
public string SeatNumber { get; }
public SeatType Type { get; }
public SeatClass Class { get; }
public Enums.SeatType Type { get; }
public Enums.SeatClass Class { get; }
public long FlightId { get; }
public bool IsDeleted { get; }
}

View File

@ -5,8 +5,8 @@ public class SeatReadModel
public long Id { get; init; }
public long SeatId { get; init; }
public string SeatNumber { get; init; }
public SeatType Type { get; init; }
public SeatClass Class { get; init; }
public Enums.SeatType Type { get; init; }
public Enums.SeatClass Class { get; init; }
public long FlightId { get; init; }
public bool IsDeleted { get; init; }
}

View File

@ -7,7 +7,7 @@ namespace Flight.Seats.Models;
public record Seat : Aggregate<long>
{
public static Seat Create(long id, string seatNumber, SeatType type, SeatClass @class, long flightId,
public static Seat Create(long id, string seatNumber, Enums.SeatType type, Enums.SeatClass @class, long flightId,
bool isDeleted = false)
{
var seat = new Seat()
@ -52,7 +52,7 @@ public record Seat : Aggregate<long>
}
public string SeatNumber { get; private set; }
public SeatType Type { get; private set; }
public SeatClass Class { get; private set; }
public Enums.SeatType Type { get; private set; }
public Enums.SeatClass Class { get; private set; }
public long FlightId { get; private set; }
}

View File

@ -1,13 +1,13 @@
using System.Threading.Tasks;
using BuildingBlocks.Contracts.Grpc;
using BuildingBlocks.TestBase;
using Flight;
using Flight.Data;
using Flight.Flights.Features.CreateFlight.Reads;
using Flight.Flights.Features.GetFlightById;
using Flight.GrpcServer.Services;
using FluentAssertions;
using Grpc.Net.Client;
using Integration.Test.Fakes;
using MagicOnion.Client;
using Xunit;
namespace Integration.Test.Flight.Features;
@ -16,7 +16,9 @@ public class GetFlightByIdTests : IntegrationTestBase<Program, FlightDbContext,
{
private readonly GrpcChannel _channel;
public GetFlightByIdTests(IntegrationTestFixture<Program, FlightDbContext, FlightReadDbContext> integrationTestFixture) : base(integrationTestFixture)
public GetFlightByIdTests(
IntegrationTestFixture<Program, FlightDbContext, FlightReadDbContext> integrationTestFixture) : base(
integrationTestFixture)
{
_channel = Fixture.Channel;
}
@ -49,10 +51,10 @@ public class GetFlightByIdTests : IntegrationTestBase<Program, FlightDbContext,
await Fixture.ShouldProcessedPersistInternalCommand<CreateFlightMongoCommand>();
var flightGrpcClient = MagicOnionClient.Create<IFlightGrpcService>(_channel);
var flightGrpcClient = new FlightGrpcService.FlightGrpcServiceClient(_channel);
// Act
var response = await flightGrpcClient.GetById(command.Id);
var response = await flightGrpcClient.GetByIdAsync(new GetByIdRequest {Id = command.Id});
// Assert
response?.Should().NotBeNull();

View File

@ -1,15 +1,12 @@
using System.Linq;
using System.Threading.Tasks;
using BuildingBlocks.Contracts.Grpc;
using System.Threading.Tasks;
using BuildingBlocks.TestBase;
using Flight;
using Flight.Data;
using Flight.Flights.Features.CreateFlight.Reads;
using Flight.Seats.Features.CreateSeat.Reads;
using FluentAssertions;
using Grpc.Net.Client;
using Integration.Test.Fakes;
using MagicOnion.Client;
using MassTransit.Testing;
using Xunit;
namespace Integration.Test.Seat.Features;
@ -39,13 +36,13 @@ public class GetAvailableSeatsTests : IntegrationTestBase<Program, FlightDbConte
await Fixture.ShouldProcessedPersistInternalCommand<CreateSeatMongoCommand>();
var flightGrpcClient = MagicOnionClient.Create<IFlightGrpcService>(_channel);
var flightGrpcClient = new FlightGrpcService.FlightGrpcServiceClient(_channel);
// Act
var response = await flightGrpcClient.GetAvailableSeats(flightCommand.Id);
var response = await flightGrpcClient.GetAvailableSeatsAsync(new GetAvailableSeatsRequest{FlightId = flightCommand.Id});
// Assert
response?.Should().NotBeNull();
response?.Count().Should().BeGreaterOrEqualTo(1);
response?.Items?.Count.Should().BeGreaterOrEqualTo(1);
}
}

View File

@ -1,13 +1,12 @@
using System.Threading.Tasks;
using BuildingBlocks.Contracts.Grpc;
using BuildingBlocks.TestBase;
using Flight;
using Flight.Data;
using Flight.Flights.Features.CreateFlight.Reads;
using Flight.Seats.Features.CreateSeat.Reads;
using FluentAssertions;
using Grpc.Net.Client;
using Integration.Test.Fakes;
using MagicOnion.Client;
using Xunit;
namespace Integration.Test.Seat.Features;
@ -39,10 +38,10 @@ public class ReserveSeatTests : IntegrationTestBase<Program, FlightDbContext, Fl
await Fixture.ShouldProcessedPersistInternalCommand<CreateSeatMongoCommand>();
var flightGrpcClient = MagicOnionClient.Create<IFlightGrpcService>(_channel);
var flightGrpcClient = new FlightGrpcService.FlightGrpcServiceClient(_channel);
// Act
var response = await flightGrpcClient.ReserveSeat(new ReserveSeatRequestDto
var response = await flightGrpcClient.ReserveSeatAsync(new ReserveSeatRequest()
{
FlightId = seatCommand.FlightId, SeatNumber = seatCommand.SeatNumber
});

View File

@ -3,7 +3,9 @@ using System.Collections.Generic;
using Flight.Aircrafts.Models;
using Flight.Airports.Models;
using Flight.Data;
using Flight.Flights.Enums;
using Flight.Flights.Models;
using Flight.Seats.Enums;
using Flight.Seats.Models;
using Microsoft.EntityFrameworkCore;

View File

@ -1,5 +1,6 @@
using AutoBogus;
using BuildingBlocks.IdsGenerator;
using Flight.Seats.Enums;
using Flight.Seats.Features.CreateSeat;
using Flight.Seats.Models;

View File

@ -1,5 +1,6 @@
using System;
using AutoBogus;
using Flight.Flights.Enums;
using Flight.Flights.Features.CreateFlight;
using Flight.Flights.Models;

View File

@ -1,4 +1,5 @@
using AutoBogus;
using Flight.Seats.Enums;
using Flight.Seats.Features.CreateSeat;
using Flight.Seats.Models;

View File

@ -0,0 +1 @@
{"Version":1,"Id":"A7DD53B5F56AA3937910E9A73600964A","Created":"2022-10-13T20:19:56.3638146Z","Algorithm":"RS256","IsX509Certificate":false,"Data":"CfDJ8GjYME9sg_tHn5C8_j2EtO1vvYKDE1hzq5Kp8GHesS2tNpBMrM_9i8n6u2MDK4eczb8P8fRryy3dD8HS7GCrRB4dY-qvHq7eRrDdJz8C-HHEZTRt-H843A7O0aOLgXJoR6P0Yk3EbBBJcmFsEGHy9G9PfmVU6LyCpRcuv9be6YLfBI-ahU3XvkSLTLEV0YIs9HRY24Ue71_vWp7G_IawDuUMjQZgTT-ATwvu97iE1ubfIwh-HQWF34Rfk7xvuyWgWftqWxfzCDqP1VIE088c8zCvGTIDb6P7TCC-PFOtOlb6xNy7TlyHNSnvDLCu8WJvc9r5S0P_6Byl9zHVwxmXtGira_tu-7ZkqaVHHrLVwoV0-f4bTRnXn7nvXvvLdOHCTZcl9zh9pyNYVDlLy6arQcfLY09B6pqdaPxw8lt6hrs9D5vwVzknt9PANW7wod9-hN8RBX0oAf0qOyF2Kn6Bt4wWDTO9-JbNebHmGfq0rkaFHepugz4XS40-NfCYwo9Wn3H0uw5Wq0r4SWxhdU9Vbrgb0SCl4am46O_smKVsM5VVSA_3IqvYSWLq1OpqIasJthonDq8ZAxX9w6i3nXjea-T3kD-eoZxUnB5hSnMN62HoyvBSTIlYjUtw_bRHNmnb7oqneL2zCNGZBPIdOf9QByYOCifJvK27AFeSm6xMEZPMK1MRMr4K64_9Jj61wAZTtYp6tIMig8plHwnxpqjrMBYGvjH_L5YHcnDl784IujzwjYfuTqrD8BzlaTTcbCYl4Jxk5DKP6Jlhg7srv6MwO-Y4jvBfOq9PSLR8nN0KdNMHN-OV46b_aRIyefpAfkJIuAWKc2Qoh3jd6yPObVXwP9qYJ21DnaMuBigxt7c6m71z_3NKiSV1d-O9XNPwbwQ8mlkKwc236KnlSRmLli1HMQIJKJhiC5XI1gBXP6XbSi3bvVXgeEj9cVpBnXFkvkRxfXxZ3jZZG97VNuKuH0LSaeHatZIR8IcdeRYRieyAKFyQlXAhWqqTibaZKBx6SCs-9vSi7zojEkng4Aodgjpf79zH_FRQRWg9xufftZpJlAclE1T5119uCMcwDkOngL3xVbTY7S9Ak9lB7lDyuDKm2kIilbuhtrsLx3XJqLEsH0ub8PUuaBmnOZRz8fGikUoy2ZU9F0fIm3_Advymdohu8jjMQ6x4Cp3YWNusn-FblEC8HcdOgdQOw63ZRwJuij1xdLQlTgx3qsRf8xl2zOMnF3nUWodULk2ynTpkYjCp4lbQGGboZ1lxbrKeMeybvr2hmjANtYQz2ahRdMGoVDB2HSruEOrYHx7lFIk5EVaOBhqLigBkLFw5RtDMMo3x8SdLBkNzP2C3oYTy_iGaTwpSRktabgDTmCySLPXGXWiRy0AVTGwozvLhPl1xz-14vlidGvfxcW-n2ahfpN9kqM3MyB5GT85wq-aYxyYi6m3eF2Gek6xSmtVRhCwWnXlUysfawsCQDK_Bio9nEJiYF9WVHjKXvB7ZKq8dQy0IBWrXFz2p04JdBl6s9rzXUvTWtfJUePRh0gQqV24iFCRkg86_WpLXpgPMg91AXwurgrhgJjfUKhxeyl0Riej_hVndAF16H-lz0-N4mDhyy2liRTX56dlC4cWTv2DU4iPtLOarfCna_m7QTdzyeidkcNzTjdtCkTAciZaA_8HZifDbjyIJeCTHBHoIZqfO1VaRiXytx5gEdtt-DQSanj4EA5cvlRLF1xRD7VGI5FTYR-mua1xdw0xpPj28GQ9Tjh8fi2a1WbwRzmjKnOAPwJyG5YelMRHgYnpLqqmESNuLjgfvlAlFmXqPjaJ8FSiPKZEFpwoy2lH_cSx4lYprq2Z9evdY6yaP6lI0lDi2WF3ddCzEeWj2MMg1Rz7fqYzjxum4RINbugJNeck584dTJnESySSZCqQ67m_vd6QE4u6ujB6yzu7TpXc_sOG2AS3r39XtuiPn570r8Z6LWUfdv8WnyRHJ_P5Sf4YAZ8LrGRuz6UHyKesL3bQnHvj0tv9PlOUv9hxemQcfkDi0nrN_3E3HGXbFHQLJ-9RHuVgE0SvL2JIA0aOfpIl69eN8CkBRHVvFnmfKh6OzFDMToBXkhczBWAmCW9nwGVARd_7yjWpu9y8oeAL3yuPXUvJQTDvjLxFHvGntNISf8KM5g9SW_QIxzUFClw2qlqhRLo29TFiSsVQM-hxSu05Js_fGocSek4nkLAotNpCG1AAoWP8TeI1NpLZVRwwzngaBijL_AbIoeFyTY31XdUztEQsi0dvBpu0iLd68GBTKOdeFeZ2vo9AFa4OwTFZ7SATUYRMgixzLlcuWqn60JKdWSQgSPCTLBHHZfgkmfxXLLwT65GEHlwuI8pb20xBeorUhbs3cc9LqXGnuwno5K5v0LaeFailxaBMl4fcgoBDj8TpdKqFKrcsIPol6suVKZRW_drrkc18p5Nt_MR5Xnd6D9SrkXYOUlc8c7gRrnAxE","DataProtected":true}

View File

@ -19,6 +19,7 @@ using Microsoft.AspNetCore.Mvc.ApiExplorer;
using Passenger;
using Passenger.Data;
using Passenger.Extensions;
using Passenger.GrpcServer.Services;
using Prometheus;
using Serilog;
@ -51,7 +52,6 @@ builder.Services.AddGrpc(options =>
{
options.Interceptors.Add<GrpcExceptionInterceptor>();
});
builder.Services.AddMagicOnion();
SnowFlakIdGenerator.Configure(2);
@ -78,7 +78,7 @@ app.UseEndpoints(endpoints =>
{
endpoints.MapControllers();
endpoints.MapMetrics();
endpoints.MapMagicOnionService();
endpoints.MapGrpcService<PassengerGrpcServices>();
});
app.MapGet("/", x => x.Response.WriteAsync(appOptions.Name));

View File

@ -3,8 +3,6 @@ using BuildingBlocks.Core;
using BuildingBlocks.Core.Event;
using Passenger.Passengers.Events.Domain;
using Passenger.Passengers.Features.CompleteRegisterPassenger.Reads;
using Passenger.Passengers.Models;
using PassengerType = BuildingBlocks.Contracts.Grpc.PassengerType;
namespace Passenger;
@ -26,7 +24,7 @@ public sealed class EventMapper : IEventMapper
{
PassengerRegistrationCompletedDomainEvent e => new CompleteRegisterPassengerMongoCommand(e.Id, e.PassportNumber, e.Name, e.PassengerType,
e.Age, e.IsDeleted),
PassengerCreatedDomainEvent e => new CompleteRegisterPassengerMongoCommand(e.Id, e.PassportNumber, e.Name, Passengers.Models.PassengerType.Unknown,
PassengerCreatedDomainEvent e => new CompleteRegisterPassengerMongoCommand(e.Id, e.PassportNumber, e.Name, Passengers.Enums.PassengerType.Unknown,
0, e.IsDeleted),
_ => null
};

View File

@ -1,24 +0,0 @@
using BuildingBlocks.Contracts.Grpc;
using MagicOnion;
using MagicOnion.Server;
using Mapster;
using MediatR;
using Passenger.Passengers.Features.GetPassengerById;
namespace Passenger.GrpcServer;
public class PassengerGrpcService : ServiceBase<IPassengerGrpcService>, IPassengerGrpcService
{
private readonly IMediator _mediator;
public PassengerGrpcService(IMediator mediator)
{
_mediator = mediator;
}
public async UnaryResult<PassengerResponseDto> GetById(long id)
{
var result = await _mediator.Send(new GetPassengerQueryById(id));
return result.Adapt<PassengerResponseDto>();
}
}

View File

@ -0,0 +1,30 @@
syntax = "proto3";
package passenger;
service PassengerGrpcService {
rpc GetById (GetByIdRequest) returns (PassengerResponse);
}
message GetByIdRequest {
int64 Id = 1;
}
message PassengerResponse {
int64 Id = 1;
string Name = 2;
string PassportNumber = 3;
PassengerType PassengerType = 4;
int32 Age = 5;
string Email = 6;
}
enum PassengerType {
PASSENGER_TYPE_UNKNOWN = 0;
PASSENGER_TYPE_MALE = 1;
PASSENGER_TYPE_FEMALE = 2;
PASSENGER_TYPE_BABY = 3;
}

View File

@ -0,0 +1,22 @@
using Grpc.Core;
using Mapster;
using MediatR;
using Passenger.Passengers.Features.GetPassengerById;
namespace Passenger.GrpcServer.Services;
public class PassengerGrpcServices : PassengerGrpcService.PassengerGrpcServiceBase
{
private readonly IMediator _mediator;
public PassengerGrpcServices(IMediator mediator)
{
_mediator = mediator;
}
public override async Task<PassengerResponse> GetById(GetByIdRequest request, ServerCallContext context)
{
var result = await _mediator.Send(new GetPassengerQueryById(request.Id));
return result.Adapt<PassengerResponse>();
}
}

View File

@ -6,6 +6,7 @@
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Grpc.AspNetCore" Version="2.49.0" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="6.0.1">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
@ -21,4 +22,8 @@
<ProjectReference Include="..\..\..\..\BuildingBlocks\BuildingBlocks.csproj" />
</ItemGroup>
<ItemGroup>
<Protobuf Include="GrpcServer\Protos\passenger.proto" GrpcServices="Both" />
</ItemGroup>
</Project>

View File

@ -0,0 +1,9 @@
namespace Passenger.Passengers.Enums;
public enum PassengerType
{
Unknown = 0,
Male,
Female,
Baby
}

View File

@ -4,4 +4,4 @@ using Passenger.Passengers.Models;
namespace Passenger.Passengers.Events.Domain;
public record PassengerRegistrationCompletedDomainEvent(long Id, string Name, string PassportNumber,
PassengerType PassengerType, int Age, bool IsDeleted = false) : IDomainEvent;
Enums.PassengerType PassengerType, int Age, bool IsDeleted = false) : IDomainEvent;

View File

@ -6,7 +6,7 @@ using Passenger.Passengers.Models;
namespace Passenger.Passengers.Features.CompleteRegisterPassenger;
public record CompleteRegisterPassengerCommand(string PassportNumber, PassengerType PassengerType, int Age) : ICommand<PassengerResponseDto>, IInternalCommand
public record CompleteRegisterPassengerCommand(string PassportNumber, Enums.PassengerType PassengerType, int Age) : ICommand<PassengerResponseDto>, IInternalCommand
{
public long Id { get; init; } = SnowFlakIdGenerator.NewId();
}

View File

@ -12,10 +12,10 @@ public class CompleteRegisterPassengerCommandValidator : AbstractValidator<Compl
RuleFor(x => x.PassportNumber).NotNull().WithMessage("The PassportNumber is required!");
RuleFor(x => x.Age).GreaterThan(0).WithMessage("The Age must be greater than 0!");
RuleFor(x => x.PassengerType).Must(p => p.GetType().IsEnum &&
p == PassengerType.Baby ||
p == PassengerType.Female ||
p == PassengerType.Male ||
p == PassengerType.Unknown)
p == Enums.PassengerType.Baby ||
p == Enums.PassengerType.Female ||
p == Enums.PassengerType.Male ||
p == Enums.PassengerType.Unknown)
.WithMessage("PassengerType must be Male, Female, Baby or Unknown");
}
}

View File

@ -6,7 +6,7 @@ namespace Passenger.Passengers.Features.CompleteRegisterPassenger.Reads;
public class CompleteRegisterPassengerMongoCommand : InternalCommand
{
public CompleteRegisterPassengerMongoCommand(long id, string passportNumber, string name,
PassengerType passengerType, int age, bool isDeleted)
Enums.PassengerType passengerType, int age, bool isDeleted)
{
Id = id;
PassportNumber = passportNumber;
@ -18,7 +18,7 @@ public class CompleteRegisterPassengerMongoCommand : InternalCommand
public string PassportNumber { get; }
public string Name { get; }
public PassengerType PassengerType { get; }
public Enums.PassengerType PassengerType { get; }
public int Age { get; }
public bool IsDeleted { get; }
}

View File

@ -5,7 +5,7 @@ namespace Passenger.Passengers.Models;
public record Passenger : Aggregate<long>
{
public Passenger CompleteRegistrationPassenger(long id, string name, string passportNumber, PassengerType passengerType, int age, bool isDeleted = false)
public Passenger CompleteRegistrationPassenger(long id, string name, string passportNumber, Enums.PassengerType passengerType, int age, bool isDeleted = false)
{
var passenger = new Passenger
{
@ -40,6 +40,6 @@ public record Passenger : Aggregate<long>
public string PassportNumber { get; private set; }
public string Name { get; private set; }
public PassengerType PassengerType { get; private set; }
public Enums.PassengerType PassengerType { get; private set; }
public int Age { get; private set; }
}

View File

@ -1,9 +0,0 @@
namespace Passenger.Passengers.Models;
public enum PassengerType
{
Male,
Female,
Baby,
Unknown
}

View File

@ -6,7 +6,7 @@ public class PassengerReadModel
public long PassengerId { get; init; }
public string PassportNumber { get; private set; }
public string Name { get; private set; }
public PassengerType PassengerType { get; private set; }
public Enums.PassengerType PassengerType { get; private set; }
public int Age { get; private set; }
public bool IsDeleted { get; init; }
}

View File

@ -1,5 +1,6 @@
using AutoBogus;
using BuildingBlocks.IdsGenerator;
using Passenger.Passengers.Enums;
using Passenger.Passengers.Features.CompleteRegisterPassenger;
using Passenger.Passengers.Models;

View File

@ -1,10 +1,10 @@
using AutoBogus;
using BuildingBlocks.Contracts.Grpc;
using BuildingBlocks.IdsGenerator;
using Passenger;
public class FakePassengerResponseDto : AutoFaker<PassengerResponseDto>
public class FakePassengerResponse : AutoFaker<PassengerResponse>
{
public FakePassengerResponseDto()
public FakePassengerResponse()
{
RuleFor(r => r.Id, _ => SnowFlakIdGenerator.NewId());
}

View File

@ -13,7 +13,8 @@ public class CompleteRegisterPassengerTests : IntegrationTestBase<Program, Passe
{
private readonly ITestHarness _testHarness;
public CompleteRegisterPassengerTests(IntegrationTestFixture<Program, PassengerDbContext> integrationTestFixture) : base(integrationTestFixture)
public CompleteRegisterPassengerTests(IntegrationTestFixture<Program, PassengerDbContext> integrationTestFixture) :
base(integrationTestFixture)
{
_testHarness = Fixture.TestHarness;
}
@ -36,7 +37,7 @@ public class CompleteRegisterPassengerTests : IntegrationTestBase<Program, Passe
response.Should().NotBeNull();
response?.Name.Should().Be(userCreated.Name);
response?.PassportNumber.Should().Be(command.PassportNumber);
response?.PassengerType.Should().Be(command.PassengerType);
response?.PassengerType.ToString().Should().Be(command.PassengerType.ToString());
response?.Age.Should().Be(command.Age);
}
}

View File

@ -1,16 +1,12 @@
using System.Threading.Tasks;
using BuildingBlocks.Contracts.EventBus.Messages;
using BuildingBlocks.Contracts.Grpc;
using BuildingBlocks.TestBase;
using FluentAssertions;
using Grpc.Net.Client;
using Integration.Test.Fakes;
using MagicOnion;
using MagicOnion.Client;
using MassTransit.Testing;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.DependencyInjection.Extensions;
using NSubstitute;
using Passenger;
using Passenger.Data;
using Passenger.Passengers.Features.GetPassengerById;
using Xunit;
@ -22,7 +18,8 @@ public class GetPassengerByIdTests : IntegrationTestBase<Program, PassengerDbCon
private readonly ITestHarness _testHarness;
private readonly GrpcChannel _channel;
public GetPassengerByIdTests(IntegrationTestFixture<Program, PassengerDbContext> integrationTestFixture) : base(integrationTestFixture)
public GetPassengerByIdTests(IntegrationTestFixture<Program, PassengerDbContext> integrationTestFixture) : base(
integrationTestFixture)
{
_channel = Fixture.Channel;
_testHarness = Fixture.TestHarness;
@ -30,7 +27,6 @@ public class GetPassengerByIdTests : IntegrationTestBase<Program, PassengerDbCon
protected override void RegisterTestsServices(IServiceCollection services)
{
MockPassengerGrpcServices(services);
}
@ -64,25 +60,13 @@ public class GetPassengerByIdTests : IntegrationTestBase<Program, PassengerDbCon
var passengerEntity = FakePassengerCreated.Generate(userCreated);
await Fixture.InsertAsync(passengerEntity);
var passengerGrpcClient = MagicOnionClient.Create<IPassengerGrpcService>(_channel);
var passengerGrpcClient = new PassengerGrpcService.PassengerGrpcServiceClient(_channel);
// Act
var response = await passengerGrpcClient.GetById(passengerEntity.Id);
var response = await passengerGrpcClient.GetByIdAsync(new GetByIdRequest {Id = passengerEntity.Id});
// Assert
response?.Should().NotBeNull();
response?.Id.Should().Be(passengerEntity.Id);
}
private void MockPassengerGrpcServices(IServiceCollection services)
{
services.Replace(ServiceDescriptor.Singleton(x =>
{
var mock = Substitute.For<IPassengerGrpcService>();
mock.GetById(Arg.Any<long>())
.Returns(new UnaryResult<PassengerResponseDto>(new FakePassengerResponseDto().Generate()));
return mock;
}));
}
}