Newer
Older
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
Libraries Used:
1. NLog. We used NLog for logging. By default all logs with level Informational are saved. This setting can be changed in the appsettings.json
nlog.config contains the file that is used to configure our NLog.
2. Automapper: This is a great utility tool that allows mapping one model or DTO (Data Transfer Object) to another. It helps to us avoid repetitve code.
To use, we first add a line in the `ConfigureServices` method of our `Startup.cs`:
`services.AddAutoMapper(typeof(Startup));`
The we declare a class that contains our mappings:
```
public class FlightBookingProfile : Profile
{
public FlightBookingProfile()
{
CreateMap<FlightFare, FlightFareDTO>()
.ForMember(dest => dest.AvailableSeats, opt => opt.MapFrom(src => src.SeatCapacity - src.SeatReserved))
.ForMember(dest => dest.FlightNumber, opt => opt.MapFrom(src => src.FlightInformation.FlightNumber));
}
}
```
3. We also installed NewtonSoftJson library for working with Json input and output.
4. Swashbuckle was also installed so we can generate Swagger documentation from our controllers.
5. Some libraries for using Jwt for authentication were also added.
## Program Flow.
Controller -> Services -> Repository -> EntityFramework -> Data
### Controllers
They represent our endpoints based on the MVC pattern.
Each controller has been decorated with attributes that makes it easy to read what input and output to expect.
For example, our `ReservedSeatController`:
```
[Route("api/[controller]")]
[ApiController]
public class SeatsController : ControllerBase
{
private readonly IReservedSeatService _service;
public SeatsController(IReservedSeatService service)
{
_service = service;
}
[HttpGet]
[ProducesResponseType(StatusCodes.Status200OK, Type = typeof(IEnumerable<ReservedSeatDTO>))]
[ProducesResponseType(StatusCodes.Status400BadRequest, Type = typeof(ProblemDetails))]
public IActionResult GetAvailableSeats([FromQuery] string flightNumber)
{
ServiceResponse<IEnumerable<ReservedSeatDTO>> result = _service.GetAvailableSeatsByFlightNumber(flightNumber);
return result.FormatResponse();
}
[HttpPost]
[Consumes(MediaTypeNames.Application.Json)]
[ProducesResponseType(StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status422UnprocessableEntity, Type = typeof(ProblemDetails))]
[ProducesResponseType(StatusCodes.Status400BadRequest, Type = typeof(ProblemDetails))]
public async Task<IActionResult> ReserveSeat([FromBody] ReservedSeatRequestDTO requestDTO)
{
ServiceResponse<string> result = await _service.ReserveSeatAsync(requestDTO);
return result.FormatResponse();
}
}
```
In the above, we have to methods in the controller which corresponds to 2 endpoints.
First, we inject our `IReservedSeatService` into the controller.
The first endpoint is a GET endpoint:
And the attributes show that it can return a 200, 400 and 422 response. The 400 and 422 responses are in types of ProblemDetails which is a specification for returning API responses.
This part: `([FromQuery] string flightNumber)` indicates that a query string named ``flightNumber`` is passed to the endpoint generated by this method.
The `_service` returns a type of `ServiceResponse` which is then formatted to return the appropriate response. More on this later
The second endpoint is a POST endpoint:
The attributes show it accepts a body (`[FromBody]`) of type application/json (`MediaTypeNames.Application.Json`) and response produced are 200, 422, 400.
### Services
They represent the logic for our app. We this pattern to make it easy to test the services.
By abstracting the services to use Interfaces, we can easily write tests that can be flexible.
We rely on the built-in Dependency Injection framework to resolve service dependencies.
In each service, we inject Repository classes and other Services.
The services also have to be registered and we do this by declaring a static class that will do the registration:
```
public static class ServicesModule
{
public static void AddServices(this IServiceCollection services)
{
services.AddScoped<IBookingService, BookingService>();
services.AddScoped<IBookingOrderService, BookingOrderService>();
services.AddScoped<IReservedSeatService, ReservedSeatService>();
services.AddScoped<IFlightFareService, FlightFareService>();
services.AddScoped<IFlightService, FlightService>();
services.AddScoped<IStripeService, StripeService>();
}
}
```
Here: We register each service as a Scoped dependency. Scoped means that the service will only be active for the duration of a request (i.e Http Request).
Then we call the method in our Startup.cs:
` services.AddServices();`
Each service also returns a type `ServiceResponse<T>` where T is a class
The `ServiceResponse<T>` is declared in the `ResponseFormatter.cs`
```
public class ServiceResponse<T>
{
public InternalCode ServiceCode { get; set; } = InternalCode.Failed;
public T Data { get; set; }// = null;
public string Message { get; set; }
public ServiceResponse(T data, InternalCode serviceCode = InternalCode.Failed, string message = "")
{
Message = message;
ServiceCode = serviceCode;
Data = data;
}
}
```
We declare the `ServiceResponse` to be a generic class. It's properties are:
`InternalCode` that indicates the status. InternalCode is an enum
`Data`: which is a type of `T`
`Message`: optional message
The `FormatResponse` is an extension method that accepts a `ServiceResponse<T>` and then checks the Internal Code and uses that to
return an appropriate response.
This makes it easy for us to return a uniform type of response.
2xx responses return a simple 2xx and an optional data
4xx and 5xx responses return a type of `ProblemDetails`.
The helps to give more context to the nature of the response.
### Repository
We use a Repository pattern that wraps EntityFramework unit of work pattern. The class is declared as a Generic class so that we can pass any model to it.
We then declare helper methods that in turn call EntityFramework methods:
```
public class GenericRepository<T> : IGenericRepository<T> where T : class, new()
{
//Responses: failed=0, success=1
//IEnumerable iterates over an in-memory collection while IQueryable does so on the DB
// call to .ToList to enable instant query against DB
protected FlightBookingContext _db;
protected ILogger _logger;
//...omitted for brevity
public async Task<T?> GetByGuidAsync(Guid id)
{
var entity = await _db.Set<T>().FindAsync(id);
return entity;
}
}
```
In the aboved, we pass a type of `DbContext` and a `Logger`.
In the `GetByGuidAsync()` method, we use the `_db` of that model to find the data.
We must also inject the Repository of each model so we can use anywhere in our project:
```
public static class RepositoryModule
{
public static void AddRepository(this IServiceCollection services)
{
services.AddScoped<IGenericRepository<Booking>, GenericRepository<Booking>>();
services.AddScoped<IGenericRepository<FlightInformation>, GenericRepository<FlightInformation>>();
services.AddScoped<IGenericRepository<FlightFare>, GenericRepository<FlightFare>>();
services.AddScoped<IGenericRepository<Payment>, GenericRepository<Payment>>();
services.AddScoped<IGenericRepository<BookingOrder>, GenericRepository<BookingOrder>>();
services.AddScoped<IGenericRepository<ReservedSeat>, GenericRepository<ReservedSeat>>();
}
}
```
Just like in the Services, we also add the dependencies as a Scoped service.
This is important because we want to quickly use a DbContext which in turn holds a connection to the database. If we use it and quickly return it to the connection pool, we can avoid issues with resource exhaustion.
### Data
We are using MySql. So we must install the following MySqlConnector and the Pomelo.EntityFramework.MySql libraries.
Let's add these 2 lines to our Package reference.
1. `<PackageReference Include="MySqlConnector" Version="2.3.5" />`
2. `<PackageReference Include="Pomelo.EntityFrameworkCore.MySql" Version="8.0.1" />`
To use our model, we use classes to represent our data. Each class in the model is used to create tables in the database.
We declare properties of the class and also configure relationships.
The models are in `Model` folder.
We also wish to do some relations that are not automatically done by the framework.
These configurations are applied to the model and is used to configure how the tables should be created during table creation:
e.g:
```
public class BookingConfiguration : IEntityTypeConfiguration<Booking>
{
public void Configure(EntityTypeBuilder<Booking> entity)
{
entity.HasOne(d => d.BookingOrder).WithMany(p => p.Bookings)
.HasPrincipalKey(p => p.Id)
.HasForeignKey(d => d.BookingOrderId)
.OnDelete(DeleteBehavior.ClientSetNull);
entity.HasOne(d => d.FlightInformation).WithMany(p => p.Bookings)
.HasPrincipalKey(p => p.Id)
.HasForeignKey(d => d.FlightId)
.OnDelete(DeleteBehavior.ClientSetNull);
entity.HasOne(d => d.FlightFare).WithMany(p => p.Bookings)
.HasPrincipalKey(p => p.Id)
.HasForeignKey(d => d.FlightFareId)
.OnDelete(DeleteBehavior.ClientSetNull);
}
}
```
The above code for the `Booking.cs` model configures the foreign key relationship.
To make use of our models, we create a `FlightBookingDbContext.cs` where we declare all our models and also apply the configurations:
We also added a change in the `OnModelCreating()` method of the `FlightDbContext` to make sure that when a model (data in the database) is updated, the `UpdatedAt` and `CreatedAt` is saved as `UTC`.
```
foreach (var entityType in modelBuilder.Model.GetEntityTypes())
{
foreach (var property in entityType.GetProperties())
{
if (property.ClrType == typeof(DateTime))
{
modelBuilder.Entity(entityType.ClrType)
.Property<DateTime>(property.Name)
.HasConversion(
v => v.ToUniversalTime(),
v => DateTime.SpecifyKind(v, DateTimeKind.Utc));
}
else if (property.ClrType == typeof(DateTime?))
{
modelBuilder.Entity(entityType.ClrType)
.Property<DateTime?>(property.Name)
.HasConversion(
v => v.HasValue ? v.Value.ToUniversalTime() : v,
v => v.HasValue ? DateTime.SpecifyKind(v.Value, DateTimeKind.Utc) : v);
}
}
}
```
To get our database schema updated:
Once we have configured our models, and added the `FlightBookingContext`.
We also configure the `FlightBookingContext` in the `ConfigureService` method in our `Startup.cs` and initialize it.
```
string mysqlConnectionString = Configuration.GetConnectionString("FlightBookingServiceDb_Mysql")!;
var mySqlServerVersion = new MySqlServerVersion(new Version(8, 0, 36));
services.AddDbContext<FlightBookingContext>(options =>
{
options.UseMySql(mysqlConnectionString, mySqlServerVersion, opt => opt.EnableRetryOnFailure())
.LogTo(Console.WriteLine, LogLevel.Warning)
.EnableSensitiveDataLogging()
.EnableDetailedErrors();
});
```
The code indicates that we get our connection string from appsettings.json
To get the version of MySql, run this on the MySql server:
> SELECT VERSION();
Once we have our models, configuration and DbContext ready, we need to run Migrations.
Migrations take a snapshot of our models and configuration and defines how they will be used to updated the database schema at that point in time.
To run migrations, open the Package Manager Console and run:
> Add-Migration InitialCreate
This will create a migration named `InitialCreateMysql`.
We then run
>Update-Database
This will configure the database with the code generated in `InitialCreateMysql`.
Whenever we make a change to our models and configuration, we must create a new migration and update our database so that the database schema is kept updated
If we wish to use a different database, we first configure the DbContext. E.g if using Sql Server:
```
services.AddDbContext<FlightBookingContext>(options =>
{
options.UseSqlServer(Configuration.GetConnectionString("FlightBookingServiceDb"));
});
```
Then we must create a new migration. __It is important to note that Migrations are scoped to a database. So we need a new migration when we switched to a different database__
To run migrations, open the Package Manager Console and run:
> Add-Migration InitialCreateMysql
This will create a migration named `InitialCreateMysql`.
We then run
>Update-Database
The `ConfigSettings` contains a strongly type mapping of the content of appsettings.json and helps to avoid errors
Our DTO folder contains DTOs for models. These are basically data structure we use to transfer data around
## Running this Project
1. Open the solution in Visual Studio. Automatically, nugets are installed.
2. Start your MySql Server. The project currently uses MySql version 8.0.36. If you wish to use a different version, update the version in ``Startup.cs`
```
var mySqlServerVersion = new MySqlServerVersion(new Version(8, 0, 36));
```
3. Add your database connection string in appsettings.json and appsettings.Development.json
3. In Visual Studio, go to Tools -> Nuget Package Manager -> Package Manager Console. Click the Package Manager Console and it open at the bottom
4. In the Package Manager Console, Run
> Update-Database
The Database and all tables will be created using the `InitialMysqlMigration` that is included in the Migrations folder.
5. Press F5 or run the project by clicking the play button.
6. Included in the project in the `Program.cs` is a method that seeds the database with some default data. The `DatabaseSeeding.cs` contains the code that adds `FlightInformation`, `FlightFares` and `ReservedSeats`
Once you run the project for the first time, if all goes well, the data is added to the database.
Useful Links:
https://github.com/PomeloFoundation/Pomelo.EntityFrameworkCore.MySql
https://learn.microsoft.com/en-us/ef/core/managing-schemas/migrations/providers?tabs=dotnet-core-cli
https://learn.microsoft.com/en-us/ef/core/modeling/relationships
https://learn.microsoft.com/en-us/ef/ef6/fundamentals/working-with-dbcontext