refactor: change folder structure
This commit is contained in:
@@ -0,0 +1,280 @@
|
||||
# Overview of Clean Architecture
|
||||
|
||||
Clean Architecture is a software design pattern that promotes separation of concerns, testability, and maintainability. It structures an application into layers, ensuring dependencies flow inwards (towards business logic) and that the core logic is independent of frameworks and external dependencies.
|
||||
|
||||
---
|
||||
|
||||
## **Clean Architecture Layers**
|
||||
|
||||
Clean Architecture consists of **four main layers**:
|
||||
|
||||
1. **Domain Layer (Core Business Rules)**
|
||||
2. **Application Layer (Use Cases)**
|
||||
3. **Infrastructure Layer (External Services & Data Access)**
|
||||
4. **Presentation Layer (UI & API)**
|
||||
|
||||
### **1. Domain Layer (Enterprise Business Rules)**
|
||||
|
||||
- **Purpose**: Contains core business logic and rules that should be independent of frameworks and external systems.
|
||||
- **Key Components**:
|
||||
- **Entities** (Aggregates, Value Objects) → Represent business models.
|
||||
- **Domain Events** → Events triggered by business logic.
|
||||
- **Domain Services** → Logic that spans multiple entities.
|
||||
- **Dependencies**: No external dependencies (completely independent).
|
||||
- **Example**:
|
||||
|
||||
```csharp
|
||||
public class Product
|
||||
{
|
||||
public int Id { get; private set; }
|
||||
public string Name { get; private set; }
|
||||
public decimal Price { get; private set; }
|
||||
|
||||
public Product(string name, decimal price)
|
||||
{
|
||||
Name = name;
|
||||
Price = price;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### **2. Application Layer (Use Cases & Business Logic)**
|
||||
|
||||
- **Purpose**: Implements application-specific business logic, coordinating workflows, and executing use cases.
|
||||
- **Key Components**:
|
||||
- **Use Cases (Application Services)** → Define what the application does.
|
||||
- **Commands & Queries** → For operations (CQRS pattern).
|
||||
- **DTOs (Data Transfer Objects)** → Pass data without exposing domain models.
|
||||
- **Interfaces for Repositories & Services** → Abstract dependencies (repositories, external APIs).
|
||||
- **Dependencies**:
|
||||
- Can reference **Domain Layer**.
|
||||
- No dependency on Infrastructure or Presentation layers.
|
||||
- **Example (Use Case)**:
|
||||
|
||||
```csharp
|
||||
public class CreateProductCommand
|
||||
{
|
||||
public string Name { get; set; }
|
||||
public decimal Price { get; set; }
|
||||
}
|
||||
|
||||
public class ProductService
|
||||
{
|
||||
private readonly IProductRepository _productRepository;
|
||||
|
||||
public ProductService(IProductRepository productRepository)
|
||||
{
|
||||
_productRepository = productRepository;
|
||||
}
|
||||
|
||||
public async Task<int> CreateProduct(CreateProductCommand command)
|
||||
{
|
||||
var product = new Product(command.Name, command.Price);
|
||||
await _productRepository.AddAsync(product);
|
||||
return product.Id;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
|
||||
---
|
||||
|
||||
### **3. Infrastructure Layer (Data & External Services)**
|
||||
|
||||
- **Purpose**: Provides implementations for repositories, external APIs, database access, logging, and file storage.
|
||||
- **Key Components**:
|
||||
- **Repositories (EF Core, Dapper, etc.)** → Implement database operations.
|
||||
- **External Service Integrations** → Calls to third-party APIs.
|
||||
- **Logging, Email, File Storage** → External services.
|
||||
- **Dependencies**:
|
||||
- References **Application Layer** (implementing interfaces).
|
||||
- No direct reference to **Presentation Layer**.
|
||||
- **Example (EF Core Repository)**:
|
||||
|
||||
```csharp
|
||||
public class ProductRepository : IProductRepository
|
||||
{
|
||||
private readonly ApplicationDbContext _context;
|
||||
|
||||
public ProductRepository(ApplicationDbContext context)
|
||||
{
|
||||
_context = context;
|
||||
}
|
||||
|
||||
public async Task AddAsync(Product product)
|
||||
{
|
||||
_context.Products.Add(product);
|
||||
await _context.SaveChangesAsync();
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
|
||||
---
|
||||
|
||||
### **4. Presentation Layer (UI & API)**
|
||||
|
||||
- **Purpose**: Handles HTTP requests, user interactions, and returns responses.
|
||||
- **Key Components**:
|
||||
- **Controllers (Web API in ASP.NET Core)** → Handle HTTP requests.
|
||||
- **Views (Razor Pages, React, Blazor, etc.)** → UI rendering.
|
||||
- **DTO Mapping (AutoMapper, MediatR, etc.)** → Maps domain models to response objects.
|
||||
- **Dependencies**:
|
||||
- References **Application Layer** (calls use cases).
|
||||
- Should not directly reference **Infrastructure Layer**.
|
||||
- **Example (Controller in ASP.NET Core)**:
|
||||
|
||||
```csharp
|
||||
[ApiController]
|
||||
[Route("api/products")]
|
||||
public class ProductController : ControllerBase
|
||||
{
|
||||
private readonly ProductService _productService;
|
||||
|
||||
public ProductController(ProductService productService)
|
||||
{
|
||||
_productService = productService;
|
||||
}
|
||||
|
||||
[HttpPost]
|
||||
public async Task<IActionResult> CreateProduct([FromBody] CreateProductCommand command)
|
||||
{
|
||||
var productId = await _productService.CreateProduct(command);
|
||||
return CreatedAtAction(nameof(CreateProduct), new { id = productId });
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
|
||||
---
|
||||
|
||||
## **Dependencies Flow**
|
||||
|
||||
- **Presentation Layer** depends on **Application Layer**.
|
||||
- **Application Layer** depends on **Domain Layer**.
|
||||
- **Infrastructure Layer** depends on **Application Layer**.
|
||||
- **Domain Layer** has NO dependencies.
|
||||
|
||||
This ensures the **business logic is central** and **not coupled** to frameworks, databases, or UI.
|
||||
|
||||
---
|
||||
|
||||
## **Example ASP.NET Core Clean Architecture Folder Structure**
|
||||
|
||||
```
|
||||
/src
|
||||
/Domain
|
||||
/Entities
|
||||
/ValueObjects
|
||||
/DomainServices
|
||||
/Application
|
||||
/Interfaces
|
||||
/Services
|
||||
/DTOs
|
||||
/UseCases
|
||||
/Infrastructure
|
||||
/Persistence
|
||||
/Repositories
|
||||
/ExternalServices
|
||||
/Presentation
|
||||
/Controllers
|
||||
/Views (if MVC)
|
||||
/ReactApp (if using React)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
# Backend Project Structure
|
||||
## **Solution Name: MyApp**
|
||||
|
||||
📂 **MyApp.sln** (Solution file)
|
||||
|
||||
### **1. Presentation Layer** (📂 `MyApp.WebAPI`)
|
||||
- **Project Type:** ASP .NET Core Web API - with default settings
|
||||
- **Purpose:** Exposes the application via a web API.
|
||||
|
||||
📂 `MyApp.WebAPI`
|
||||
- 📂 **Controllers** – Defines API endpoints.
|
||||
- 📂 **Middlewares** – Implements custom middleware (logging, exception handling).
|
||||
|
||||
### Dependencies:
|
||||
- Projects:
|
||||
- Application Project
|
||||
- Domain Project
|
||||
- Packages:
|
||||
- AutoMapper
|
||||
- Microsoft.AspNetCore.Authentication.JwtBearer
|
||||
- System.IdentityModel.Tokens.Jwt
|
||||
- Microsoft.EntityFrameworkCore.Design
|
||||
- Microsoft.EntityFrameworkCore.SqlServer
|
||||
- Microsoft.EntityFrameworkCore.Tools
|
||||
- Newtonsoft.Json
|
||||
---
|
||||
|
||||
### **2. Application Layer** (📂 `MyApp.Application`)
|
||||
- **Project Type:** C# Class Library - with default settings
|
||||
- **Purpose:** Contains the application logic, use cases, and service abstractions.
|
||||
|
||||
📂 `MyApp.Application`
|
||||
- 📂 **Interfaces** – Defines services like `IUserService`, `IOrderService`, etc.
|
||||
- 📂 **Services** – Defines services like `IUserService`, `IOrderService`, etc.
|
||||
- 📂 **DTOs** – Data Transfer Objects for input/output models.
|
||||
- 📂 **Mappers** – Maps domain models to DTOs (using AutoMapper or manual mapping).
|
||||
- 📂 **Validators** – Contains validation rules using FluentValidation.
|
||||
|
||||
### Dependencies:
|
||||
- Projects
|
||||
- Domain Project
|
||||
- Packages:
|
||||
- AutoMapper
|
||||
- Microsoft.Extensions.DependencyInjection
|
||||
---
|
||||
|
||||
### **3. Core Domain Layer** (📂 `MyApp.Domain`)
|
||||
- **Project Type:** C# Class Library - with default settings
|
||||
- **Purpose:** Represents the core business logic and entities without dependencies on infrastructure or frameworks.
|
||||
|
||||
📂 `MyApp.Domain`
|
||||
- 📂 **Aggregates** – Groups related entities following DDD principles.
|
||||
- 📂 **Framework** - **Interfaces** – Contains domain-level abstractions like repository interfaces and entity interface.
|
||||
- 📂 **Framework** - **Interfaces** – **Repositories**
|
||||
- 📂 **Framework** - **Base**
|
||||
- 📂 **Enums** – Defines domain-specific enumerations.
|
||||
- 📂 **Factories**
|
||||
### Dependencies:
|
||||
- Packages:
|
||||
- Microsoft.Extensions.DependencyInjection
|
||||
---
|
||||
|
||||
### **4. Infrastructure Layer** (📂 `MyApp.Infrastructure`)
|
||||
- **Project Type:** C# Class Library - with default settings
|
||||
- **Purpose:** Implements external dependencies such as databases, logging, APIs, and caching.
|
||||
|
||||
📂 `MyApp.Infrastructure`
|
||||
- 📂 **Framework** - **Base** – Implements `IRepository<TEntity>` for data access.
|
||||
- 📂 **Configurations** – Stores EF Core entity configurations.
|
||||
- 📂 **Services** - Repositories
|
||||
- 📂 **Migrations** - Auto Created
|
||||
- **DB-Context**
|
||||
|
||||
### Dependencies:
|
||||
- Projects:
|
||||
- Domain Project
|
||||
- Packages:
|
||||
- Microsoft.EntityFrameworkCore.SqlServer
|
||||
- Microsoft.EntityFrameworkCore.Design
|
||||
- Microsoft.EntityFrameworkCore.Proxies
|
||||
- Microsoft.EntityFrameworkCore.Tools
|
||||
---
|
||||
|
||||
### **Additional Projects (Optional)**
|
||||
|
||||
- 📂 `MyApp.Tests` – Unit and integration tests.
|
||||
- 📂 `MyApp.Shared` – Shared utilities (cross-cutting concerns like constants, helpers).
|
||||
|
||||
# Acknowledgements
|
||||
- ChatGPT for snippet refinement and explanations
|
||||
|
||||
|
||||
@@ -0,0 +1,149 @@
|
||||
## **Choosing the right datatype for integer values, specially IDs (Chat GPT):**
|
||||
|
||||
### **1️⃣ Integer Data Types (int, short, long) in C# and SQL Server**
|
||||
|
||||
| **C# Type** | **SQL Server Type** | **Size** | **Range** |
|
||||
| ----------- | ------------------- | -------- | ------------------------------------------------------- |
|
||||
| `byte` | `TINYINT` | 1 byte | 0 to 255 |
|
||||
| `short` | `SMALLINT` | 2 bytes | -32,768 to 32,767 |
|
||||
| `int` | `INT` | 4 bytes | -2,147,483,648 to 2,147,483,647 |
|
||||
| `long` | `BIGINT` | 8 bytes | -9,223,372,036,854,775,808 to 9,223,372,036,854,775,807 |
|
||||
|
||||
❗ **Important Notes:**
|
||||
|
||||
- The **ranges are the same in C# and SQL Server** because both use the same underlying storage.
|
||||
- `SMALLINT` and `TINYINT` **save space**, but be careful about hitting the limit.
|
||||
- `BIGINT` is needed only if you expect **billions** of records.
|
||||
---
|
||||
|
||||
### **2️⃣ What to Use for User ID, Ticket ID, Gender ID? (Some examples)**
|
||||
|
||||
| **Field** | **Recommended C# Type** | **SQL Server Type** | **Why?** |
|
||||
| ----------- | ----------------------- | ----------------------- | ----------------------------------------------------------- |
|
||||
| `UserId` | `int` or `long` | `INT` or `BIGINT` | `INT` is usually enough unless expecting billions of users. |
|
||||
| `TicketId` | `int` or `long` | `INT` or `BIGINT` | Use `BIGINT` if expecting massive ticket volumes. |
|
||||
| `GenderId` | `byte` or `short` | `TINYINT` or `SMALLINT` | Gender options are limited, so `TINYINT` is sufficient. |
|
||||
| `CompanyId` | `int` | `INT` | Companies are limited, `INT` is fine. |
|
||||
| `VehicleId` | `int` | `INT` | Use `INT`, as vehicle count is manageable. |
|
||||
| `Price` | `decimal(18,2)` | `DECIMAL(18,2)` | Avoid `float`/`double` due to rounding issues. |
|
||||
|
||||
---
|
||||
|
||||
### **3️⃣ Should I Use GUIDs for User IDs or Ticket IDs?**
|
||||
|
||||
- **Use `GUID` (`UNIQUEIDENTIFIER`) for IDs only if:**
|
||||
- Data is distributed across multiple databases.
|
||||
- Security is critical (e.g., preventing sequential guessing of IDs).
|
||||
- Otherwise, **stick with `int` or `long`** for performance.
|
||||
|
||||
📌 **Example in C# (EF Core Model):**
|
||||
|
||||
```csharp
|
||||
public class Ticket
|
||||
{
|
||||
public int TicketId { get; set; } // Primary key
|
||||
public int UserId { get; set; } // Foreign key
|
||||
public decimal Price { get; set; } // Use decimal for money
|
||||
public DateTime PurchaseDate { get; set; }
|
||||
}
|
||||
```
|
||||
|
||||
📌 **Fluent API (SQL Mapping)**
|
||||
|
||||
```csharp
|
||||
protected override void OnModelCreating(ModelBuilder modelBuilder)
|
||||
{
|
||||
modelBuilder.Entity<Ticket>()
|
||||
.Property(t => t.Price)
|
||||
.HasColumnType("DECIMAL(18,2)");
|
||||
}
|
||||
```
|
||||
|
||||
|
||||
|
||||
## **`required` keyword (Chat GPT) :
|
||||
|
||||
C# **IntelliSense** suggests adding `required` to string properties because of **nullable reference types (NRT)** introduced in **C# 8.0+**.
|
||||
|
||||
### **1️⃣ What Does `required` Do?**
|
||||
|
||||
- `required` **forces initialization** of the property when creating an object.
|
||||
- It is **not a data annotation** (like `[Required]` in EF Core), but a **C# keyword** that affects **compile-time checks**.
|
||||
|
||||
📌 **Example Without `required`**
|
||||
|
||||
```csharp
|
||||
public class User
|
||||
{
|
||||
public string Name { get; set; } // Warning: "Non-nullable property 'Name' is uninitialized"
|
||||
}
|
||||
```
|
||||
|
||||
🔴 **Problem**: The compiler warns that `Name` is not initialized.
|
||||
✅ **Fix**: Add `required` or initialize the property.
|
||||
|
||||
📌 **Example With `required`**
|
||||
|
||||
```csharp
|
||||
public class User
|
||||
{
|
||||
public required string Name { get; set; } // No warning
|
||||
}
|
||||
```
|
||||
|
||||
✅ **Effect**: You **must** provide `Name` when creating a `User` object.
|
||||
|
||||
```csharp
|
||||
var user = new User { Name = "Mehrdad" }; // ✅ Works
|
||||
var invalidUser = new User(); // ❌ Compilation Error: Name is required
|
||||
```
|
||||
|
||||
|
||||
|
||||
## **A note about strings in C# (Chat GPT) :**
|
||||
**`string` is nullable in C#**, but in **nullable reference types (C# 8+), `string` is treated as non-nullable unless explicitly marked `string?`**.
|
||||
- **`string`** → Default behavior (non-nullable by default in nullable context).
|
||||
- **`string?`** → Explicitly nullable.
|
||||
|
||||
## **Why virtual navigation properties? (Chat GPT):**
|
||||
|
||||
- If you mark a navigation property as `virtual`, EF Core **creates a proxy class** at runtime that overrides the property and loads related data **only when accessed**.
|
||||
- This is called **Lazy Loading**, meaning data is not fetched until needed.
|
||||
- If you don’t mark it as `virtual`, you **must** load relationships using `.Include()` (Eager Loading).
|
||||
|
||||
### **What Type Should Navigation Properties Be?**
|
||||
|
||||
| **Scenario** | **Recommended Type** | **Why?** |
|
||||
| --------------------------------------------------------------------- | ----------------------------- | --------------------------------------------------------------------- |
|
||||
| **Single reference** (e.g., `Ticket → Transportation`) | `virtual Transportation` | Represents a **one-to-one** or **many-to-one** relationship. |
|
||||
| **Collection of related entities** (e.g., `Transportation → Tickets`) | `virtual ICollection<Ticket>` | Best for **one-to-many** relationships, supports lazy loading. |
|
||||
| **Alternative for collections** | `virtual List<Ticket>` | Works the same, but **EF prefers `ICollection<T>`**. |
|
||||
| **Using `IEnumerable<T>`** | ❌ **Avoid** | EF **does not recognize** `IEnumerable<T>` for navigation properties. |
|
||||
### **One Scenario to look after if using lazy loading:**
|
||||
#### **What is the N+1 Query Problem?**
|
||||
|
||||
The **N+1 query problem** happens when EF Core **makes too many separate database queries** instead of loading data efficiently.
|
||||
|
||||
##### **Example Scenario**
|
||||
|
||||
Let’s say you have **100 tickets**, and each ticket has a related **Transportation** entity.
|
||||
|
||||
You run this code:
|
||||
|
||||
```c#
|
||||
var tickets = context.Tickets.ToList(); // Loads all tickets foreach (var ticket in tickets)
|
||||
{
|
||||
Console.WriteLine(ticket.Transportation.Name); // Lazy loads Transportation for each ticket
|
||||
}
|
||||
```
|
||||
|
||||
##### **What Happens?**
|
||||
|
||||
1. **1 Query:** EF Core first loads all `Tickets`.
|
||||
2. **N Queries:** Then, for each **Ticket**, EF Core makes a separate query to fetch `Transportation` (so 100 additional queries).
|
||||
3. **Total Queries:** **1 + 100 = 101 queries!** 🚨 **Bad performance!**
|
||||
|
||||
---
|
||||
|
||||
|
||||
|
||||
@@ -0,0 +1,166 @@
|
||||
|
||||
# 🛠️ Task Checklist
|
||||
|
||||
## Preparation
|
||||
### EF Core Code First
|
||||
- [ ] Watch this [video](https://www.youtube.com/watch?v=b8fFRX0T38M&ab_channel=PatrickGod)
|
||||
## 🚧 Branching
|
||||
- [ ] Create the develop branch
|
||||
- [ ] Create the feature/domain-entities branch based on develop
|
||||
## `IEntity.cs`
|
||||
- [ ] Create the IEntity **interface**
|
||||
📂 Suggested Folder: `Domain/Framework/Interfaces`
|
||||
```C#
|
||||
public interface IEntity<TKey>
|
||||
{
|
||||
public TKey Id { get; set; }
|
||||
}
|
||||
```
|
||||
## `Entity.cs`
|
||||
- [ ] Create the Entity **class**
|
||||
📂 Suggested Folder: `Domain/Framework/Base`
|
||||
```C#
|
||||
public class Entity<TKey> : IEntity<TKey>
|
||||
{
|
||||
public TKey Id{ get; set; }
|
||||
}
|
||||
```
|
||||
|
||||
|
||||
## Create Entities
|
||||
📂 Suggested Folder: `Domain/Aggregates/[RelatedFolder]`
|
||||
Entities represent the core business objects in our domain model. By defining them explicitly and consistently, we ensure that:
|
||||
- Our domain logic remains **clear and maintainable**.
|
||||
- We follow **Domain-Driven Design (DDD)** principles, keeping the business rules close to the data they govern.
|
||||
- All developers have a **standardized structure** to follow, improving code readability and collaboration.
|
||||
### Guidelines
|
||||
1. **Base Class**
|
||||
- All entities (except join tables) must inherit from `Entity` and explicitly specify the datatype of their `Id`.
|
||||
2. **Properties**
|
||||
- Use the **latest version of the ERD** to define properties and relationships.
|
||||
3. **Reference Project**
|
||||
- For implementation details, you can refer to this project:
|
||||
👉 [AlibabaClone-Backend Domain Layer](https://github.com/MehrdadShirvani/AlibabaClone-Backend/tree/develop/AlibabaClone.Domain/Aggregates)
|
||||
4. **ERD Reference**
|
||||
- Latest ERD available here:
|
||||
👉 [Project ERD](obsidian://open?vault=ASP.NET&file=Repo%2FProjectOrientedSessions%2Fdocs%2FAlibabaERD-Version02.pdf)### One Example:
|
||||
### Example
|
||||
```C#
|
||||
public class Account : Entity<long>
|
||||
{
|
||||
public required string PhoneNumber { get; set; }
|
||||
public required string Password { set; get; }
|
||||
public string? Email { get; set; }
|
||||
public long? PersonId { get; set; }
|
||||
//...
|
||||
}
|
||||
```
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## Add Navigation Properties
|
||||
|
||||
Navigation properties in Entity Framework Core (EF Core) **represent relationships between entities**. They allow you to **navigate** (follow) the relationships between different tables using **C# objects** instead of writing SQL joins manually.
|
||||
### Add the needed navigation properties inside entities
|
||||
- [ ] Figure out what navigation properties are needed based on the ERD, and use this project as a reference:
|
||||
https://github.com/MehrdadShirvani/AlibabaClone-Backend/tree/develop/AlibabaClone.Domain/Aggregates
|
||||
- [ ] Don't forget to mark all the navigation properties as `virtual`
|
||||
|
||||
### **🔹 Examples of Defining Navigation Properties**
|
||||
|
||||
|
||||
>These are just examples, and not how the project should look like
|
||||
#### **🔹 One-to-Many Example**
|
||||
A **Buyer** can have **multiple Tickets**, but each **Ticket** belongs to one **Buyer**.
|
||||
```csharp
|
||||
public class Buyer
|
||||
{
|
||||
public int Id { get; set; }
|
||||
public string Name { get; set; }
|
||||
|
||||
// Navigation Property (One Buyer → Many Tickets)
|
||||
public virtual ICollection<Ticket> Tickets { get; set; } = new List<Ticket>();
|
||||
}
|
||||
|
||||
public class Ticket
|
||||
{
|
||||
public int Id { get; set; }
|
||||
public int BuyerId { get; set; }
|
||||
|
||||
// Navigation Property (Many Tickets → One Buyer)
|
||||
public virtual Buyer Buyer { get; set; }
|
||||
}
|
||||
```
|
||||
|
||||
#### **🔹 One-to-One Example**
|
||||
|
||||
A **Ticket** can have only **one Transaction**, and a **Transaction** belongs to exactly **one Ticket**.
|
||||
|
||||
```csharp
|
||||
public class Ticket
|
||||
{
|
||||
public int Id { get; set; }
|
||||
|
||||
// One-to-One Navigation Property
|
||||
public virtual Transaction Transaction { get; set; }
|
||||
}
|
||||
|
||||
public class Transaction
|
||||
{
|
||||
public int Id { get; set; }
|
||||
public int TicketId { get; set; }
|
||||
|
||||
// One-to-One Navigation Property
|
||||
public virtual Ticket Ticket { get; set; }
|
||||
}
|
||||
```
|
||||
|
||||
#### **🔹 Many-to-Many Example**
|
||||
|
||||
A **Buyer** can buy **many Tickets**, and each **Ticket** can be bought by **many Buyers** (if resale is allowed).
|
||||
|
||||
```csharp
|
||||
public class Buyer
|
||||
{
|
||||
public int Id { get; set; }
|
||||
public string Name { get; set; }
|
||||
|
||||
// Many-to-Many Navigation Property
|
||||
public virtual ICollection<Ticket> Tickets { get; set; } = new List<Ticket>();
|
||||
}
|
||||
|
||||
public class Ticket
|
||||
{
|
||||
public int Id { get; set; }
|
||||
|
||||
// Many-to-Many Navigation Property
|
||||
public virtual ICollection<Buyer> Buyers { get; set; } = new List<Buyer>();
|
||||
}
|
||||
```
|
||||
|
||||
## Add Packages to Infrastructure Project
|
||||
- [ ] Add `Microsoft.EntityFrameworkCore.Proxies` to Infrastructure Project
|
||||
## 🚧 Merge
|
||||
- [ ] Create a PR and merge the current branch with develop
|
||||
# 🧠 Hints & Notes
|
||||
- Mark navigation properties `virtual`
|
||||
# 🙌 Acknowledgements
|
||||
|
||||
- ChatGPT for snippet refinement and explanations
|
||||
|
||||
# 🔍 References
|
||||
[[Session01 Additional Info]]
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -0,0 +1,199 @@
|
||||
## Cardinality
|
||||
### Cardinality in Database Relationships
|
||||
|
||||
Cardinality in databases refers to the number of relationships between records in two tables. It defines how many instances of one entity can be associated with instances of another entity. Cardinality is a crucial concept in database design because it ensures data integrity and optimizes query performance.
|
||||
|
||||
---
|
||||
|
||||
### **Types of Cardinality**
|
||||
|
||||
1. **One-to-One (1:1)**
|
||||
|
||||
- Each record in Table A is related to exactly one record in Table B, and vice versa.
|
||||
- Example: A _person_ has one _passport_, and a _passport_ belongs to only one _person_.
|
||||
- Implementation: Typically enforced with a **unique foreign key**.
|
||||
|
||||
2. **One-to-Many (1:M)**
|
||||
|
||||
- A record in Table A can have multiple related records in Table B, but a record in Table B is linked to only one record in Table A.
|
||||
- Example: A _customer_ can place multiple _orders_, but each _order_ is placed by only one _customer_.
|
||||
- Implementation: A **foreign key** in Table B referring to the primary key in Table A.
|
||||
|
||||
3. **Many-to-Many (M:M)**
|
||||
|
||||
- Multiple records in Table A can relate to multiple records in Table B.
|
||||
- Example: _Students_ enroll in multiple _courses_, and each _course_ has multiple _students_.
|
||||
- Implementation: A **junction (bridge) table** with foreign keys referencing both tables.
|
||||
|
||||
---
|
||||
|
||||
### **Cardinality Constraints**
|
||||
|
||||
Cardinality can be further specified using **minimum and maximum** constraints:
|
||||
|
||||
- **(0,1): Optional One** → A record may or may not be related.
|
||||
- **(1,1): Mandatory One** → A record must always be related to exactly one record.
|
||||
- **(0,N): Optional Many** → A record may have many related records or none.
|
||||
- **(1,N): Mandatory Many** → A record must have at least one related record.
|
||||
|
||||
---
|
||||
|
||||
### **Practical Example**
|
||||
|
||||
Consider a database with `Students` and `Courses`:
|
||||
|
||||
- **One-to-Many:** A _teacher_ teaches multiple _courses_, but each _course_ has only one _teacher_.
|
||||
- **Many-to-Many:** _Students_ enroll in multiple _courses_, and _courses_ have multiple _students_. This is implemented using a **StudentCourses** junction table.
|
||||
## Using GUIDs That Should Be Auto-Generated
|
||||
|
||||
GUIDs (Globally Unique Identifiers) can be used as primary keys in your entities. In EF Core, you can configure them to auto-generate when a new entity is created.
|
||||
|
||||
### **Example Entity Using GUID**
|
||||
|
||||
```csharp
|
||||
public class SomeEntity
|
||||
{
|
||||
public Guid Id { get; set; } = Guid.NewGuid(); // Auto-generate GUID
|
||||
public string Name { get; set; }
|
||||
}
|
||||
```
|
||||
|
||||
### **Configuration for GUID**
|
||||
|
||||
When configuring an entity with a GUID as the primary key, you don’t need a specific setup in the configuration, but you can enforce that the `Id` is generated on addition.
|
||||
|
||||
```csharp
|
||||
builder.Property(e => e.Id)
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasDefaultValueSql("NEWSEQUENTIALID()"); // Optionally use NEWID() for random GUID
|
||||
```
|
||||
|
||||
### **How It Works**
|
||||
|
||||
- **`Guid.NewGuid()`** generates a new GUID when a new entity instance is created.
|
||||
- **Database**: If you use `NEWSEQUENTIALID()` in SQL Server, it generates sequential GUIDs, which can improve indexing performance.
|
||||
|
||||
### **Example Configuration in DbContext**
|
||||
|
||||
Here's how you might define an entity with GUIDs in your `DbContext`:
|
||||
|
||||
```csharp
|
||||
public class ApplicationDbContext : DbContext
|
||||
{
|
||||
public DbSet<SomeEntity> SomeEntities { get; set; }
|
||||
|
||||
protected override void OnModelCreating(ModelBuilder modelBuilder)
|
||||
{
|
||||
modelBuilder.Entity<SomeEntity>(builder =>
|
||||
{
|
||||
builder.HasKey(e => e.Id);
|
||||
builder.Property(e => e.Id)
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasDefaultValueSql("NEWSEQUENTIALID()");
|
||||
});
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
## `AccountRole`
|
||||
|
||||
The **accountRole** table is a **many-to-many join table** with only two foreign keys (`AccountId`, `RoleId`) and no extra fields. Since it's just linking **Accounts** and **Roles**, you might not need a repository for it. Let's explore the best approach.
|
||||
|
||||
### **Option 1: No Separate Repository (Preferred)**
|
||||
|
||||
Since EF Core **automatically** manages many-to-many relationships using `DbSet<Account>` and `DbSet<Role>`, you usually **don’t need a repository** for the join table.
|
||||
|
||||
You can simply work with navigation properties in **AccountRepository** and **RoleRepository**:
|
||||
|
||||
#### **Example: Adding a Role to an Account**
|
||||
|
||||
```csharp
|
||||
public async Task AssignRoleToAccountAsync(int accountId, int roleId)
|
||||
{
|
||||
var account = await _context.Accounts
|
||||
.Include(a => a.Roles) // Load roles
|
||||
.FirstOrDefaultAsync(a => a.Id == accountId);
|
||||
|
||||
var role = await _context.Roles.FindAsync(roleId);
|
||||
|
||||
if (account != null && role != null)
|
||||
{
|
||||
account.Roles.Add(role);
|
||||
await _context.SaveChangesAsync();
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
EF Core **automatically inserts into the join table** when you modify the `Roles` collection.
|
||||
|
||||
---
|
||||
|
||||
### **Option 2: Create a Repository for the Join Table (If Needed)**
|
||||
|
||||
If you **need direct control over the join table** (e.g., custom queries, logging, performance tuning), then a repository may be useful.
|
||||
|
||||
#### **Interface for `AccountRole` Repository**
|
||||
|
||||
Since the join table **doesn't behave like a typical entity**, we can define a custom repository:
|
||||
|
||||
```csharp
|
||||
public interface IAccountRoleRepository
|
||||
{
|
||||
Task AddAsync(int accountId, int roleId);
|
||||
Task RemoveAsync(int accountId, int roleId);
|
||||
Task<bool> ExistsAsync(int accountId, int roleId);
|
||||
}
|
||||
```
|
||||
|
||||
#### **Implementation**
|
||||
|
||||
```csharp
|
||||
public class AccountRoleRepository : IAccountRoleRepository
|
||||
{
|
||||
private readonly AppDbContext _context;
|
||||
|
||||
public AccountRoleRepository(AppDbContext context)
|
||||
{
|
||||
_context = context;
|
||||
}
|
||||
|
||||
public async Task AddAsync(int accountId, int roleId)
|
||||
{
|
||||
var accountRole = new AccountRole { AccountId = accountId, RoleId = roleId };
|
||||
_context.AccountRoles.Add(accountRole);
|
||||
await _context.SaveChangesAsync();
|
||||
}
|
||||
|
||||
public async Task RemoveAsync(int accountId, int roleId)
|
||||
{
|
||||
var accountRole = await _context.AccountRoles
|
||||
.FirstOrDefaultAsync(ar => ar.AccountId == accountId && ar.RoleId == roleId);
|
||||
|
||||
if (accountRole != null)
|
||||
{
|
||||
_context.AccountRoles.Remove(accountRole);
|
||||
await _context.SaveChangesAsync();
|
||||
}
|
||||
}
|
||||
|
||||
public async Task<bool> ExistsAsync(int accountId, int roleId)
|
||||
{
|
||||
return await _context.AccountRoles
|
||||
.AnyAsync(ar => ar.AccountId == accountId && ar.RoleId == roleId);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### **When Should You Use a Repository for the Join Table?**
|
||||
|
||||
✔ **If you need to execute custom queries** (e.g., checking if an account has a role).
|
||||
✔ **If you need to add business logic** when assigning/removing roles.
|
||||
✔ **If the join table will have extra fields** (e.g., `DateAssigned`, `IsActive`).
|
||||
|
||||
🚀 **If the join table is purely a linking table, let EF Core handle it automatically through navigation properties.** Otherwise, use a repository for more control.
|
||||
|
||||
---
|
||||
|
||||
@@ -0,0 +1,380 @@
|
||||
|
||||
|
||||
# 🛠️ Task Checklist
|
||||
## 🚧 Branching (Configurations)
|
||||
- [ ] Create the feature/entity-configurations branch based on develop
|
||||
## Preparation
|
||||
- [ ] Read the [documentation](https://learn.microsoft.com/en-us/ef/core/modeling/):
|
||||
## Create configuration classes
|
||||
📂 Suggested Folder: `Infrastructure/Configurations`
|
||||
- [ ] Create the classes with this format: `[Entity]Configutaion.cs`
|
||||
- [ ] The class should implement the `IEntityTypeConfiguration<[Entity]>`
|
||||
|
||||
### Where Should You Place Configuration Files?
|
||||
✅ **Best Practice:** Place all configuration files in the **Infrastructure** layer.
|
||||
#### **Reason:**
|
||||
- The **Domain layer** should be **clean** (only entities, no database-related logic).
|
||||
- The **Infrastructure layer** handles **database interactions**, so configurations belong here.
|
||||
### Use this as a reference:
|
||||
[reference](https://github.com/MehrdadShirvani/AlibabaClone-Backend/tree/develop/AlibabaClone.Infrastructure/Configurations)
|
||||
### Explanations and Details
|
||||
|
||||
#### **How to Define Keys (Primary Keys & Identity)**
|
||||
|
||||
You **don’t** need to explicitly define the **primary key (PK)** if you follow EF Core conventions (`Id` or `EntityNameId`). However, if you want to be explicit:
|
||||
|
||||
```csharp
|
||||
public class TicketConfiguration : IEntityTypeConfiguration<Ticket>
|
||||
{
|
||||
public void Configure(EntityTypeBuilder<Ticket> builder)
|
||||
{
|
||||
builder.HasKey(t => t.Id); // Explicitly defining PK (optional)
|
||||
|
||||
builder.Property(t => t.Id)
|
||||
.ValueGeneratedOnAdd(); // Sets Identity (auto-increment)
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
> 🛑 **NOTE:** If you’re using a GUID as the ID, you might need `.ValueGeneratedNever()` instead.
|
||||
|
||||
---
|
||||
#### **How to Define Foreign Keys?**
|
||||
|
||||
Use `HasOne()` and `WithMany()` for **one-to-many** relationships.
|
||||
|
||||
```csharp
|
||||
public class TicketConfiguration : IEntityTypeConfiguration<Ticket>
|
||||
{
|
||||
public void Configure(EntityTypeBuilder<Ticket> builder)
|
||||
{
|
||||
builder.HasKey(t => t.Id);
|
||||
|
||||
// Foreign Key - Ticket to Transportation
|
||||
builder.HasOne(t => t.Transportation)
|
||||
.WithMany(tr => tr.Tickets)
|
||||
.HasForeignKey(t => t.TransportationId)
|
||||
.OnDelete(DeleteBehavior.Restrict); // Optional: No cascade delete
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
#### **How to Introduce Navigation Properties with Different Names?**
|
||||
|
||||
If your navigation property **doesn’t match** the entity name, you should explicitly specify it using `HasOne()` and `WithMany()`.
|
||||
|
||||
#### **Example: Ticket has a Buyer (which is an Account)**
|
||||
|
||||
```csharp
|
||||
builder.HasOne(t => t.Buyer) // Navigation property (Ticket → Account)
|
||||
.WithMany(a => a.TicketsBought) // Corresponding collection in Account
|
||||
.HasForeignKey(t => t.BuyerId);
|
||||
```
|
||||
|
||||
> **Tip:** If your navigation property names don't match table names, always define them explicitly in the Fluent API.
|
||||
|
||||
---
|
||||
|
||||
#### **How to Configure Column Types? (nvarchar, date, etc.)**
|
||||
|
||||
You can **manually specify column types** using `.HasColumnType()`.
|
||||
|
||||
#### **All Strings Should Be `nvarchar` with Specific Lengths**
|
||||
|
||||
```csharp
|
||||
builder.Property(t => t.TicketNumber)
|
||||
.IsRequired()
|
||||
.HasMaxLength(20) // Limits nvarchar length
|
||||
.HasColumnType("nvarchar(20)");
|
||||
```
|
||||
|
||||
#### **Store Some DateTime Fields as SQL `DATE` Instead of `DATETIME2`**
|
||||
|
||||
```csharp
|
||||
builder.Property(t => t.PurchaseDate)
|
||||
.HasColumnType("date"); // Instead of default "datetime2"
|
||||
```
|
||||
|
||||
---
|
||||
#### **How to Define Constraints? (Not Null, Length, etc.)**
|
||||
|
||||
Use `.IsRequired()` for **NOT NULL** and `.HasMaxLength()` for length constraints.
|
||||
|
||||
##### **Example: Ticket Number Must Be Unique & Required**
|
||||
|
||||
```csharp
|
||||
builder.Property(t => t.TicketNumber)
|
||||
.IsRequired() // NOT NULL
|
||||
.HasMaxLength(20);
|
||||
|
||||
builder.HasIndex(t => t.TicketNumber)
|
||||
.IsUnique(); // Unique constraint
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
|
||||
#### **Join Tables with Multiple IDs**
|
||||
In many-to-many relationships, a join table is created to link two entities. This join table typically contains foreign keys referencing the primary keys of the two entities involved in the relationship.
|
||||
|
||||
|
||||
Suppose we have two entities, `Student` and `Course`, and we want to create a many-to-many relationship between them. We'll create a join table called `StudentCourses`.
|
||||
|
||||
##### **Entities**
|
||||
|
||||
```csharp
|
||||
public class Student : Entity<long>
|
||||
{
|
||||
public required string Name { get; set; }
|
||||
public virtual ICollection<StudentCourse> StudentCourses { get; set; }
|
||||
}
|
||||
|
||||
public class Course : Entity<long>
|
||||
{
|
||||
public required string Title { get; set; }
|
||||
public virtual ICollection<StudentCourse> StudentCourses { get; set; }
|
||||
}
|
||||
|
||||
public class StudentCourse
|
||||
{
|
||||
public long StudentId { get; set; }
|
||||
public virtual Student Student { get; set; }
|
||||
|
||||
public long CourseId { get; set; }
|
||||
public virtual Course Course { get; set; }
|
||||
}
|
||||
```
|
||||
|
||||
##### **Configuration for Join Table**
|
||||
|
||||
You would configure the join table using the Fluent API:
|
||||
|
||||
```csharp
|
||||
public class StudentCourseConfiguration : IEntityTypeConfiguration<StudentCourse>
|
||||
{
|
||||
public void Configure(EntityTypeBuilder<StudentCourse> builder)
|
||||
{
|
||||
// Composite Primary Key
|
||||
builder.HasKey(sc => new { sc.StudentId, sc.CourseId });
|
||||
|
||||
// Foreign Key Relationships
|
||||
builder.HasOne(sc => sc.Student)
|
||||
.WithMany(s => s.StudentCourses)
|
||||
.HasForeignKey(sc => sc.StudentId);
|
||||
|
||||
builder.HasOne(sc => sc.Course)
|
||||
.WithMany(c => c.StudentCourses)
|
||||
.HasForeignKey(sc => sc.CourseId);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Final Configuration File Example (`TicketConfiguration.cs`)
|
||||
|
||||
Here’s an example of a configuration file:
|
||||
|
||||
```csharp
|
||||
public class TicketConfiguration : IEntityTypeConfiguration<Ticket>
|
||||
{
|
||||
public void Configure(EntityTypeBuilder<Ticket> builder)
|
||||
{
|
||||
builder.HasKey(t => t.Id);
|
||||
builder.Property(a => a.Id)
|
||||
.ValueGeneratedOnAdd();
|
||||
|
||||
builder.Property(t => t.TicketOrderId)
|
||||
.IsRequired();
|
||||
|
||||
builder.Property(t => t.SeatId)
|
||||
.IsRequired();
|
||||
|
||||
builder.Property(t => t.TravelerId)
|
||||
.IsRequired();
|
||||
|
||||
builder.Property(t => t.CreatedAt)
|
||||
.IsRequired();
|
||||
|
||||
builder.Property(t => t.CanceledAt)
|
||||
.IsRequired(false);
|
||||
|
||||
builder.Property(t => t.CompanionId)
|
||||
.IsRequired(false);
|
||||
|
||||
builder.Property(t => t.TicketStatusId)
|
||||
.IsRequired();
|
||||
|
||||
builder.Property(t => t.SerialNumber)
|
||||
.IsRequired()
|
||||
.HasMaxLength(50)
|
||||
.IsUnicode(false);
|
||||
builder.HasIndex(x => x.SerialNumber).IsUnique();
|
||||
|
||||
|
||||
builder.Property(t => t.Description)
|
||||
.HasMaxLength(200)
|
||||
.IsUnicode(false);
|
||||
|
||||
// Relationships
|
||||
builder.HasOne(t => t.TicketOrder)
|
||||
.WithMany(t => t.Tickets)
|
||||
.HasForeignKey(t => t.TicketOrderId)
|
||||
.OnDelete(DeleteBehavior.Restrict);
|
||||
|
||||
builder.HasOne(t => t.Seat)
|
||||
.WithMany(s => s.Tickets)
|
||||
.HasForeignKey(t => t.SeatId)
|
||||
.OnDelete(DeleteBehavior.Restrict);
|
||||
|
||||
builder.HasOne(t => t.Traveler)
|
||||
.WithMany(p => p.TraveledTickets)
|
||||
.HasForeignKey(t => t.TravelerId)
|
||||
.OnDelete(DeleteBehavior.Restrict);
|
||||
|
||||
builder.HasOne(t => t.Companion)
|
||||
.WithMany()
|
||||
.HasForeignKey(t => t.CompanionId)
|
||||
.OnDelete(DeleteBehavior.Restrict);
|
||||
|
||||
builder.HasOne(t => t.TicketStatus)
|
||||
.WithMany()
|
||||
.HasForeignKey(t => t.TicketStatusId)
|
||||
.OnDelete(DeleteBehavior.Restrict);
|
||||
}
|
||||
}
|
||||
```
|
||||
## 🚧 Merge
|
||||
- [ ] Create a PR and merge the current branch with develop
|
||||
|
||||
## 🚧 Branching (Application `DBContext` and Connection String Configurations)
|
||||
|
||||
## Preparation
|
||||
- [ ] Read the [documentation](https://learn.microsoft.com/en-us/ef/core/modeling/):
|
||||
|
||||
## Branching
|
||||
- [ ] Create the feature/setup-dbContext branch based on develop
|
||||
|
||||
## Database Context
|
||||
📂 Suggested Folder: `Infrastructure/ApplicationDbContext.cs
|
||||
- [ ] Create ApplicationDBContext
|
||||
- [ ] Inherits DbContext
|
||||
- [ ] Create the constructor like the code below
|
||||
- [ ] Add the necessary DbSets
|
||||
- [ ] Override `OnModelCreating` and `OnConfiguring` as below
|
||||
```csharp
|
||||
public class ApplicationDBContext : DbContext
|
||||
{
|
||||
public ApplicationDBContext(DbContextOptions<ApplicationDBContext> options) : base(options)
|
||||
{
|
||||
|
||||
}
|
||||
|
||||
public DbSet<Account> Accounts { get; set; }
|
||||
public DbSet<AccountRole> AccountRoles { get; set; }
|
||||
public DbSet<Gender> Genders{ get; set; }
|
||||
public DbSet<BankAccountDetail> BankAccountDetails{ get; set; }
|
||||
public DbSet<Person> People { get; set; }
|
||||
public DbSet<Role> Roles { get; set; }
|
||||
|
||||
public DbSet<Company> Companies { get; set; }
|
||||
|
||||
public DbSet<City> Cities { get; set; }
|
||||
public DbSet<Location> Locations { get; set; }
|
||||
public DbSet<LocationType> LocationTypes{ get; set; }
|
||||
|
||||
public DbSet<Transaction> Transactions { get; set; }
|
||||
public DbSet<Coupon> Coupons{ get; set; }
|
||||
public DbSet<TransactionType> TransactionTypes { get; set; }
|
||||
|
||||
public DbSet<Ticket> Tickets { get; set; }
|
||||
public DbSet<TicketOrder> TicketOrders { get; set; }
|
||||
public DbSet<TicketStatus> TicketStatuses { get; set; }
|
||||
public DbSet<Transportation> Transportations { get; set; }
|
||||
|
||||
public DbSet<Seat> Seats { get; set; }
|
||||
public DbSet<Vehicle> Vehicles { get; set; }
|
||||
public DbSet<VehicleType> VehicleTypes { get; set; }
|
||||
|
||||
|
||||
protected override void OnModelCreating(ModelBuilder modelBuilder)
|
||||
{
|
||||
modelBuilder.UseCollation("Persian_100_CI_AI");
|
||||
modelBuilder.ApplyConfigurationsFromAssembly(typeof(ApplicationDBContext).Assembly);
|
||||
base.OnModelCreating(modelBuilder);
|
||||
}
|
||||
|
||||
protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
|
||||
{
|
||||
optionsBuilder.UseLazyLoadingProxies();
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
## Configuring the Database in ASP.NET Core
|
||||
|
||||
### 📌 **Connection String**
|
||||
- [ ] Modify `appsettings.json` and add the following.
|
||||
- [ ] Adjust the Connection String to meet your needs
|
||||
### Option 1:
|
||||
```json
|
||||
{
|
||||
"ConnectionStrings": {
|
||||
"DefaultConnection": "Server=YOUR_SERVER;Database=YourDb;User Id=USERNAME;Password=PASSWORD;TrustServerCertificate=True;"
|
||||
}
|
||||
}
|
||||
```
|
||||
### Option 2:
|
||||
```json
|
||||
{
|
||||
"ConnectionStrings": {
|
||||
"DefaultConnection": "Server=YOUR_SERVER;Database=YourDb;Integrated Security=True;TrustServerCertificate=True;"
|
||||
}
|
||||
}
|
||||
```
|
||||
- [ ] Put this `appsettings.json` in **`gitignore`**
|
||||
### **Registering EF Core in `Program.cs`**
|
||||
- [ ] Modify `Program.cs`
|
||||
|
||||
```csharp
|
||||
using Infrastructure.Persistence;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
|
||||
var builder = WebApplication.CreateBuilder(args);
|
||||
|
||||
builder.Services.AddDbContext<AppDbContext>(options =>
|
||||
options.UseSqlServer(builder.Configuration.GetConnectionString("DefaultConnection")));
|
||||
|
||||
var app = builder.Build();
|
||||
app.Run();
|
||||
```
|
||||
|
||||
## 🚧 Merge
|
||||
- [ ] Create a PR and merge the current branch with develop
|
||||
---
|
||||
## 🚧Branching (Migrations and Database Setup)
|
||||
- [ ] Create the feature/migrations branch based on develop
|
||||
|
||||
## Using Package Manager Console
|
||||
- [ ] Make sure to set the project to Infrastructure
|
||||
- [ ] Make sure you have installed Microsoft.EntityFrameworkCore.Tools
|
||||
- [ ] Run the following command
|
||||
```
|
||||
Add-Migration InitialCreate
|
||||
```
|
||||
- [ ] In case of succus:
|
||||
```
|
||||
Update-Database
|
||||
```
|
||||
## 🚧Merge
|
||||
- [ ] Create a PR and merge the current branch with develop
|
||||
|
||||
# 🧠 Hints & Notes
|
||||
# 🙌 Acknowledgements
|
||||
|
||||
- ChatGPT for snippet refinement and explanations
|
||||
# 🔍 References
|
||||
[[Session02 Additional Info]]
|
||||
|
||||
|
||||
@@ -0,0 +1,120 @@
|
||||
## Introduction to Repository Pattern (Chat GPT):
|
||||
The **Repository Pattern** in ASP.NET Core is a design pattern used to separate business logic from data access logic by providing an abstraction layer over database operations. This pattern improves maintainability, testability, and flexibility in applications by encapsulating database operations in dedicated repository classes.
|
||||
|
||||
---
|
||||
### **Why Use the Repository Pattern?**
|
||||
|
||||
#### **Pros:**
|
||||
1. **Abstraction from ORM (Entity Framework Core)**
|
||||
- Prevents direct dependency on EF Core, making it easier to swap out the data access layer in the future.
|
||||
|
||||
2. **Better Code Organization**
|
||||
- Separates concerns by keeping data logic in repositories and business logic in services/controllers.
|
||||
|
||||
3. **Improved Testability**
|
||||
- Makes it easier to mock repositories in unit tests.
|
||||
|
||||
4. **Encapsulation of Queries**
|
||||
- Common queries can be abstracted, reducing repetition.
|
||||
|
||||
5. **Centralized Data Access Logic**
|
||||
- Ensures a single location for handling CRUD operations.
|
||||
|
||||
|
||||
---
|
||||
|
||||
### **Comparison: Repository Pattern vs. Direct DbSet Operations**
|
||||
|
||||
| Feature | Using Repository Pattern | Using DbSet Directly in Controllers |
|
||||
| -------------------------- | -------------------------------------- | -------------------------------------- |
|
||||
| **Separation of Concerns** | ✅ Maintains separation | ❌ Business and data access logic mixed |
|
||||
| **Testability** | ✅ Easy to mock and test | ❌ Harder to mock DbContext |
|
||||
| **Code Reusability** | ✅ Common operations are encapsulated | ❌ Repetitive DbSet calls |
|
||||
| **Flexibility** | ✅ Can switch database providers easily | ❌ Tightly coupled to EF Core |
|
||||
|
||||
---
|
||||
|
||||
## Introduction to Unit of Work Pattern (Chat GPT):
|
||||
|
||||
### **What is the Unit of Work Pattern?**
|
||||
|
||||
The **Unit of Work (`UoW`)** pattern is a **centralized mechanism** to manage **database transactions** and ensure that multiple repository operations are treated as a single unit of execution. It acts as a wrapper around multiple repositories to **coordinate their changes and commit them in one go**.
|
||||
|
||||
---
|
||||
|
||||
### **Advantages of Unit of Work**
|
||||
|
||||
#### **1. Single Transaction for Multiple Operations**
|
||||
|
||||
- If you're performing **multiple database operations** across different repositories, **Unit of Work ensures atomicity**.
|
||||
- If one operation fails, everything is **rolled back** (when using explicit transactions).
|
||||
|
||||
#### **2. Better Performance**
|
||||
|
||||
- **Without UoW:** Every repository would call `SaveChangesAsync()` separately, causing multiple round trips to the database.
|
||||
- **With UoW:** All changes are saved **at once**, reducing the number of database calls.
|
||||
#### **3. Maintains Consistency**
|
||||
- When multiple repositories modify related entities, **UoW ensures that all changes are either committed or discarded together**.
|
||||
#### **4. Improves Testability**
|
||||
- Unit of Work allows you to **mock database changes** and write unit tests efficiently without worrying about inconsistent data states.
|
||||
#### **5. Prevents Partial Updates**
|
||||
- If multiple repositories handle different entities in the same operation, calling `SaveChangesAsync()` in individual repositories could lead to **partial updates** if one operation succeeds and another fails.
|
||||
|
||||
---
|
||||
|
||||
### **Why Should `SaveChanges()` NOT Be in the Repository?**
|
||||
|
||||
#### **1. Each Repository Should Not Control Transactions**
|
||||
|
||||
If each repository calls `SaveChangesAsync()`, **you lose control over transactions**.
|
||||
|
||||
##### **Example Problem (Without `UoW`)**
|
||||
|
||||
Imagine you have two repositories: `CustomerRepository` and `OrderRepository`.
|
||||
If you try to **add a customer** and **add an order** separately, each calling `SaveChangesAsync()`:
|
||||
|
||||
```csharp
|
||||
var customer = new Customer { Name = "John Doe" };
|
||||
await _customerRepository.AddAsync(customer);
|
||||
await _customerRepository.SaveChangesAsync(); // ❌ First database call
|
||||
|
||||
var order = new Order { CustomerId = customer.Id, TotalAmount = 100 };
|
||||
await _orderRepository.AddAsync(order);
|
||||
await _orderRepository.SaveChangesAsync(); // ❌ Second database call
|
||||
```
|
||||
|
||||
**What happens if the second `SaveChangesAsync()` fails?**
|
||||
|
||||
- The customer has already been saved, but the order is missing.
|
||||
- **Your database is left in an inconsistent state!**
|
||||
|
||||
#### **2. Database Round Trips (Performance Issue)**
|
||||
|
||||
If each repository calls `SaveChangesAsync()`, you end up with **multiple database calls** instead of batching them into a single transaction.
|
||||
|
||||
```csharp
|
||||
await _customerRepository.SaveChangesAsync(); // ❌ DB call
|
||||
await _orderRepository.SaveChangesAsync(); // ❌ Another DB call
|
||||
```
|
||||
|
||||
Using **Unit of Work**, all changes can be saved in one go:
|
||||
|
||||
```csharp
|
||||
await _unitOfWork.SaveChangesAsync(); // ✅ One database call
|
||||
```
|
||||
|
||||
This **reduces network latency** and improves **database performance**.
|
||||
|
||||
#### **3. Promotes Separation of Concerns**
|
||||
|
||||
- **Repositories should focus on CRUD operations** (data retrieval and manipulation).
|
||||
- **Unit of Work should manage transactions**.
|
||||
- This makes the code **cleaner and easier to maintain**.
|
||||
---
|
||||
|
||||
### **Key Takeaways**
|
||||
|
||||
✔ **Unit of Work ensures all database operations are part of a single transaction**.
|
||||
✔ **Repositories should NOT call `SaveChangesAsync()` to avoid multiple transactions**.
|
||||
✔ **EF Core tracks changes, so calling `SaveChangesAsync()` once is enough**.
|
||||
✔ **Using UoW improves performance, consistency, and maintainability**.
|
||||
@@ -0,0 +1,214 @@
|
||||
|
||||
# 🛠️ Task Checklist
|
||||
|
||||
## Repository Pattern Preparation
|
||||
- [ ] Watch [video 1 (Brief Introduction](https://www.youtube.com/watch?v=Wiy54682d1w&ab_channel=PatrickGod) & [video 2 (More Detailed Introduction)](https://youtu.be/rtXpYpZdOzM)
|
||||
### Purpose:
|
||||
Meditates between the domain and data mapping layers, acting like an **in-memory collection** of domain objects
|
||||
### Benefits
|
||||
- Minimizes duplicate query logic
|
||||
- Decouples your application from persistence frameworks
|
||||
- Promotes testability
|
||||
> Repository should not have methods like Update and Save
|
||||
## Unit of Work Preparation
|
||||
Keeps track of changes and coordinates the writings and savings
|
||||
### Implementation:
|
||||
- [ ] Watch [video](https://youtu.be/rtXpYpZdOzM?t=703)
|
||||
|
||||
## 🚧Branching (Implementing Repository Pattern)
|
||||
- [ ] Create the feature/repositories branch based on develop
|
||||
|
||||
## Creating `IRepository` in Domain
|
||||
- [ ] Create the interface and add the following code
|
||||
📂 Suggested Folder: `Domain/Framework/Interfaces/Respositories`
|
||||
```c#
|
||||
public interface IRepository<T_Entity, U_PrimaryKey> where T_Entity : class
|
||||
{
|
||||
Task<T_Entity?> GetByIdAsync(U_PrimaryKey id);
|
||||
Task<IEnumerable<T_Entity>> GetAllAsync();
|
||||
Task<IEnumerable<T_Entity>> FindAsync(Expression<Func<T_Entity, bool>> predicate);
|
||||
Task AddAsync(T_Entity entity);
|
||||
void Update(T_Entity entity);
|
||||
void Remove(T_Entity entity);
|
||||
}
|
||||
```
|
||||
## Creating a class implementing `IRepository`
|
||||
- [ ] create a class named `BaseRepository` or `Repository` (choose one) in Infrastructure and implement `IRepository`
|
||||
|
||||
📂 Suggested Folder: `Infrastructure/Framework/Base`
|
||||
- [ ] provide method definitions for the methods
|
||||
```c#
|
||||
public class BaseRepository<K_DbContext, T_Entity, U_PrimaryKey> : IRepository<T_Entity, U_PrimaryKey>
|
||||
where T_Entity : class
|
||||
where K_DbContext : DbContext
|
||||
{
|
||||
public virtual K_DbContext DbContext { get; set; }
|
||||
public virtual DbSet<T_Entity> DBSet{ get; set; }
|
||||
|
||||
public BaseRepository(K_DbContext dbContext)
|
||||
{
|
||||
DbContext = dbContext;
|
||||
DBSet = dbContext.Set<T_Entity>();
|
||||
}
|
||||
|
||||
public async Task AddAsync(T_Entity entity)
|
||||
{
|
||||
await DBSet.AddAsync(entity);
|
||||
}
|
||||
|
||||
public async Task<T_Entity?> GetByIdAsync(U_PrimaryKey id)
|
||||
{
|
||||
return await DBSet.FindAsync(id);
|
||||
}
|
||||
public async Task<IEnumerable<T_Entity>> GetAllAsync()
|
||||
{
|
||||
var entityList = DBSet.ToListAsync();
|
||||
return await entityList;
|
||||
}
|
||||
public void Update(T_Entity entity)
|
||||
{
|
||||
DBSet.Update(entity);
|
||||
}
|
||||
public void Remove(T_Entity entity)
|
||||
{
|
||||
DBSet.Remove(entity);
|
||||
}
|
||||
|
||||
public async Task<IEnumerable<T_Entity>> FindAsync(Expression<Func<T_Entity, bool>> predicate)
|
||||
{
|
||||
return await DBSet.Where(predicate).ToListAsync();
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Creating an interface for each entity
|
||||
> (not for the join tables)
|
||||
|
||||
- [ ] For each entity, create an interface that inherits `IRepository`
|
||||
- [ ] (Optional): Add method definitions as you deem needed for that entity (not recommended right now. We will come back to this part later)
|
||||
|
||||
📂 Suggested Folder: Domain/Framework/Base/Interfaces/[Related Folder]`
|
||||
for example:
|
||||
```c#
|
||||
public interface IAccountRepository : IRepository<Account, long>
|
||||
{
|
||||
|
||||
}
|
||||
```
|
||||
|
||||
### Reference Project:
|
||||
[Reference](https://github.com/MehrdadShirvani/AlibabaClone-Backend/tree/develop/AlibabaClone.Domain/Framework/Interfaces/Repositories)
|
||||
## Implementing each `I[Entity]Repository`
|
||||
|
||||
📂 Suggested Folder: Infrastructure/Services/[Related Folder]
|
||||
- [ ] For each entity, create an class named `[Entity]Repository` that implements `I[Entity]Repository` and inherits `BaseRepository`
|
||||
|
||||
for example:
|
||||
```c#
|
||||
public class AccountRepository :
|
||||
BaseRepository<ApplicationDBContext, Account, long>,
|
||||
IAccountRepository
|
||||
{
|
||||
public AccountRepository(ApplicationDBContext dbContext) : base(dbContext)
|
||||
{
|
||||
|
||||
}
|
||||
}
|
||||
```
|
||||
### Reference Project:
|
||||
[Reference](https://github.com/MehrdadShirvani/AlibabaClone-Backend/tree/develop/AlibabaClone.Infrastructure/Services)
|
||||
|
||||
## Registering Services
|
||||
- [ ] Modify `Program.cs` in Presentation, and register for each `I[Entity]Repository` the related `[Entity]Repository`
|
||||
|
||||
```c#
|
||||
//some code
|
||||
//Register Repositories
|
||||
builder.Services.AddScoped<IAccountRepository, AccountRepository>();
|
||||
builder.Services.AddScoped<IGenderRepository, GenderRepository>();
|
||||
builder.Services.AddScoped<IPersonRepository, PersonRepository>();
|
||||
builder.Services.AddScoped<IRoleRepository, RoleRepository>();
|
||||
//...
|
||||
|
||||
//some code
|
||||
```
|
||||
## 🚧Merge
|
||||
- [ ] Create a PR and merge the current branch with develop
|
||||
|
||||
|
||||
---
|
||||
|
||||
## 🚧Branching
|
||||
- [ ] Create the feature/UnitOfWork branch based on develop
|
||||
|
||||
|
||||
```c#
|
||||
public interface IUnitOfWork : IDisposable
|
||||
{
|
||||
Task<int> SaveChangesAsync();
|
||||
}
|
||||
```
|
||||
|
||||
## Creating `IUnitOfWork`
|
||||
- [ ] Create the interface that inherits `IDisposable` and add the following code
|
||||
|
||||
📂 Suggested Folder: `Domain/Framework/Interfaces`
|
||||
|
||||
```c#
|
||||
public interface IUnitOfWork : IDisposable
|
||||
{
|
||||
Task<int> SaveChangesAsync();
|
||||
}
|
||||
```
|
||||
|
||||
## Implementing `IUnitOfWork`
|
||||
- [ ] create a class named `UnitOfWork` in Infrastructure and implement `IUnitOfWork`
|
||||
|
||||
📂 Suggested Folder: `Infrastructure/Framework/Base`
|
||||
|
||||
- [ ] provide method definitions for the methods
|
||||
|
||||
```c#
|
||||
public class UnitOfWork : IUnitOfWork
|
||||
{
|
||||
private readonly ApplicationDBContext _context;
|
||||
|
||||
public UnitOfWork(ApplicationDBContext context)
|
||||
{
|
||||
_context = context;
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
_context.Dispose();
|
||||
}
|
||||
|
||||
public async Task<int> SaveChangesAsync()
|
||||
{
|
||||
return await _context.SaveChangesAsync();
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Registering Services
|
||||
- [ ] Modify `Program.cs` in Presentation, and register the `UnitOfWork` Service
|
||||
|
||||
```c#
|
||||
//some code
|
||||
//Register Repositories
|
||||
builder.Services.AddScoped<IUnitOfWork, UnitOfWork>();
|
||||
//some code
|
||||
```
|
||||
|
||||
## 🚧Merge
|
||||
- [ ] Create a PR and merge the current branch with develop
|
||||
|
||||
|
||||
# 🧠 Hints & Notes
|
||||
# 🙌 Acknowledgements
|
||||
|
||||
- ChatGPT for snippet refinement and explanations
|
||||
# 🔍 References
|
||||
[[Session03 Additional Info]]
|
||||
|
||||
|
||||
@@ -0,0 +1,259 @@
|
||||
## *Should I have an `IEntityService` and then `EntityService` for each of my entities?**
|
||||
|
||||
Not necessarily **for _every_** entity — only if it **makes sense**.
|
||||
- The Application Layer should expose **use cases** — not just CRUD logic for each entity.
|
||||
- If an entity has business logic or interactions that need orchestration (e.g., validations, aggregations, calling repositories, etc.), then **yes**, create a service.
|
||||
- Otherwise, for basic operations, **directly using a repository (via a unit of work or interface)** from the use case handler might be fine.
|
||||
## **Is it OK to have services not related to a specific entity?**
|
||||
|
||||
Absolutely, **yes**. In fact, that’s expected in a Clean Architecture setup.
|
||||
|
||||
Examples:
|
||||
- A `ReportGenerationService` that combines bookings, customers, and payments.
|
||||
- A `TokenService` for authentication tokens.
|
||||
- A `CurrencyConversionService` that hits an external API.
|
||||
- A `NotificationService` that sends emails or SMS.
|
||||
|
||||
👉 As long as these services **live in the Application Layer** and follow **dependency inversion** (i.e., they depend only on interfaces, not implementations), you’re doing great.
|
||||
|
||||
## **Is it necessary to have an interface for each service?**
|
||||
|
||||
## **What’s the Difference Between Services and Repositories?**
|
||||
|
||||
| Aspect | **Service** | **Repository** |
|
||||
| ------------------ | ---------------------------------------------------------------------- | --------------------------------------------------------------- |
|
||||
| **Layer** | Application Layer | Domain Layer (interface), Infrastructure Layer (implementation) |
|
||||
| **Responsibility** | **Orchestrates business logic** / use cases | **Data access abstraction** |
|
||||
| **Focus** | Coordinates multiple domain/repo operations, validation, business flow | Fetching/storing data for a specific entity |
|
||||
| **Example** | `PlaceOrderService`, `ReportService` | `ICustomerRepository`, `IOrderRepository` |
|
||||
|
||||
## What does `init` mean?
|
||||
|
||||
`init` is an **access modifier for properties** that allows you to **set a property only during object initialization**, **but not after**.
|
||||
|
||||
|
||||
## Should I use `class` or `record` for DTOs in Clean Architecture?
|
||||
|
||||
### 🔵 Short answer:
|
||||
|
||||
> **Use `record` for DTOs when possible** — it's clean, immutable by default, and semantically perfect for data transfer.
|
||||
|
||||
---
|
||||
|
||||
### 🔍 Why `record` is a great fit for DTOs
|
||||
|
||||
| Feature | `record` | `class` |
|
||||
| ----------------------- | ----------------- | ---------------------------- |
|
||||
| Immutable by default | ✅ (with `init`) | ❌ (need manual setup) |
|
||||
| Value-based equality | ✅ | ❌ (ref-based by default) |
|
||||
| Concise syntax | ✅ | ❌ (more boilerplate) |
|
||||
| Use for data containers | ✅ (perfect fit) | ✅ (but more verbose) |
|
||||
| Custom behavior/logic | ❌ (less suitable) | ✅ (better for rich behavior) |
|
||||
### But when should you prefer `class`?
|
||||
|
||||
Use `class` if your DTO or model:
|
||||
- Needs to be **mutable** after creation
|
||||
- Has to **interact with legacy APIs/libraries**
|
||||
- Needs **inheritance or polymorphism** (not well supported in `record`)
|
||||
- Has **rich behavior** (logic, methods, validation, etc.)
|
||||
|
||||
> For example, in the Domain Layer (Entities, ValueObjects), you'll usually stick to **`class`** — because that's where behavior lives.
|
||||
|
||||
## RESTful APIs:
|
||||
[read more](https://aws.amazon.com/what-is/restful-api/#:~:text=RESTful%20API%20is%20an%20interface,applications%20to%20perform%20various%20tasks.)
|
||||
## What Conditions Make an API RESTful?
|
||||
|
||||
### Key Principles of REST:
|
||||
|
||||
1. **Statelessness**:
|
||||
|
||||
- Each API call must contain all the information the server needs to fulfill the request (no session state). Each request is independent.
|
||||
|
||||
2. **Resource Identification**:
|
||||
|
||||
- Resources (e.g., customers, orders) should be identified using URIs. Use nouns in URIs, not verbs.
|
||||
|
||||
3. **HTTP Methods**:
|
||||
|
||||
- Use standard HTTP methods to represent actions:
|
||||
|
||||
- **GET**: Retrieve a resource.
|
||||
|
||||
- **POST**: Create a new resource.
|
||||
|
||||
- **PUT**: Update a resource entirely.
|
||||
|
||||
- **PATCH**: Update a resource partially.
|
||||
|
||||
- **DELETE**: Remove a resource.
|
||||
|
||||
4. **Use of Standard Status Codes**:
|
||||
|
||||
- Return appropriate HTTP status codes (e.g., `200 OK`, `201 Created`, `404 Not Found`, `500 Internal Server Error`).
|
||||
|
||||
5. **HATEOAS**:
|
||||
|
||||
- (Hypermedia as the Engine of Application State) - Provide links to related resources within the responses.
|
||||
|
||||
|
||||
### Multiple GET Methods in One Controller:
|
||||
|
||||
- **Yes, you can have multiple GET methods in one controller**. The key is to differentiate them based on routes and parameters.
|
||||
|
||||
- For example:
|
||||
|
||||
|
||||
```c#
|
||||
[ApiController]
|
||||
[Route("api/[controller]")]
|
||||
public class CustomerController : ControllerBase {
|
||||
|
||||
[HttpGet("{id}")]
|
||||
public IActionResult GetCustomerById(int id) { /*...*/ }
|
||||
|
||||
[HttpGet]
|
||||
public IActionResult GetAllCustomers() { /*...*/ }
|
||||
|
||||
[HttpGet("{id}/orders")]
|
||||
public IActionResult GetCustomerOrders(int id) { /*...*/ }
|
||||
}
|
||||
```
|
||||
|
||||
|
||||
- **Routing**: ASP.NET Core uses route templates to differentiate these actions. The combination of route parameters, query strings, and action names can help separate the GET requests.
|
||||
|
||||
|
||||
## List-like stuff in `C#`
|
||||
They all serve similar purposes—holding multiple items—but differ in functionality, performance, and use cases. Here’s a detailed breakdown:
|
||||
|
||||
🔷 1. `IEnumerable`
|
||||
- Namespace: System.Collections.Generic
|
||||
- Most basic "list-like" abstraction.
|
||||
- Read-only (forward-only iteration).
|
||||
- You can use foreach on it.
|
||||
- Doesn’t support indexing (no .Count, no [i]).
|
||||
- Often used as the return type to expose a stream of data without giving full collection control.
|
||||
|
||||
Example:
|
||||
|
||||
```csharp
|
||||
IEnumerable<int> numbers = GetNumbers(); // Lazy-loaded maybe
|
||||
foreach (var num in numbers)
|
||||
Console.WriteLine(num);
|
||||
```
|
||||
|
||||
💡 Ideal when:
|
||||
- You want to return a sequence without exposing modification.
|
||||
- You’re using LINQ chains.
|
||||
- You’re returning data from a database query.
|
||||
|
||||
---
|
||||
|
||||
🔷 2. ICollection
|
||||
- Extends IEnumerable.
|
||||
- Adds Count and Add/Remove/Clear methods.
|
||||
- Still abstract—List and HashSet implement it.
|
||||
|
||||
💡 Useful when:
|
||||
- You want to expose a collection that can be modified (e.g. Add or Remove).
|
||||
- You care about the Count.
|
||||
|
||||
|
||||
---
|
||||
|
||||
🔷 3. `IList`
|
||||
- Extends `ICollection` and `IEnumerable`.
|
||||
- Adds index access: `list[0]` etc.
|
||||
- Think of it like a mutable array with dynamic size.
|
||||
|
||||
💡 Use when:
|
||||
|
||||
- You want ordered collection with indexing.
|
||||
|
||||
- You need to insert, remove, or replace items at specific positions.
|
||||
|
||||
|
||||
---
|
||||
|
||||
🔷 4. List
|
||||
|
||||
- A concrete class (not interface).
|
||||
- Implements `IList`, `ICollection`, `IEnumerable`.
|
||||
- Backed by an array (auto-resizes).
|
||||
- Fast read and write.
|
||||
- Supports `Add`, `Remove`, `Insert`, `IndexOf`, etc.
|
||||
|
||||
Example:
|
||||
|
||||
```csharp
|
||||
var list = new List<string>();
|
||||
list.Add("One");
|
||||
list.Add("Two");
|
||||
var second = list[1]; // "Two"
|
||||
```
|
||||
|
||||
💡 Go-to general purpose collection.
|
||||
|
||||
---
|
||||
|
||||
🔷 5. `IReadOnlyCollection` & `IReadOnlyList`
|
||||
|
||||
- `IReadOnlyCollection`: Just Count and `IEnumerable`.
|
||||
- `IReadOnlyList`: Adds indexing without modification.
|
||||
- Used to expose lists safely (read-only).
|
||||
|
||||
💡 Used when:
|
||||
- You want to return a list, but prevent any changes.
|
||||
|
||||
---
|
||||
|
||||
🔷 6. `Array(T[])`
|
||||
|
||||
- Fixed-size.
|
||||
- Fastest for indexing.
|
||||
- Cannot change size.
|
||||
- Implements `IList` (via Array).
|
||||
|
||||
Example:
|
||||
|
||||
```csharp
|
||||
int[] numbers = new int[5];
|
||||
numbers[0] = 42;
|
||||
```
|
||||
|
||||
💡 Great for performance-sensitive or fixed-size data.
|
||||
|
||||
---
|
||||
|
||||
🔷 7. `ObservableCollection`
|
||||
|
||||
- For WPF/`Blazor`/WinForms data-binding.
|
||||
- Notifies UI when items are added/removed.
|
||||
- Implements IList.
|
||||
|
||||
💡 Use in UI apps when the view needs to react to collection changes.
|
||||
|
||||
---
|
||||
|
||||
🔷 8. HashSet
|
||||
- Unordered, no duplicates.
|
||||
- Implements `ICollection`, not `IList`.
|
||||
- No index access.
|
||||
|
||||
💡 Best for fast membership checking (contains x).
|
||||
|
||||
---
|
||||
|
||||
🔷 Summary Table
|
||||
|
||||
|Type|Indexing|Modifiable|Ordered|Duplicates|Use Case|
|
||||
|---|---|---|---|---|---|
|
||||
|IEnumerable|❌|❌|✔️|✔️|Basic read-only stream or LINQ chaining|
|
||||
|ICollection|❌|✔️|✔️|✔️|Expose modifiable group of items|
|
||||
|IList|✔️|✔️|✔️|✔️|Full-featured list abstraction|
|
||||
|List|✔️|✔️|✔️|✔️|Standard go-to collection|
|
||||
|IReadOnlyList|✔️|❌|✔️|✔️|Return data safely from service or repo|
|
||||
|Array (T[])|✔️|✔️|✔️|✔️|Fixed-size, performant indexing|
|
||||
|ObservableCollection|✔️|✔️|✔️|✔️|UI binding for collections|
|
||||
|HashSet|❌|✔️|❌|❌|Fast lookup, uniqueness enforcement|
|
||||
|
||||
@@ -0,0 +1,365 @@
|
||||
|
||||
|
||||
# 🛠️ Task Checklist
|
||||
|
||||
## Preparation:
|
||||
The unedited conversation with Chat GPT, concerning almost all of the aspects of this session:
|
||||
(optional): read this to get a better understanding of the topic:
|
||||
https://chatgpt.com/share/67f18460-1c1c-8010-bc57-9f3b683ec87a
|
||||
|
||||
## 🚧Branching
|
||||
- [ ] Create the feature/transportation-search branch based on develop
|
||||
|
||||
## DTO
|
||||
In order to develop transportation search flow, three DTOs need to be created in the application layer.
|
||||
|
||||
- [ ] Create DTOs related to transportation search flow
|
||||
📂 Suggested Folder: Application/DTOs/City`
|
||||
```cs
|
||||
public class CityDto
|
||||
{
|
||||
public int Id { get; init; }
|
||||
public required string Title { get; init; }
|
||||
}
|
||||
```
|
||||
|
||||
📂 Suggested Folder: Application/DTOs/Transportation
|
||||
```cs
|
||||
public class TransportationSearchRequestDto
|
||||
{
|
||||
public short? VehicleTypeId { get; init; }
|
||||
public int? FromCityId { get; init; }
|
||||
public int? ToCityId { get; init; }
|
||||
public DateTime? StartDate { get; init; }
|
||||
public DateTime? EndDate { get; init; }
|
||||
```
|
||||
|
||||
```cs
|
||||
public class TransportationSearchResultDto
|
||||
{
|
||||
public long Id { get; init; }
|
||||
public int VehicleTypeId { get; init; }
|
||||
public string? VehicleTitle { get; init; }
|
||||
public required string CompanyTitle { get; init; }
|
||||
public required string FromLocationTitle { get; init; }
|
||||
public required string ToLocationTitle { get; init; }
|
||||
public required string FromCityTitle { get; init; }
|
||||
public required string ToCityTitle { get; init; }
|
||||
public DateTime StartDateTime { get; init; }
|
||||
public DateTime? EndDateTime { get; init; }
|
||||
public decimal Price { get; init; }
|
||||
public int RemainingCapacity { get; init; }
|
||||
|
||||
```
|
||||
|
||||
## Repository
|
||||
There are a few things to be add to some repositories for transportation search flow.
|
||||
|
||||
- [ ] Create DTOs related to transportation search flow
|
||||
|
||||
📂 Suggested Folder: Domain/Framework/Interfaces/Repositories/
|
||||
TransportationRepositories
|
||||
|
||||
```cs
|
||||
public interface ITransportationRepository : IRepository<Transportation, long>
|
||||
{
|
||||
Task<IEnumerable<Transportation>> SearchTransportationsAsync(
|
||||
short? vehicleTypeId,
|
||||
int? fromCityId,
|
||||
int? toCityId,
|
||||
DateTime? startDate,
|
||||
DateTime? endDate);
|
||||
}
|
||||
```
|
||||
|
||||
|
||||
📂 Suggested Folder: Infrastructure/Services/Services/TransportationRepositories
|
||||
|
||||
```cs
|
||||
public class TransportationRepository :
|
||||
BaseRepository<ApplicationDBContext, Transportation, long>,
|
||||
ITransportationRepository
|
||||
{
|
||||
public TransportationRepository(ApplicationDBContext dbContext) : base(dbContext)
|
||||
{
|
||||
|
||||
}
|
||||
|
||||
public async Task<IEnumerable<Transportation>> SearchTransportationsAsync(
|
||||
short? vehicleTypeId,
|
||||
int? fromCityId,
|
||||
int? toCityId,
|
||||
DateTime? startDate,
|
||||
DateTime? endDate)
|
||||
{
|
||||
var query = DbContext.Transportations
|
||||
.Include(x => x.Vehicle)
|
||||
.Include(x => x.FromLocation).ThenInclude(x => x.City)
|
||||
.Include(x => x.ToLocation).ThenInclude(x => x.City)
|
||||
.Include(x => x.Company)
|
||||
.AsQueryable();
|
||||
query = query.Where(x => vehicleTypeId == null || x.Vehicle.VehicleTypeId == vehicleTypeId.Value);
|
||||
query = query.Where(x => fromCityId == null || x.FromLocation.CityId == fromCityId.Value);
|
||||
query = query.Where(x => toCityId == null || x.ToLocation.CityId == toCityId.Value);
|
||||
query = query.Where(x => startDate == null || x.StartDateTime.Date == startDate.Value.Date);
|
||||
query = query.Where(x => endDate == null || (x.EndDateTime.HasValue && x.EndDateTime.Value == endDate.Value.Date));
|
||||
|
||||
return await query.ToListAsync();
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
|
||||
## Auto Mapper
|
||||
Auto Mapper simplifies mapping between aggregates and DTOs in both directions.
|
||||
|
||||
- [ ] Create a `MappingProfile` that inherits `Profile`, and use it to add configurations for mappings
|
||||
|
||||
📂 Suggested Folder: Application/Mappers/Profiles
|
||||
|
||||
```cs
|
||||
public class MappingProfile : Profile
|
||||
{
|
||||
public MappingProfile()
|
||||
{
|
||||
CreateMap<Transportation, TransportationSearchResultDto>()
|
||||
.ForMember(dest => dest.CompanyTitle,
|
||||
opt => opt.MapFrom(src => src.Company.Title))
|
||||
|
||||
.ForMember(dest => dest.FromLocationTitle,
|
||||
opt => opt.MapFrom(src => src.FromLocation.Title))
|
||||
|
||||
.ForMember(dest => dest.ToLocationTitle,
|
||||
opt => opt.MapFrom(src => src.ToLocation.Title))
|
||||
|
||||
.ForMember(dest => dest.FromCityTitle,
|
||||
opt => opt.MapFrom(src => src.FromLocation.City.Title))
|
||||
|
||||
.ForMember(dest => dest.ToCityTitle,
|
||||
opt => opt.MapFrom(src => src.ToLocation.City.Title));
|
||||
|
||||
CreateMap<City, CityDto>();
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
- [ ] Register AutoMapper config file in `Program.cs`
|
||||
|
||||
```cs
|
||||
.
|
||||
.
|
||||
.
|
||||
|
||||
builder.Services.AddAutoMapper(typeof(MappingProfile));
|
||||
|
||||
var app = builder.Build();
|
||||
|
||||
.
|
||||
.
|
||||
.
|
||||
```
|
||||
|
||||
## Result & Result Status
|
||||
|
||||
- [ ] Create `ResultStatus` enum and `Result` class
|
||||
📂 Suggested Folder: Application/Result
|
||||
|
||||
|
||||
Result is a template to transfer data between services and controllers (in backend), so will use a generic type
|
||||
|
||||
```cs
|
||||
public class Result<T>
|
||||
{
|
||||
public ResultStatus Status { get; set; }
|
||||
public string? ErrorMessage { get; set; }
|
||||
public T? Data { get; set; }
|
||||
public bool IsSuccess => Status == ResultStatus.Success;
|
||||
|
||||
public static Result<T> Success(T data)
|
||||
{
|
||||
return new Result<T>
|
||||
{
|
||||
Status = ResultStatus.Success,
|
||||
Data = data
|
||||
};
|
||||
}
|
||||
|
||||
public static Result<T> Error(T data)
|
||||
{
|
||||
return new Result<T>
|
||||
{
|
||||
Status = ResultStatus.Error,
|
||||
Data = data
|
||||
};
|
||||
}
|
||||
|
||||
public static Result<T> NotFound(T data)
|
||||
{
|
||||
return new Result<T>
|
||||
{
|
||||
Status = ResultStatus.NotFound,
|
||||
Data = data
|
||||
};
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
As you can see, there's a property of type ResultStatus, which is a enum for status of request
|
||||
|
||||
```cs
|
||||
public enum ResultStatus
|
||||
{
|
||||
Success,
|
||||
NotFound,
|
||||
ValidationError,
|
||||
Conflict,
|
||||
Unauthorized,
|
||||
Forbidden,
|
||||
Error
|
||||
}
|
||||
```
|
||||
|
||||
You can read more about enums: [W3Schools](https://www.w3schools.com/cs/cs_enums.php)
|
||||
|
||||
|
||||
## `IService` & Service
|
||||
|
||||
Now, use `I[Entity]Repositry` and `IUnitOfWork` in services to implement business logic
|
||||
- [ ] Create `I[Entity]Service` and `[Entity]Service` which implements it
|
||||
|
||||
📂 Suggested Folder for `I[Entity]Service`: Application/Interfaces
|
||||
|
||||
📂 Suggested Folder for Services: Application/Services
|
||||
- existence of an interface for each service class is optional
|
||||
- services can have multiple repositories in them -> logic-based structure
|
||||
|
||||
An example of `I[Entity]Service`:
|
||||
```c#
|
||||
public interface ITransportationService
|
||||
{
|
||||
Task<Result<IEnumerable<TransportationSearchResultDto>>> SearchTransportationsAsync(TransportationSearchRequestDto searchRequest);
|
||||
}
|
||||
```
|
||||
|
||||
An example of `[Entity]Service`:
|
||||
```cs
|
||||
public class TransportationService : ITransportationService
|
||||
{
|
||||
private readonly ITransportationRepository _transportationRepository;
|
||||
private readonly IMapper _mapper;
|
||||
private readonly IUnitOfWork _unitOfWork;
|
||||
|
||||
public TransportationService(ITransportationRepository transportationRepository, IMapper mapper, IUnitOfWork unitOfWork)
|
||||
{
|
||||
_transportationRepository = transportationRepository;
|
||||
_mapper = mapper;
|
||||
_unitOfWork = unitOfWork;
|
||||
}
|
||||
|
||||
public async Task<Result<IEnumerable<TransportationSearchResultDto>>> SearchTransportationsAsync(TransportationSearchRequestDto requestDto)
|
||||
{
|
||||
var result = await _transportationRepository.SearchTransportationsAsync(
|
||||
vehicleTypeId: requestDto.VehicleTypeId,
|
||||
fromCityId: requestDto.FromCityId,
|
||||
toCityId: requestDto.ToCityId,
|
||||
startDateTime: requestDto.StartDate,
|
||||
endDateTime: requestDto.EndDate);
|
||||
|
||||
if (result.Any())
|
||||
{
|
||||
var dto = _mapper.Map<IEnumerable<TransportationSearchResultDto>>(result);
|
||||
return Result<IEnumerable<TransportationSearchResultDto>>.Success(dto);
|
||||
}
|
||||
|
||||
return Result<IEnumerable<TransportationSearchResultDto>>.NotFound(null);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
- [ ] Register services in `Program.cs`:
|
||||
```c#
|
||||
.
|
||||
.
|
||||
.
|
||||
builder.Services.AddScoped<ITransportationService, TransportationService>();
|
||||
builder.Services.AddScoped<ICityService, CityService>();
|
||||
.
|
||||
.
|
||||
.
|
||||
```
|
||||
|
||||
## Controller
|
||||
Now we're getting to endpoints, you should communicate with client side through web-api. So every controller uses Services in Application layer to receive requests and send responses with DTOs.
|
||||
|
||||
- [ ] Create an APIController (right click on the folder, and then under Add, select Controller, and then make sure to select the APIController type)
|
||||
📂 Suggested Folder: WebAPI/Controller
|
||||
|
||||
You should use ```[ApiController]``` attribute on top of them, route them and handle different status codes. TransportationController:
|
||||
|
||||
```cs
|
||||
[ApiController]
|
||||
[Route("api/[controller]")]
|
||||
public class TransportationController : ControllerBase
|
||||
{
|
||||
private readonly ITransportationService _transportationService;
|
||||
|
||||
public TransportationController(ITransportationService transportationService)
|
||||
{
|
||||
_transportationService = transportationService;
|
||||
}
|
||||
|
||||
[HttpGet("search")]
|
||||
public async Task<IActionResult> SearchTransportations([FromQuery] TransportationSearchRequestDto searchRequest)
|
||||
{
|
||||
if (searchRequest == null)
|
||||
{
|
||||
return BadRequest("Invalid search request");
|
||||
}
|
||||
|
||||
var result = await _transportationService.SearchTransportationsAsync(searchRequest);
|
||||
if (result.IsSuccess)
|
||||
{
|
||||
return Ok(result);
|
||||
}
|
||||
|
||||
// any unsuccessful status
|
||||
return result.Status switch
|
||||
{
|
||||
ResultStatus.NotFound => NotFound(result.ErrorMessage),
|
||||
ResultStatus.ValidationError => BadRequest(result.ErrorMessage),
|
||||
_ => StatusCode(500, result.ErrorMessage),
|
||||
};
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
- `HttpGet`: handles a GET request from client -> important for routing
|
||||
- Ok, `BadRequest`, `NotFound` and `StatusCode` are json results to send through API
|
||||
- Use `TransportationService` to communicate with Application
|
||||
|
||||
## Inserting Sample Data
|
||||
|
||||
For testing purposes, add some data into the related tables.
|
||||
You are provided with a SQL script, that adds some sample data into the following tables
|
||||
|
||||
**Important Notes:** Note that different database names, and different table names will produce errors while executing the script. Consider adjusting these names before executing the script
|
||||
- Cities
|
||||
- Companies
|
||||
- LocationTypes
|
||||
- Locations
|
||||
- VehicleTypes
|
||||
- Vehicles
|
||||
- Transportation
|
||||
|
||||
- [ ] Open `TransportationRelatedSampleData.sql` with SSMS, and execute the query
|
||||
> Note: there has been changes in database structure since this note and this file has been written.
|
||||
|
||||
|
||||
## 🚧Merge
|
||||
- [ ] Create a PR and merge the current branch with develop
|
||||
|
||||
# 🧠 Hints & Notes
|
||||
# 🙌 Acknowledgements
|
||||
|
||||
- ChatGPT for snippet refinement and explanations
|
||||
# 🔍 References
|
||||
Binary file not shown.
@@ -0,0 +1,436 @@
|
||||
|
||||
## What is CORS? (Chat GPT)
|
||||
### CORS (Cross-Origin Resource Sharing)
|
||||
|
||||
**What it is:** CORS is a browser security feature that blocks requests to a different domain unless explicitly allowed by the server.
|
||||
### 🔐 What is CORS **really** for?
|
||||
|
||||
CORS is **not** about protecting the backend server.
|
||||
It’s about **protecting users** from **malicious websites** using their browser as a weapon.
|
||||
|
||||
---
|
||||
|
||||
### 🧠 Imagine this attack:
|
||||
|
||||
You’re logged into your **bank** in one browser tab (`bank.com`).
|
||||
|
||||
Now, you visit a shady website in another tab (`evil.com`). That site has JavaScript that tries to send this:
|
||||
|
||||
```js
|
||||
fetch('https://bank.com/api/transfer?amount=5000&to=hacker', {
|
||||
credentials: 'include' // it sends your bank cookies!
|
||||
});
|
||||
```
|
||||
|
||||
➡️ If the browser allowed this freely, the request would go through **using your login session**, and you’d lose money.
|
||||
|
||||
---
|
||||
|
||||
### 💥 Enter CORS
|
||||
|
||||
So the browser says:
|
||||
|
||||
> “Hold on. This JavaScript is from `evil.com`, and it’s trying to talk to `bank.com`. I won’t let that happen **unless `bank.com` says it’s okay**.”
|
||||
|
||||
That’s why the **backend server** must respond with something like:
|
||||
|
||||
```http
|
||||
Access-Control-Allow-Origin: https://mytrusteddomain.com
|
||||
```
|
||||
|
||||
Only then will the browser say, “Okay, go ahead.”
|
||||
|
||||
---
|
||||
|
||||
### So the purpose of CORS is:
|
||||
|
||||
✅ To **restrict browsers** from sending or accepting responses from **cross-origin** sources
|
||||
❌ Not to protect the backend
|
||||
❌ Not to restrict Postman, curl, servers, or mobile apps
|
||||
|
||||
---
|
||||
|
||||
### 🔄 In Dev Work (like your React + API case):
|
||||
|
||||
- You’re running React at `http://localhost:5173`
|
||||
- You’re running ASP.NET Core API at `https://localhost:7001`
|
||||
- The browser sees this as two **different origins** → blocks the request unless CORS is enabled on the API.
|
||||
|
||||
---
|
||||
|
||||
### 🧪 Why Postman works:
|
||||
- Postman isn’t a browser
|
||||
- Postman doesn’t care about same-origin policy
|
||||
- Postman just sends requests like your backend would
|
||||
|
||||
---
|
||||
|
||||
### ✅ Conclusion:
|
||||
|
||||
- **CORS is a browser feature to protect users**
|
||||
- **It restricts frontend JavaScript from calling other domains unless explicitly allowed**
|
||||
- **You must configure your server to say “Yes, I allow your frontend to talk to me”**
|
||||
|
||||
|
||||
## Feature-based Folder Structure
|
||||
>
|
||||
## Explanation of the `agent.ts`
|
||||
>
|
||||
## React.FC
|
||||
|
||||
### what is `React.FC`?
|
||||
|
||||
- `React.FC` (or `React.FunctionComponent`) is a **TypeScript type** that you can use to type your functional React components.
|
||||
|
||||
- It **tells TypeScript** that:
|
||||
- This component is a function
|
||||
- It **receives props** (in your case, `Props`)
|
||||
- It **returns JSX** (it returns something React can render)
|
||||
|
||||
---
|
||||
|
||||
### Why use it?
|
||||
|
||||
Here’s what you get when you use `React.FC`:
|
||||
1. ✅ **Prop typing** — You get auto-complete and error checking for props.
|
||||
2. ✅ **Children** are automatically included. (More on this below.)
|
||||
3. ✅ **Cleaner code** because TypeScript understands the shape of the component.
|
||||
|
||||
---
|
||||
|
||||
### Without `React.FC`
|
||||
|
||||
You could just write:
|
||||
|
||||
```tsx
|
||||
const TransportationCard = ({ transportation }: Props) => { ... }
|
||||
```
|
||||
You lose some "extra typing safety" like automatic `children` typing.
|
||||
|
||||
---
|
||||
|
||||
### Small Detail: `children`
|
||||
|
||||
When you use `React.FC`, **TypeScript automatically** allows your component to accept `children` too — even if you didn’t define it in your `Props`.
|
||||
|
||||
For example:
|
||||
|
||||
```tsx
|
||||
<TransportationCard transportation={t}>
|
||||
<p>Hello</p> // This would be valid automatically
|
||||
</TransportationCard>
|
||||
```
|
||||
|
||||
Because `children` is **always** part of a `React.FC`.
|
||||
|
||||
👉 If you **don't** use `React.FC`, and you want to accept `children`, you have to **manually** add it to your props.
|
||||
|
||||
---
|
||||
|
||||
### Some developers today...
|
||||
**Some people** (even in big companies) prefer **NOT** to use `React.FC` anymore because:
|
||||
- It **forces children** even when you don’t want children.
|
||||
- It's **a little bit redundant** — you can already just type props without it.
|
||||
|
||||
> So in modern codebases, **both styles are OK** — it’s just a preference.
|
||||
|
||||
---
|
||||
|
||||
### Quick Summary:
|
||||
|
||||
| Using `React.FC` | Not using `React.FC` |
|
||||
| :------------------------------------------- | :---------------------------------------------- |
|
||||
| Good for simple, typed functional components | Good if you want full manual control over props |
|
||||
| Auto-includes `children` prop | You must manually add `children` if needed |
|
||||
| Easy and quick | More customizable |
|
||||
|
||||
---
|
||||
|
||||
## `CityDropdown` Component:
|
||||
|
||||
### 1. **State Variables**
|
||||
|
||||
```tsx
|
||||
const [cities, setCities] = useState<City[]>([]);
|
||||
const [selectedCity, setSelectedCity] = useState<number | undefined>();
|
||||
```
|
||||
|
||||
- `cities`: holds the list of cities retrieved from the backend (starts empty `[]`).
|
||||
- `selectedCity`: holds the currently selected city’s ID (`number`) or `undefined` if nothing is selected yet.
|
||||
|
||||
|
||||
---
|
||||
|
||||
### 2. **Fetching Cities on Mount**
|
||||
|
||||
```tsx
|
||||
useEffect(() => {
|
||||
agent.Cities.list()
|
||||
.then(setCities)
|
||||
.catch((err) => console.error("error loading cities", err));
|
||||
}, []);
|
||||
```
|
||||
|
||||
- When the component **mounts** (`[]` dependency array = run once), it calls `agent.Cities.list()`.
|
||||
- `agent.Cities.list()` presumably returns a promise that resolves to an array of `City` objects.
|
||||
- On success → `setCities` updates the `cities` state.
|
||||
- On failure → logs an error to the console.
|
||||
|
||||
---
|
||||
|
||||
### 3. **Rendering the Dropdown**
|
||||
|
||||
```tsx
|
||||
<select
|
||||
value={selectedCity}
|
||||
onChange={(e) => setSelectedCity(Number(e.target.value))}
|
||||
>
|
||||
<option value="">Select a City</option>
|
||||
{cities.map((city) => (
|
||||
<option key={city.id} value={city.id}>
|
||||
{city.title}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
```
|
||||
|
||||
- Renders a `<select>` (dropdown).
|
||||
- Its value is bound to `selectedCity`.
|
||||
- When the user changes the selection (`onChange`), it updates `selectedCity` by converting the selected `value` from a string to a number (`Number(e.target.value)`).
|
||||
- The dropdown always starts with a placeholder option: **"Select a City"**.
|
||||
- It dynamically creates an `<option>` for each city in the `cities` array:
|
||||
- `key` and `value` are the city's `id`.
|
||||
- Displayed text is the city's `title`.
|
||||
|
||||
---
|
||||
|
||||
### What is `useEffect`?
|
||||
|
||||
🔹 **`useEffect` is a React Hook**.
|
||||
It **tells React to run some code after the component renders**.
|
||||
|
||||
|
||||
In code:
|
||||
|
||||
```tsx
|
||||
useEffect(() => {
|
||||
agent.Cities.list()
|
||||
.then(setCities)
|
||||
.catch((err) => console.error("error loading cities", err));
|
||||
}, []);
|
||||
```
|
||||
|
||||
- This function (`() => { ... }`) **runs right after** the component is first shown (because of `[]` — the empty array).
|
||||
- Inside it, you are **calling your API** (`agent.Cities.list()`) to get the cities.
|
||||
- When the server **responds with a list of cities**, you **save** them into your component's memory (state) by calling `setCities(data)`.
|
||||
|
||||
---
|
||||
|
||||
### What is `useState`?
|
||||
|
||||
🔹 **`useState` is another React Hook**.
|
||||
It **creates a piece of memory** for your component.
|
||||
|
||||
In code:
|
||||
|
||||
```tsx
|
||||
const [cities, setCities] = useState<City[]>([]);
|
||||
const [selectedCity, setSelectedCity] = useState<number | undefined>();
|
||||
```
|
||||
|
||||
Here’s what is happening:
|
||||
- `cities` is a variable that starts as an **empty array** (`[]`).
|
||||
- `setCities` is a **function** you use to **change** the value of `cities`.
|
||||
|
||||
Same with `selectedCity`:
|
||||
- `selectedCity` starts as `undefined` (nothing selected yet).
|
||||
- `setSelectedCity` lets you **update** which city is selected.
|
||||
|
||||
|
||||
---
|
||||
|
||||
## `TransportationCard` Component
|
||||
|
||||
### 1. **The Component Function**
|
||||
|
||||
```tsx
|
||||
const TransportationCard: React.FC<Props> = ({ transportation }) => {
|
||||
```
|
||||
|
||||
- This is a **React Functional Component**.
|
||||
- It takes `transportation` from props (destructured directly).
|
||||
- It **returns JSX** that shows the transportation info in a styled card.
|
||||
|
||||
---
|
||||
### 2. **Inside the JSX**
|
||||
|
||||
#### 2.1 Left Side — Price and Button
|
||||
|
||||
```tsx
|
||||
<div className="flex flex-col items-center">
|
||||
<div className="text-blue-600 font-bold text-lg">
|
||||
{transportation.price} Toman
|
||||
</div>
|
||||
<button className="bg-blue-500 text-white px-4 py-2 rounded-md mt-2 hover:bg-blue-600">
|
||||
Select Ticket
|
||||
</button>
|
||||
</div>
|
||||
```
|
||||
|
||||
- Shows the **price** (`price` field) styled with blue, bold text.
|
||||
- Has a **Select Ticket** button — blue, rounded, changes shade on hover.
|
||||
|
||||
---
|
||||
|
||||
#### 2.2 Middle — Trip Info
|
||||
|
||||
```tsx
|
||||
<div className="flex-1 mx-4 text-center">
|
||||
<div className="font-semibold text-gray-700">
|
||||
{transportation.companyTitle}
|
||||
</div>
|
||||
<div className="flex items-center justify-center mt-2">
|
||||
<div className="mx-2">{transportation.fromCityTitle}</div>
|
||||
<span className="text-gray-400">→</span>
|
||||
<div className="mx-2">{transportation.toCityTitle}</div>
|
||||
</div>
|
||||
<div className="text-sm text-gray-500 mt-1">
|
||||
{new Date(transportation.startDateTime).toLocaleDateString("en", {
|
||||
hour: "2-digit",
|
||||
minute: "2-digit",
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
```
|
||||
|
||||
- Shows the **company name**.
|
||||
- Shows **From → To** cities in a neat way with an arrow (`→`) between them.
|
||||
- Shows the **departure time**:
|
||||
- `startDateTime` is parsed using `new Date(...)`.
|
||||
- `toLocaleDateString("en", { hour: "2-digit", minute: "2-digit" })` formats it to show just **hours and minutes**.
|
||||
|
||||
---
|
||||
#### 2.3 Right Side — Company Logo
|
||||
|
||||
```tsx
|
||||
<div className="w-12 h-12">
|
||||
<img
|
||||
src="/images/company-placeholder.png"
|
||||
alt="company"
|
||||
className="w-full h-full object-contain"
|
||||
/>
|
||||
</div>
|
||||
```
|
||||
|
||||
- Displays a small company **logo image** (placeholder image for now).
|
||||
- `object-contain` keeps the image inside the box without stretching.
|
||||
|
||||
## `TrasnportationSearchForm` Component
|
||||
|
||||
### 🧠 Main Concepts Used:
|
||||
|
||||
| Feature | Purpose |
|
||||
| :------------------------ | :--------------------------------------------------------------------------- |
|
||||
| `useState` | To **store and manage** the form inputs, cities, results, and loading status |
|
||||
| `useEffect` | To **load the list of cities once** when the component appears |
|
||||
| **Typing with models** | `City`, `TransportationSearchRequest`, `TransportationSearchResult` |
|
||||
| **Event handling** | To update form values and trigger the search |
|
||||
| **Conditional rendering** | Show "Loading", "No results", or "Results" dynamically |
|
||||
| | |
|
||||
|
||||
---
|
||||
|
||||
### 📦 Let's break down the code:
|
||||
|
||||
#### 1. ✍️ States
|
||||
|
||||
```tsx
|
||||
const [searchResults, setSearchResults] = useState<TransportationSearchResult[]>([]);
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [cities, setCities] = useState<City[]>([]);
|
||||
const [form, setForm] = useState<TransportationSearchRequest>({
|
||||
fromCityId: undefined,
|
||||
toCityId: undefined,
|
||||
startDate: null,
|
||||
endDate: null,
|
||||
vehicleTypeId: undefined,
|
||||
});
|
||||
```
|
||||
|
||||
You define **4 states**:
|
||||
|
||||
- `searchResults`: List of found transportations
|
||||
- `loading`: True/false if waiting for API
|
||||
- `cities`: List of available cities
|
||||
- `form`: The form input values the user is selecting
|
||||
|
||||
All typed properly ✅
|
||||
|
||||
---
|
||||
|
||||
#### 2. 📡 Fetch cities automatically (useEffect)
|
||||
|
||||
```tsx
|
||||
useEffect(() => {
|
||||
agent.Cities.list().then(setCities);
|
||||
}, []);
|
||||
```
|
||||
|
||||
**Meaning**:
|
||||
- When the page **first loads**, it **calls the API** to get cities.
|
||||
- `agent.Cities.list()` calls your backend, and when it gets the cities, it puts them into `cities` state.
|
||||
- The empty `[]` **dependency array** means this happens **only once**, not every time anything changes.
|
||||
|
||||
---
|
||||
|
||||
#### 3. ✏️ Handle input changes
|
||||
|
||||
```tsx
|
||||
const handleChange = (e: React.ChangeEvent<HTMLSelectElement | HTMLInputElement>) => { ... }
|
||||
```
|
||||
|
||||
Whenever a user types/selects:
|
||||
- You detect **which field** (`name`) and **what value** (`value`) they changed
|
||||
- Update the `form` state accordingly:
|
||||
- `fromCityId` and `toCityId` are converted to numbers (`parseInt`)
|
||||
- `startDate` and `endDate` allow null
|
||||
- Other fields (if any) are copied directly
|
||||
|
||||
---
|
||||
|
||||
#### 4. 🔍 Handle Search button
|
||||
|
||||
```tsx
|
||||
const handleSearch = () => { ... }
|
||||
```
|
||||
|
||||
When the user clicks **Search**:
|
||||
- Set `loading` to `true`
|
||||
- Call the backend API `agent.TransportationSearch.search(form)`
|
||||
- When the result comes back:
|
||||
- Save it into `searchResults`
|
||||
- If error: log it
|
||||
- Finally, set `loading` to `false` again
|
||||
|
||||
---
|
||||
|
||||
#### 5. 🖥️ Return (render) JSX
|
||||
You build a UI:
|
||||
|
||||
- **Vehicle Types** (Bus, Train, Airplane) selectable with a click → sets `vehicleTypeId`
|
||||
- **From City** and **To City** dropdowns
|
||||
- **Start and End Date** inputs
|
||||
- **Search Button** to trigger the search
|
||||
- **Result area** that shows:
|
||||
- If loading: "Loading..."
|
||||
- If no results: "No results found"
|
||||
- If results: List of `TransportationCard` components for each found item.
|
||||
|
||||
|
||||
---
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -0,0 +1,27 @@
|
||||
|
||||
# 🛠️ Task Checklist
|
||||
## CORS (Backend Repository)
|
||||
- [ ] open `program.cs` and add the following lines
|
||||
|
||||
```c#
|
||||
builder.Services.AddCors(options =>
|
||||
{
|
||||
options.AddPolicy("Frontend", policy =>
|
||||
{
|
||||
policy.WithOrigins("http://localhost:5173")
|
||||
.AllowAnyHeader()
|
||||
.AllowAnyMethod();
|
||||
});
|
||||
});
|
||||
|
||||
...
|
||||
|
||||
app.UseCors("Frontend");
|
||||
```
|
||||
|
||||
|
||||
# 🧠 Hints & Notes
|
||||
# 🙌 Acknowledgements
|
||||
|
||||
- ChatGPT for snippet refinement and explanations
|
||||
# 🔍 References
|
||||
@@ -0,0 +1,221 @@
|
||||
|
||||
# 🛠️ Task Checklist
|
||||
## Preparation
|
||||
- [ ] Watch https://www.youtube.com/watch?v=SqcY0GlETPk&ab_channel=ProgrammingwithMosh
|
||||
## 🚧Branching (Project Setup)
|
||||
- [ ] Create the develop branch
|
||||
- [ ] Create the feature/project-setup branch based on develop
|
||||
|
||||
## Creating a project (using Vite)
|
||||
|
||||
```
|
||||
npm create vite@latest alibabaclone-frontend --template react-ts
|
||||
```
|
||||
|
||||
## React Folder Structure
|
||||
- [ ] Create the folders as shown in the picture below
|
||||
![[Pasted image 20250427154917.png]]
|
||||
- [ ] move `App.tsx` and `App.css` to 'shared/layout/'
|
||||
- [ ] adjust the dependencies in these files and `index.html`
|
||||
|
||||
## Installing packages
|
||||
- [ ] run this command to install these packages: `uuid`, `react-router-dom`, and `axios`
|
||||
```bash
|
||||
npm install uuid react-router-dom axios
|
||||
```
|
||||
- [ ] run this command to install redux
|
||||
```bash
|
||||
npm install @reduxjs/toolkit react-redux
|
||||
```
|
||||
|
||||
## CSS/Component Library
|
||||
There are different ways to handle `CSS` styling when creating components.
|
||||
You can choose any approach you prefer:
|
||||
1. **Plain CSS**
|
||||
- Write regular `.css` files and import them into your components.
|
||||
2. **CSS Modules**
|
||||
- Create component-specific `.module.css` files to automatically scope styles.
|
||||
3. `**TailwindCSS**`
|
||||
- A utility-first CSS framework where you apply classes directly in your HTML/JSX.
|
||||
4. **Sass / SCSS**
|
||||
- An extension of CSS with variables, nesting, and more features. Can be used alone or with modules.
|
||||
5. **PostCSS**
|
||||
- A tool for transforming CSS with JavaScript plugins (often used behind the scenes).
|
||||
6. **Framework-Specific UI Libraries** (which come with their own styles)
|
||||
- Example: Material-UI (MUI), Ant Design, Chakra UI, etc.
|
||||
- These often include ready-made components with built-in styling.
|
||||
|
||||
- [ ] install the css/component library of your choice
|
||||
### Tailwind
|
||||
If you use tailwind with ShadCN, use this link (skip the first step)
|
||||
[Tailwind with ShadCN](https://ui.shadcn.com/docs/installation/vite)
|
||||
|
||||
|
||||
## 🚧Merge
|
||||
- [ ] Create a PR and merge the current branch with develop
|
||||
## 🚧Branching
|
||||
- [ ] Create the feature/transportation-search branch based on develop
|
||||
## Creating Models
|
||||
- [ ] Create models for the DTOs that are used to convey data between backend and frontend
|
||||
📂 `Suggested Folder: shared/models/[RelatedFolder]`
|
||||
|
||||
## Example:
|
||||
```ts
|
||||
export interface TransportationSearchRequest{
|
||||
vehicleTypeId ?: number;
|
||||
fromCityId ?: number;
|
||||
toCityId ?: number;
|
||||
startDate ?: Date | null;
|
||||
endDate ?: Date | null;
|
||||
}
|
||||
```
|
||||
|
||||
## Handling API Calls
|
||||
|
||||
- [ ] Create `agent.ts` to handle API calls using axios
|
||||
📂 Suggested Folder: shared/api/
|
||||
> Note: the name and the location of this file has changed since writing this note
|
||||
- [ ] Adjust the `baseURL` to address your web API port
|
||||
```ts
|
||||
import axios, { AxiosResponse } from 'axios';
|
||||
|
||||
import { TransportationSearchRequest } from '../models/transportation/transportationSearchRequest';
|
||||
import { TransportationSearchResult } from '../models/transportation/transportationSearchResult';
|
||||
|
||||
import { City } from '../models/location/city';
|
||||
|
||||
axios.defaults.baseURL = 'https://localhost:[REPLACE THIS WITH YOUR BACKEND WEB API PORT]/api';
|
||||
|
||||
const responseBody = <T>(response: AxiosResponse<T>) => response.data;
|
||||
|
||||
const request = {
|
||||
get: <T>(url: string) => axios.get<T>(url).then(responseBody),
|
||||
post: <T>(url: string, body: {}) => axios.post<T>(url, body).then(responseBody),
|
||||
put: <T>(url: string, body: {}) => axios.put<T>(url, body).then(responseBody),
|
||||
delete: <T>(url: string) => axios.delete<T>(url).then(responseBody)
|
||||
}
|
||||
|
||||
const TransportationSearch = {
|
||||
search: (data: TransportationSearchRequest) => request.post<TransportationSearchResult[]>('/transportation/search', data),
|
||||
}
|
||||
|
||||
const Cities = {
|
||||
list: () => request.get<City[]>('/city'),
|
||||
}
|
||||
|
||||
|
||||
const agent = {
|
||||
TransportationSearch,
|
||||
Cities
|
||||
}
|
||||
|
||||
export default agent;
|
||||
```
|
||||
|
||||
## Creating `CityDropdown` Component
|
||||
📂 Suggested Folder: features/city/
|
||||
- [ ] Create components for the parts of the UI you need
|
||||
|
||||
```ts
|
||||
import agent from "@/shared/api/agent";
|
||||
import { City } from "@/shared/models/location/city";
|
||||
import { useEffect, useState } from "react";
|
||||
|
||||
const CityDropdown = () => {
|
||||
const [cities, setCities] = useState<City[]>([]);
|
||||
const [selectedCity, setSelectedCity] = useState<number | undefined>();
|
||||
|
||||
useEffect(() => {
|
||||
agent.Cities.list()
|
||||
.then(setCities)
|
||||
.catch((err) => console.error("error loading cities", err));
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<select
|
||||
value={selectedCity}
|
||||
onChange={(e) => setSelectedCity(Number(e.target.value))}
|
||||
>
|
||||
|
||||
<option value="">Select a City</option>
|
||||
{cities.map((city) => (
|
||||
<option key={city.id} value={city.id}>
|
||||
{city.title}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
);
|
||||
};
|
||||
|
||||
export default CityDropdown;
|
||||
```
|
||||
- [ ] **See the analysis of `CityDropdown` component code in the additional info**
|
||||
## Creating `transportationCard` Component
|
||||
```ts
|
||||
import { TransportationSearchResult } from "@/shared/models/transportation/transportationSearchResult";
|
||||
import React from "react";
|
||||
|
||||
interface Props {
|
||||
transportation: TransportationSearchResult;
|
||||
}
|
||||
|
||||
const TransportationCard: React.FC<Props> = ({ transportation }) => {
|
||||
return (
|
||||
<div className="flex items-center justify-between p-4 border rounded-md shadow-md mb-4">
|
||||
{/* Price and Select Button */}
|
||||
<div className="flex flex-col items-center">
|
||||
<div className="text-blue-600 font-bold text-lg">
|
||||
{transportation.price} Toman
|
||||
</div>
|
||||
<button className="bg-blue-500 text-white px-4 py-2 rounded-md mt-2 hover:bg-blue-600">
|
||||
Select Ticket
|
||||
</button>
|
||||
</div>
|
||||
{/* Trip Info */}
|
||||
<div className="flex-1 mx-4 text-center">
|
||||
<div className="font-semibold text-gray-700">
|
||||
{transportation.companyTitle}
|
||||
</div>
|
||||
<div className="flex items-center justify-center mt-2">
|
||||
<div className="mx-2">{transportation.fromCityTitle}</div>
|
||||
<span className="text-gray-400">→</span>
|
||||
<div className="mx-2">{transportation.toCityTitle}</div>
|
||||
</div>
|
||||
|
||||
<div className="text-sm text-gray-500 mt-1">
|
||||
{new Date(transportation.startDateTime).toLocaleDateString("en", {
|
||||
hour: "2-digit",
|
||||
minute: "2-digit",
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Company Logo or Placeholder */}
|
||||
<div className="w-12 h-12">
|
||||
<img
|
||||
src="/images/company-placeholder.png"
|
||||
alt="company"
|
||||
className="w-full h-full object-contain"
|
||||
/>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default TransportationCard;
|
||||
```
|
||||
|
||||
## Creating `transportationSearchForm` Component
|
||||
|
||||
See the code from:
|
||||
https://github.com/MehrdadShirvani/AlibabaClone-Frontend/tree/develop/alibabaclone-frontend/src/features/transportation
|
||||
|
||||
|
||||
# 🧠 Hints & Notes
|
||||
# 🙌 Acknowledgements
|
||||
|
||||
- ChatGPT for snippet refinement and explanations
|
||||
# 🔍 References
|
||||
- [ ] Check out [[Session05 Additional Info]]
|
||||
- [ ] Watch for [React](https://www.youtube.com/watch?v=SqcY0GlETPk&ab_channel=ProgrammingwithMosh)
|
||||
@@ -0,0 +1,92 @@
|
||||
## `SearchResultsPage`
|
||||
|
||||
This page is responsible for:
|
||||
1. **Reading route parameters and query strings from the URL**
|
||||
2. **Sending those values as a form to the backend**
|
||||
3. **Showing the result (or loading/error message)**
|
||||
|
||||
|
||||
---
|
||||
### ✅ 1. **Route Parameters**
|
||||
When you define this route in `App.jsx`:
|
||||
|
||||
```jsx
|
||||
<Route path="/:vehicleId/:fromCityId/:toCityId" element={<SearchResultsPage />} />
|
||||
```
|
||||
|
||||
It means the URL will look like:
|
||||
```
|
||||
/1/21/45
|
||||
```
|
||||
Those values are extracted using:
|
||||
```js
|
||||
const { vehicleId, fromCityId, toCityId } = useParams();
|
||||
```
|
||||
|
||||
🔹 `useParams()` comes from React Router and gives you access to the dynamic parts of the URL.
|
||||
|
||||
---
|
||||
|
||||
### ✅ 2. **Query String Parameters**
|
||||
Suppose your full URL is:
|
||||
|
||||
```
|
||||
/1/21/45?departing=2025-06-01&arriving=2025-06-10
|
||||
```
|
||||
|
||||
These extra values after the `?` are **query string parameters**. They're accessed using:
|
||||
|
||||
```js
|
||||
const query = useQuery(); // Custom helper
|
||||
const departing = query.get("departing");
|
||||
const arriving = query.get("arriving");
|
||||
```
|
||||
|
||||
The helper `useQuery()` is:
|
||||
|
||||
```js
|
||||
function useQuery() {
|
||||
return new URLSearchParams(useLocation().search);
|
||||
}
|
||||
```
|
||||
|
||||
This uses React Router's `useLocation()` to access the full URL, and then parses the query string.
|
||||
|
||||
---
|
||||
|
||||
### ✅ 3. **Parsing and Converting Values**
|
||||
|
||||
React Router gives you everything as strings. So:
|
||||
|
||||
```js
|
||||
const vehicleTypeId = vehicleId ? parseInt(vehicleId, 10) : 1;
|
||||
const fromId = fromCityId ? parseInt(fromCityId, 10) : undefined;
|
||||
const toId = toCityId ? parseInt(toCityId, 10) : undefined;
|
||||
```
|
||||
|
||||
This ensures you have **numbers**, not strings, when building your form object.
|
||||
|
||||
---
|
||||
|
||||
### ✅ 4. **Building the Search Form and Fetching Data**
|
||||
|
||||
Now all data is combined into one `form` object:
|
||||
|
||||
```js
|
||||
const form = {
|
||||
vehicleTypeId,
|
||||
fromCityId: fromId,
|
||||
toCityId: toId,
|
||||
startDate: departing || null,
|
||||
endDate: arriving || null,
|
||||
};
|
||||
```
|
||||
|
||||
Then it sends that to the backend:
|
||||
|
||||
```js
|
||||
agent.TransportationSearch.search(form)
|
||||
.then(setResults)
|
||||
.catch(err => console.error(err))
|
||||
.finally(() => setLoading(false));
|
||||
```
|
||||
@@ -0,0 +1,293 @@
|
||||
|
||||
# 🛠️ Task Checklist
|
||||
|
||||
## 🚧Branching
|
||||
- [ ] Create the feature/navbar branch based on develop
|
||||
|
||||
## Adding a Navbar
|
||||
### 🔹 What Is a Navbar?
|
||||
- A **navigation bar (navbar)** is a UI element typically placed at the **top** or **side** of a web app.
|
||||
|
||||
- [ ] Use a `<nav>` with `flex`, `justify-between`, `items-center`.
|
||||
- [ ] Add buttons or links like Home, Search, About. (the links can be empty now)
|
||||
|
||||
use this as a reference: [link](https://github.com/MehrdadShirvani/AlibabaClone-Frontend/tree/develop/alibabaclone-frontend/src/shared/components)
|
||||
|
||||
📂 Suggested Folder for navbar component: `src/shared/components/Navbar`
|
||||
📂 Suggested Folder for images: `public/images/`
|
||||
|
||||
## 🚧Merge
|
||||
- [ ] Create a PR and merge the current branch with develop
|
||||
## 🚧Branching
|
||||
- [ ] Create the feature/routing branch based on develop
|
||||
|
||||
## Converting Search Functionality From Single Component to Routed Pages
|
||||
|
||||
Originally, transportation search logic and UI may have all been inside one component — which quickly becomes messy and hard to manage as your app grows.
|
||||
|
||||
Now we’ve **split the logic into two proper pages**:
|
||||
|
||||
### ✅ `SearchPage.jsx`
|
||||
- Responsible only for showing the **search form**.
|
||||
- Clean and minimal.
|
||||
- Uses the reusable `TransportationSearchForm` component.
|
||||
- Use this as a reference: [link](https://github.com/MehrdadShirvani/AlibabaClone-Frontend/tree/develop/alibabaclone-frontend/src/features/transportation/pages)
|
||||
|
||||
### ✅ `SearchResultsPage.jsx`
|
||||
- Responsible for **fetching and showing results**.
|
||||
- Reads route parameters and query strings.
|
||||
- Calls the backend using `agent`.
|
||||
- Shows a loading state, handles empty results, and renders cards.
|
||||
- Use this as a reference: [link](https://github.com/MehrdadShirvani/AlibabaClone-Frontend/tree/develop/alibabaclone-frontend/src/features/transportation/pages)
|
||||
|
||||
|
||||
This separation improves:
|
||||
- Routing and navigation
|
||||
- Code readability and maintainability
|
||||
- Reusability of components like the search form and result cards
|
||||
---
|
||||
|
||||
### **Create Pages into a Pages Folder**
|
||||
|
||||
📂 Suggested Folder for navbar component
|
||||
Inside your `features/transportation` folder:
|
||||
```
|
||||
pages/
|
||||
├── SearchPage.jsx
|
||||
├── SearchResultsPage.jsx
|
||||
components/
|
||||
├── TransportationSearchForm.jsx
|
||||
├── TransportationCard.jsx
|
||||
```
|
||||
|
||||
- [ ] Create `SearchPage`
|
||||
```jsx
|
||||
import TransportationSearchForm from "@/features/transportation/transportationSearchForm";
|
||||
|
||||
const SearchPage = () => {
|
||||
return (
|
||||
<div className="container mx-auto py-6">
|
||||
<TransportationSearchForm />
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default SearchPage;
|
||||
```
|
||||
> Keep the form clean and layout minimal.
|
||||
|
||||
- [ ] Create `SearchResultsPage`
|
||||
- Use `useParams()` for URL parameters (`vehicleId`, `fromCityId`, `toCityId`)
|
||||
- Use `useLocation()` and `URLSearchParams` to read query strings (`departing`, `arriving`)
|
||||
- Fetch data from backend using a shared `agent`
|
||||
- Display a loading state, empty message, and the result list
|
||||
|
||||
Code Example:
|
||||
```jsx
|
||||
import { useEffect, useState } from "react";
|
||||
import { useLocation, useParams } from "react-router-dom";
|
||||
import agent from "@/shared/api/agent";
|
||||
import { TransportationSearchResult } from "@/shared/models/transportation/transportationSearchResult";
|
||||
import TransportationCard from "@/features/transportation/transportationCard";
|
||||
|
||||
function useQuery() {
|
||||
return new URLSearchParams(useLocation().search);
|
||||
}
|
||||
|
||||
const SearchResultsPage = () => {
|
||||
const { vehicleId, fromCityId, toCityId } = useParams();
|
||||
const vehicleTypeId = vehicleId ? parseInt(vehicleId, 1) : 1;
|
||||
const fromId = fromCityId ? parseInt(fromCityId, 1) : undefined;
|
||||
const toId = toCityId ? parseInt(toCityId, 1) : undefined;
|
||||
|
||||
const query = useQuery();
|
||||
|
||||
const departing = query.get("departing");
|
||||
const arriving = query.get("arriving");
|
||||
|
||||
|
||||
const [results, setResults] = useState<TransportationSearchResult[]>([]);
|
||||
const [loading, setLoading] = useState(true);
|
||||
|
||||
|
||||
|
||||
useEffect(() => {
|
||||
|
||||
const form = {
|
||||
vehicleTypeId,
|
||||
fromCityId: fromId,
|
||||
toCityId: toId,
|
||||
startDate: departing || null,
|
||||
endDate: arriving || null,
|
||||
};
|
||||
|
||||
|
||||
|
||||
agent.TransportationSearch.search(form)
|
||||
.then(setResults)
|
||||
.catch((err) => console.error(err))
|
||||
.finally(() => setLoading(false));
|
||||
}, [vehicleId, fromCityId, toCityId, departing, arriving]);
|
||||
|
||||
|
||||
return (
|
||||
<div className="container mx-auto py-6">
|
||||
<h2 className="text-2xl font-bold mb-4">Search Results</h2>
|
||||
{loading ? (
|
||||
<p>Loading...</p>
|
||||
) : results.length === 0 ? (
|
||||
<p>No results found.</p>
|
||||
) : (
|
||||
<div className="space-y-4">
|
||||
{results.map((r) => (
|
||||
<TransportationCard key={r.id} transportation={r} />
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
|
||||
export default SearchResultsPage;
|
||||
```
|
||||
|
||||
#### 🔍 `SearchResultsPage` – Understanding Parameters and Arguments
|
||||
- [ ] Check out [[Session06 Additional Info]]
|
||||
|
||||
---
|
||||
### Modifying `TransportationSearchForm`:
|
||||
> Use this as a reference: [link](https://github.com/MehrdadShirvani/AlibabaClone-Frontend/tree/develop/alibabaclone-frontend/src/features/transportation)
|
||||
|
||||
- [ ] Remove Search Result fetching from inside the component
|
||||
- [ ] Update `handleSearch` to Navigate with Parameters
|
||||
|
||||
```tsx
|
||||
const handleSearch = () => {
|
||||
if (!form.fromCityId || !form.toCityId || !form.startDate || !form.vehicleTypeId)
|
||||
return;
|
||||
|
||||
const params = new URLSearchParams();
|
||||
|
||||
if (form.startDate instanceof Date)
|
||||
params.append("departing", form.startDate.toISOString());
|
||||
else if (typeof form.startDate === "string")
|
||||
params.append("departing", form.startDate);
|
||||
|
||||
if (form.endDate instanceof Date)
|
||||
params.append("arriving", form.endDate.toISOString());
|
||||
else if (typeof form.endDate === "string")
|
||||
params.append("arriving", form.endDate);
|
||||
|
||||
navigate(
|
||||
`/${form.vehicleTypeId}/${form.fromCityId}/${form.toCityId}?${params.toString()}`
|
||||
);
|
||||
};
|
||||
```
|
||||
|
||||
**Changes made:**
|
||||
- It **checks form validity** first.
|
||||
- Then it builds a **URL using `URLSearchParams`** for `departing` and `arriving` dates.
|
||||
- Then it calls `navigate(...)` to go to a **route like**:
|
||||
```
|
||||
/1/2/3?departing=2025-06-15T00%3A00%3A00.000Z&arriving=2025-06-18T00%3A00%3A00.000Z
|
||||
```
|
||||
|
||||
> That route (`/vehicleTypeId/fromCityId/toCityId`) will be handled by your `SearchResultPage` via `react-router`.
|
||||
|
||||
---
|
||||
|
||||
- [ ] Used `useNavigate` from `react-router-dom`
|
||||
```tsx
|
||||
import { useNavigate } from "react-router-dom";
|
||||
```
|
||||
- [ ] Removed Result Display Section
|
||||
|
||||
## Add Routing
|
||||
- [ ] Change `App.tsx` as the following:
|
||||
```tsx
|
||||
import Navbar from "@/shared/components/navbar";
|
||||
import "./App.css";
|
||||
|
||||
import { BrowserRouter as Router, Routes, Route } from "react-router-dom";
|
||||
import SearchPage from "@/features/transportation/pages/SearchPage";
|
||||
import SearchResultsPage from "@/features/transportation/pages/SearchResultsPage";
|
||||
|
||||
function App() {
|
||||
return (
|
||||
<Router>
|
||||
<Navbar /> {/* Navbar will show on all pages */}
|
||||
<div className="pt-16">
|
||||
{" "}
|
||||
{/* padding top if navbar is fixed */}
|
||||
<Routes>
|
||||
<Route path="/" element={<SearchPage />} />
|
||||
<Route
|
||||
path="/:vehicleId/:fromCityId/:toCityId"
|
||||
element={<SearchResultsPage />}
|
||||
/>
|
||||
</Routes>
|
||||
</div>
|
||||
</Router>
|
||||
);
|
||||
}
|
||||
|
||||
export default App;
|
||||
```
|
||||
|
||||
### Explanation of `App.jsx`
|
||||
|
||||
#### 🔁 `Router` & `Routes`:
|
||||
- Wraps the entire app in `<Router>` so that React Router can manage navigation.
|
||||
- `<Routes>` contains all the individual page routes.
|
||||
---
|
||||
#### 📌 Routes:
|
||||
- `/`: Loads `SearchPage`. This is your home/search form.
|
||||
- `/:vehicleId/:fromCityId/:toCityId`: Loads `SearchResultsPage`. This URL carries parameters to display results based on user input.
|
||||
|
||||
---
|
||||
|
||||
#### 🎯 Navbar Placement:
|
||||
- Placed **outside** `<Routes>`, so it shows on **all pages**.
|
||||
|
||||
---
|
||||
|
||||
## Setting Up Routing
|
||||
|
||||
- [ ] **Install React Router** (If you haven't already)
|
||||
|
||||
```bash
|
||||
npm install react-router-dom
|
||||
```
|
||||
|
||||
- [ ] **Wrap Your App in Router**
|
||||
|
||||
```jsx
|
||||
<Router>
|
||||
<Navbar />
|
||||
<Routes>
|
||||
{/* your routes here */}
|
||||
</Routes>
|
||||
</Router>
|
||||
```
|
||||
|
||||
- [ ] **Define Routes**
|
||||
|
||||
```jsx
|
||||
<Route path="/" element={<SearchPage />} />
|
||||
<Route path="/:vehicleId/:fromCityId/:toCityId" element={<SearchResultsPage />} />
|
||||
```
|
||||
## 🚧Merge
|
||||
- [ ] Create a PR and merge the current branch with develop
|
||||
|
||||
# 🧠 Hints & Notes
|
||||
# 🙌 Acknowledgements
|
||||
|
||||
- ChatGPT for snippet refinement and explanations
|
||||
# 🔍 References
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -0,0 +1,785 @@
|
||||
|
||||
# AUTHENTICATION METHODS FOR ASP.NET CORE WEB API
|
||||
|
||||
### 1. **JWT (JSON Web Token) Authentication**
|
||||
|
||||
**Most common for SPAs (like your React frontend)**
|
||||
|
||||
- ✅ **Stateless**: No session stored on the server.
|
||||
|
||||
- 🔐 **Token structure**: Header + Payload (including roles) + Signature.
|
||||
|
||||
- 🎯 Good for: SPAs, mobile apps, APIs.
|
||||
|
||||
- 📦 Libraries: `Microsoft.AspNetCore.Authentication.JwtBearer`.
|
||||
|
||||
|
||||
**Setup Highlights:**
|
||||
|
||||
- Client sends credentials → API generates token with user roles → Client stores token → Client sends token in `Authorization` header (`Bearer <token>`).
|
||||
|
||||
- Server verifies signature and extracts user identity and roles from the token.
|
||||
|
||||
|
||||
**Role support**:
|
||||
|
||||
- You embed roles inside the JWT (`"roles": ["Admin", "User"]`).
|
||||
|
||||
- Use `[Authorize(Roles = "Admin")]`.
|
||||
|
||||
|
||||
---
|
||||
|
||||
### 2. **Cookie-Based Authentication**
|
||||
|
||||
**Traditional approach for server-rendered apps (not ideal for APIs)**
|
||||
|
||||
- 🛑 **Not recommended for APIs** due to CSRF vulnerability and session overhead.
|
||||
|
||||
- 🗂 Stores session identifier in browser cookie.
|
||||
|
||||
- Useful when using ASP.NET Core MVC or Razor Pages (not Web API).
|
||||
|
||||
|
||||
---
|
||||
|
||||
### 3. **OAuth2 + OpenID Connect (OIDC)**
|
||||
|
||||
**Best for federated login, single sign-on (SSO), or external providers**
|
||||
|
||||
- 🔗 Integrates with Identity Providers (IDPs) like:
|
||||
|
||||
- Azure AD
|
||||
|
||||
- Google, Facebook, GitHub
|
||||
|
||||
- Auth0, Okta, Duende IdentityServer
|
||||
|
||||
- 📦 Library: `Microsoft.AspNetCore.Authentication.OpenIdConnect`
|
||||
|
||||
- Uses **access tokens** (JWT) issued by an authority.
|
||||
|
||||
|
||||
**Role support**:
|
||||
|
||||
- Roles/claims provided by the Identity Provider.
|
||||
|
||||
- You map these claims to roles in the API.
|
||||
|
||||
- `[Authorize(Roles = "Admin")]` still works.
|
||||
|
||||
|
||||
---
|
||||
|
||||
### 4. **API Key Authentication**
|
||||
|
||||
**Lightweight alternative (not ideal for user-based roles)**
|
||||
|
||||
- Client includes a static API key in headers or query string.
|
||||
|
||||
- 🔐 No user context → ❌ no role-based support unless you map API keys to roles in a custom way.
|
||||
|
||||
- 🔧 Implemented manually in middleware or filters.
|
||||
|
||||
|
||||
---
|
||||
|
||||
### 5. **Basic Authentication**
|
||||
|
||||
- User provides `username:password` in Base64 via `Authorization` header.
|
||||
|
||||
- ❌ Insecure unless used with HTTPS.
|
||||
|
||||
- ⚠️ Rarely used anymore — not good for role-based systems or production apps.
|
||||
|
||||
|
||||
---
|
||||
|
||||
### 6. **ASP.NET Core Identity**
|
||||
|
||||
**Full-featured user management system (often combined with JWT)**
|
||||
|
||||
- ✅ Provides login, registration, role management, password hashing, etc.
|
||||
|
||||
- 🎯 Good choice if you want to **own the user system** and **manage roles** yourself.
|
||||
|
||||
- Can be used with:
|
||||
|
||||
- JWT tokens (custom token generation)
|
||||
|
||||
- Cookie auth (not for APIs)
|
||||
|
||||
- 🔧 Use `UserManager`, `RoleManager`.
|
||||
|
||||
|
||||
**Example**: Use Identity for creating users and roles, then issue JWTs on login.
|
||||
|
||||
---
|
||||
|
||||
|
||||
## Summary Table
|
||||
|
||||
|Method|Stateless|Token-Based|Role Support|Ideal For|
|
||||
|---|---|---|---|---|
|
||||
|JWT Authentication|✅|✅|✅|APIs, SPAs (React, etc.)|
|
||||
|Cookie Authentication|❌|❌|✅|Server-side apps only|
|
||||
|OAuth2 + OpenID Connect|✅|✅|✅|External login, SSO, enterprise|
|
||||
|API Key|✅|❌|❌ (manual)|Simple apps, service-to-service|
|
||||
|Basic Auth|✅|❌|❌ (manual)|Very basic use, not recommended|
|
||||
|ASP.NET Core Identity|❌|Optional|✅|User/Role management|
|
||||
|
||||
---
|
||||
|
||||
# Web Security and JWT Terms
|
||||
|
||||
|
||||
### 🧨 **CSP (Content Security Policy)**
|
||||
|
||||
**Definition:**
|
||||
A **browser security mechanism** that helps prevent **XSS attacks** by controlling which sources the browser can load content from.
|
||||
|
||||
**Example Usage:**
|
||||
It can prevent JavaScript from running unless it's from a trusted source.
|
||||
|
||||
**Example CSP header:**
|
||||
|
||||
```http
|
||||
Content-Security-Policy: default-src 'self'; script-src 'self' https://trustedscripts.example.com
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 💥 **XSS (Cross-Site Scripting)**
|
||||
|
||||
**Definition:**
|
||||
A vulnerability where an attacker **injects malicious JavaScript** into a web page that gets executed in a user’s browser.
|
||||
|
||||
**Why It Matters for JWT:**
|
||||
If you store JWTs in `localStorage`, and your app is vulnerable to XSS, the attacker can steal the token and impersonate the user.
|
||||
|
||||
**Prevention:**
|
||||
|
||||
- Never trust user input (sanitize!)
|
||||
|
||||
- Use **CSP headers**
|
||||
|
||||
- Avoid inline scripts
|
||||
|
||||
- Use frameworks that auto-escape HTML (like React, Razor)
|
||||
|
||||
|
||||
---
|
||||
|
||||
### 📦 **Payload (in JWT)**
|
||||
|
||||
**Definition:**
|
||||
The **middle part** of a JWT. It contains the **claims** (user data, permissions, roles, etc.) in a **Base64-encoded JSON** format.
|
||||
|
||||
**Example:**
|
||||
|
||||
```json
|
||||
{
|
||||
"sub": "1234567890",
|
||||
"name": "John Doe",
|
||||
"role": "Admin",
|
||||
"exp": 1716850984
|
||||
}
|
||||
```
|
||||
|
||||
⚠️ **Note:** Payload is **not encrypted**, just encoded — anyone can read it, but not modify it without invalidating the signature.
|
||||
|
||||
---
|
||||
|
||||
### ✍️ **Signature (in JWT)**
|
||||
|
||||
**Definition:**
|
||||
The **third part** of the token. It's a cryptographic hash (HMAC or RSA/ECDSA) of the header and payload, signed with a **secret** or private key.
|
||||
|
||||
**Purpose:**
|
||||
To verify that the token **hasn’t been tampered with**.
|
||||
|
||||
**Structure:**
|
||||
|
||||
```
|
||||
JWT = base64(header) + '.' + base64(payload) + '.' + signature
|
||||
```
|
||||
|
||||
Only the server (with the secret key) can validate the signature.
|
||||
|
||||
---
|
||||
|
||||
### 🦠 **CSRF (Cross-Site Request Forgery)**
|
||||
|
||||
**Definition:**
|
||||
A vulnerability where an attacker tricks a user’s **browser** (with a valid session/cookie) into making an unwanted request to your site **without the user’s knowledge**.
|
||||
|
||||
**Example:**
|
||||
If you're logged in and visit a malicious site, that site may submit a POST request using your cookies to perform actions on your behalf.
|
||||
|
||||
**Why It Matters:**
|
||||
|
||||
- If you store JWTs in **HttpOnly cookies**, you must guard against CSRF.
|
||||
|
||||
|
||||
**Defense:**
|
||||
|
||||
- Use `SameSite=Strict` cookies
|
||||
|
||||
- Use anti-CSRF tokens
|
||||
|
||||
- Prefer `Authorization` header with tokens (doesn’t auto-send like cookies)
|
||||
|
||||
|
||||
---
|
||||
|
||||
### 🧠 **Session Overhead**
|
||||
|
||||
**Definition:**
|
||||
The **memory and server resource cost** of maintaining a session for each logged-in user on the server.
|
||||
|
||||
**Why It Matters:**
|
||||
|
||||
- Traditional **cookie-based auth** stores session info on the server.
|
||||
|
||||
- With JWT, the session is **stateless** (no server memory), reducing overhead and scaling better.
|
||||
|
||||
|
||||
---
|
||||
|
||||
## 🔑 **Authentication Ecosystem Concepts**
|
||||
|
||||
---
|
||||
|
||||
### 🌐 **Federated Login**
|
||||
|
||||
**Definition:**
|
||||
Users log in to your application using **another trusted identity provider** (IdP), such as:
|
||||
|
||||
- Google
|
||||
|
||||
- Facebook
|
||||
|
||||
- Microsoft
|
||||
|
||||
- GitHub
|
||||
|
||||
|
||||
You delegate authentication to the third party and just receive user info (often as a JWT or OpenID Connect token).
|
||||
|
||||
**Protocol Examples:**
|
||||
|
||||
- OAuth2
|
||||
|
||||
- OpenID Connect
|
||||
|
||||
|
||||
---
|
||||
|
||||
### 🔁 **SSO (Single Sign-On)**
|
||||
|
||||
**Definition:**
|
||||
A user logs in **once** and gains access to **multiple systems or applications** without logging in again.
|
||||
|
||||
**Common in Enterprises**:
|
||||
|
||||
- Logging into one dashboard gives you access to HR system, email, file storage, etc.
|
||||
|
||||
|
||||
**How It Works:**
|
||||
|
||||
- A **central identity provider (IdP)** issues tokens
|
||||
|
||||
- Applications **trust the token** and skip login
|
||||
|
||||
- Often uses **OAuth2**, **OpenID Connect**, or **SAML**
|
||||
|
||||
|
||||
---
|
||||
|
||||
## 🔄 Summary Table
|
||||
|
||||
|Term|Meaning|
|
||||
|---|---|
|
||||
|**CSP**|Restricts sources of scripts/styles to prevent XSS|
|
||||
|**XSS**|Injected JavaScript that steals data like JWT tokens|
|
||||
|**JWT Payload**|JSON with user claims (readable but not secure on its own)|
|
||||
|**JWT Signature**|Proves the token is untampered (signed by server)|
|
||||
|**CSRF**|Unauthorized actions using authenticated user's browser/session|
|
||||
|**Session Overhead**|Cost of storing user sessions in memory on the server|
|
||||
|**Federated Login**|Login using a third-party identity provider (e.g., Google)|
|
||||
|**SSO**|One login grants access to multiple trusted systems|
|
||||
|
||||
|
||||
# ASP.NET Identity vs JWT Authentication
|
||||
|
||||
---
|
||||
|
||||
### ✅ **What is ASP.NET Identity?**
|
||||
|
||||
**ASP.NET Identity** is a full **membership system** that:
|
||||
|
||||
- Manages users, roles, passwords, claims, tokens, and external logins.
|
||||
|
||||
- Stores user data in a **database** (usually using Entity Framework).
|
||||
|
||||
- Works well with **cookie-based authentication** by default.
|
||||
|
||||
- Handles login, logout, registration, password hashing, email confirmation, two-factor auth, etc.
|
||||
|
||||
|
||||
**Default storage:** SQL Server (via Entity Framework)
|
||||
|
||||
---
|
||||
|
||||
### 🔑 **How Authentication Works in ASP.NET Identity (Cookie-Based)**
|
||||
|
||||
1. **Login request**: The user submits a form with username/password.
|
||||
|
||||
2. **Server validates** the credentials using ASP.NET Identity.
|
||||
|
||||
3. If valid, the server issues a **cookie** (with a session token).
|
||||
|
||||
4. The browser stores this **authentication cookie**.
|
||||
|
||||
5. For future requests, the browser **automatically sends the cookie**.
|
||||
|
||||
6. The server validates the cookie (and reads the user session from memory or database).
|
||||
|
||||
|
||||
✅ **Stateful**
|
||||
❌ Doesn't scale well for large APIs unless you add external session storage (like Redis).
|
||||
|
||||
---
|
||||
|
||||
### 🔐 What is JWT Authentication?
|
||||
|
||||
JWT Authentication is:
|
||||
|
||||
- **Stateless**.
|
||||
|
||||
- Based on tokens — not sessions.
|
||||
|
||||
- Works well for APIs and SPAs/mobile apps.
|
||||
|
||||
|
||||
#### Flow:
|
||||
|
||||
1. **User logs in**, and if credentials are valid...
|
||||
|
||||
2. Server **creates a JWT** containing user info (e.g., roles).
|
||||
|
||||
3. Server **signs the token** and sends it to the client.
|
||||
|
||||
4. Client stores it (e.g., in localStorage or cookies).
|
||||
|
||||
5. On every request, the client sends the JWT in the `Authorization` header.
|
||||
|
||||
6. Server **verifies the JWT signature** using a secret or key.
|
||||
|
||||
7. If valid → allow access (no server-side session needed).
|
||||
|
||||
|
||||
✅ **Stateless**
|
||||
✅ Scales easily
|
||||
✅ Good for distributed APIs
|
||||
|
||||
---
|
||||
|
||||
### 📊 Comparison Table
|
||||
|
||||
|Feature|ASP.NET Identity (Cookie)|JWT Authentication (Token-Based)|
|
||||
|---|---|---|
|
||||
|**Stateful/Stateless**|Stateful|Stateless|
|
||||
|**Storage**|Cookie on client, session on server|Token on client only|
|
||||
|**Default Transport**|Cookie (auto-sent by browser)|Authorization header (manual send)|
|
||||
|**Built-in Support**|ASP.NET Identity (UI + EF Core)|ASP.NET Core + Manual JWT setup|
|
||||
|**Scalability**|Limited (server stores session)|High (no session to manage)|
|
||||
|**Security**|Cookie CSRF risk|XSS risk if stored in JS-accessible storage|
|
||||
|**Use Case**|Web apps with UI (MVC, Razor)|APIs, SPAs, mobile apps|
|
||||
|**External login support**|Built-in|Needs integration|
|
||||
|**Token expiration**|Server-controlled session|Token has expiration embedded|
|
||||
|
||||
---
|
||||
|
||||
### 🚨 Statelessness — What Does It Mean?
|
||||
|
||||
- **Stateful Authentication**: Server **stores a session** (usually in memory or a database) for each user. The client just stores a cookie with a session ID.
|
||||
|
||||
- When user logs in, server keeps a record of that.
|
||||
|
||||
- Logout → delete session.
|
||||
|
||||
- **Stateless Authentication** (JWT):
|
||||
|
||||
- Server **does not remember anything**.
|
||||
|
||||
- JWT has all info about the user in itself (claims, roles, expiry).
|
||||
|
||||
- Logout = just delete token on client side (server has no "memory").
|
||||
|
||||
|
||||
⚠️ **You can't "force logout" someone server-side in pure JWT unless you blacklist tokens manually** (or rotate secrets).
|
||||
|
||||
---
|
||||
|
||||
### 💡 Can You Use ASP.NET Identity with JWT?
|
||||
|
||||
Yes — ASP.NET Identity can be configured to:
|
||||
|
||||
- Authenticate user credentials
|
||||
|
||||
- Then issue a **JWT** instead of using a cookie
|
||||
|
||||
- This way, you get:
|
||||
|
||||
- ASP.NET Identity’s user management
|
||||
|
||||
- JWT’s **stateless** API authentication
|
||||
|
||||
|
||||
This is often used in **hybrid apps**:
|
||||
|
||||
- Use Identity for registration/login
|
||||
|
||||
- Use JWT for frontend and mobile API calls
|
||||
|
||||
|
||||
|
||||
# What does `null!` mean?
|
||||
|
||||
It's a **null-forgiving operator**.
|
||||
|
||||
When you write:
|
||||
|
||||
```csharp
|
||||
string? maybeNull = null;
|
||||
string definitelyNotNull = null!; // ← This tells the compiler: “Trust me, it’s not null.”
|
||||
```
|
||||
|
||||
You're telling the compiler:
|
||||
|
||||
> _“I know this is technically null, but treat it as non-null and don’t warn me.”_
|
||||
|
||||
---
|
||||
|
||||
### 🧩 Why is this needed?
|
||||
|
||||
C# 8 introduced **nullable reference types**, where the compiler gives warnings if you assign or use `null` where it's not safe.
|
||||
|
||||
For example:
|
||||
|
||||
```csharp
|
||||
string name = null; // Warning: assigning null to non-nullable reference
|
||||
```
|
||||
|
||||
To silence the warning, you can use `null!`:
|
||||
|
||||
```csharp
|
||||
string name = null!; // No warning
|
||||
```
|
||||
|
||||
But this is **your responsibility**: the compiler **assumes you're guaranteeing** that it won’t cause a `NullReferenceException`.
|
||||
|
||||
---
|
||||
|
||||
### ⚠️ When to use `null!`?
|
||||
|
||||
Use it **sparingly and carefully**, only when:
|
||||
|
||||
- You know an object **will be initialized later**.
|
||||
|
||||
- You’re working with **ORMs like EF Core** where properties are set **after construction**.
|
||||
|
||||
- You're dealing with **dependency injection** and the value is assigned **automatically later**.
|
||||
|
||||
|
||||
#### Example:
|
||||
|
||||
```csharp
|
||||
public class Account
|
||||
{
|
||||
public string PhoneNumber { get; set; } = null!; // EF will populate it
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### ✅ Summary
|
||||
|
||||
|Expression|Meaning|
|
||||
|---|---|
|
||||
|`null!`|"This is null, but treat it as if it's non-null (trust me)."|
|
||||
|Use case|Avoiding compiler warnings for nullable reference types|
|
||||
|Risk|Can lead to runtime `NullReferenceException` if used incorrectly|
|
||||
|
||||
---
|
||||
|
||||
Let me know if you want help turning on/off nullable reference types or dealing with EF Core model warnings.
|
||||
|
||||
|
||||
# What is a JWT Generator?
|
||||
|
||||
A **JWT Generator** is a class or service in your backend that:
|
||||
|
||||
1. **Generates** a signed JWT (JSON Web Token) when a user logs in.
|
||||
|
||||
2. **Encodes** the user's identity, roles, and other claims.
|
||||
|
||||
3. **Signs** the token with a secret or private key so it can be validated later.
|
||||
|
||||
|
||||
---
|
||||
|
||||
## 🔧 Structure of a JWT
|
||||
|
||||
A JWT has 3 parts:
|
||||
|
||||
```plaintext
|
||||
xxxxx.yyyyy.zzzzz
|
||||
```
|
||||
|
||||
1. **Header** (Base64-encoded JSON):
|
||||
|
||||
```json
|
||||
{
|
||||
"alg": "HS256",
|
||||
"typ": "JWT"
|
||||
}
|
||||
```
|
||||
|
||||
2. **Payload** (Base64-encoded JSON):
|
||||
Contains user data and claims (e.g., user ID, role, expiry).
|
||||
|
||||
```json
|
||||
{
|
||||
"sub": "userId123",
|
||||
"phone": "0930xxx",
|
||||
"role": "Admin",
|
||||
"exp": 1717502800
|
||||
}
|
||||
```
|
||||
|
||||
3. **Signature**:
|
||||
HMACSHA256(header + "." + payload, secret key)
|
||||
|
||||
|
||||
---
|
||||
|
||||
## ✅ What Goes into the Payload?
|
||||
|
||||
Include things you want to check _without querying the DB every time_:
|
||||
|
||||
- `sub` (Subject – usually user ID)
|
||||
|
||||
- `phone` or username
|
||||
|
||||
- `role` (e.g., "Admin", "User")
|
||||
|
||||
- `exp` (expiration timestamp)
|
||||
|
||||
- Any custom claim, like `companyId`, `verified`, etc.
|
||||
|
||||
|
||||
---
|
||||
|
||||
## 🔐 Signature
|
||||
|
||||
The **signature ensures** that the token hasn't been tampered with. If the signature doesn't match (due to modification or incorrect secret), the token is invalid.
|
||||
|
||||
---
|
||||
|
||||
## 🧱 Example: JWT Generator in ASP.NET Core
|
||||
|
||||
```csharp
|
||||
public interface IJwtGenerator
|
||||
{
|
||||
string GenerateToken(Account account);
|
||||
}
|
||||
```
|
||||
|
||||
```csharp
|
||||
public class JwtGenerator : IJwtGenerator
|
||||
{
|
||||
private readonly IConfiguration _config;
|
||||
|
||||
public JwtGenerator(IConfiguration config)
|
||||
{
|
||||
_config = config;
|
||||
}
|
||||
|
||||
public string GenerateToken(Account account)
|
||||
{
|
||||
var claims = new[]
|
||||
{
|
||||
new Claim(JwtRegisteredClaimNames.Sub, account.Id.ToString()),
|
||||
new Claim(JwtRegisteredClaimNames.PhoneNumber, account.PhoneNumber),
|
||||
new Claim(ClaimTypes.Role, account.Role),
|
||||
};
|
||||
|
||||
var key = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(_config["Jwt:Key"]!));
|
||||
var creds = new SigningCredentials(key, SecurityAlgorithms.HmacSha256);
|
||||
|
||||
var token = new JwtSecurityToken(
|
||||
issuer: _config["Jwt:Issuer"],
|
||||
audience: _config["Jwt:Audience"],
|
||||
claims: claims,
|
||||
expires: DateTime.UtcNow.AddHours(2),
|
||||
signingCredentials: creds
|
||||
);
|
||||
|
||||
return new JwtSecurityTokenHandler().WriteToken(token);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 🛠 Configuration in `appsettings.json`
|
||||
|
||||
```json
|
||||
"Jwt": {
|
||||
"Key": "YourSuperSecureSecretKey123!",
|
||||
"Issuer": "YourAppName",
|
||||
"Audience": "YourFrontendApp"
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 🧪 Token Validation
|
||||
|
||||
When the frontend sends this token on requests (via `Authorization: Bearer <token>` header), ASP.NET Core automatically validates:
|
||||
|
||||
- Signature (using the same secret)
|
||||
|
||||
- Expiry (`exp`)
|
||||
|
||||
- Audience, Issuer
|
||||
|
||||
- Claims (like role)
|
||||
|
||||
|
||||
This is done via `JwtBearer` middleware.
|
||||
|
||||
---
|
||||
|
||||
## 🔐 Roles in the Token?
|
||||
|
||||
Yes, you can (and should) include user roles. The backend will **automatically enforce** `[Authorize(Roles = "Admin")]` using that claim.
|
||||
|
||||
But:
|
||||
|
||||
- Don't rely **only** on frontend logic — always **protect endpoints** on the backend too.
|
||||
|
||||
- Frontend can use roles to **hide UI elements**, but not for enforcing access.
|
||||
|
||||
|
||||
---
|
||||
|
||||
## 🤔 Should You Encrypt the Token?
|
||||
|
||||
No, standard practice is:
|
||||
|
||||
- Don't encrypt JWTs — they’re just base64-encoded.
|
||||
|
||||
- **Don’t put sensitive data** inside.
|
||||
|
||||
- Sign them to prevent tampering.
|
||||
|
||||
- Secure the token in frontend (e.g., HttpOnly cookies or `localStorage` with care).
|
||||
|
||||
|
||||
---
|
||||
|
||||
## ✅ Summary of Responsibilities
|
||||
|
||||
|Responsibility|Backend (JWT Generator)|Frontend|
|
||||
|---|---|---|
|
||||
|Token generation|✅|❌|
|
||||
|Storing token|❌|✅ (localStorage / cookie)|
|
||||
|Sending token|❌|✅ (Authorization header)|
|
||||
|Token validation|✅ (`JwtBearer`)|❌|
|
||||
|Role enforcement|✅ (`[Authorize]`)|✅ (UI-level only)|
|
||||
|
||||
|
||||
|
||||
---
|
||||
|
||||
# How multiple roles in JWT claims work
|
||||
|
||||
- The JWT claims are basically a list of key-value pairs.
|
||||
|
||||
- For roles, the key is usually `"role"` or `ClaimTypes.Role`.
|
||||
|
||||
- You add **multiple claims with the same key** — one for each role.
|
||||
|
||||
|
||||
**Example:**
|
||||
|
||||
```json
|
||||
{
|
||||
"sub": "1234567890",
|
||||
"phone_number": "123456789",
|
||||
"role": "Admin",
|
||||
"role": "Editor",
|
||||
"role": "User",
|
||||
"exp": 1711600000
|
||||
}
|
||||
```
|
||||
|
||||
- When the token is created, it contains multiple `"role"` entries.
|
||||
|
||||
- On the backend, ASP.NET Core `ClaimsPrincipal` reads all of them, so `[Authorize(Roles="Admin,Editor")]` works by checking if **any** of the roles match.
|
||||
|
||||
|
||||
---
|
||||
|
||||
### How frontend handles multiple roles in the JWT
|
||||
|
||||
1. The frontend receives the JWT (usually after login).
|
||||
|
||||
2. It **decodes** the JWT payload (using a library like `jwt-decode`).
|
||||
|
||||
3. It extracts the roles as an **array of strings**.
|
||||
|
||||
|
||||
Example in React using `jwt-decode`:
|
||||
|
||||
```js
|
||||
import jwtDecode from 'jwt-decode';
|
||||
|
||||
const token = localStorage.getItem('token');
|
||||
const decoded = jwtDecode(token);
|
||||
const roles = decoded.role; // roles is usually an array if multiple roles exist
|
||||
|
||||
console.log(roles); // ["Admin", "Editor", "User"]
|
||||
```
|
||||
|
||||
4. The frontend can then use these roles to:
|
||||
|
||||
- Conditionally render UI components or routes.
|
||||
|
||||
- Show/hide buttons, pages, or features.
|
||||
|
||||
|
||||
---
|
||||
|
||||
### Important security note for frontend roles
|
||||
|
||||
- The frontend can **only do UI-level checks** based on roles.
|
||||
|
||||
- **Never trust frontend role checks to secure data or APIs.**
|
||||
|
||||
- The backend must always verify the token and roles via `[Authorize]` attributes or middleware.
|
||||
|
||||
|
||||
---
|
||||
|
||||
### Summary
|
||||
|
||||
| Step | How it works |
|
||||
| --------------------- | ----------------------------------------------------- |
|
||||
| JWT creation | Multiple `"role"` claims added to token |
|
||||
| Backend authorization | Validates token and checks if user has required roles |
|
||||
| Frontend decoding | Extracts `role` claim(s) as array of strings |
|
||||
| Frontend UI control | Shows or hides content based on roles |
|
||||
@@ -0,0 +1,543 @@
|
||||
|
||||
|
||||
# 🛠️ Task Checklist
|
||||
## 🚧 Branching
|
||||
|
||||
- [ ] Create the `feature/[name]` branch from `develop`
|
||||
|
||||
## Task
|
||||
- [ ] Task
|
||||
📂 Suggested Folder: `Domain/Framework/Interfaces/Respositories`
|
||||
# 🧠 Hints & Notes
|
||||
# 🙌 Acknowledgements
|
||||
|
||||
- ChatGPT for snippet refinement and explanations
|
||||
# 🔍 References
|
||||
|
||||
|
||||
|
||||
This file
|
||||
---
|
||||
## Add a field in Database
|
||||
- [ ] Add `User` field in `Roles` table using SSMS or Seed data in DbContext file
|
||||
- [ ] Make sure the property `PersonId` is nullable in `Account`, so you can add fields related to "Person" later after registration
|
||||
|
||||
## Branching
|
||||
- [ ] Create the feature/authentication branch based on develop
|
||||
|
||||
## Adjusting Account and Configurations
|
||||
|
||||
- [ ] Add navigation property for `AccountRoles` in Account
|
||||
```c#
|
||||
public virtual ICollection<AccountRole> AccountRoles { get; set; }
|
||||
```
|
||||
|
||||
- [ ] Add navigation properties for Account and Role in `AccountRole`
|
||||
```C#
|
||||
public virtual Role Role { get; set; }
|
||||
public virtual Account Account{ get; set; }
|
||||
```
|
||||
|
||||
- [ ] Update the entity configuration to reflect relationship mappings:
|
||||
```c#
|
||||
builder.HasOne<Account>(ar => ar.Account)
|
||||
.WithMany(a => a.AccountRoles)
|
||||
.HasForeignKey(ar => ar.AccountId)
|
||||
.OnDelete(DeleteBehavior.Restrict);
|
||||
|
||||
builder.HasOne<Role>(ar => ar.Role)
|
||||
.WithMany()
|
||||
.HasForeignKey(ar => ar.RoleId)
|
||||
.OnDelete(DeleteBehavior.Restrict);
|
||||
```
|
||||
|
||||
## Creating DTOs
|
||||
📂 Suggested Folder: ApplicationLayer/DTOs/[RelatedFolder]
|
||||
### AccountDto
|
||||
- [ ] Create `AccountDto` to expose relevant account information:
|
||||
```c#
|
||||
public class AccountDto
|
||||
{
|
||||
public long Id { get; set; }
|
||||
public required string PhoneNumber { get; set; }
|
||||
public required string Password { set; get; }
|
||||
public string? Email { get; set; }
|
||||
public long? PersonId { get; set; }
|
||||
public List<string> Roles { get; set; }
|
||||
}
|
||||
```
|
||||
### AuthResponseDto
|
||||
- [ ] Define a DTO for authentication responses:
|
||||
```c#
|
||||
public class AuthResponseDto
|
||||
{
|
||||
public long Id { get; set; }
|
||||
public string Token { get; set; } = null!;
|
||||
public string PhoneNumber { get; set; } = null!;
|
||||
public List<string> Roles { get; set; }
|
||||
}
|
||||
```
|
||||
- [ ] Define a DTO for login requests:
|
||||
```c#
|
||||
public class LoginRequestDto
|
||||
{
|
||||
public string PhoneNumber { get; set; } = null!;
|
||||
public string Password { get; set; } = null!;
|
||||
}
|
||||
```
|
||||
### RegisterRequestDto
|
||||
- [ ] Define a DTO for registration with validation attributes:
|
||||
```c#
|
||||
public class RegisterRequestDto
|
||||
{
|
||||
[Required(ErrorMessage = "Phone number is required.")]
|
||||
[Phone(ErrorMessage = "Phone number format is invalid.")]
|
||||
public required string PhoneNumber { get; set; }
|
||||
|
||||
[Required(ErrorMessage = "Password is required.")]
|
||||
[MinLength(6, ErrorMessage = "Password must be at least 6 characters long.")]
|
||||
public required string Password { get; set; }
|
||||
|
||||
[Compare("Password", ErrorMessage = "Passwords do not match.")]
|
||||
public required string ConfirmPassword { get; set; }
|
||||
}
|
||||
```
|
||||
|
||||
|
||||
### 🔹 **1. What do the annotations like `[Required]`, `[Phone]`, `[MinLength]`, `[Compare]` on the DTO do?**
|
||||
|
||||
These are **Data Annotations** from `System.ComponentModel.DataAnnotations`.
|
||||
|
||||
They’re used by:
|
||||
|
||||
- The ASP.NET Core `[ApiController]` attribute
|
||||
|
||||
- **Model binding & automatic validation**
|
||||
|
||||
|
||||
**What happens:**
|
||||
If your controller is marked with `[ApiController]`, ASP.NET Core will **automatically validate** the DTO against these annotations **before entering your action method**.
|
||||
|
||||
Example:
|
||||
|
||||
```csharp
|
||||
[ApiController]
|
||||
public class AuthController : ControllerBase
|
||||
```
|
||||
|
||||
Then this:
|
||||
|
||||
```csharp
|
||||
[HttpPost("register")]
|
||||
public async Task<IActionResult> Register(RegisterRequestDto dto)
|
||||
```
|
||||
|
||||
If `dto.PhoneNumber` is missing, it **won’t even run your logic**, and will return a `400 Bad Request` with validation errors.
|
||||
|
||||
|
||||
> “Why are these here if my frontend is separate?”
|
||||
|
||||
✅ **Answer**: They're still useful:
|
||||
|
||||
- For **security and safety**: you _must_ validate on the backend — never trust the frontend.
|
||||
|
||||
- For **auto validation** before hitting your logic — saving you boilerplate checks.
|
||||
|
||||
- You can use them for **Swagger/OpenAPI documentation** as well.
|
||||
|
||||
|
||||
Frontend validation is for **user experience**, not security.
|
||||
|
||||
---
|
||||
|
||||
### 🔹 **2. Where should password requirements be checked? Frontend or backend?**
|
||||
|
||||
✅ **Both.**
|
||||
|
||||
- **Frontend**: show real-time UX feedback (“Password must be 6+ characters”).
|
||||
- **Backend**: enforce security.
|
||||
|
||||
**Backend is the source of truth.**
|
||||
Frontend can be bypassed (e.g., Postman).
|
||||
|
||||
In the backend, you can either:
|
||||
|
||||
- Use annotations like `[MinLength(6)]`
|
||||
- Or do manual checks:
|
||||
|
||||
```csharp
|
||||
if (dto.Password.Length < 6)
|
||||
return BadRequest("Password must be at least 6 characters long.");
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 🔹 **3. Should confirm password be in the backend?**
|
||||
|
||||
✅ **Yes — if you're doing password comparison in backend.**
|
||||
|
||||
- `[Compare("Password")]` will validate if `ConfirmPassword` matches.
|
||||
|
||||
- Otherwise, you’ll need to check manually.
|
||||
|
||||
|
||||
You **can skip sending ConfirmPassword to backend** and just validate in frontend if you’re confident your frontend handles it.
|
||||
|
||||
But again: if someone sends malformed input manually (e.g., via Postman), backend should defend.
|
||||
|
||||
💡 **Best practice:**
|
||||
|
||||
- Validate `ConfirmPassword` in frontend (UX)
|
||||
- Do one last check in backend, or use `[Compare]` for auto-validation
|
||||
|
||||
|
||||
---
|
||||
|
||||
### 🔹 **4. Is it OK to send plain password in request? Or should we hash it on frontend?**
|
||||
|
||||
**✅ YES — it is OK and standard to send raw password in the login/signup request.**
|
||||
|
||||
Why?
|
||||
|
||||
- Passwords are sent over **HTTPS**, which encrypts the entire request.
|
||||
|
||||
- Hashing on frontend is _not_ secure, because:
|
||||
|
||||
- Your algorithm/salt would be exposed
|
||||
|
||||
- It defeats the purpose of salting and hashing correctly
|
||||
|
||||
- You lose control over security management
|
||||
|
||||
|
||||
### 🔹 **5. Error Response from Automatic Model Validation**
|
||||
|
||||
If your DTO looks like this:
|
||||
|
||||
```csharp
|
||||
public class RegisterRequestDto
|
||||
{
|
||||
[Required(ErrorMessage = "Phone number is required.")]
|
||||
[Phone(ErrorMessage = "Phone number format is invalid.")]
|
||||
public string PhoneNumber { get; set; }
|
||||
|
||||
[Required(ErrorMessage = "Password is required.")]
|
||||
[MinLength(6, ErrorMessage = "Password must be at least 6 characters long.")]
|
||||
public string Password { get; set; }
|
||||
|
||||
[Compare("Password", ErrorMessage = "Passwords do not match.")]
|
||||
public string ConfirmPassword { get; set; }
|
||||
}
|
||||
```
|
||||
|
||||
And the frontend sends this:
|
||||
|
||||
```json
|
||||
{
|
||||
"phoneNumber": "",
|
||||
"password": "123",
|
||||
"confirmPassword": "abc"
|
||||
}
|
||||
```
|
||||
|
||||
#### The backend will automatically return:
|
||||
|
||||
```json
|
||||
{
|
||||
"type": "https://tools.ietf.org/html/rfc7231#section-6.5.1",
|
||||
"title": "One or more validation errors occurred.",
|
||||
"status": 400,
|
||||
"errors": {
|
||||
"PhoneNumber": [
|
||||
"Phone number is required."
|
||||
],
|
||||
"Password": [
|
||||
"Password must be at least 6 characters long."
|
||||
],
|
||||
"ConfirmPassword": [
|
||||
"Passwords do not match."
|
||||
]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
This is thanks to `[ApiController]` on your controller class. The framework uses the **ModelState** and returns errors in a structured way.
|
||||
|
||||
---
|
||||
|
||||
|
||||
|
||||
## Adding Mappings
|
||||
- [ ] Update `MappingProfile` with the following mappings:
|
||||
|
||||
```c#
|
||||
CreateMap<Account, AccountDto>()
|
||||
.ForMember(dest => dest.Roles, opt => opt.MapFrom(src => src.AccountRoles.Select(x=>x.Role.Title)));
|
||||
CreateMap<AccountDto, Account>()
|
||||
.ForMember(dest => dest.AccountRoles, opt => opt.Ignore());
|
||||
```
|
||||
|
||||
# Add Password Hasher Utility
|
||||
- [ ] Create a password hashing utility class
|
||||
📂 Suggested Folder: ApplicationLayer/Utils/`PasswordHasher.cs`
|
||||
```c#
|
||||
public static class PasswordHasher
|
||||
{
|
||||
public static string HashPassword(string password)
|
||||
{
|
||||
byte[] salt = RandomNumberGenerator.GetBytes(16);
|
||||
|
||||
var pbkdf2 = new Rfc2898DeriveBytes(password, salt, 100000, HashAlgorithmName.SHA256);
|
||||
byte[] hash = pbkdf2.GetBytes(32);
|
||||
|
||||
byte[] hashBytes = new byte[48];
|
||||
Array.Copy(salt, 0, hashBytes, 0, 16);
|
||||
Array.Copy(hash, 0, hashBytes, 16, 32);
|
||||
|
||||
return Convert.ToBase64String(hashBytes);
|
||||
}
|
||||
|
||||
public static bool VerifyPassword(string password, string hashedPassword)
|
||||
{
|
||||
byte[] hashBytes = Convert.FromBase64String(hashedPassword);
|
||||
|
||||
byte[] salt = new byte[16];
|
||||
Array.Copy(hashBytes, 0, salt, 0, 16);
|
||||
|
||||
var pbkdf2 = new Rfc2898DeriveBytes(password, salt, 100000, HashAlgorithmName.SHA256);
|
||||
byte[] hash = pbkdf2.GetBytes(32);
|
||||
|
||||
for (int i = 0; i < 32; i++)
|
||||
{
|
||||
if (hashBytes[i + 16] != hash[i])
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
|
||||
# Modifying Account Repository
|
||||
|
||||
- [ ] Add the following methods in **`IAccountRepository`**
|
||||
|
||||
```c#
|
||||
Task<Account> GetByPhoneNumberAsync(string phoneNumber);
|
||||
Task AddAccountRoleAsync(AccountRole accountRole);
|
||||
```
|
||||
|
||||
- [ ] Implement the methods in **`AccountRepository`**
|
||||
```c#
|
||||
public async Task AddAccountRoleAsync(AccountRole accountRole)
|
||||
{
|
||||
await DbContext.AccountRoles.AddAsync(accountRole);
|
||||
}
|
||||
|
||||
public async Task<Account> GetByPhoneNumberAsync(string phoneNumber)
|
||||
{
|
||||
var user = await DbContext.Accounts.Include(x => x.AccountRoles).ThenInclude(x => x.Role).FirstOrDefaultAsync(x => x.PhoneNumber == phoneNumber);
|
||||
return user;
|
||||
}
|
||||
```
|
||||
|
||||
# Creating Service
|
||||
## Fix `Result.cs` Error Method
|
||||
- [ ] Update the `Error` method to include error messages:
|
||||
```c#
|
||||
public static Result<T> Error(T data, string errorMessage) => new() { Status = ResultStatus.Error, Data = data, ErrorMessage = errorMessage };
|
||||
```
|
||||
|
||||
## Creating `IAuthService.cs` and `AuthService.cs`
|
||||
- [ ] Define the `IAuthService` interface
|
||||
```c#
|
||||
public interface IAuthService
|
||||
{
|
||||
Task<Result<AuthResponseDto>> RegisterAsync(RegisterRequestDto request);
|
||||
Task<Result<AuthResponseDto>> LoginAsync(LoginRequestDto request);
|
||||
}
|
||||
```
|
||||
|
||||
- [ ] Implement the interface in `AuthService.cs`
|
||||
|
||||
Use this project as a reference:
|
||||
https://github.com/MehrdadShirvani/AlibabaClone-Backend/blob/develop/AlibabaClone.Application/Services/AuthService.cs
|
||||
|
||||
## Register `IAuthService` in Service in `Program.cs`
|
||||
|
||||
- [ ] Add to `Program.cs`
|
||||
```c#
|
||||
//...
|
||||
builder.Services.AddScoped<IAuthService, AuthService>();
|
||||
//...
|
||||
```
|
||||
|
||||
|
||||
# Adding JWT
|
||||
|
||||
## Installing Required NuGet Packages
|
||||
Install these packages in the **`WebApi` (Presentation Layer)** project:
|
||||
```
|
||||
Microsoft.AspNetCore.Authentication.JwtBearer
|
||||
Microsoft.IdentityModel.Tokens
|
||||
System.IdentityModel.Tokens.Jwt
|
||||
```
|
||||
|
||||
## Create JWT Configuration Classes
|
||||
|
||||
- [ ] Add JWT section to `appsettings.json`
|
||||
```xaml
|
||||
"Jwt": {
|
||||
"Key": "[supersecretkeyyoustoresecurely]",
|
||||
"Issuer": "[Issuer]",
|
||||
"Audience": "MyAppUsers",
|
||||
"ExpiryMinutes": 60
|
||||
}
|
||||
```
|
||||
Note that you should fill the values as you wish - these are just samples
|
||||
|
||||
- [ ] Create `JwtSettings` and add the following method
|
||||
📂 Suggested Folder: WebAPI/Authentication
|
||||
```c#
|
||||
public class JwtSettings
|
||||
{
|
||||
public string Key { get; set; } = null!;
|
||||
public string Issuer { get; set; } = null!;
|
||||
public string Audience { get; set; } = null!;
|
||||
public int ExpiryMinutes { get; set; }
|
||||
}
|
||||
```
|
||||
|
||||
- [ ] Create `IJwtGenerator` and add the following method
|
||||
📂 Suggested Folder: WebAPI/Authentication
|
||||
```c#
|
||||
string GenerateToken(AuthResponseDto authResponseDto);
|
||||
```
|
||||
|
||||
|
||||
- [ ] Create `JwtGenerator`, implementing `IJwtGenerator`
|
||||
📂 Suggested Folder: WebAPI/Authentication
|
||||
|
||||
use this project as a reference
|
||||
https://github.com/MehrdadShirvani/AlibabaClone-Backend/blob/develop/AlibabaClone.WebAPI/Authentication/JwtGenerator.cs
|
||||
|
||||
|
||||
## Configuring Jwt in Program.cs
|
||||
- [ ] Register `JwtGenerator` service
|
||||
```c#
|
||||
builder.Services.AddScoped<IJwtGenerator, JwtGenerator>();
|
||||
```
|
||||
- [ ] Bind `JwtSettings` from configuration
|
||||
```c#
|
||||
builder.Services.Configure<JwtSettings>(builder.Configuration.GetSection("Jwt"));
|
||||
```
|
||||
- [ ] Configure JWT authentication
|
||||
```c#
|
||||
var jwtSettings = builder.Configuration.GetSection("Jwt").Get<JwtSettings>();
|
||||
builder.Services.AddAuthentication("Bearer")
|
||||
.AddJwtBearer("Bearer", options =>
|
||||
{
|
||||
options.TokenValidationParameters = new TokenValidationParameters
|
||||
{
|
||||
ValidateIssuer = true,
|
||||
ValidIssuer = jwtSettings.Issuer,
|
||||
|
||||
ValidateAudience = true,
|
||||
ValidAudience = jwtSettings.Audience,
|
||||
|
||||
ValidateIssuerSigningKey = true,
|
||||
IssuerSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(jwtSettings.Key)),
|
||||
|
||||
ValidateLifetime = true,
|
||||
ClockSkew = TimeSpan.Zero
|
||||
};
|
||||
});
|
||||
|
||||
builder.Services.AddAuthorization();
|
||||
|
||||
```
|
||||
|
||||
- [ ] Add `app.UseAuthentication` before `app.UseAuthorization`
|
||||
```c#
|
||||
app.UseAuthentication();
|
||||
app.UseAuthorization();
|
||||
```
|
||||
# Add ApiControllers
|
||||
|
||||
- [ ] Create `AuthController`
|
||||
📂 Suggested Folder: WebApi/Controllers/`AuthController.cs`
|
||||
|
||||
- [ ] Add Class and Constructor
|
||||
```c#
|
||||
private readonly IAuthService _authService;
|
||||
private readonly IJwtGenerator _jwtGenerator;
|
||||
|
||||
public AuthController(IAuthService authService, IJwtGenerator jwtGenerator)
|
||||
{
|
||||
_authService = authService;
|
||||
_jwtGenerator = jwtGenerator;
|
||||
}
|
||||
```
|
||||
- [ ] Add Register Method
|
||||
```c#
|
||||
public async Task<IActionResult> Register(RegisterRequestDto request)
|
||||
{
|
||||
var result = await _authService.RegisterAsync(request);
|
||||
|
||||
if (!result.IsSuccess)
|
||||
return BadRequest(result.ErrorMessage);
|
||||
|
||||
var token = _jwtGenerator.GenerateToken(result.Data);
|
||||
var response = new AuthResponseDto
|
||||
{
|
||||
PhoneNumber = result.Data.PhoneNumber,
|
||||
Roles = result.Data.Roles,
|
||||
Token = token
|
||||
};
|
||||
|
||||
return Ok(response);
|
||||
}
|
||||
```
|
||||
- [ ] Add Login Method
|
||||
|
||||
```c#
|
||||
[HttpPost("login")]
|
||||
public async Task<IActionResult> Login(LoginRequestDto request)
|
||||
{
|
||||
var result = await _authService.LoginAsync(request);
|
||||
|
||||
if (!result.IsSuccess)
|
||||
return Unauthorized(result.ErrorMessage);
|
||||
|
||||
var token = _jwtGenerator.GenerateToken(result.Data);
|
||||
var response = new AuthResponseDto
|
||||
{
|
||||
PhoneNumber = result.Data.PhoneNumber,
|
||||
Roles = result.Data.Roles,
|
||||
Token = token
|
||||
};
|
||||
|
||||
return Ok(response);
|
||||
}
|
||||
```
|
||||
|
||||
- [ ] Create AccountController
|
||||
📂 Suggested Folder: WebApi/Controllers/AccountController.cs
|
||||
```c#
|
||||
public class AccountController : ControllerBase
|
||||
{
|
||||
[Authorize(Roles = "User")]
|
||||
[HttpGet("profile")]
|
||||
public IActionResult GetProfile()
|
||||
{
|
||||
return Ok("Hi there, hello");
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
# Merge
|
||||
- [ ] Create a PR and merge the current branch with develop
|
||||
|
||||
|
||||
|
||||
@@ -0,0 +1,803 @@
|
||||
# Branching
|
||||
- [ ] Create the feature/authentication branch based on develop
|
||||
|
||||
# Adding Models
|
||||
Create three files inside this folder:
|
||||
📂 Suggested Folder: shared/models/authentication
|
||||
|
||||
- [ ] `AuthResponseDto.ts`
|
||||
```ts
|
||||
export interface AuthResponseDto {
|
||||
token: string;
|
||||
phoneNumber: string;
|
||||
roles: string[];
|
||||
}
|
||||
```
|
||||
|
||||
- [ ] `LoginRequestDto.ts`
|
||||
```ts
|
||||
export interface LoginRequestDto {
|
||||
phoneNumber: string;
|
||||
password: string;
|
||||
}
|
||||
```
|
||||
|
||||
- [ ] `RegisterRequestDto.ts`
|
||||
```ts
|
||||
export interface RegisterRequestDto {
|
||||
phoneNumber: string;
|
||||
password: string;
|
||||
confirmPassword: string;
|
||||
}
|
||||
```
|
||||
|
||||
# Adding Authentication API Calls in `agent.ts`
|
||||
- [ ] Add the `Auth` object to `agent.ts`:
|
||||
```ts
|
||||
const Auth = {
|
||||
register: (data: RegisterRequestDto) =>
|
||||
request.post<{ token: string }>('/auth/register', data),
|
||||
login: (data: LoginRequestDto) =>
|
||||
request.post<{ token: string }>('/auth/login', data),
|
||||
};
|
||||
```
|
||||
- [ ] Ensure `agent.ts` ends like this:
|
||||
```tsx
|
||||
const agent = {
|
||||
TransportationSearch,
|
||||
Cities,
|
||||
Auth
|
||||
}
|
||||
```
|
||||
|
||||
# Creating `authStore.ts`
|
||||
Suggested Folder
|
||||
📂 Suggested Folder: shared/store/
|
||||
- [ ] Create authStore.ts
|
||||
```tsx
|
||||
import { AuthResponseDto } from '@/shared/models/authentication/AuthResponseDto';
|
||||
import {create} from 'zustand';
|
||||
|
||||
interface User {
|
||||
phoneNumber: string;
|
||||
roles: string[];
|
||||
}
|
||||
|
||||
|
||||
|
||||
interface AuthState {
|
||||
isLoggedIn: boolean;
|
||||
user: User | null;
|
||||
token: string | null;
|
||||
login: (response: AuthResponseDto) => void;
|
||||
logout: () => void;
|
||||
setToken: (token: string) => void;
|
||||
}
|
||||
|
||||
|
||||
|
||||
export const useAuthStore = create<AuthState>((set) => ({
|
||||
isLoggedIn: false,
|
||||
user: null,
|
||||
token: null,
|
||||
|
||||
|
||||
login: (response) =>
|
||||
set(() => ({
|
||||
token: response.token,
|
||||
user: {
|
||||
phoneNumber: response.phoneNumber,
|
||||
roles: response.roles
|
||||
},
|
||||
|
||||
isLoggedIn: true,
|
||||
})),
|
||||
|
||||
logout: () =>
|
||||
set(() => ({
|
||||
token: null,
|
||||
user: null,
|
||||
isLoggedIn: false,
|
||||
})),
|
||||
|
||||
|
||||
|
||||
setToken: (token) =>
|
||||
set((state) => ({
|
||||
token,
|
||||
isLoggedIn: !!token,
|
||||
user: state.user,
|
||||
})),
|
||||
}));
|
||||
```
|
||||
|
||||
## About `authStore.ts`
|
||||
|
||||
This file defines a centralized **authentication state store** using [`Zustand`](https://github.com/pmndrs/zustand), a minimal and scalable state management library for React. It helps manage login state, user data, and authentication token across your application.
|
||||
|
||||
---
|
||||
|
||||
## 🔹 `User` Interface
|
||||
|
||||
```ts
|
||||
interface User {
|
||||
phoneNumber: string;
|
||||
roles: string[];
|
||||
}
|
||||
```
|
||||
|
||||
This interface defines the shape of the `user` object stored in the auth state. It currently includes:
|
||||
- `phoneNumber`: A string representing the user's phone number.
|
||||
- `roles`: An array representing user roles
|
||||
|
||||
---
|
||||
|
||||
## 🔹 `AuthState` Interface
|
||||
|
||||
```ts
|
||||
interface AuthState {
|
||||
isLoggedIn: boolean;
|
||||
user: User | null;
|
||||
token: string | null;
|
||||
login: (response: AuthResponseDto) => void;
|
||||
logout: () => void;
|
||||
setToken: (token: string) => void;
|
||||
}
|
||||
```
|
||||
|
||||
This defines the overall structure of the authentication store:
|
||||
|
||||
- `isLoggedIn`: Indicates whether a user is logged in.
|
||||
- `user`: Stores user-specific data if authenticated; otherwise `null`.
|
||||
- `token`: JWT or access token from the server.
|
||||
- `login()`: Accepts an `AuthResponseDto` and updates the state.
|
||||
- `logout()`: Clears all authentication-related data.
|
||||
- `setToken()`: Sets the token and toggles login status accordingly.
|
||||
|
||||
|
||||
---
|
||||
|
||||
## 🔹 Zustand Store Definition
|
||||
|
||||
```ts
|
||||
export const useAuthStore = create<AuthState>((set) => ({ ... });
|
||||
```
|
||||
|
||||
Creates a global auth store using Zustand. `create()` accepts a function that receives `set` (used to update state) and returns the initial store state and methods.
|
||||
|
||||
---
|
||||
|
||||
## 🔹 Initial State
|
||||
|
||||
```ts
|
||||
isLoggedIn: false,
|
||||
user: null,
|
||||
token: null,
|
||||
```
|
||||
|
||||
These lines define the initial, default state for an unauthenticated user.
|
||||
|
||||
---
|
||||
|
||||
## 🔹 `login()` Method
|
||||
|
||||
```ts
|
||||
login: (response) =>
|
||||
set(() => ({
|
||||
token: response.token,
|
||||
user: {
|
||||
phoneNumber: response.phoneNumber,
|
||||
roles: response.roles
|
||||
},
|
||||
isLoggedIn: true,
|
||||
})),
|
||||
```
|
||||
|
||||
- Accepts an `AuthResponseDto` object after a successful login.
|
||||
|
||||
- Extracts the `token`, `phoneNumber`, and `roles`, and sets them in state.
|
||||
|
||||
- Marks the user as `isLoggedIn: true`.
|
||||
|
||||
|
||||
---
|
||||
|
||||
## 🔹 `logout()` Method
|
||||
|
||||
```ts
|
||||
logout: () =>
|
||||
set(() => ({
|
||||
token: null,
|
||||
user: null,
|
||||
isLoggedIn: false,
|
||||
})),
|
||||
```
|
||||
|
||||
- Clears all auth-related data (token and user).
|
||||
|
||||
- Effectively logs the user out by setting `isLoggedIn` to `false`.
|
||||
|
||||
|
||||
---
|
||||
|
||||
## 🔹 `setToken()` Method
|
||||
|
||||
```ts
|
||||
setToken: (token) =>
|
||||
set((state) => ({
|
||||
token,
|
||||
isLoggedIn: !!token,
|
||||
user: state.user,
|
||||
})),
|
||||
```
|
||||
|
||||
- Updates the token in the store.
|
||||
|
||||
- Sets `isLoggedIn` based on whether a non-empty token exists.
|
||||
|
||||
- Retains the current `user` object.
|
||||
|
||||
|
||||
---
|
||||
# Add LoginModal
|
||||
📂 Suggested Folder: shared/features/authentication/modals
|
||||
## Example
|
||||
```tsx
|
||||
import React, { useState } from "react";
|
||||
import { useAuthStore } from "@/store/authStore";
|
||||
import agent from "@/shared/api/agent";
|
||||
import { LoginRequestDto } from "@/shared/models/authentication/LoginRequestDto";
|
||||
|
||||
interface LoginModalProps {
|
||||
onClose: () => void;
|
||||
}
|
||||
|
||||
|
||||
|
||||
const LoginModal: React.FC<LoginModalProps> = ({ onClose }) => {
|
||||
|
||||
const login = useAuthStore((state) => state.login);
|
||||
|
||||
const [form, setForm] = useState<LoginRequestDto>({
|
||||
phoneNumber: "",
|
||||
password: "",
|
||||
});
|
||||
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
|
||||
const validate = () => {
|
||||
const phoneRegex = /^(?:\+98|0)?9\d{9}$/;
|
||||
if (!phoneRegex.test(form.phoneNumber)) {
|
||||
return "Invalid phone number format";
|
||||
}
|
||||
|
||||
if (!form.password || form.password.length < 8) {
|
||||
return "Password must be at least 8 characters";
|
||||
}
|
||||
return null;
|
||||
};
|
||||
|
||||
const handleSubmit = async () => {
|
||||
const validationError = validate();
|
||||
if (validationError) {
|
||||
setError(validationError);
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const response = await agent.Auth.login(form);
|
||||
login(response);
|
||||
setError(null);
|
||||
onClose();
|
||||
} catch (err: any) {
|
||||
setError(err.response?.data?.message || "Login failed");
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
|
||||
return (
|
||||
<div style={styles.overlay}>
|
||||
<div style={styles.modal}>
|
||||
<h2 style={{ marginBottom: "1rem" }}>Login</h2>
|
||||
<input
|
||||
type="text"
|
||||
placeholder="Phone Number"
|
||||
value={form.phoneNumber}
|
||||
onChange={(e) => setForm({ ...form, phoneNumber: e.target.value })}
|
||||
style={styles.input}
|
||||
/>
|
||||
|
||||
<input
|
||||
type="password"
|
||||
placeholder="Password"
|
||||
value={form.password}
|
||||
onChange={(e) => setForm({ ...form, password: e.target.value })}
|
||||
style={styles.input}
|
||||
/>
|
||||
<button onClick={handleSubmit} style={styles.button}>
|
||||
Login
|
||||
</button>
|
||||
{error && (
|
||||
<p style={{ color: "red", marginTop: "0.5rem", fontWeight: "bold" }}>
|
||||
{error}
|
||||
</p>
|
||||
)}
|
||||
|
||||
<button
|
||||
onClick={onClose}
|
||||
style={{
|
||||
...styles.button,
|
||||
marginTop: "0.5rem",
|
||||
backgroundColor: "#ccc",
|
||||
color: "#333",
|
||||
}}
|
||||
>
|
||||
Cancel
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const styles: { [key: string]: React.CSSProperties } = {
|
||||
//ADD STYLES
|
||||
};
|
||||
|
||||
export default LoginModal;
|
||||
```
|
||||
|
||||
## About LoginModal
|
||||
|
||||
This component provides a modal UI that allows users to log in using their **phone number and password**. It integrates with the authentication store and API to perform login logic and handle errors.
|
||||
|
||||
---
|
||||
## 🔹 Props Interface
|
||||
|
||||
```tsx
|
||||
interface LoginModalProps {
|
||||
onClose: () => void;
|
||||
}
|
||||
```
|
||||
|
||||
- `onClose`: A callback to be called when the modal should be closed (e.g., user clicks "Cancel" or logs in successfully).
|
||||
|
||||
---
|
||||
|
||||
## 🔹 Component Setup
|
||||
|
||||
```tsx
|
||||
const LoginModal: React.FC<LoginModalProps> = ({ onClose }) => { ... };
|
||||
```
|
||||
|
||||
Defines a functional React component with the `onClose` prop destructured.
|
||||
|
||||
### 🔸 Accessing Auth Store
|
||||
|
||||
```tsx
|
||||
const login = useAuthStore((state) => state.login);
|
||||
```
|
||||
|
||||
Retrieves the `login` method from Zustand’s `authStore` so that the global auth state can be updated after successful login.
|
||||
|
||||
### 🔸 Local Form State
|
||||
|
||||
```tsx
|
||||
const [form, setForm] = useState<LoginRequestDto>({
|
||||
phoneNumber: "",
|
||||
password: "",
|
||||
});
|
||||
```
|
||||
|
||||
Initializes `form` state with empty values for the phone number and password.
|
||||
|
||||
### 🔸 Error Handling State
|
||||
|
||||
```tsx
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
```
|
||||
|
||||
Stores any error messages resulting from validation or login attempt.
|
||||
|
||||
---
|
||||
|
||||
## 🔹 Validation Logic
|
||||
|
||||
```tsx
|
||||
const validate = () => {
|
||||
const phoneRegex = /^(?:\+98|0)?9\d{9}$/;
|
||||
if (!phoneRegex.test(form.phoneNumber)) {
|
||||
return "Invalid phone number format";
|
||||
}
|
||||
|
||||
if (!form.password || form.password.length < 8) {
|
||||
return "Password must be at least 8 characters";
|
||||
}
|
||||
return null;
|
||||
};
|
||||
```
|
||||
|
||||
- Validates the phone number format (Iranian phone format in this case).
|
||||
- Ensures password is at least 8 characters long.
|
||||
- Returns a string error message or `null` if validation passes.
|
||||
|
||||
---
|
||||
## 🔹 Submit Handler
|
||||
|
||||
```tsx
|
||||
const handleSubmit = async () => {
|
||||
const validationError = validate();
|
||||
if (validationError) {
|
||||
setError(validationError);
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const response = await agent.Auth.login(form);
|
||||
login(response);
|
||||
setError(null);
|
||||
onClose();
|
||||
} catch (err: any) {
|
||||
setError(err.response?.data?.message || "Login failed");
|
||||
}
|
||||
};
|
||||
```
|
||||
|
||||
- Calls `validate()` and prevents submission if there's an error.
|
||||
- Calls the backend API using `agent.Auth.login()`.
|
||||
- On success: updates auth state, clears error, closes modal.
|
||||
- On failure: shows error message.
|
||||
|
||||
|
||||
---
|
||||
|
||||
## 🔹 UI Layout
|
||||
|
||||
```tsx
|
||||
return (
|
||||
<div style={styles.overlay}>
|
||||
<div style={styles.modal}>
|
||||
<h2>Login</h2>
|
||||
<input ... />
|
||||
<input ... />
|
||||
<button onClick={handleSubmit}>Login</button>
|
||||
{error && <p>{error}</p>}
|
||||
<button onClick={onClose}>Cancel</button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
```
|
||||
|
||||
### Elements:
|
||||
|
||||
- **Phone Number Input**
|
||||
|
||||
- **Password Input**
|
||||
|
||||
- **Login Button**: Triggers `handleSubmit`.
|
||||
|
||||
- **Error Message**: Shown only if there's an error.
|
||||
|
||||
- **Cancel Button**: Triggers `onClose` callback.
|
||||
|
||||
|
||||
---
|
||||
|
||||
## 🔹 Styles Placeholder
|
||||
|
||||
```tsx
|
||||
const styles: { [key: string]: React.CSSProperties } = {
|
||||
// Add modal styles here
|
||||
};
|
||||
```
|
||||
|
||||
This placeholder defines inline CSS styles for the modal. Each style (e.g., `overlay`, `modal`, `input`, `button`) should be defined here.
|
||||
|
||||
---
|
||||
|
||||
## ✅ Summary
|
||||
|
||||
This modal:
|
||||
|
||||
- Provides a simple, reusable login form.
|
||||
|
||||
- Validates input before calling the API.
|
||||
|
||||
- Updates global auth state via Zustand.
|
||||
|
||||
- Handles success/failure states.
|
||||
|
||||
- Uses modal-friendly inline styles (with room for improvement).
|
||||
|
||||
|
||||
---
|
||||
# Add RegisterModal
|
||||
- [ ] Create RegisterModal
|
||||
📂 Suggested Folder: shared/features/authentication/modals
|
||||
```tsx
|
||||
import agent from "@/shared/api/agent";
|
||||
import { RegisterRequestDto } from "@/shared/models/authentication/RegisterRequestDto";
|
||||
import { useAuthStore } from "@/store/authStore";
|
||||
import React, { useState } from "react";
|
||||
|
||||
|
||||
interface Props {
|
||||
onClose: () => void;
|
||||
}
|
||||
|
||||
|
||||
const RegisterModal: React.FC<Props> = ({ onClose }) => {
|
||||
|
||||
const [form, setForm] = useState<RegisterRequestDto>({
|
||||
phoneNumber: "",
|
||||
password: "",
|
||||
confirmPassword: "",
|
||||
});
|
||||
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
|
||||
const login = useAuthStore((state) => state.login);
|
||||
|
||||
const validate = () => {
|
||||
|
||||
const { phoneNumber, password, confirmPassword } = form;
|
||||
if (!phoneNumber || !password || !confirmPassword) {
|
||||
return "All fields are required.";
|
||||
}
|
||||
|
||||
if (!/^\d{11}$/.test(phoneNumber)) {
|
||||
return "Phone number must be 11 digits.";
|
||||
}
|
||||
|
||||
if (password.length < 6) {
|
||||
return "Password must be at least 6 characters.";
|
||||
}
|
||||
|
||||
if (password !== confirmPassword) {
|
||||
return "Passwords do not match.";
|
||||
}
|
||||
|
||||
return null;
|
||||
};
|
||||
|
||||
|
||||
|
||||
const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
setForm({ ...form, [e.target.name]: e.target.value });
|
||||
};
|
||||
|
||||
|
||||
|
||||
const handleSubmit = async (e: React.FormEvent) => {
|
||||
e.preventDefault();
|
||||
setError(null);
|
||||
|
||||
const validationError = validate();
|
||||
|
||||
if (validationError) {
|
||||
setError(validationError);
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
|
||||
try {
|
||||
|
||||
// Use the form as RegisterRequestDto explicitly
|
||||
|
||||
const requestData: RegisterRequestDto = {
|
||||
phoneNumber: form.phoneNumber,
|
||||
password: form.password,
|
||||
confirmPassword: form.confirmPassword,
|
||||
};
|
||||
|
||||
|
||||
|
||||
const response = await agent.Auth.register(requestData);
|
||||
login(response);
|
||||
setForm({ phoneNumber: "", password: "", confirmPassword: "" });
|
||||
onClose();
|
||||
} catch (err: any) {
|
||||
setError(err.response?.data?.message || "Registration failed.");
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div style={styles.overlay}>
|
||||
<div style={styles.modal}>
|
||||
<h2 style={{ marginBottom: "1rem" }}>Register</h2>
|
||||
<form
|
||||
onSubmit={handleSubmit}
|
||||
style={{ display: "flex", flexDirection: "column" }}
|
||||
>
|
||||
|
||||
<input
|
||||
type="text"
|
||||
name="phoneNumber"
|
||||
value={form.phoneNumber}
|
||||
onChange={handleChange}
|
||||
placeholder="Phone Number"
|
||||
style={styles.input}
|
||||
/>
|
||||
|
||||
<input
|
||||
type="password"
|
||||
name="password"
|
||||
value={form.password}
|
||||
onChange={handleChange}
|
||||
placeholder="Password"
|
||||
style={styles.input}
|
||||
/>
|
||||
|
||||
<input
|
||||
type="password"
|
||||
name="confirmPassword"
|
||||
value={form.confirmPassword}
|
||||
onChange={handleChange}
|
||||
placeholder="Confirm Password"
|
||||
style={styles.input}
|
||||
/>
|
||||
|
||||
{error && (
|
||||
|
||||
<p
|
||||
style={{ color: "red", marginTop: "0.5rem", fontWeight: "bold" }}
|
||||
>
|
||||
{error}
|
||||
</p>
|
||||
)}
|
||||
|
||||
<button type="submit" style={styles.button}>
|
||||
Register
|
||||
</button>
|
||||
|
||||
</form>
|
||||
|
||||
<button
|
||||
|
||||
onClick={onClose}
|
||||
|
||||
style={{
|
||||
...styles.button,
|
||||
marginTop: "0.5rem",
|
||||
backgroundColor: "#ccc",
|
||||
color: "#333",
|
||||
}}
|
||||
>
|
||||
Cancel
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
|
||||
const styles: { [key: string]: React.CSSProperties } = {
|
||||
//ADD STYLES
|
||||
};
|
||||
|
||||
export default RegisterModal;
|
||||
```
|
||||
|
||||
## About RegisterModal
|
||||
|
||||
|
||||
### **Component Structure**
|
||||
|
||||
### 1. **Props**
|
||||
|
||||
```tsx
|
||||
interface Props {
|
||||
onClose: () => void;
|
||||
}
|
||||
```
|
||||
|
||||
- The modal only expects one prop: `onClose`, a function to close the modal (e.g., hide it from the screen).
|
||||
|
||||
---
|
||||
|
||||
### 2. **State Management**
|
||||
|
||||
```tsx
|
||||
const [form, setForm] = useState<RegisterRequestDto>({
|
||||
phoneNumber: "",
|
||||
password: "",
|
||||
confirmPassword: "",
|
||||
});
|
||||
```
|
||||
|
||||
- Initializes the form state for inputs, based on the `RegisterRequestDto` shape.
|
||||
|
||||
```tsx
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
```
|
||||
|
||||
- Stores any validation or server error message to display in the UI.
|
||||
|
||||
```tsx
|
||||
const login = useAuthStore((state) => state.login);
|
||||
```
|
||||
|
||||
- Accesses the `login` method from your global auth store, to automatically log in the user after successful registration.
|
||||
|
||||
|
||||
---
|
||||
|
||||
### 3. **Validation Logic**
|
||||
|
||||
```tsx
|
||||
const validate = () => {
|
||||
// Checks for empty fields
|
||||
// Validates phone number format (must be 11 digits)
|
||||
// Ensures password length is sufficient
|
||||
// Confirms password and confirmation match
|
||||
};
|
||||
```
|
||||
|
||||
- Ensures client-side validation before making a request to the server.
|
||||
|
||||
---
|
||||
|
||||
### 4. **Input Handling**
|
||||
|
||||
```tsx
|
||||
const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
setForm({ ...form, [e.target.name]: e.target.value });
|
||||
};
|
||||
```
|
||||
|
||||
- Updates the correct field in the `form` object dynamically based on the input `name`.
|
||||
|
||||
---
|
||||
|
||||
### 5. **Form Submission**
|
||||
|
||||
```tsx
|
||||
const handleSubmit = async (e: React.FormEvent) => {
|
||||
e.preventDefault();
|
||||
setError(null);
|
||||
const validationError = validate();
|
||||
// If validation passes, submit the data to the backend
|
||||
// If backend response is successful, log in and close modal
|
||||
// If it fails, show error message
|
||||
};
|
||||
```
|
||||
|
||||
- Prevents default form submission
|
||||
- Validates inputs
|
||||
- Sends the data to `agent.Auth.register`
|
||||
- On success: logs in user and clears form
|
||||
- On failure: shows error from server
|
||||
|
||||
---
|
||||
|
||||
### 6. **JSX Render**
|
||||
|
||||
```tsx
|
||||
<div style={styles.overlay}>...</div>
|
||||
```
|
||||
|
||||
- **Modal Overlay**: darkened background behind the modal
|
||||
- **Modal Box**: contains title, form, and buttons
|
||||
|
||||
### Inside `<form>`:
|
||||
|
||||
- Inputs for:
|
||||
- `phoneNumber`
|
||||
- `password`
|
||||
- `confirmPassword`
|
||||
|
||||
- Submit button for Register
|
||||
|
||||
- Error message display (if any)
|
||||
|
||||
- Cancel button that calls `onClose`
|
||||
|
||||
---
|
||||
|
||||
# Handle login/logout and register buttons in navbar
|
||||
- [ ] implement a way for showing login and register buttons in navbar when user is not signed in
|
||||
- [ ] when clicked, the button should show the related modal, for the user to sign in or register
|
||||
# Merge
|
||||
- [ ] Create a PR and merge the current branch with develop
|
||||
@@ -0,0 +1,56 @@
|
||||
vehicles = [
|
||||
{"id": 1, "title": "Volvo 9700", "type": 1, "capacity": 50},
|
||||
{"id": 2, "title": "Scania Touring", "type": 1, "capacity": 52},
|
||||
{"id": 3, "title": "TGV Duplex", "type": 2, "capacity": 510},
|
||||
{"id": 4, "title": "Shinkansen N700", "type": 2, "capacity": 1323},
|
||||
{"id": 5, "title": "Boeing 777", "type": 3, "capacity": 396},
|
||||
{"id": 6, "title": "Airbus A380", "type": 3, "capacity": 469}
|
||||
]
|
||||
|
||||
def generate_seats(vehicle):
|
||||
seats = []
|
||||
vehicle_id = vehicle["id"]
|
||||
capacity = vehicle["capacity"]
|
||||
vtype = vehicle["type"]
|
||||
vip_rows = 3 if capacity >= 50 else 1
|
||||
|
||||
if vtype == 1: # Bus: 2+2 layout
|
||||
seats_per_row = 4
|
||||
elif vtype == 2: # Train: 2+2 layout
|
||||
seats_per_row = 4
|
||||
elif vtype == 3: # Airplane: 3+4+3 (approx 10 per row)
|
||||
seats_per_row = 10
|
||||
else:
|
||||
seats_per_row = 4 # default
|
||||
|
||||
rows = (capacity + seats_per_row - 1) // seats_per_row
|
||||
|
||||
seat_id = 1
|
||||
for row in range(1, rows + 1):
|
||||
for col in range(1, seats_per_row + 1):
|
||||
if seat_id > capacity:
|
||||
break
|
||||
seat = {
|
||||
"Id": seat_id,
|
||||
"VehicleId": vehicle_id,
|
||||
"Row": row,
|
||||
"Column": col,
|
||||
"IsVIP": row <= vip_rows,
|
||||
"IsAvailable": True,
|
||||
"Description": ""
|
||||
}
|
||||
seats.append(seat)
|
||||
seat_id += 1
|
||||
return seats
|
||||
|
||||
# Generate and print all
|
||||
all_seats = []
|
||||
for v in vehicles:
|
||||
seats = generate_seats(v)
|
||||
all_seats.extend(seats)
|
||||
|
||||
# Print as SQL INSERTs (optional)
|
||||
for s in all_seats:
|
||||
print(f"INSERT INTO Seats (Id, VehicleId, Row, Column, IsVIP, IsAvailable, Description) VALUES "
|
||||
f"({s['Id']}, {s['VehicleId']}, {s['Row']}, {s['Column']}, {str(s['IsVIP']).lower()}, "
|
||||
f"{str(s['IsAvailable']).lower()}, '{s['Description']}');")
|
||||
@@ -0,0 +1,560 @@
|
||||
|
||||
# 🛠️ Task Checklist
|
||||
|
||||
## Fixes
|
||||
- [ ] Check for missing `.ValueGeneratedOnAdd()` in all entity configurations in ```Infrastructure/Configurations```.
|
||||
|
||||
- [ ] Adjust the database setup according to the second version of ERD
|
||||
- Some aggregates are added/modified to generate the new database
|
||||
- New ERD: [Here](https://github.com/TheOrderOfPhoenix/ASP.NET/blob/main/ProjectOrientedSessions/docs/AlibabaERD-Version02.pdf)
|
||||
- Note: Pay attention to the changes in logic and implementation of Person table (Id number is not unique anymore). So remove this code in PersonConfiguration:
|
||||
|
||||
```csharp
|
||||
builder.HasIndex(p => p.IdNumber)
|
||||
.IsUnique();
|
||||
```
|
||||
|
||||
- [ ] Add actual data in `TicketStatus`, `Gender`, `TransactionTypes`
|
||||
- You can either add the data in DbContext or the database itself
|
||||
|
||||
- [ ] Add data in `Seat`, `Person`, `TicketOrder`, `Transaction`, `Ticket` for test. It is recommended to write python code for generating data for Seat table, according to the data already stored in Transportation and related Vehicle data. There is also a [SeatGenerator](https://github.com/TheOrderOfPhoenix/ASP.NET/blob/main/ProjectOrientedSessions/Session08/SeatGenerator.py) in this repository, as well.
|
||||
|
||||
- [ ] Fix claim extraction (`sub` → standardize JWT claim mapping). (`IUserContext` implementation)
|
||||
|
||||
First, create an interface in Application layer:
|
||||
```csharp
|
||||
public interface IUserContext
|
||||
{
|
||||
long GetUserId();
|
||||
}
|
||||
```
|
||||
Then, implement it in WebAPI in Auth folder:
|
||||
```csharp
|
||||
public class UserContext : IUserContext
|
||||
{
|
||||
private readonly IHttpContextAccessor _httpContextAccessor;
|
||||
|
||||
public UserContext(IHttpContextAccessor httpContextAccessor)
|
||||
{
|
||||
_httpContextAccessor = httpContextAccessor;
|
||||
}
|
||||
|
||||
public long GetUserId()
|
||||
{
|
||||
var userIdStr = _httpContextAccessor.HttpContext?.User?.FindFirst(ClaimTypes.NameIdentifier)?.Value;
|
||||
if (long.TryParse(userIdStr, out var userId))
|
||||
{
|
||||
return userId;
|
||||
}
|
||||
throw new InvalidOperationException("User ID is not available or invalid.");
|
||||
}
|
||||
}
|
||||
```
|
||||
Finally, register things in Program.cs
|
||||
```csharp
|
||||
// register user context
|
||||
builder.Services.AddHttpContextAccessor();
|
||||
builder.Services.AddScoped<IUserContext, UserContext>();
|
||||
```
|
||||
|
||||
## ✅ Profile Page (Account Info Tab)
|
||||
|
||||
- [ ] Create `ProfileDto` to represent combined data for account, person, bank detail, balance.
|
||||
```csharp
|
||||
public class ProfileDto
|
||||
{
|
||||
// from account
|
||||
public string AccountPhoneNumber { get; set; }
|
||||
public string Email { get; set; }
|
||||
public decimal Balance { get; set; }
|
||||
|
||||
// from person
|
||||
public string FirstName { get; set; }
|
||||
public string LastName { get; set; }
|
||||
public string IdNumber { get; set; }
|
||||
public string PersonPhoneNumber { get; set; }
|
||||
public DateTime? BirthDate { get; set; }
|
||||
|
||||
// from bank-account
|
||||
public string IBAN { get; set; }
|
||||
public string BankAccountNumber { get; set; }
|
||||
public int CardNumber { get; set; }
|
||||
}
|
||||
```
|
||||
You can also find another version of this DTO in [Here](https://github.com/MehrdadShirvani/AlibabaClone-Backend/blob/develop/AlibabaClone.Application/DTOs/Account/ProfileDto.cs)
|
||||
|
||||
- [ ] Add `GetProfileAsync` in `AccountRepository` and expose via `AccountController`.
|
||||
- First, map dto to aggregate
|
||||
```csharp
|
||||
CreateMap<Account, ProfileDto>()
|
||||
.ForMember(dest => dest.PhoneNumber, opt => opt.MapFrom(src => src.PhoneNumber))
|
||||
.ForMember(dest => dest.Email, opt => opt.MapFrom(src => src.Email))
|
||||
.ForMember(dest => dest.Balance, opt => opt.MapFrom(src => src.Balance))
|
||||
.ForMember(dest => dest.FirstName, opt => opt.MapFrom(src => src.Person != null ? src.Person.FirstName : ""))
|
||||
.ForMember(dest => dest.LastName, opt => opt.MapFrom(src => src.Person != null ? src.Person.LastName : ""))
|
||||
.ForMember(dest => dest.IdNumber, opt => opt.MapFrom(src => src.Person != null ? src.Person.IdNumber : ""))
|
||||
.ForMember(dest => dest.BirthDate, opt => opt.MapFrom(src => src.Person != null ? src.Person.BirthDate : (DateTime?) null))
|
||||
.ForMember(dest => dest.IBAN, opt => opt.MapFrom(src => src.BankAccount != null ? src.BankAccount.IBAN : ""))
|
||||
.ForMember(dest => dest.BankAccountNumber, opt => opt.MapFrom(src => src.BankAccount != null ? src.BankAccount.BankAccountNumber : ""))
|
||||
.ForMember(dest => dest.CardNumber, opt => opt.MapFrom(src => src.BankAccount != null ? src.BankAccount.CardNumber : ""));
|
||||
```
|
||||
- Then, add the equivalent methods for AccountRepository, AccountService and AccountController
|
||||
```csharp
|
||||
public class AccountRepository : BaseRepository<AlibabaDbContext, Account, long>, IAccountRepository
|
||||
{
|
||||
public AccountRepository(AlibabaDbContext context) : base(context)
|
||||
{
|
||||
|
||||
}
|
||||
|
||||
public async Task<Account> GetByPhoneNumberAsync(string phoneNumber)
|
||||
{
|
||||
var user = await DbSet.Include(a => a.AccountRoles).ThenInclude(x => x.Role).FirstOrDefaultAsync(x => x.PhoneNumber == phoneNumber);
|
||||
return user;
|
||||
}
|
||||
|
||||
public async Task<Account> GetProfileAsync(long accountId)
|
||||
{
|
||||
var profile = await DbSet
|
||||
.Include(a => a.Person)
|
||||
.Include(a => a.BankAccount)
|
||||
.FirstOrDefaultAsync(a => a.Id == accountId);
|
||||
|
||||
return profile;
|
||||
}
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
```csharp
|
||||
public class AccountService : IAccountService
|
||||
{
|
||||
private readonly IAccountRepository _accountRepository;
|
||||
private readonly IMapper _mapper;
|
||||
|
||||
public async Task<Result<ProfileDto>> GetProfileAsync(long accountId)
|
||||
{
|
||||
var result = await _accountRepository.GetProfileAsync(accountId);
|
||||
if (result == null)
|
||||
{
|
||||
return Result<ProfileDto>.NotFound(null);
|
||||
}
|
||||
|
||||
return Result<ProfileDto>.Success(_mapper.Map<ProfileDto>(result));
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
```csharp
|
||||
[ApiController]
|
||||
[Route("api/[controller]")]
|
||||
public class AccountController : ControllerBase
|
||||
{
|
||||
private readonly IUserContext _userContext;
|
||||
private readonly IAccountService _accountService;
|
||||
|
||||
public AccountController(IUserContext userContext, IAccountService accountService)
|
||||
{
|
||||
_userContext = userContext;
|
||||
_accountService = accountService;
|
||||
}
|
||||
|
||||
[HttpGet("profile")]
|
||||
public async Task<IActionResult> GetProfile()
|
||||
{
|
||||
// get account-id from token
|
||||
long userId = _userContext.GetUserId();
|
||||
// check for user-id to be valid
|
||||
if (userId <= 0)
|
||||
{
|
||||
return Unauthorized();
|
||||
}
|
||||
|
||||
var result = await _accountService.GetProfileAsync(userId);
|
||||
if (result.IsSuccess)
|
||||
{
|
||||
return Ok(result.Data);
|
||||
}
|
||||
|
||||
return result.Status switch
|
||||
{
|
||||
ResultStatus.Unauthorized => Unauthorized(result.ErrorMessage),
|
||||
ResultStatus.NotFound => NotFound(result.ErrorMessage),
|
||||
ResultStatus.ValidationError => BadRequest(result.ErrorMessage),
|
||||
_ => StatusCode(500, result.ErrorMessage)
|
||||
};
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Note: You should add necessary interfaces and implement them
|
||||
|
||||
- [ ] Implement:
|
||||
- [ ] Edit Email (with validation): `EditEmailDto`, service method, and controller endpoint.
|
||||
```csharp
|
||||
public class EditEmailDto
|
||||
{
|
||||
[EmailAddress(ErrorMessage = "Invalid email address format")]
|
||||
public string NewEmail { get; set; }
|
||||
}
|
||||
```
|
||||
- [ ] Edit Password: `EditPasswordDto`, service and controller.
|
||||
```csharp
|
||||
public class EditPasswordDto
|
||||
{
|
||||
[Required(ErrorMessage = "Old password is required")]
|
||||
public string OldPassword { get; set; }
|
||||
|
||||
[Required(ErrorMessage = "New password is required")]
|
||||
[MinLength(8, ErrorMessage = "At least 8 chars")]
|
||||
public string NewPassword { get; set; }
|
||||
|
||||
[Compare("Password", ErrorMessage = "Password doesn't match")]
|
||||
public string ConfirmNewPassword { get; set; }
|
||||
}
|
||||
```
|
||||
- [ ] Edit Person Info: `PersonDto`, and endpoint to `upsert` personal data.
|
||||
```csharp
|
||||
public class PersonDto
|
||||
{
|
||||
public long Id { get; set; }
|
||||
public long CreatorId { get; set; }
|
||||
|
||||
[Required(ErrorMessage = "Firstname is required")]
|
||||
public string FirstName { get; set; }
|
||||
|
||||
[Required(ErrorMessage = "Lastname is required")]
|
||||
public string LastName { get; set; }
|
||||
|
||||
[Required(ErrorMessage = "National Id number is required")]
|
||||
[RegularExpression(@"^\d{10}$", ErrorMessage = "National ID number must be exactly 10 digits")]
|
||||
public string IdNumber { get; set; }
|
||||
|
||||
[Required(ErrorMessage = "Gender is required")]
|
||||
public short GenderId { get; set; }
|
||||
|
||||
[Required(ErrorMessage = "Phone number is required")]
|
||||
public string PhoneNumber { get; set; }
|
||||
|
||||
[Required(ErrorMessage = "Birth date is required")]
|
||||
public DateTime BirthDate { get; set; }
|
||||
}
|
||||
```
|
||||
- [ ] Edit `BankAccountDetail`: `UpsertBankAccountDetailDto` and relevant logic.
|
||||
```csharp
|
||||
public class UpsertBankAccountDto
|
||||
{
|
||||
[MinLength(24)]
|
||||
[MaxLength(24)]
|
||||
public string? IBAN { get; set; }
|
||||
|
||||
[MinLength(16)]
|
||||
[MaxLength(16)]
|
||||
public string? CardNumber { get; set; }
|
||||
|
||||
[MinLength(8)]
|
||||
public string? BankAccountNumber { get; set; }
|
||||
}
|
||||
```
|
||||
|
||||
- [ ] Add mapping for all Dtos and check related properties like `CreatorAccountId`.
|
||||
|
||||
## ✅ List of Travelers
|
||||
|
||||
- [ ] Add `GetMyPeople` endpoint in `AccountController`. To do so, first add essential methods in `PersonRepository`, `AccountService` and related interfaces.
|
||||
|
||||
- [ ] Implement `UpsertAccountPerson` and `UpsertPerson`. Note that they should be considered separated.
|
||||
```csharp
|
||||
public async Task<Result<long>> UpsertAccountPersonAsync(long accountId, PersonDto dto)
|
||||
{
|
||||
var account = await _accountRepository.GetByIdAsync(accountId);
|
||||
if (account == null)
|
||||
{
|
||||
throw new Exception("Account not found");
|
||||
}
|
||||
|
||||
// if account is not null, update its person
|
||||
Person person;
|
||||
if (account.PersonId.HasValue)
|
||||
{
|
||||
person = await _personRepository.GetByIdAsync(account.PersonId.Value);
|
||||
if (person == null)
|
||||
{
|
||||
return Result<long>.Error(0, "No person found for this account");
|
||||
}
|
||||
|
||||
_mapper.Map(dto, person);
|
||||
person.CreatorId = account.Id;
|
||||
person.Id = account.PersonId.Value;
|
||||
_personRepository.Update(person);
|
||||
}
|
||||
else
|
||||
{
|
||||
person = _mapper.Map<Person>(dto);
|
||||
person.CreatorId = account.Id;
|
||||
await _personRepository.InsertAsync(person);
|
||||
}
|
||||
await _unitOfWork.CompleteAsync();
|
||||
|
||||
account.PersonId = person.Id;
|
||||
_accountRepository.Update(account);
|
||||
await _unitOfWork.CompleteAsync();
|
||||
|
||||
return Result<long>.Success(person.Id);
|
||||
}
|
||||
|
||||
public async Task<Result<long>> UpsertPersonAsync(long accountId, PersonDto dto)
|
||||
{
|
||||
var account = await _accountRepository.GetByIdAsync(accountId);
|
||||
if (account == null)
|
||||
{
|
||||
throw new Exception("Account not found");
|
||||
}
|
||||
|
||||
Person person = (await _personRepository.FindAsync(p => p.IdNumber == dto.IdNumber && p.CreatorId == accountId)).FirstOrDefault();
|
||||
if (person != null)
|
||||
{
|
||||
if (dto.Id > 0 && dto.Id != person.Id)
|
||||
{
|
||||
return Result<long>.Error(0, "A person with this id number exists");
|
||||
}
|
||||
_mapper.Map(dto, person);
|
||||
person.CreatorId = accountId;
|
||||
_personRepository.Update(person);
|
||||
}
|
||||
else
|
||||
{
|
||||
person = _mapper.Map<Person>(dto);
|
||||
person.CreatorId = accountId;
|
||||
await _personRepository.InsertAsync(person);
|
||||
}
|
||||
await _unitOfWork.CompleteAsync();
|
||||
|
||||
return Result<long>.Success(person.Id);
|
||||
}
|
||||
```
|
||||
|
||||
```csharp
|
||||
[HttpPost("account-person")]
|
||||
public async Task<IActionResult> UpsertAccountPerson([FromBody] PersonDto dto)
|
||||
{
|
||||
long accountId = _userContext.GetUserId();
|
||||
if (accountId <= 0)
|
||||
{
|
||||
return Unauthorized();
|
||||
}
|
||||
|
||||
var result = await _personService.UpsertAccountPersonAsync(accountId, dto);
|
||||
return result.Status switch
|
||||
{
|
||||
ResultStatus.Success => NoContent(),
|
||||
ResultStatus.Unauthorized => Unauthorized(result.ErrorMessage),
|
||||
ResultStatus.NotFound => NotFound(result.ErrorMessage),
|
||||
ResultStatus.ValidationError => BadRequest(result.ErrorMessage),
|
||||
_ => StatusCode(500, result.ErrorMessage)
|
||||
};
|
||||
}
|
||||
|
||||
[HttpPost("person")]
|
||||
public async Task<IActionResult> UpsertPerson([FromBody] PersonDto dto)
|
||||
{
|
||||
long accountId = _userContext.GetUserId();
|
||||
if (accountId <= 0)
|
||||
{
|
||||
return Unauthorized();
|
||||
}
|
||||
|
||||
var result = await _personService.UpsertPersonAsync(accountId, dto);
|
||||
return result.Status switch
|
||||
{
|
||||
ResultStatus.Success => NoContent(),
|
||||
ResultStatus.Unauthorized => Unauthorized(result.ErrorMessage),
|
||||
ResultStatus.NotFound => NotFound(result.ErrorMessage),
|
||||
ResultStatus.ValidationError => BadRequest(result.ErrorMessage),
|
||||
_ => StatusCode(500, result.ErrorMessage)
|
||||
};
|
||||
}
|
||||
```
|
||||
|
||||
|
||||
## ✅ My Travels Tab
|
||||
|
||||
- [ ] Create `TicketOrderSummaryDto` (includes cities, vehicle name, price, etc.).
|
||||
```csharp
|
||||
public class TicketOrderSummaryDto
|
||||
{
|
||||
public long Id { get; set; }
|
||||
public string SerialNumber { get; set; }
|
||||
public DateTime BoughtAt { get; set; }
|
||||
|
||||
// transaction
|
||||
public decimal Price { get; set; }
|
||||
|
||||
// transportation
|
||||
public DateTime TravelStartDate { get; set; }
|
||||
public DateTime? TravelEndDate { get; set; }
|
||||
|
||||
// city
|
||||
public string FromCity { get; set; }
|
||||
public string ToCity { get; set; }
|
||||
|
||||
// company
|
||||
public string CompanyName { get; set; }
|
||||
|
||||
// vehicle data
|
||||
public short VehicleTypeId { get; set; }
|
||||
public string VehicleName { get; set; }
|
||||
}
|
||||
```
|
||||
|
||||
- [ ] Add `GetTravels` in `AccountService`, and expose `GetMyTravels` in controller.
|
||||
|
||||
First, update interfaces and TicketOrderRepository, add the **mappings** and then go for the other things
|
||||
|
||||
In AccountService:
|
||||
```csharp
|
||||
public async Task<Result<List<TicketOrderSummaryDto>>> GetTravelsAsync(long accountId)
|
||||
{
|
||||
var result = await _ticketOrderRepository.GetAllByBuyerId(accountId);
|
||||
if (result == null)
|
||||
{
|
||||
return Result<List<TicketOrderSummaryDto>>.NotFound(null);
|
||||
}
|
||||
|
||||
return Result<List<TicketOrderSummaryDto>>.Success(_mapper.Map<List<TicketOrderSummaryDto>>(result));
|
||||
}
|
||||
```
|
||||
|
||||
In AccountController:
|
||||
```csharp
|
||||
[HttpGet("my-travels")]
|
||||
public async Task<IActionResult> GetMyTravels()
|
||||
{
|
||||
long buyerId = _userContext.GetUserId();
|
||||
if (buyerId <= 0)
|
||||
{
|
||||
return Unauthorized();
|
||||
}
|
||||
|
||||
var result = await _accountService.GetTravelsAsync(buyerId);
|
||||
if (result.IsSuccess)
|
||||
{
|
||||
return Ok(result.Data);
|
||||
}
|
||||
|
||||
return result.Status switch
|
||||
{
|
||||
ResultStatus.Unauthorized => Unauthorized(result.ErrorMessage),
|
||||
ResultStatus.NotFound => NotFound(result.ErrorMessage),
|
||||
ResultStatus.ValidationError => BadRequest(result.ErrorMessage),
|
||||
_ => StatusCode(500, result.ErrorMessage)
|
||||
};
|
||||
}
|
||||
```
|
||||
|
||||
## ✅ My Transactions Tab
|
||||
|
||||
- [ ] Create `TransactionDto` and mapping.
|
||||
```csharp
|
||||
public class TransactionDto
|
||||
{
|
||||
public long Id { get; set; }
|
||||
public short TransactionTypeId { get; set; }
|
||||
public long AccountId { get; set; }
|
||||
public long? TicketOrderId { get; set; }
|
||||
public decimal BaseAmount { get; set; }
|
||||
public decimal FinalAmount { get; set; }
|
||||
public required string SerialNumber { get; set; }
|
||||
public DateTime CreatedAt { get; set; }
|
||||
public string? Description { get; set; }
|
||||
public string TransactionType { get; set; }
|
||||
}
|
||||
```
|
||||
|
||||
- [ ] Add method to get transactions by `AccountId` in `TransactionRepository`.
|
||||
```csharp
|
||||
public async Task<List<Transaction>> GetTransactionsByAccountIdAsync(long accountId)
|
||||
{
|
||||
var transactions = await DbSet
|
||||
.Include(t => t.TransactionType)
|
||||
.Include(t => t.TicketOrder)
|
||||
.Where(t => t.AccountId == accountId).ToListAsync();
|
||||
return transactions;
|
||||
}
|
||||
```
|
||||
|
||||
- [ ] Expose `GetMyTransactions` in `AccountController`. It's obvious you should first add the essential method `GetTransactionsAsync` in `AccountService`.
|
||||
```csharp
|
||||
[HttpGet("my-transactions")]
|
||||
public async Task<IActionResult> GetMyTransactions()
|
||||
{
|
||||
long accountId = _userContext.GetUserId();
|
||||
if (accountId <= 0)
|
||||
{
|
||||
return Unauthorized();
|
||||
}
|
||||
|
||||
var result = await _accountService.GetTransactionsAsync(accountId);
|
||||
if (result.IsSuccess)
|
||||
{
|
||||
return Ok(result.Data);
|
||||
}
|
||||
|
||||
return result.Status switch
|
||||
{
|
||||
ResultStatus.Unauthorized => Unauthorized(result.ErrorMessage),
|
||||
ResultStatus.NotFound => NotFound(result.ErrorMessage),
|
||||
ResultStatus.ValidationError => BadRequest(result.ErrorMessage),
|
||||
_ => StatusCode(500, result.ErrorMessage)
|
||||
};
|
||||
}
|
||||
```
|
||||
- [ ] Add modal to simulate balance top-up (manual input).
|
||||
```csharp
|
||||
public class TopUpDto
|
||||
{
|
||||
public decimal Amount { get; set; }
|
||||
}
|
||||
```
|
||||
- [ ] Add `TransactionService` and use its method `CreateTopUpAsync` to create and add a new transaction in `AccountService`. Then add an endpoint just like before.
|
||||
```csharp
|
||||
public async Task<Result<long>> TopUpAsync(long accountId, TopUpDto dto)
|
||||
{
|
||||
var account = await _accountRepository.GetByIdAsync(accountId);
|
||||
if (account == null)
|
||||
{
|
||||
return Result<long>.Error(0, "Account not found");
|
||||
}
|
||||
|
||||
account.Deposit(dto.Amount);
|
||||
_accountRepository.Update(account);
|
||||
await _unitOfWork.CompleteAsync();
|
||||
|
||||
var transactionId = await _transactionService.CreateTopUpAsync(accountId, dto.Amount);
|
||||
return Result<long>.Success(transactionId.Data);
|
||||
}
|
||||
```
|
||||
|
||||
## Postman
|
||||
Considering that all endpoints in `AccountController` require Authorization, You need to test your API in **Postman**.
|
||||
|
||||
<br />
|
||||
<img src="https://upload.wikimedia.org/wikipedia/commons/c/c2/Postman_%28software%29.png" width="60%">
|
||||
|
||||
Postman is a client which lets the user test api professionally.
|
||||
You can download it in [this link](https://www.postman.com/downloads/) and get started with it using [this video](https://www.youtube.com/watch?v=wEOLZq-7DYs&pp=0gcJCfwAo7VqN5tD)
|
||||
|
||||
# 🧠 Hints & Notes
|
||||
- Check the codes as they're put here before **debugging**, you can check them with the repositories.
|
||||
- Complete the task step by step in each endpoint to preserve the principals of *Clean Architecture*.
|
||||
|
||||
# 🙌 Acknowledgements
|
||||
|
||||
- ChatGPT for snippet refinement and explanations
|
||||
# 🔍 References
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -0,0 +1,247 @@
|
||||
|
||||
# 🛠️ Task Checklist
|
||||
|
||||
## ⚙️ Tooling & Fixes
|
||||
- [ ] Install and configure `react-hook-form`
|
||||
```
|
||||
npm install react-hook-form
|
||||
```
|
||||
|
||||
## 🔐 Account & Authentication
|
||||
- [ ] Use Axios request interceptor to attach token to requests and handle logout logic
|
||||
```ts
|
||||
// add a request interceptor to include JWT token if available
|
||||
agent.interceptors.request.use((config) => {
|
||||
const token = useAuthStore.getState().token;
|
||||
if (token) {
|
||||
config.headers = config.headers || {};
|
||||
config.headers["Authorization"] = `Bearer ${token}`;
|
||||
}
|
||||
return config;
|
||||
});
|
||||
|
||||
// add a response interceptor to handle 401 Unauthorized
|
||||
agent.interceptors.response.use(
|
||||
(res) => res,
|
||||
(error) => {
|
||||
if (error.response?.status === 401) {
|
||||
useAuthStore.getState().logout();
|
||||
window.location.href = "/login"; // redirect to login
|
||||
}
|
||||
return Promise.reject(error);
|
||||
}
|
||||
);
|
||||
```
|
||||
|
||||
## API
|
||||
- [ ] Provide a method for each new endpoint implemented in backend
|
||||
```ts
|
||||
const Profile = {
|
||||
getProfile: () => request.get<ProfileDto>('/account/profile'),
|
||||
editEmail: (data: EditEmailDto) => request.put<void>('/account/email', data),
|
||||
editPassword: (data: EditPasswordDto) => request.put<void>('/account/password', data),
|
||||
upsertAccountPerson: (data: PersonDto) => request.post<number>('/account/account-person', data),
|
||||
upsertPerson: (data: PersonDto) => request.post<number>('/account/person', data),
|
||||
upsertBankDetail: (data: UpsertBankAccountDetailDto) => request.post<void>('/account/bank-detail', data),
|
||||
getMyPeople: () => request.get<PersonDto[]>('/account/my-people'),
|
||||
getMyTravels: () => request.get<TicketOrderSummaryDto[]>('/account/my-travels'),
|
||||
getMyTransactions: () => request.get<TransactionDto[]>('/account/my-transactions'),
|
||||
topUp: (data : topUpDto) => request.post<number>('/account/top-up', data)
|
||||
};
|
||||
|
||||
```
|
||||
|
||||
## 🗂️ Additional Profile Tabs (initial setup)
|
||||
|
||||
- [ ] Add empty pages/tabs for:
|
||||
- [ ] `ProfileSummary`
|
||||
- [ ] `MyTravels`
|
||||
- [ ] `MyTransactions`
|
||||
- [ ] `MyPeople`
|
||||
|
||||
- [ ] Implement `ProfilePage` using the components created. This is an example of how it can be done:
|
||||
```ts
|
||||
import InfoAccount from "./InfoAccount";
|
||||
import InfoPeople from "./InfoPeople";
|
||||
import InfoTransactions from "./InfoTransactions";
|
||||
import InfoTravels from "./InfoTravels";
|
||||
import { useState } from "react";
|
||||
|
||||
const tabs = [
|
||||
{ label: "Account", component: <InfoAccount /> },
|
||||
{ label: "Transactions", component: <InfoTransactions /> },
|
||||
{ label: "Travels", component: <InfoTravels /> },
|
||||
{ label: "People", component: <InfoPeople /> }
|
||||
];
|
||||
|
||||
const Profile = () => {
|
||||
const [selected, setSelected] = useState(0);
|
||||
return (
|
||||
<div className="max-w-4xl mx-auto py-8 px-4">
|
||||
<div className="flex flex-col md:flex-row gap-8">
|
||||
<aside className="md:w-64 w-full bg-white rounded-lg shadow p-4 flex md:flex-col flex-row gap-2 md:gap-0 mb-4 md:mb-0">
|
||||
{tabs.map((tab, idx) => (
|
||||
<button
|
||||
key={tab.label}
|
||||
className={`text-right px-4 py-2 rounded transition font-medium text-base md:text-lg w-full ${
|
||||
selected === idx
|
||||
? "bg-blue-100 text-blue-700"
|
||||
: "hover:bg-gray-100 text-gray-700"
|
||||
}`}
|
||||
onClick={() => setSelected(idx)}
|
||||
type="button"
|
||||
>
|
||||
{tab.label}
|
||||
</button>
|
||||
))}
|
||||
</aside>
|
||||
<main className="flex-1 min-w-0">{tabs[selected].component}</main>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default Profile;
|
||||
|
||||
```
|
||||
|
||||
- [ ] Create prototype of `ProfilePage` and integrate with navbar. (Create a button or link to access `ProfilePage`, only when user is logged in)
|
||||
|
||||
- [ ] Define and adjust routes for profile and its tabs
|
||||
```ts
|
||||
const AppRoutes = () => {
|
||||
return (
|
||||
<Routes>
|
||||
<Route path="/" element={<Home />} />
|
||||
<Route path="/login" element={<Login />} />
|
||||
<Route path="/register" element={<Register />} />
|
||||
<Route path="/profile" element={<Profile />} />
|
||||
<Route path="*" element={<NotFound />} />
|
||||
</Routes>
|
||||
);
|
||||
};
|
||||
|
||||
export default AppRoutes;
|
||||
|
||||
```
|
||||
|
||||
- [ ] Implement tab handling inside `ProfilePage`. You can also do it **route-based**.
|
||||
|
||||
## Profile Summary
|
||||
### 📦 DTOs / Models
|
||||
- Add models for:
|
||||
- [ ] `EditEmailDto`
|
||||
```ts
|
||||
export interface EditEmailDto {
|
||||
newEmail: string;
|
||||
}
|
||||
```
|
||||
- [ ] `EditPasswordDto`
|
||||
```ts
|
||||
export interface EditPasswordDto {
|
||||
oldPassword: string;
|
||||
newPassword: string;
|
||||
confirmNewPassword: string;
|
||||
}
|
||||
```
|
||||
- [ ] `PersonDto`
|
||||
```ts
|
||||
export interface PersonDto {
|
||||
id?: number;
|
||||
creatorAccountId?: number;
|
||||
firstName: string;
|
||||
lastName: string;
|
||||
idNumber: string;
|
||||
genderId: number;
|
||||
phoneNumber: string;
|
||||
birthDate: string;
|
||||
}
|
||||
```
|
||||
- [ ] `ProfileDto`
|
||||
```ts
|
||||
export interface ProfileDto {
|
||||
accountPhoneNumber: string;
|
||||
email: string;
|
||||
balance: number;
|
||||
|
||||
firstName: string;
|
||||
lastName: string;
|
||||
idNumber: string;
|
||||
personPhoneNumber: string;
|
||||
birthDate: Date | string | null;
|
||||
|
||||
iban: string;
|
||||
bankAccountNumber: string;
|
||||
cardNumber: string;
|
||||
}
|
||||
```
|
||||
- [ ] `UpsertBankAccountDetailDto`
|
||||
```ts
|
||||
export interface UpsertBankAccountDto {
|
||||
iban?: string;
|
||||
bankAccountNumber?: string;
|
||||
cardNumber?: string;
|
||||
}
|
||||
```
|
||||
|
||||
- [ ] Implement the process of showing and editing the data. To do so, you can use modal components and set their functionality in the pages.
|
||||
|
||||
## 🧍 List of Travelers
|
||||
- [ ] Implement `ListOfTravelers` page for showing, editing, and adding new people. You can use the same modal used for `UpsertAccountPerson`.
|
||||
|
||||
## 💳 Transactions Module
|
||||
- [ ] Add `TransactionDto` model
|
||||
```ts
|
||||
export interface TransactionDto {
|
||||
id: number;
|
||||
transactionTypeId: number;
|
||||
accountId: number;
|
||||
ticketOrderId?: number;
|
||||
baseAmount: number;
|
||||
finalAmount: number;
|
||||
serialNumber: string;
|
||||
createdAt: Date | string;
|
||||
description?: string;
|
||||
transactionType: string;
|
||||
}
|
||||
```
|
||||
- [ ] Implement `MyTransactions` page
|
||||
Note: It is recommended to add a component to be displayed as a card, including the table of information, then use it in the page of MyTransactions
|
||||
|
||||
## 🚆 Travel Module
|
||||
- [ ] Add `TicketOrderSummaryDto` model
|
||||
```ts
|
||||
export interface TicketOrderSummaryDto {
|
||||
id: number;
|
||||
serialNumber: string;
|
||||
boughtAt: Date | string;
|
||||
|
||||
price: number;
|
||||
|
||||
travelStartDate: Date | string;
|
||||
travelEndDate: Date | string;
|
||||
|
||||
fromCity: string;
|
||||
toCity: string;
|
||||
|
||||
companyName: string;
|
||||
|
||||
vehicleTypeId: number;
|
||||
vehicleName: string;
|
||||
}
|
||||
```
|
||||
|
||||
- [ ] Implement `MyTravels` page. Note that this page can be similarly implemented by a card.
|
||||
|
||||
|
||||
# 🧠 Hints & Notes
|
||||
# 🙌 Acknowledgements
|
||||
|
||||
- ChatGPT for snippet refinement and explanations
|
||||
# 🔍 References
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -0,0 +1,775 @@
|
||||
|
||||
### ⚠ Important Tip
|
||||
If your `agent.ts` uses `useAuthStore().token` **at the top level**, remember:
|
||||
- On very first load, Zustand will **rehydrate** from storage _after_ initial render, so token may be `null` until rehydration is done.
|
||||
- Fix: either delay access until after `hasHydrated`, or refactor `agent` to inject token per request.
|
||||
|
||||
|
||||
|
||||
|
||||
To **safely handle concurrency** in your `CreateTicketOrderAsync` method — particularly for **seat reservation** on the same `Transportation` — you need to **prevent race conditions** where two users might reserve the same seat or oversell capacity.
|
||||
|
||||
This is a **classic critical section problem**, and you can solve it using **application-level locking**, **database-level locking**, or both.
|
||||
|
||||
---
|
||||
|
||||
## ✅ Recommended: **Database-level concurrency control + optional distributed lock**
|
||||
|
||||
### 👇 Here’s what you should do:
|
||||
|
||||
---
|
||||
|
||||
## ✅ 1. Use **pessimistic concurrency** (row-level locking) on the `Transportation` row
|
||||
|
||||
### Why?
|
||||
|
||||
You need to **lock the transportation row** to prevent multiple reservations from modifying the related seats/capacity **at the same time**.
|
||||
|
||||
### How?
|
||||
|
||||
Use EF Core's **raw SQL or transaction-scoped SELECT ... FOR UPDATE** (which translates to SQL row locks):
|
||||
|
||||
```csharp
|
||||
using var transaction = await _context.Database.BeginTransactionAsync(IsolationLevel.Serializable);
|
||||
|
||||
var transportation = await _context.Transportations
|
||||
.FromSqlRaw("SELECT * FROM Transportations WITH (UPDLOCK, ROWLOCK) WHERE Id = {0}", dto.TransportationId)
|
||||
.FirstOrDefaultAsync();
|
||||
```
|
||||
|
||||
- `UPDLOCK`: prevents others from reading for update
|
||||
|
||||
- `ROWLOCK`: locks only the specific row
|
||||
|
||||
|
||||
> If using PostgreSQL or MySQL, you’d use `FOR UPDATE` instead.
|
||||
|
||||
---
|
||||
|
||||
## ✅ 2. Alternatively, use **distributed locking** if you have multiple app instances
|
||||
|
||||
Use something like:
|
||||
|
||||
- **Redis distributed lock**
|
||||
|
||||
- **SQL application locks** (`sp_getapplock`)
|
||||
|
||||
- **SemaphoreSlim static lock** (only for single-instance apps — not safe in web farms)
|
||||
|
||||
|
||||
### Example using `sp_getapplock` (SQL Server):
|
||||
|
||||
```sql
|
||||
EXEC sp_getapplock @Resource = 'lock_transportation_123', @LockMode = 'Exclusive', @LockTimeout = 10000;
|
||||
```
|
||||
|
||||
EF version:
|
||||
|
||||
```csharp
|
||||
await _dbContext.Database.ExecuteSqlRawAsync(
|
||||
$"EXEC sp_getapplock @Resource = 'transportation_lock_{dto.TransportationId}', @LockMode = 'Exclusive', @LockTimeout = 10000;");
|
||||
```
|
||||
|
||||
✅ This will ensure **only one thread** gets to proceed per `TransportationId`.
|
||||
|
||||
---
|
||||
|
||||
## ✅ 3. Wrap the reservation logic in a **transaction**
|
||||
|
||||
```csharp
|
||||
using var transaction = await _context.Database.BeginTransactionAsync(IsolationLevel.Serializable);
|
||||
|
||||
// locking query here
|
||||
// seat validation
|
||||
// insert tickets
|
||||
// save ticket order
|
||||
// commit
|
||||
|
||||
await _unitOfWork.SaveChangesAsync();
|
||||
await transaction.CommitAsync();
|
||||
```
|
||||
|
||||
Use `IsolationLevel.Serializable` or `RepeatableRead` if you’re not doing explicit locking.
|
||||
|
||||
---
|
||||
|
||||
## ✅ 4. Add a **unique constraint** on `SeatId + TransportationId` (if not already)
|
||||
|
||||
To **enforce uniqueness at DB level** and prevent race conditions from succeeding:
|
||||
|
||||
```sql
|
||||
ALTER TABLE Tickets ADD CONSTRAINT UQ_Tickets_Seat_Transport UNIQUE (SeatId, TicketStatusId)
|
||||
WHERE TicketStatusId != 3; -- exclude cancelled
|
||||
```
|
||||
|
||||
Or make a logic-safe alternative depending on your schema.
|
||||
|
||||
---
|
||||
|
||||
## ✅ Final version: Summary of changes
|
||||
|
||||
### In your service:
|
||||
|
||||
```csharp
|
||||
using var transaction = await _dbContext.Database.BeginTransactionAsync(IsolationLevel.Serializable);
|
||||
|
||||
// Optional: Lock the transportation row to prevent race
|
||||
await _dbContext.Database.ExecuteSqlRawAsync(
|
||||
$"EXEC sp_getapplock @Resource = 'transportation_lock_{dto.TransportationId}', @LockMode = 'Exclusive', @LockTimeout = 10000;");
|
||||
|
||||
// Re-fetch transportation + tickets
|
||||
var transportation = await _transportationRepository.GetByIdWithTickets(dto.TransportationId);
|
||||
|
||||
// Validate seats
|
||||
var seatCheck = ValidateTransportationAndSeats(transportation, dto.Travelers);
|
||||
if (!string.IsNullOrEmpty(seatCheck)) return Result<long>.Error(0, seatCheck);
|
||||
|
||||
// Proceed with reservation
|
||||
...
|
||||
|
||||
await _unitOfWork.SaveChangesAsync();
|
||||
await transaction.CommitAsync();
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 🧠 Summary Table
|
||||
|
||||
|Strategy|Prevents Race?|Web-farm Safe|Recommended For|
|
||||
|---|---|---|---|
|
||||
|`sp_getapplock`|✅ Yes|✅ Yes|SQL Server web apps|
|
||||
|EF raw SQL `WITH (UPDLOCK)`|✅ Yes|✅ Yes|SQL Server monoliths|
|
||||
|`SemaphoreSlim`|✅ Yes|❌ No|Local-only apps|
|
||||
|`FOR UPDATE`|✅ Yes|✅ Yes|PostgreSQL, MySQL|
|
||||
|
||||
---
|
||||
|
||||
Let me know your DB provider and environment (single-instance or multi-instance), and I can generate exact code tailored for that.
|
||||
Perfect — you're absolutely right to want **the whole process to be atomic**:
|
||||
→ **Seat allocation**,
|
||||
→ **Ticket creation**,
|
||||
→ **Balance checking & payment**,
|
||||
→ **Transaction creation**,
|
||||
→ **Traveler creation/upsert**, etc.
|
||||
|
||||
This needs more than just _database-level locks_. You want to **serialize execution** for the same transportation to avoid concurrency conflicts — **across the entire flow**, not just the DB writes.
|
||||
|
||||
---
|
||||
|
||||
## ✅ Your Goal
|
||||
|
||||
> **For a given `TransportationId`, only one call to `CreateTicketOrderAsync` should execute at a time**, regardless of app instance or user.
|
||||
|
||||
This includes logic **outside EF** (like payment, seat assignment, person creation, etc.).
|
||||
|
||||
---
|
||||
|
||||
## 🛠️ The Right Solution: **Distributed Application Locking**
|
||||
|
||||
### Use **SQL Server’s `sp_getapplock`** to lock **by `TransportationId`**
|
||||
|
||||
Why?
|
||||
|
||||
- It locks **application-level logic**, not just SQL rows
|
||||
|
||||
- It works across threads, processes, and even app servers
|
||||
|
||||
- It ensures only **one process** can work on that transportation at a time
|
||||
|
||||
|
||||
---
|
||||
|
||||
## ✅ How to Implement in Your Method
|
||||
|
||||
### 🔒 Step 1: Lock by Transportation ID
|
||||
|
||||
Add this at the **start** of your method:
|
||||
|
||||
```csharp
|
||||
await _dbContext.Database.ExecuteSqlRawAsync(
|
||||
$"EXEC sp_getapplock @Resource = 'transportation_lock_{dto.TransportationId}', " +
|
||||
"@LockMode = 'Exclusive', @LockTimeout = 10000;");
|
||||
```
|
||||
|
||||
This ensures that **only one call per transportation** runs at a time.
|
||||
|
||||
If a second request tries to lock the same ID, it waits up to 10 seconds.
|
||||
|
||||
---
|
||||
|
||||
### 🔁 Step 2: Begin EF Transaction
|
||||
|
||||
This will include all EF operations in a single atomic unit:
|
||||
|
||||
```csharp
|
||||
using var transaction = await _dbContext.Database.BeginTransactionAsync();
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 🧠 Step 3: Keep the Full Logic As Is, But Within Transaction
|
||||
|
||||
Like this:
|
||||
|
||||
```csharp
|
||||
public async Task<Result<long>> CreateTicketOrderAsync(long accountId, CreateTicketOrderDto dto)
|
||||
{
|
||||
// 🔒 Lock to prevent concurrency per transportation
|
||||
await _dbContext.Database.ExecuteSqlRawAsync(
|
||||
$"EXEC sp_getapplock @Resource = 'transportation_lock_{dto.TransportationId}', " +
|
||||
"@LockMode = 'Exclusive', @LockTimeout = 10000;");
|
||||
|
||||
// 🧾 Begin transaction
|
||||
using var transaction = await _dbContext.Database.BeginTransactionAsync();
|
||||
|
||||
// 💰 Load account and transportation
|
||||
var account = await _accountRepository.GetByIdAsync(accountId);
|
||||
if (account == null) return Result<long>.Error(0, "Account not found");
|
||||
|
||||
var transportation = await _transportationRepository.GetByIdAsync(dto.TransportationId);
|
||||
if (transportation == null) return Result<long>.Error(0, "Transportation not found");
|
||||
|
||||
// 💵 Check balance
|
||||
var price = transportation.BasePrice * dto.Travelers.Count;
|
||||
if (account.CurrentBalance < price)
|
||||
return Result<long>.Error(0, "Not enough money");
|
||||
|
||||
// ✅ Validate seat availability
|
||||
var seatCheck = ValidateTransportationAndSeats(transportation, dto.Travelers);
|
||||
if (!string.IsNullOrEmpty(seatCheck))
|
||||
return Result<long>.Error(0, seatCheck);
|
||||
|
||||
// 🪑 Assign seats (if dynamic logic)
|
||||
await AssignSeatsIfDynamic(transportation.VehicleId, dto.Travelers);
|
||||
|
||||
// 👤 Upsert travelers (can also use same transaction)
|
||||
await UpsertTravelers(account.Id, dto.Travelers);
|
||||
|
||||
// 🎟️ Create ticket order
|
||||
var ticketOrder = new TicketOrder
|
||||
{
|
||||
BuyerId = account.Id,
|
||||
CreatedAt = DateTime.UtcNow,
|
||||
Description = "",
|
||||
SerialNumber = Guid.NewGuid().ToString("N"),
|
||||
TransportationId = dto.TransportationId,
|
||||
};
|
||||
await _ticketOrderRepository.AddAsync(ticketOrder);
|
||||
|
||||
// 🎫 Add tickets
|
||||
foreach (var traveler in dto.Travelers)
|
||||
{
|
||||
var ticket = new Ticket
|
||||
{
|
||||
CreatedAt = DateTime.UtcNow,
|
||||
Description = traveler.Description,
|
||||
SeatId = traveler.SeatId!.Value,
|
||||
SerialNumber = Guid.NewGuid().ToString("N"),
|
||||
TicketOrder = ticketOrder,
|
||||
TicketStatusId = 1,
|
||||
TravelerId = traveler.Id,
|
||||
};
|
||||
await _ticketRepository.AddAsync(ticket);
|
||||
}
|
||||
|
||||
// 💳 Commit everything
|
||||
await _unitOfWork.SaveChangesAsync();
|
||||
|
||||
// 💸 Withdraw money and create transaction
|
||||
await _accountService.PayForTicketOrderAsync(account.Id, ticketOrder.Id, price);
|
||||
|
||||
// ✅ Commit transaction
|
||||
await transaction.CommitAsync();
|
||||
|
||||
return Result<long>.Success(ticketOrder.Id);
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 🧱 Summary of Protection
|
||||
|
||||
|Step|Protected?|How|
|
||||
|---|---|---|
|
||||
|Prevent concurrent seat assignment|✅|`sp_getapplock` per `TransportationId`|
|
||||
|Prevent concurrent DB changes|✅|EF `DbTransaction`|
|
||||
|Prevent double-spend|✅|Balance check inside the same lock + transaction|
|
||||
|Ensure traveler upserts/tickets are safe|✅|Wrapped in same transaction|
|
||||
|
||||
---
|
||||
|
||||
## ⚠️ Important Note
|
||||
|
||||
Make sure all the operations (`AssignSeatsIfDynamic`, `UpsertTravelers`, etc.):
|
||||
|
||||
- Use the **same `DbContext`/UnitOfWork**
|
||||
|
||||
- Don’t commit changes prematurely (no `SaveChangesAsync()` before you're ready to commit the whole transaction)
|
||||
|
||||
|
||||
---
|
||||
|
||||
## ✅ Optional (Safety): Add retry logic
|
||||
|
||||
In case of a **deadlock or lock timeout**, you might want to retry the whole operation once.
|
||||
|
||||
---
|
||||
|
||||
Let me know if you want me to extract this into a reusable helper like:
|
||||
|
||||
```csharp
|
||||
await _appLockService.ExecuteWithLockAsync($"transportation_lock_{dto.TransportationId}", async () => {
|
||||
// your reservation logic
|
||||
});
|
||||
```
|
||||
|
||||
That would clean things up nicely.
|
||||
Sure. Let’s break down everything you need to know about **transactions** in Entity Framework Core (EF Core) — especially for your case where you're doing **complex operations that must all succeed or fail together**.
|
||||
|
||||
---
|
||||
|
||||
## 🧾 What is a Transaction?
|
||||
|
||||
A **transaction** is a unit of work that is treated as a **single, indivisible operation**. Either **all of it succeeds** (committed), or **none of it does** (rolled back).
|
||||
|
||||
### 🧠 Properties of a transaction (ACID):
|
||||
|
||||
|Property|Meaning|
|
||||
|---|---|
|
||||
|**Atomicity**|All steps succeed or none do|
|
||||
|**Consistency**|DB remains valid before and after|
|
||||
|**Isolation**|Transactions don’t interfere with each other|
|
||||
|**Durability**|Once committed, data is persisted permanently|
|
||||
|
||||
---
|
||||
|
||||
## ✅ Why You Need Transactions
|
||||
|
||||
In your case, you’re:
|
||||
|
||||
- Assigning seats
|
||||
|
||||
- Creating or updating people
|
||||
|
||||
- Creating a `TicketOrder`
|
||||
|
||||
- Creating multiple `Tickets`
|
||||
|
||||
- Updating account balance & transaction history
|
||||
|
||||
|
||||
All of these must either:
|
||||
|
||||
- Complete **fully and safely**, or
|
||||
|
||||
- **Fail entirely**, with no partial data left behind.
|
||||
|
||||
|
||||
---
|
||||
|
||||
## 🧰 Transaction in EF Core: Full Example
|
||||
|
||||
### 🔧 Step-by-step Example:
|
||||
|
||||
```csharp
|
||||
public async Task<Result<long>> CreateTicketOrderAsync(long accountId, CreateTicketOrderDto dto)
|
||||
{
|
||||
// Begin EF Core transaction
|
||||
using var transaction = await _dbContext.Database.BeginTransactionAsync();
|
||||
|
||||
try
|
||||
{
|
||||
// Load required entities
|
||||
var account = await _accountRepository.GetByIdAsync(accountId);
|
||||
if (account == null)
|
||||
return Result<long>.Error(0, "Account not found");
|
||||
|
||||
var transportation = await _transportationRepository.GetByIdAsync(dto.TransportationId);
|
||||
if (transportation == null)
|
||||
return Result<long>.Error(0, "Transportation not found");
|
||||
|
||||
var price = transportation.BasePrice * dto.Travelers.Count;
|
||||
if (account.CurrentBalance < price)
|
||||
return Result<long>.Error(0, "Not enough money");
|
||||
|
||||
// Validate seat assignment
|
||||
var seatCheck = ValidateTransportationAndSeats(transportation, dto.Travelers);
|
||||
if (!string.IsNullOrEmpty(seatCheck))
|
||||
return Result<long>.Error(0, seatCheck);
|
||||
|
||||
await AssignSeatsIfDynamic(transportation.VehicleId, dto.Travelers);
|
||||
await UpsertTravelers(account.Id, dto.Travelers);
|
||||
|
||||
// Create ticket order
|
||||
var ticketOrder = new TicketOrder
|
||||
{
|
||||
BuyerId = account.Id,
|
||||
CreatedAt = DateTime.UtcNow,
|
||||
SerialNumber = Guid.NewGuid().ToString("N"),
|
||||
TransportationId = dto.TransportationId,
|
||||
};
|
||||
await _ticketOrderRepository.AddAsync(ticketOrder);
|
||||
|
||||
foreach (var traveler in dto.Travelers)
|
||||
{
|
||||
await _ticketRepository.AddAsync(new Ticket
|
||||
{
|
||||
CreatedAt = DateTime.UtcNow,
|
||||
Description = traveler.Description,
|
||||
SeatId = traveler.SeatId.Value,
|
||||
SerialNumber = Guid.NewGuid().ToString("N"),
|
||||
TicketOrder = ticketOrder,
|
||||
TicketStatusId = 1,
|
||||
TravelerId = traveler.Id,
|
||||
});
|
||||
}
|
||||
|
||||
// Save changes
|
||||
await _unitOfWork.SaveChangesAsync();
|
||||
|
||||
// Withdraw money from account
|
||||
await _accountService.PayForTicketOrderAsync(account.Id, ticketOrder.Id, price);
|
||||
|
||||
// Commit transaction
|
||||
await transaction.CommitAsync();
|
||||
|
||||
return Result<long>.Success(ticketOrder.Id);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
// Rollback if anything failed
|
||||
await transaction.RollbackAsync();
|
||||
return Result<long>.Error(0, "Reservation failed: " + ex.Message);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 🔄 Isolation Levels (Advanced)
|
||||
|
||||
### You can specify isolation level when beginning a transaction:
|
||||
|
||||
```csharp
|
||||
using var transaction = await _dbContext.Database.BeginTransactionAsync(IsolationLevel.Serializable);
|
||||
```
|
||||
|
||||
### Common levels:
|
||||
|
||||
|Isolation Level|Description|Use Case|
|
||||
|---|---|---|
|
||||
|`ReadCommitted` (default)|No dirty reads|Usually enough|
|
||||
|`RepeatableRead`|Prevents non-repeatable reads|Seat assignment logic|
|
||||
|`Serializable`|Full isolation (slower, safer)|Financial or reservation systems|
|
||||
|
||||
---
|
||||
|
||||
## 🧩 When `SaveChangesAsync()` Commits vs. Not
|
||||
|
||||
Calling `SaveChangesAsync()` **does not commit** the outer transaction — it just writes pending changes to the database.
|
||||
|
||||
The real commit happens only when you call:
|
||||
|
||||
```csharp
|
||||
await transaction.CommitAsync();
|
||||
```
|
||||
|
||||
If anything fails before that, the changes are **not persisted**.
|
||||
|
||||
---
|
||||
|
||||
## ✅ Best Practices for Transactions in EF Core
|
||||
|
||||
|Best Practice|Why|
|
||||
|---|---|
|
||||
|Keep transactions **short**|Locks and contention increase with time|
|
||||
|Catch exceptions and **rollback**|Prevent dirty/incomplete data|
|
||||
|Use **only one DbContext** inside the transaction|EF tracks entities via one unit of work|
|
||||
|Don’t call SaveChanges too early|May break the atomicity|
|
||||
|Use **appropriate isolation level**|Based on your concurrency needs|
|
||||
|Consider retry policy for transient errors|Especially in distributed systems|
|
||||
|
||||
---
|
||||
|
||||
## ❗ Common Pitfalls
|
||||
|
||||
|Mistake|Problem|
|
||||
|---|---|
|
||||
|Mixing DbContext instances inside one transaction|Breaks consistency|
|
||||
|Committing too early|Later failure still leaves partial data|
|
||||
|Forgetting rollback in catch|Leaves transaction open or inconsistent|
|
||||
|Async code outside transaction scope|Could break atomicity or introduce race|
|
||||
|
||||
---
|
||||
|
||||
## 🧠 Summary: Transaction Essentials
|
||||
|
||||
|Concept|Description|
|
||||
|---|---|
|
||||
|`BeginTransactionAsync()`|Starts a manual transaction|
|
||||
|`SaveChangesAsync()`|Writes changes but doesn’t commit outer transaction|
|
||||
|`CommitAsync()`|Finalizes all DB changes|
|
||||
|`RollbackAsync()`|Reverts all changes since the transaction began|
|
||||
|Scope|All EF/DB actions done within the transaction block|
|
||||
|
||||
---
|
||||
|
||||
If you'd like, I can create a **generic transaction helper**, or even make this a **middleware** or **interceptor**, depending on how reusable you want this to be.
|
||||
|
||||
Let me know!
|
||||
Great! These questions show deep thinking about **how transactions and locks actually work** in EF Core and layered architecture. Let’s answer them one by one with real clarity and direction.
|
||||
|
||||
---
|
||||
|
||||
## ❓ 1. _"I have this method in my `TicketOrderService`, and I don't have access to `_dbContext`."_
|
||||
|
||||
### ✅ Solution:
|
||||
|
||||
If you're using **Repository + UnitOfWork pattern**, your `UnitOfWork` should expose a method to access the underlying **DbContext** or allow you to start a transaction.
|
||||
|
||||
**👉 Extend your `IUnitOfWork` interface like this:**
|
||||
|
||||
```csharp
|
||||
public interface IUnitOfWork
|
||||
{
|
||||
Task<int> SaveChangesAsync();
|
||||
Task<IDbContextTransaction> BeginTransactionAsync(IsolationLevel level = IsolationLevel.ReadCommitted);
|
||||
DbContext Context { get; } // optional
|
||||
}
|
||||
```
|
||||
|
||||
And in your implementation:
|
||||
|
||||
```csharp
|
||||
public class UnitOfWork : IUnitOfWork
|
||||
{
|
||||
private readonly AppDbContext _context;
|
||||
|
||||
public UnitOfWork(AppDbContext context) => _context = context;
|
||||
|
||||
public Task<int> SaveChangesAsync() => _context.SaveChangesAsync();
|
||||
|
||||
public Task<IDbContextTransaction> BeginTransactionAsync(IsolationLevel level = IsolationLevel.ReadCommitted)
|
||||
=> _context.Database.BeginTransactionAsync(level);
|
||||
|
||||
public DbContext Context => _context;
|
||||
}
|
||||
```
|
||||
|
||||
### ✅ Then in your `TicketOrderService`:
|
||||
|
||||
```csharp
|
||||
using var transaction = await _unitOfWork.BeginTransactionAsync();
|
||||
// your operations
|
||||
await _unitOfWork.SaveChangesAsync();
|
||||
await transaction.CommitAsync();
|
||||
```
|
||||
|
||||
This keeps your `Service` clean and architecture-respecting.
|
||||
|
||||
---
|
||||
|
||||
## ❓ 2. _"How does the transaction hold stuff? Wouldn’t `unitOfWork.SaveChangesAsync()` just save everything?"_
|
||||
|
||||
### 🔍 Clarification:
|
||||
|
||||
- `SaveChangesAsync()` **writes the changes to the database**, but…
|
||||
|
||||
- **If you're in a transaction**, the database holds those changes in a **temporary, invisible state** until you explicitly call:
|
||||
|
||||
|
||||
```csharp
|
||||
await transaction.CommitAsync();
|
||||
```
|
||||
|
||||
💡 If you **don’t commit**, or you **rollback**, then **those changes are discarded**, even if you called `SaveChangesAsync()`.
|
||||
|
||||
### 👇 Timeline of how EF works in a transaction:
|
||||
|
||||
1. EF tracks entities in memory.
|
||||
|
||||
2. You call `SaveChangesAsync()`.
|
||||
|
||||
3. Changes are written to the DB — but:
|
||||
|
||||
- If inside a transaction, they are **not visible to others yet**.
|
||||
|
||||
- They are held until `CommitAsync()`.
|
||||
|
||||
4. If you call `RollbackAsync()`, all writes are undone.
|
||||
|
||||
|
||||
---
|
||||
|
||||
## ❓ 3. _"How would the lock know that the work with that transportation is finished?"_
|
||||
|
||||
### 🔒 About `sp_getapplock`:
|
||||
|
||||
This SQL Server feature locks a **named resource** (like `transportation_lock_123`) for **the duration of the current SQL connection and transaction**.
|
||||
|
||||
Here’s how it works:
|
||||
|
||||
|Action|Result|
|
||||
|---|---|
|
||||
|You call `sp_getapplock`|SQL Server grants an exclusive lock for the resource|
|
||||
|You complete the transaction (commit or rollback)|Lock is **automatically released**|
|
||||
|Your connection is closed or disposed|Lock is released|
|
||||
|
||||
### 🔍 So you don’t need to manually release the lock.
|
||||
|
||||
Just make sure:
|
||||
|
||||
- The **lock and transaction use the same connection**
|
||||
|
||||
- You commit or rollback after the work is done
|
||||
|
||||
|
||||
✅ EF Core handles all that automatically when using `Database.BeginTransactionAsync()`.
|
||||
|
||||
---
|
||||
|
||||
## ✅ Final Advice Recap
|
||||
|
||||
|Question|Answer|
|
||||
|---|---|
|
||||
|I don’t have access to `_dbContext`|Expose `BeginTransactionAsync` on your `IUnitOfWork`|
|
||||
|How does transaction matter if I save mid-way?|It isolates changes — no one sees them until committed|
|
||||
|How does lock get released?|Automatically when transaction completes or connection closes|
|
||||
|
||||
---
|
||||
|
||||
Let me know if you want:
|
||||
|
||||
- Sample `UnitOfWork` and `Repository` setup for full support
|
||||
|
||||
- A lock + transaction helper method
|
||||
|
||||
- Unit tests to simulate concurrent reservations
|
||||
|
||||
|
||||
You're doing great — this is how robust, real-world software gets built.
|
||||
|
||||
You've unpacked crucial concerns around **transactions**, **unit-of-work (UoW)**, and **application-wide consistency**. Here's a polished breakdown with clear answers and guidance:
|
||||
|
||||
---
|
||||
|
||||
### ✅ 1. Is it okay to use both Transactions **and** Unit of Work?
|
||||
|
||||
- **Yes, absolutely.** UoW is a design pattern that groups multiple repository operations into one logical unit, while a _transaction_ ensures these operations either all succeed or all fail atomically ([antondevtips.com](https://antondevtips.com/blog/implementing-unit-of-work-pattern-in-ef-core?utm_source=chatgpt.com "Implementing Unit of Work Pattern in EF Core - Anton DevTips")).
|
||||
|
||||
- In EF Core, the **DbContext itself implements UoW**, collecting changes in memory until `SaveChangesAsync()` is called ([reddit.com](https://www.reddit.com/r/dotnet/comments/17npjtx/unit_of_work_pattern_still_necessary_in_net_8/?utm_source=chatgpt.com "Unit of Work Pattern still necessary in .NET 8? : r/dotnet - Reddit")).
|
||||
|
||||
- When you execute **multiple SaveChanges**, or include non-repository logic (e.g., balance updates, payment creation, seat locking), **explicit transactions** ensure the entire flow remains atomic .
|
||||
|
||||
|
||||
---
|
||||
|
||||
### ✅ 2. How exactly to open/close transactions? Where to be careful?
|
||||
|
||||
#### ✨ Use EF to manage transactions in your UoW:
|
||||
|
||||
1. **Extend your `IUnitOfWork`** to include transaction methods:
|
||||
|
||||
```csharp
|
||||
Task<IDbContextTransaction> BeginTransactionAsync(IsolationLevel level = ReadCommitted);
|
||||
Task<int> SaveChangesAsync();
|
||||
```
|
||||
|
||||
2. **In your service**, do:
|
||||
|
||||
```csharp
|
||||
using var tx = await _unitOfWork.BeginTransactionAsync();
|
||||
try {
|
||||
// perform all operations (seat locks, entity changes, balance, tickets)
|
||||
await _unitOfWork.SaveChangesAsync();
|
||||
await tx.CommitAsync();
|
||||
} catch {
|
||||
await tx.RollbackAsync();
|
||||
throw;
|
||||
}
|
||||
```
|
||||
|
||||
3. **Know the boundaries**: Keep the transaction as short as possible. Open it before critical operations and close it after the final `CommitAsync()`.
|
||||
|
||||
|
||||
> 💡 Use `(IsolationLevel.Serializable)` for scenarios like seat reservation where concurrent writes must be prevented ([milanjovanovic.tech](https://www.milanjovanovic.tech/blog/working-with-transactions-in-ef-core?utm_source=chatgpt.com "Working With Transactions In EF Core"), [learn.microsoft.com](https://learn.microsoft.com/en-us/aspnet/mvc/overview/older-versions/getting-started-with-ef-5-using-mvc-4/implementing-the-repository-and-unit-of-work-patterns-in-an-asp-net-mvc-application?utm_source=chatgpt.com "Implementing the Repository and Unit of Work Patterns in an ASP ...")).
|
||||
|
||||
---
|
||||
|
||||
### ✅ 3. Why did things “work” without explicit transactions?
|
||||
|
||||
- **Simple SaveChanges** calls act like atomic units—if they fail, nothing is saved. With just one SaveChanges call, EF wraps it in a default transaction ([stackoverflow.com](https://stackoverflow.com/questions/24636352/unit-of-work-i-dont-need-to-use-transactions?utm_source=chatgpt.com "c# - unit of work - I don't need to use transactions? - Stack Overflow")).
|
||||
|
||||
- However, as soon as your workflow involves **multiple SaveChanges**, **external services**, or **cross-repository updates**, the default behavior is no longer sufficient. A failure midway could leave your data in a partial state.
|
||||
|
||||
- That's where **explicit transaction scopes** come in—to guarantee everything is batched and rolled back if any part fails ([antondevtips.com](https://antondevtips.com/blog/implementing-unit-of-work-pattern-in-ef-core?utm_source=chatgpt.com "Implementing Unit of Work Pattern in EF Core - Anton DevTips")).
|
||||
|
||||
|
||||
---
|
||||
|
||||
### 🧠 TL;DR Summary
|
||||
|
||||
|Concept|Purpose|When Needed|
|
||||
|---|---|---|
|
||||
|**Unit of Work**|Organizes multiple EF operations into one commit|Always used with EF DbContext|
|
||||
|**Implicit transaction**|EF wraps each SaveChanges automatically|Ok for single SaveChanges calls|
|
||||
|**Explicit transaction**|Manually control commit/rollback across many operations|Needed when multiple SaveChanges, external services, concurrency, etc.|
|
||||
|**IsolationLevel.Serializable**|Highest locking level to prevent concurrent conflicts|Use for critical resource locks (e.g., seat booking)|
|
||||
|**Be careful**|Keep transactions short to avoid locks and deadlocks|Avoid long-running operations inside the transaction|
|
||||
|
||||
---
|
||||
|
||||
If you want, I can provide:
|
||||
|
||||
- **Code templates** for UoW with transaction management
|
||||
|
||||
- **Interceptor or decorator** to automate transaction handling in your services
|
||||
|
||||
- **Concurrency demos** to test seat reservation under load
|
||||
|
||||
|
||||
Just say the word!
|
||||
Absolutely! Let’s clarify both of your statements with authoritative sources:
|
||||
|
||||
---
|
||||
|
||||
## 1. If I haven't called `BeginTransaction` and call `SaveChanges`, will changes be saved?
|
||||
|
||||
✅ **Yes.** EF Core automatically wraps **every** `SaveChanges()` call in its **own transaction**. This means:
|
||||
|
||||
- If **all operations succeed**, the data is saved.
|
||||
- If **anything fails**, the entire set is **rolled back**, leaving the DB unchanged. citeturn0search0turn0search9
|
||||
|
||||
🔁 So even without explicitly starting a transaction, `SaveChanges()` ensures atomicity for that batch of changes.
|
||||
|
||||
---
|
||||
|
||||
## 2. If I have called `BeginTransaction`, and then call `SaveChanges`, do changes go to the DB immediately?
|
||||
|
||||
✅ **Yes, but context matters.** When you wrap operations in an explicit transaction:
|
||||
|
||||
- Each `SaveChanges()` writes within that transaction.
|
||||
- However, **none** of these writes become visible to other connections until you call `Commit()`.
|
||||
|
||||
EF also uses **savepoints** internally (for multiple `SaveChanges()` calls) to enable partial rollbacks without rolling back the entire transaction. citeturn0search0turn0search2turn0search3
|
||||
|
||||
---
|
||||
|
||||
### 🧩 In Plain English
|
||||
|
||||
| Scenario | SaveChanges Only | BeginTransaction + SaveChanges |
|
||||
|------------------|--------------------------------------------------|----------------------------------------------------|
|
||||
| No transaction | Auto-wrapped in its own transaction–immediate. | — |
|
||||
| With explicit transaction | Writes are within the transaction, not yet committed. | ✔ |
|
||||
| Rollback before commit | Not possible. SaveChanges is final. | ✔ Rolls everything back. |
|
||||
|
||||
---
|
||||
|
||||
### ✅ TL;DR
|
||||
|
||||
- ✅ **Without `BeginTransaction`**: `SaveChanges()` is atomic per call—commits or rolls back automatically.
|
||||
- ✅ **With `BeginTransaction`**: `SaveChanges()` writes into the transaction, but the changes are **held until** you call `Commit()`; `Rollback()` discards them all.
|
||||
|
||||
---
|
||||
|
||||
Want some **ready-to-use code** to introduce `BeginTransaction()` into your UnitOfWork and services? Just let me know and I’ll supply a clean implementation!
|
||||
@@ -0,0 +1,512 @@
|
||||
# Session 9 - Ticket Reservation
|
||||
As this order is comming to an end, we are completing the functionality of application with the vital feature of reserving tickets
|
||||
|
||||
## Miscellaneous / Fixes
|
||||
|
||||
- [ ] Fix GUID generation and async handling in ticket creation. (Use `Guid.NewGuid()` instead of `new Guid()`)
|
||||
```csharp
|
||||
SerialNumber = Guid.NewGuid().ToString("N")
|
||||
```
|
||||
|
||||
- [ ] Make `SerialNumber`, `TicketOrderId`, `BaseAmount` publicly settable in DTOs if they're private.
|
||||
- [ ] Check `Transaction` and make sure `SerialNumber` is not a required property.
|
||||
|
||||
## Branching
|
||||
- [ ] Create the feature/ticket-reservation branch based on develop
|
||||
|
||||
# Ticket Ordering System
|
||||
|
||||
## 🧱 Domain and Infrastructure Setup
|
||||
|
||||
- [ ] Check out new ERD: [Here](https://github.com/TheOrderOfPhoenix/ASP.NET/blob/main/ProjectOrientedSessions/docs/AlibabaERD-Version02.pdf)
|
||||
- [ ] Add migrations for the above database changes.
|
||||
|
||||
## 🧑💼 Path to Service Layer
|
||||
|
||||
- [ ] Create DTOs `CreateTravellerTicketDto` and `CreateTicketOrderDto` and the **mappings**:
|
||||
```csharp
|
||||
public class CreateTravellerTicketDto
|
||||
{
|
||||
public long Id { get; set; }
|
||||
public long CreatorId { get; set; }
|
||||
|
||||
[Required(ErrorMessage = "First name is required")]
|
||||
public required string FirstName { get; set; }
|
||||
|
||||
[Required(ErrorMessage = "Last name is required")]
|
||||
public required string LastName { get; set; }
|
||||
|
||||
[Required(ErrorMessage = "Id number is required")]
|
||||
[RegularExpression(@"^\d{10}$", ErrorMessage = "National ID number must be exactly 10 digits")]
|
||||
public required string IdNumber { get; set; }
|
||||
|
||||
[Required(ErrorMessage = "Gender should be identified")]
|
||||
public required short GenderId { get; set; }
|
||||
|
||||
[Required(ErrorMessage = "Phone number is required")]
|
||||
[Phone(ErrorMessage = "Invalid phone number format")]
|
||||
public required string PhoneNumber { get; set; }
|
||||
|
||||
[Required(ErrorMessage = "Birth date is required")]
|
||||
public DateTime BirthDate { get; set; }
|
||||
|
||||
public long? SeatId { get; set; }
|
||||
public bool IsVIP { get; set; }
|
||||
public string? Description { get; set; }
|
||||
}
|
||||
```
|
||||
```csharp
|
||||
public class CreateTicketOrderDto
|
||||
{
|
||||
public long TransportationId { get; set; }
|
||||
public List<CreateTravellerTicketDto> MyProperty { get; set; }
|
||||
}
|
||||
```
|
||||
```csharp
|
||||
|
||||
```
|
||||
Note: If you have the **Coupon** feature in your project, then add a `CouponCode` property in `CreateTicketOrderDto`.
|
||||
|
||||
- [ ] Modify SeatRepository to add method `GetSeatsByVehicleIdAsync`
|
||||
```csharp
|
||||
public Task<List<Seat>> GetSeatsByVehicleIdAsync(long vehicleId)
|
||||
{
|
||||
var seats = DbSet
|
||||
.Include(s => s.Vehicle)
|
||||
.Include(s => s.Tickets).ThenInclude(t => t.Traveler)
|
||||
.Where(s => s.VehicleId == vehicleId).ToListAsync();
|
||||
return seats;
|
||||
}
|
||||
```
|
||||
- [ ] Add Enum for **TicketStatus**, **VehicleType** and **TransactionType**
|
||||
```csharp
|
||||
public enum TicketStatusEnum
|
||||
{
|
||||
Reserved = 1,
|
||||
Paid = 2,
|
||||
CancelledByUser = 3,
|
||||
CancelledBySystem = 4,
|
||||
Used = 5,
|
||||
Expired = 6
|
||||
}
|
||||
```
|
||||
```csharp
|
||||
public enum VehicleTypeEnum
|
||||
{
|
||||
Airplane = 1,
|
||||
Train = 2,
|
||||
Bus = 3
|
||||
}
|
||||
```
|
||||
```csharp
|
||||
public enum TransactionTypeEnum
|
||||
{
|
||||
Deposit = 1,
|
||||
Withdraw = 2
|
||||
}
|
||||
```
|
||||
|
||||
- [ ] Add the lock-service to lock the transportation through reservation, then register it:
|
||||
```csharp
|
||||
public class TransportationLockService : ITransportationLockService
|
||||
{
|
||||
private readonly ConcurrentDictionary<long, SemaphoreSlim> _locks = new();
|
||||
|
||||
public async Task<IDisposable> AcquireLockAsync(long transportationId)
|
||||
{
|
||||
var semaphore = _locks.GetOrAdd(transportationId, new SemaphoreSlim(1, 1));
|
||||
await semaphore.WaitAsync();
|
||||
return new Releaser(() => semaphore.Release());
|
||||
}
|
||||
|
||||
private class Releaser : IDisposable
|
||||
{
|
||||
private readonly Action _release;
|
||||
|
||||
public Releaser(Action release)
|
||||
{
|
||||
_release = release;
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
_release();
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
- [ ] Add method `CreateAsync` in `TransportationService`
|
||||
```csharp
|
||||
public async Task<Result<long>> CreateAsync(long accountId, TransactionDto dto)
|
||||
{
|
||||
Transaction transaction = new();
|
||||
_mapper.Map(dto, transaction);
|
||||
transaction.AccountId = accountId;
|
||||
|
||||
await _transactionRepository.InsertAsync(transaction);
|
||||
await _unitOfWork.CompleteAsync();
|
||||
return Result<long>.Success(transaction.Id);
|
||||
}
|
||||
```
|
||||
|
||||
- [ ] Add method `PayForTicketOrderAsync` in `AccountService`
|
||||
```csharp
|
||||
public async Task<Result<long>> PayForTicketOrderAsync(long accountId, long ticketOrderId, decimal baseAmount, decimal finalAmount)
|
||||
{
|
||||
var account = await _accountRepository.GetByIdAsync(accountId);
|
||||
if (account == null)
|
||||
{
|
||||
return Result<long>.Error(0, "Account not found");
|
||||
}
|
||||
|
||||
if (account.Balance < finalAmount)
|
||||
{
|
||||
return Result<long>.Error(0, "Not enough money");
|
||||
}
|
||||
|
||||
account.Withdraw(finalAmount);
|
||||
_accountRepository.Update(account);
|
||||
await _unitOfWork.CompleteAsync();
|
||||
|
||||
TransactionDto dto = new()
|
||||
{
|
||||
CreatedAt = DateTime.UtcNow,
|
||||
Description = "Payment for ticket order #" + ticketOrderId + " at " + DateTime.UtcNow,
|
||||
BaseAmount = baseAmount,
|
||||
FinalAmount = finalAmount,
|
||||
SerialNumber = Guid.NewGuid().ToString("N"),
|
||||
TicketOrderId = ticketOrderId,
|
||||
TransactionTypeId = (int)TransactionTypeEnum.Withdraw,
|
||||
TransactionType = TransactionTypeEnum.Withdraw.ToString()
|
||||
};
|
||||
|
||||
return await _transactionService.CreateAsync(accountId, dto);
|
||||
}
|
||||
```
|
||||
|
||||
- [ ] Create `ITicketOrderService`, `TicketOrderService` and implement `CreateTicketOrderAsync`
|
||||
```csharp
|
||||
public async Task<Result<long>> CreateTicketOrderAsync(long accountId, CreateTicketOrderDto dto)
|
||||
{
|
||||
// get the account
|
||||
var account = await _accountRepository.GetByIdAsync(accountId);
|
||||
if (account == null)
|
||||
{
|
||||
return Result<long>.Error(0, "Account not found");
|
||||
}
|
||||
|
||||
// get the transportation
|
||||
var transportation = await _transportationRepository.GetByIdAsync(dto.TransportationId);
|
||||
if (transportation == null)
|
||||
{
|
||||
return Result<long>.Error(0, "Transportation not found");
|
||||
}
|
||||
|
||||
// lock the transportation through reservation
|
||||
using (await _transportationLockService.AcquireLockAsync(dto.TransportationId))
|
||||
{
|
||||
var baseAmount = transportation.BasePrice * dto.Travellers.Count;
|
||||
if (account.Balance < baseAmount)
|
||||
{
|
||||
return Result<long>.Error(0, "Not enough money");
|
||||
}
|
||||
// check validity of transportation
|
||||
var checkSeats = ValidateTransportationAndSeats(transportation, dto.Travellers);
|
||||
if (!string.IsNullOrEmpty(checkSeats))
|
||||
{
|
||||
return Result<long>.Error(0, checkSeats);
|
||||
}
|
||||
|
||||
var finalAmount = baseAmount;
|
||||
await AssignSeatsIfDynamic(transportation.VehicleId, dto.Travellers);
|
||||
await UpsertTravellers(accountId, dto.Travellers);
|
||||
|
||||
// add the ticket order by the info we have
|
||||
TicketOrder ticketOrder = new()
|
||||
{
|
||||
BuyerId = accountId,
|
||||
CreatedAt = DateTime.UtcNow,
|
||||
Description = "",
|
||||
SerialNumber = Guid.NewGuid().ToString("N"),
|
||||
TransportationId = dto.TransportationId
|
||||
};
|
||||
await _ticketOrderRepository.InsertAsync(ticketOrder);
|
||||
|
||||
foreach (var traveller in dto.Travellers)
|
||||
{
|
||||
if (!traveller.SeatId.HasValue)
|
||||
{
|
||||
return Result<long>.Error(0, "Seat ID is required for each traveller");
|
||||
}
|
||||
|
||||
Ticket ticket = new()
|
||||
{
|
||||
CreatedAt = DateTime.UtcNow,
|
||||
Description = traveller.Description,
|
||||
SeatId = traveller.SeatId.Value,
|
||||
SerialNumber = Guid.NewGuid().ToString("N"),
|
||||
TicketOrder = ticketOrder,
|
||||
TicketStatusId = 1,
|
||||
TravelerId = traveller.Id,
|
||||
};
|
||||
await _ticketRepository.InsertAsync(ticket);
|
||||
}
|
||||
|
||||
await _unitOfWork.CompleteAsync();
|
||||
await _accountService.PayForTicketOrderAsync(account.Id, ticketOrder.Id,
|
||||
baseAmount, finalAmount);
|
||||
return Result<long>.Success(ticketOrder.Id);
|
||||
}
|
||||
}
|
||||
```
|
||||
- [ ] Register `TicketOrderService` in DI container.
|
||||
|
||||
## 🎯 Controller Layer
|
||||
|
||||
- [ ] Create `TicketOrderController` with the endpoint `POST /CreateTicketOrder`
|
||||
```csharp
|
||||
[ApiController]
|
||||
[Route("api/[controller]")]
|
||||
[Authorize(Roles = "User")]
|
||||
public class TicketOrderController : ControllerBase
|
||||
{
|
||||
private readonly IUserContext _userContext;
|
||||
private readonly ITicketOrderService _ticketOrderService;
|
||||
|
||||
public TicketOrderController(IUserContext userContext,
|
||||
ITicketOrderService ticketOrderService)
|
||||
{
|
||||
_userContext = userContext;
|
||||
_ticketOrderService = ticketOrderService;
|
||||
}
|
||||
|
||||
[HttpPost("create-order")]
|
||||
public async Task<IActionResult> CreateTicketOrder([FromBody] CreateTicketOrderDto dto)
|
||||
{
|
||||
long accountId = _userContext.GetUserId();
|
||||
// check for account-id to be valid
|
||||
if (accountId <= 0)
|
||||
{
|
||||
return Unauthorized();
|
||||
}
|
||||
|
||||
var result = await _ticketOrderService.CreateTicketOrderAsync(accountId, dto);
|
||||
if (result.IsSuccess)
|
||||
{
|
||||
return Ok(result.Data);
|
||||
}
|
||||
|
||||
return result.Status switch
|
||||
{
|
||||
ResultStatus.Unauthorized => Unauthorized(result.ErrorMessage),
|
||||
ResultStatus.NotFound => NotFound(result.ErrorMessage),
|
||||
ResultStatus.ValidationError => BadRequest(result.ErrorMessage),
|
||||
_ => StatusCode(500, result.ErrorMessage)
|
||||
};
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 🗃️ Repository Layer
|
||||
|
||||
Implement there methods in `TicketOrderRepository` and its interface
|
||||
- [ ] `FindAndLoadAllDetails`
|
||||
```csharp
|
||||
public Task<TicketOrder?> FindAndLoadAllDetailsAsync(long id)
|
||||
{
|
||||
var ticketOrder = DbSet
|
||||
.Include(to => to.Transportation).ThenInclude(t => t.FromLocation).ThenInclude(fl => fl.City)
|
||||
.Include(to => to.Transportation).ThenInclude(t => t.ToLocation).ThenInclude(tl => tl.City)
|
||||
.Include(to => to.Tickets).ThenInclude(t => t.Traveler)
|
||||
.Where(to => to.Id == id).FirstOrDefaultAsync();
|
||||
|
||||
return ticketOrder;
|
||||
}
|
||||
```
|
||||
|
||||
- [ ] `GetAllByBuyerId`
|
||||
```csharp
|
||||
public async Task<List<TicketOrder>> GetAllByBuyerId(long buyerId)
|
||||
{
|
||||
var ticketOrders = await DbSet
|
||||
.Include(to => to.Transaction)
|
||||
.Include(to => to.Transportation).ThenInclude(t => t.FromLocation)
|
||||
.Include(to => to.Transportation).ThenInclude(t => t.ToLocation)
|
||||
.Include(to => to.Transportation).ThenInclude(t => t.Company)
|
||||
.Include(to => to.Transportation).ThenInclude(t => t.Vehicle)
|
||||
.Where(to => to.BuyerId == buyerId).ToListAsync();
|
||||
return ticketOrders;
|
||||
}
|
||||
```
|
||||
|
||||
# Transportation and Seat Selection
|
||||
|
||||
- [ ] Add `TransportationSeatDto`.
|
||||
```csharp
|
||||
public class TransportationSeatDto
|
||||
{
|
||||
public long Id { get; set; }
|
||||
public int Row { get; set; }
|
||||
public int Column { get; set; }
|
||||
public bool IsVIP { get; set; }
|
||||
public bool IsAvailable { get; set; }
|
||||
public string? Description { get; set; }
|
||||
public bool IsReserved { get; set; }
|
||||
public short? GenderId { get; set; }
|
||||
}
|
||||
```
|
||||
|
||||
- [ ] Add mapping from `Seat` to `TransportationSeatDto`.
|
||||
```csharp
|
||||
CreateMap<Seat, TransportationSeatDto>()
|
||||
.ForMember(dest => dest.IsReserved, opt => opt.MapFrom(src => src.Tickets.Any(t => t.TicketStatusId == (int)TicketStatusEnum.Reserved)))
|
||||
.ForMember(dest => dest.GenderId, opt => opt.MapFrom(src => src.Tickets.Any(t => t.TicketStatusId == (int)TicketStatusEnum.Reserved) ?
|
||||
src.Tickets.First(t => t.TicketStatusId == (int)TicketStatusEnum.Reserved).Traveler.GenderId : (short?)null));
|
||||
```
|
||||
|
||||
- [ ] Add method `GetSeatsByVehicleId` in `ISeatRepository` and implement it.
|
||||
```csharp
|
||||
public Task<List<Seat>> GetSeatsByVehicleIdAsync(long vehicleId)
|
||||
{
|
||||
var seats = DbSet
|
||||
.Include(s => s.Vehicle)
|
||||
.Include(s => s.Tickets).ThenInclude(t => t.Traveler)
|
||||
.Where(s => s.VehicleId == vehicleId).ToListAsync();
|
||||
return seats;
|
||||
}
|
||||
```
|
||||
|
||||
- [ ] Add `GetTransportationSeatsAsync` in `ITransportationService` and implement.
|
||||
```csharp
|
||||
public async Task<Result<List<TransportationSeatDto>>> GetTransportationSeatsAsync(long transportationId)
|
||||
{
|
||||
var transportation = await _transportationRepository.GetByIdAsync(transportationId);
|
||||
if (transportation == null)
|
||||
{
|
||||
return Result<List<TransportationSeatDto>>.Error(null, "Transportation not found");
|
||||
}
|
||||
|
||||
var seats = await _seatRepository.GetSeatsByVehicleIdAsync(transportation.VehicleId);
|
||||
if (seats == null || seats.Count != 0)
|
||||
{
|
||||
return Result<List<TransportationSeatDto>>.Success(_mapper.Map<List<TransportationSeatDto>>(seats));
|
||||
}
|
||||
|
||||
return Result<List<TransportationSeatDto>>.NotFound(null);
|
||||
}
|
||||
```
|
||||
|
||||
- [ ] Add `GetTransportationSeats` endpoint in `TransportationController`.
|
||||
```csharp
|
||||
[HttpGet("{transportationId}/seats")]
|
||||
public async Task<IActionResult> GetTransportationSeats(long transportationId)
|
||||
{
|
||||
var result = await _transportationService.GetTransportationSeatsAsync(transportationId);
|
||||
if (result.IsSuccess)
|
||||
{
|
||||
return Ok(result.Data);
|
||||
}
|
||||
|
||||
return result.Status switch
|
||||
{
|
||||
ResultStatus.Unauthorized => Unauthorized(result.ErrorMessage),
|
||||
ResultStatus.NotFound => NotFound(result.ErrorMessage),
|
||||
ResultStatus.ValidationError => BadRequest(result.ErrorMessage),
|
||||
_ => StatusCode(500, result.ErrorMessage)
|
||||
};
|
||||
}
|
||||
```
|
||||
|
||||
- [ ] Ensure `RemainingCapacity` is treated as calculated (ignored in EF, removed from schema).
|
||||
```csharp
|
||||
public int RemainingCapacity => Vehicle.Capacity -
|
||||
TicketOrders?.SelectMany(to => to.Tickets)
|
||||
.Count(t => t.TicketStatusId == 1) ?? 0;
|
||||
```
|
||||
|
||||
### ✅ Ticket Review & Confirmation
|
||||
|
||||
- [ ] Make sure you have `TravlerTicketDto`, mapped to `` with the details
|
||||
```csharp
|
||||
public class TravellerTicketDto
|
||||
{
|
||||
public long Id { get; set; }
|
||||
public required string SerialNumber { get; set; }
|
||||
public required string TravellerName { get; set; }
|
||||
public DateTime BirthDate { get; set; }
|
||||
public required string SeatNumber { get; set; }
|
||||
public required string TicketStatus { get; set; }
|
||||
public string? CompanionName { get; set; }
|
||||
public string? Description { get; set; }
|
||||
}
|
||||
```
|
||||
```csharp
|
||||
CreateMap<Ticket, TravellerTicketDto>()
|
||||
.ForMember(dest => dest.Id, opt => opt.MapFrom(src => src.Id))
|
||||
.ForMember(dest => dest.TravellerName, opt => opt.MapFrom(src => src.Traveler != null ? $"{src.Traveler.FirstName} {src.Traveler.LastName}" : ""))
|
||||
.ForMember(dest => dest.SerialNumber, opt => opt.MapFrom(src => src.SerialNumber))
|
||||
.ForMember(dest => dest.TicketStatus, opt => opt.MapFrom(src => src.TicketStatus.Ttile))
|
||||
.ForMember(dest => dest.CompanionName, opt => opt.MapFrom(src => src.Companion != null ? $"{src.Companion.FirstName} {src.Companion.LastName}" : ""))
|
||||
.ForMember(dest => dest.Description, opt => opt.MapFrom(src => src.Description));
|
||||
```
|
||||
|
||||
- [ ] Implement `GetTicketOrderDetails` endpoint to fetch ticket summary, make sure to create required methods as well
|
||||
```csharp
|
||||
[HttpGet("my-travels/{ticketOrderId}")]
|
||||
public async Task<IActionResult> GetTravelDetails(long ticketOrderId)
|
||||
{
|
||||
long accountId = _userContext.GetUserId();
|
||||
if (accountId <= 0)
|
||||
{
|
||||
return Unauthorized();
|
||||
}
|
||||
|
||||
var result = await _accountService.GetTicketOrderDetailsAsync(accountId, ticketOrderId);
|
||||
if (result.IsSuccess)
|
||||
{
|
||||
return Ok(result.Data);
|
||||
}
|
||||
|
||||
return result.Status switch
|
||||
{
|
||||
ResultStatus.Unauthorized => Unauthorized(result.ErrorMessage),
|
||||
ResultStatus.NotFound => NotFound(result.ErrorMessage),
|
||||
ResultStatus.ValidationError => BadRequest(result.ErrorMessage),
|
||||
_ => StatusCode(500, result.ErrorMessage)
|
||||
};
|
||||
}
|
||||
```
|
||||
```csharp
|
||||
public async Task<Result<List<TravellerTicketDto>>> GetTicketOrderDetailsAsync(long accoundId, long ticketOrderid)
|
||||
{
|
||||
var result = await _ticketRepository.GetTicketsByTicketOrderId(ticketOrderid);
|
||||
if (result != null)
|
||||
{
|
||||
if (result.Count > 0 && result.First().TicketOrder.BuyerId != accoundId)
|
||||
{
|
||||
return Result<List<TravellerTicketDto>>.Error(null, "Account unauthorized");
|
||||
}
|
||||
|
||||
return Result<List<TravellerTicketDto>>.Success(_mapper.Map<List<TravellerTicketDto>>(result));
|
||||
}
|
||||
|
||||
return Result<List<TravellerTicketDto>>.NotFound(null);
|
||||
}
|
||||
```
|
||||
```csharp
|
||||
public async Task<List<Ticket>> GetTicketsByTicketOrderId(long ticketOrderId)
|
||||
{
|
||||
var tickets = await DbSet
|
||||
.Include(t => t.Traveler)
|
||||
.Include(t => t.TicketStatus)
|
||||
.Include(t => t.Companion)
|
||||
.Include(t => t.Seat)
|
||||
.Include(t => t.TicketOrder)
|
||||
.Where(t => t.TicketOrderId == ticketOrderId).ToListAsync();
|
||||
return tickets;
|
||||
}
|
||||
```
|
||||
|
||||
## Merge
|
||||
- [ ] Create a PR and merge the current branch with develop
|
||||
@@ -0,0 +1,111 @@
|
||||
# Reservation System Development Guide
|
||||
|
||||
This guide helps developers understand how to implement, update, and maintain the ticket reservation system. It summarizes changes from commits and provides step-by-step actions, grouped by feature area.
|
||||
https://github.com/MehrdadShirvani/AlibabaClone-Frontend/commits/develop/
|
||||
---
|
||||
|
||||
# Fixes and Missing Things from Session 08
|
||||
## Authentication & Protected Routes
|
||||
|
||||
- [ ] Store `showLoginModal` state in `authStore` and adjust navbar
|
||||
- [ ] Add `ProtectedRoute` component
|
||||
- [ ] Adjust main `App` to route authenticated pages through protection layer
|
||||
- [ ] Add session persistence in `authStore` to store token more persistently
|
||||
|
||||
---
|
||||
## Profile and User Info Enhancements
|
||||
|
||||
- [ ] Fix and align `birthDate` types in `PersonDto`, `transportationSearchResult`, and `ListOfTravelers`
|
||||
## Agent
|
||||
- [ ] Add `topUpDto` and its method in `agent.ts`
|
||||
- [ ] Add optional config parameter to `request()` in `agent.ts`
|
||||
|
||||
# Branching
|
||||
- [ ] Create the feature/themes branch based on develop
|
||||
# 🎨 Theming and UI Styling
|
||||
- [ ] Install and import `preline`
|
||||
- [ ] Add theme colors and global styles (index.css)
|
||||
- [ ] Add `ThemeSwitcher` and integrate it into the navbar
|
||||
- [ ] If you decide to do this part after implementing pages, make sure to add theme support to the following components:
|
||||
- [ ] `transportationCard`, `transportationSearchForm`, `ReviewAndConfirm`
|
||||
- [ ] All modals: `LoginModal`, `RegisterModal`, `SelectFromPeopleModal`
|
||||
- [ ] Profile section: `ProfilePage`, `ProfileSummary`, `PersonalInformation`, `AccountInfo`, `PersonalAccountInfo`, `BankAccountDetails`, `MyTravels`, `MyTransactions`, `ListOfTravelers`
|
||||
- [ ] Reservation views and components
|
||||
|
||||
# Merge
|
||||
- [ ] Create a PR and merge the current branch with develop
|
||||
---
|
||||
# Branching
|
||||
- [ ] Create the feature/ticket-reservation branch based on develop
|
||||
# Backend Agent Methods
|
||||
- [ ] Add `createTicketOrderDto` and `createTravelerTicketDto`
|
||||
- [ ] Add `transportationSeatDto`
|
||||
- [ ] Add related methods in `TicketOrder` and add it to agent
|
||||
|
||||
# Reservation Process & Step Management
|
||||
|
||||
- [ ] Implement `useReservationStore` using `zustand` to manage reservation state
|
||||
- [ ] Create step-based routing using `ReservationLayout`
|
||||
- [ ] Add routing for reservation steps in `App.tsx`
|
||||
- [ ] Add `StepIndicator` component to show step progress visually
|
||||
- [ ] Add `stepGuard` logic to prevent accessing future steps prematurely
|
||||
- [ ] Add logic to skip back only if previous steps are completed
|
||||
- [ ] Create `TravelerForm` to gather passenger info, with the possibility to load data from the related people of the account.
|
||||
- [ ] Create `TravelerDetailsForm` to gather passengers info with `TravelerForm` integration
|
||||
- [ ] Create `ReviewAndConfirm` page to review selections
|
||||
- [ ] Create `PaymentForm` for transaction process
|
||||
- [ ] Create `TicketIssued` page for confirmation
|
||||
- [ ] Add validation to show error if `seatId` is missing
|
||||
|
||||
---
|
||||
# Seat Selection
|
||||
- [ ] Add DTO:
|
||||
```tsx
|
||||
export interface transportationSeatDto{
|
||||
id : number,
|
||||
vehicleId : number,
|
||||
row : number,
|
||||
column : number,
|
||||
isVIP : boolean,
|
||||
isAvailable : boolean,
|
||||
description : string | null,
|
||||
isReserved : boolean,
|
||||
genderId : number | null
|
||||
}
|
||||
```
|
||||
- [ ] Add `getSeats()` method to `agent.ts`
|
||||
- [ ] Add `SeatGridSelector` component for graphical seat layout in `TravelerDetailForm` and integrate it with traveler list, only for Buses
|
||||
- [ ] Modify `transportationCard` to integrate with seat selection
|
||||
- [ ] Add `SeatOnlyGridSeatMap` for simpler seat-only display
|
||||
---
|
||||
# Coupon Integration
|
||||
|
||||
- [ ] Add `couponValidationRequestDto` and `discountDto`
|
||||
- [ ] Add `validateCoupon()` method to `agent.ts`
|
||||
- [ ] Update `useReservationStore` to include `couponCode`
|
||||
- [ ] Ensure `createTicketOrderDto` uses `couponCode` instead of `couponId`
|
||||
- [ ] Connect coupon validation flow in `ReviewAndConfirm`
|
||||
|
||||
---
|
||||
# Search, Filter, and Sort Functionality
|
||||
|
||||
- [ ] Add filters (company) and sorting (time or price) UI to `SearchResultPage`
|
||||
- [ ] Add company logo support in result cards and filters
|
||||
- [ ] Add `previous/next day` buttons for time navigation
|
||||
- [ ] Add remaining capacity check to transportation cards
|
||||
- [ ] Add refund policy info display
|
||||
- [ ] Implement showing seat map in `TransportaionCard` using `ReadOnlySeatMap`
|
||||
|
||||
## 📎 Notes
|
||||
|
||||
- Ensure you run a full theme test after UI changes.
|
||||
- Test step transitions with various invalid scenarios.
|
||||
- Confirm persistent session and coupon behavior across refreshes.
|
||||
- Validate all filters, sort, and search navigation works.
|
||||
- Test seat selection and proper rendering of rotated layouts.
|
||||
|
||||
---
|
||||
|
||||
# Merge
|
||||
- [ ] Create a PR and merge the current branch with develop
|
||||
|
||||
@@ -0,0 +1,243 @@
|
||||
|
||||
## What is Docker?
|
||||
|
||||
Docker is a platform that allows you to package applications with all their dependencies into a standardized unit called a **container**. These containers are portable, isolated, and consistent across environments.
|
||||
|
||||
---
|
||||
|
||||
## What is a Container?
|
||||
|
||||
A container is a lightweight, standalone executable package that includes everything needed to run an application: code, runtime, libraries, and configurations.
|
||||
|
||||
Containers are stored in **container repositories**:
|
||||
- Public repositories: [Docker Hub](https://hub.docker.com)
|
||||
- Private repositories: Used by organizations for internal deployments
|
||||
|
||||
---
|
||||
|
||||
## Why Containers?
|
||||
|
||||
### Before Containers:
|
||||
- Developers shared artifacts (e.g., `.jar` files) with setup instructions.
|
||||
- Operators had to install dependencies manually.
|
||||
- Setup was error-prone and inconsistent across OS environments.
|
||||
|
||||
### With Containers:
|
||||
- Everything is bundled together and works the same everywhere.
|
||||
- No need to install dependencies manually.
|
||||
- Runs in its own isolated environment.
|
||||
- Easy to version, share, and deploy (just one command).
|
||||
- Multiple versions of the same app can run simultaneously.
|
||||
|
||||
---
|
||||
|
||||
## Image vs. Container
|
||||
|
||||
|Term|Description|
|
||||
|---|---|
|
||||
|**Image**|A snapshot or package (the blueprint). Immutable.|
|
||||
|**Container**|A running instance of an image. Has its own file system, environment, and process.|
|
||||
Running an image creates a container.
|
||||
|
||||
---
|
||||
|
||||
## Docker vs Virtual Machine
|
||||
|
||||
| Feature | Docker | Virtual Machine |
|
||||
| --------------- | ------------------ | ------------------ |
|
||||
| Virtualizes | Application layer | Full OS kernel |
|
||||
| Startup time | Seconds | Minutes |
|
||||
| Size | MBs | GBs |
|
||||
| Isolation | OS-level | Hardware-level |
|
||||
| Performance | Near-native | Heavier overhead |
|
||||
| Host dependency | Shares host kernel | Has its own kernel |
|
||||
|
||||
Docker runs natively on Linux; on Windows/macOS it uses **Docker Desktop**, which runs Linux under WSL2 or HyperKit.
|
||||
|
||||
---
|
||||
|
||||
## Docker Architecture
|
||||
|
||||
### Layers of a Docker Image:
|
||||
|
||||
- Base Layer: Usually a minimal Linux distribution (e.g., `alpine`)
|
||||
- Application Layer: Your app and its dependencies
|
||||
|
||||
Each image is made of **layers** stacked on top of each other.
|
||||
|
||||
---
|
||||
|
||||
## Docker Installation
|
||||
|
||||
To use Docker on:
|
||||
|
||||
- **Linux**: Install Docker engine directly.
|
||||
- **Windows/macOS**: Use Docker Desktop, which includes WSL2 integration or virtualization backend.
|
||||
|
||||
---
|
||||
|
||||
## Docker Commands: Basics
|
||||
|
||||
```bash
|
||||
# Run a container from an image
|
||||
docker run image-name
|
||||
|
||||
# List running containers
|
||||
docker ps
|
||||
|
||||
# List all containers (running and stopped)
|
||||
docker ps -a
|
||||
|
||||
# Stop a running container
|
||||
docker stop CONTAINER_ID
|
||||
|
||||
# Start a stopped container
|
||||
docker start CONTAINER_ID
|
||||
```
|
||||
|
||||
### Port Binding
|
||||
|
||||
```bash
|
||||
docker run -p HOST_PORT:CONTAINER_PORT image-name
|
||||
```
|
||||
|
||||
This binds a container’s port to a specific port on your machine.
|
||||
|
||||
---
|
||||
|
||||
## Debugging Containers
|
||||
|
||||
```bash
|
||||
# View logs
|
||||
docker logs CONTAINER_ID
|
||||
|
||||
# Start a container with detached mode and port
|
||||
docker run -d -p 3000:3000 image-name
|
||||
|
||||
# Open an interactive shell inside a running container
|
||||
docker exec -it CONTAINER_ID /bin/bash
|
||||
```
|
||||
|
||||
You can assign names to containers using `--name`.
|
||||
|
||||
---
|
||||
|
||||
## Docker Networking
|
||||
|
||||
### Concept:
|
||||
|
||||
Containers can communicate with each other over a virtual network.
|
||||
|
||||
```bash
|
||||
# List networks
|
||||
docker network ls
|
||||
|
||||
# Create a new network
|
||||
docker network create my-network
|
||||
|
||||
# Run container in a network
|
||||
docker run --net my-network ...
|
||||
```
|
||||
|
||||
Example: MongoDB + Mongo Express on same network can communicate via service name.
|
||||
|
||||
---
|
||||
|
||||
## Docker WSL2 Error (Windows)
|
||||
|
||||
### Issue:
|
||||
|
||||
```
|
||||
Failed to configure network (networkingMode Nat)...
|
||||
```
|
||||
|
||||
### Fix:
|
||||
|
||||
Create or edit the file at:
|
||||
|
||||
```
|
||||
C:\Users\LENOVO\.wslconfig
|
||||
```
|
||||
|
||||
Add:
|
||||
|
||||
```
|
||||
[wsl2]
|
||||
networkingMode=None
|
||||
```
|
||||
|
||||
Then restart Docker Desktop.
|
||||
|
||||
---
|
||||
|
||||
## Docker Compose
|
||||
|
||||
Docker Compose lets you define and run multi-container apps using YAML.
|
||||
|
||||
### Example:
|
||||
|
||||
```yaml
|
||||
version: '3'
|
||||
services:
|
||||
app:
|
||||
image: my-app
|
||||
ports:
|
||||
- "3000:3000"
|
||||
environment:
|
||||
- NODE_ENV=production
|
||||
|
||||
mongodb:
|
||||
image: mongo
|
||||
ports:
|
||||
- "27017:27017"
|
||||
environment:
|
||||
- MONGO_INITDB_ROOT_USERNAME=admin
|
||||
- MONGO_INITDB_ROOT_PASSWORD=secret
|
||||
```
|
||||
|
||||
### Commands:
|
||||
|
||||
```bash
|
||||
docker-compose -f docker-compose.yml up
|
||||
docker-compose down
|
||||
```
|
||||
|
||||
- All services run in the same default Docker network.
|
||||
- Indentation in YAML is **critical**.
|
||||
|
||||
---
|
||||
|
||||
## `Dockerfile`
|
||||
|
||||
A `Dockerfile` is a script used to build Docker images.
|
||||
|
||||
### Example:
|
||||
|
||||
```Dockerfile
|
||||
FROM node:18
|
||||
|
||||
ENV NODE_ENV=production
|
||||
|
||||
# Inside container
|
||||
RUN mkdir -p /home/app
|
||||
|
||||
# Copy from host into container
|
||||
COPY ./home/app /home/app
|
||||
|
||||
WORKDIR /home/app
|
||||
|
||||
CMD ["node", "server.js"]
|
||||
```
|
||||
|
||||
Build with:
|
||||
|
||||
```bash
|
||||
docker build -t my-node-app .
|
||||
```
|
||||
|
||||
Then run it with:
|
||||
|
||||
```bash
|
||||
docker run -p 3000:3000 my-node-app
|
||||
```
|
||||
|
||||
@@ -0,0 +1,78 @@
|
||||
ASP.NET CORE Setup ->
|
||||
1. Make sure everything works and builds successfully
|
||||
2. SetUp :
|
||||
1. add env files to git ignore
|
||||
2. Create env file
|
||||
3. install DotNetEnv
|
||||
4. Add
|
||||
```
|
||||
DotNetEnv.Env.Load();
|
||||
```
|
||||
5. Update stuff:
|
||||
```
|
||||
var connectionString = Environment.GetEnvironmentVariable("CONNECTION_STRING");
|
||||
...
|
||||
builder.Services.Configure<JwtSettings>(options =>
|
||||
{
|
||||
options.Key = Environment.GetEnvironmentVariable("JWT_KEY");
|
||||
options.Issuer = Environment.GetEnvironmentVariable("JWT_ISSUER");
|
||||
options.Audience = Environment.GetEnvironmentVariable("JWT_AUDIENCE");
|
||||
options.ExpiryMinutes = int.Parse(Environment.GetEnvironmentVariable("JWT_EXPIRY_MINUTES") ?? "60");
|
||||
});
|
||||
|
||||
JwtSettings jwtSettings = new JwtSettings
|
||||
{
|
||||
Key = Environment.GetEnvironmentVariable("JWT_KEY"),
|
||||
Issuer = Environment.GetEnvironmentVariable("JWT_ISSUER"),
|
||||
Audience = Environment.GetEnvironmentVariable("JWT_AUDIENCE"),
|
||||
ExpiryMinutes = int.Parse(Environment.GetEnvironmentVariable("JWT_EXPIRY_MINUTES") ?? "60"),
|
||||
};
|
||||
|
||||
|
||||
var corsOrigin = Environment.GetEnvironmentVariable("CORS_ORIGIN");
|
||||
builder.Services.AddCors(options =>
|
||||
{
|
||||
options.AddPolicy("Frontend", policy =>
|
||||
{
|
||||
policy.WithOrigins(corsOrigin)
|
||||
.AllowAnyHeader()
|
||||
.AllowAnyMethod();
|
||||
});
|
||||
});
|
||||
```
|
||||
|
||||
```
|
||||
CORS_ORIGIN=http://localhost:5173
|
||||
CONNECTION_STRING=Server=SERVER;Database=DB;Trusted_Connection=True;TrustServerCertificate=True
|
||||
JWT_KEY=KEY
|
||||
JWT_ISSUER=ISSUER
|
||||
JWT_AUDIENCE=MyAppUsers
|
||||
JWT_EXPIRY_MINUTES=360
|
||||
```
|
||||
|
||||
6. Update README and tell about environment variables
|
||||
|
||||
|
||||
|
||||
---
|
||||
How is this dockerfile created?
|
||||
How is it updated? with push and stuff?
|
||||
What about CORS? how do I know which port?
|
||||
What about connection string? in appsettings, how is that gonna be set?
|
||||
How is it gonna be running all the time?
|
||||
|
||||
---
|
||||
How to have seed data?
|
||||
How is the database created?
|
||||
How is it updated?
|
||||
Do I upload a backup, script?
|
||||
Is it done with migrations?
|
||||
|
||||
---
|
||||
|
||||
What is the address in the frontend api is set?
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -0,0 +1,36 @@
|
||||
# Fix
|
||||
## Backend
|
||||
- [ ] Fix stuff that are not according to Clean Architecture
|
||||
- [ ] Use ENUMs and other things we have not used yet
|
||||
- [ ] Gender
|
||||
- [ ] TransactionType
|
||||
- [ ] TicketStatus
|
||||
- [ ] Role
|
||||
- [ ] LocationType
|
||||
- [ ] VehicleType
|
||||
- [ ] Do not accept data in Error and other stuff in Result
|
||||
- [ ] Using services in other services?
|
||||
- [ ] fix: make lookup tables(Gender, Role, LocationType, TransactionType, TicketStatus, VehicleType) id not not auto generated
|
||||
- [ ] validators
|
||||
- [ ] factories
|
||||
- [ ] dependency extension
|
||||
- [ ] locking ticketOrder create method
|
||||
- [ ] API Versioning
|
||||
## Frontend
|
||||
- [ ] Folder strcuture
|
||||
- [ ] Error Handling
|
||||
- [ ] UI design at some parts
|
||||
|
||||
# Seed Data
|
||||
|
||||
# Dockerizing and Deployment
|
||||
|
||||
# Reformatting and Completing Documents
|
||||
- [ ] Figure out a format
|
||||
- [ ] Review old docs
|
||||
- [ ] Complete new ones
|
||||
- [ ] Write Read ME for GitHub
|
||||
# Going Public
|
||||
- [ ] How much?
|
||||
- [ ] What to do?
|
||||
- [ ] What about other people wanting to join?
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Reference in New Issue
Block a user