refactor: clean project based session02 notes
This commit is contained in:
@@ -10,27 +10,20 @@ Cardinality in databases refers to the number of relationships between records i
|
||||
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.
|
||||
|
||||
|
||||
---
|
||||
|
||||
@@ -39,13 +32,9 @@ Cardinality in databases refers to the number of relationships between records i
|
||||
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.
|
||||
|
||||
|
||||
---
|
||||
|
||||
@@ -54,9 +43,157 @@ Cardinality can be further specified using **minimum and maximum** constraints:
|
||||
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
|
||||
|
||||
Would you like a more detailed example or SQL implementation? 🚀
|
||||
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.
|
||||
|
||||
---
|
||||
|
||||
|
||||
@@ -1,33 +1,25 @@
|
||||
|
||||
# 1. Configurations
|
||||
## Preparation
|
||||
- [ ] Read the documentation:
|
||||
https://learn.microsoft.com/en-us/ef/core/modeling/
|
||||
|
||||
## Branching
|
||||
# 🛠️ Task Checklist
|
||||
## 🚧 Branching (Configurations)
|
||||
- [ ] Create the feature/entity-configurations branch based on develop
|
||||
|
||||
## Where Should You Place Configuration Files?
|
||||
|
||||
✅ **Best Practice:** Place all configuration files in the **Infrastructure** layer.
|
||||
📂 Suggested Folder: `Infrastructure/Configurations`
|
||||
### **Reason:**
|
||||
|
||||
- The **Domain layer** should be **clean** (only entities, no database-related logic).
|
||||
- The **Infrastructure layer** handles **database interactions**, so configurations belong here.
|
||||
|
||||
|
||||
## 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]>`
|
||||
## Examples and Details
|
||||
|
||||
### 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
|
||||
|
||||
### **Use this as a reference:**
|
||||
https://github.com/MehrdadShirvani/AlibabaClone-Backend/tree/develop/AlibabaClone.Infrastructure/Configurations
|
||||
---
|
||||
|
||||
### **How to Define Keys (Primary Keys & Identity)**
|
||||
#### **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:
|
||||
|
||||
@@ -47,8 +39,7 @@ public class TicketConfiguration : IEntityTypeConfiguration<Ticket>
|
||||
> 🛑 **NOTE:** If you’re using a GUID as the ID, you might need `.ValueGeneratedNever()` instead.
|
||||
|
||||
---
|
||||
|
||||
### **How to Define Foreign Keys?**
|
||||
#### **How to Define Foreign Keys?**
|
||||
|
||||
Use `HasOne()` and `WithMany()` for **one-to-many** relationships.
|
||||
|
||||
@@ -70,11 +61,11 @@ public class TicketConfiguration : IEntityTypeConfiguration<Ticket>
|
||||
|
||||
---
|
||||
|
||||
### **How to Introduce Navigation Properties with Different Names?**
|
||||
#### **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)**
|
||||
#### **Example: Ticket has a Buyer (which is an Account)**
|
||||
|
||||
```csharp
|
||||
builder.HasOne(t => t.Buyer) // Navigation property (Ticket → Account)
|
||||
@@ -86,11 +77,11 @@ builder.HasOne(t => t.Buyer) // Navigation property (Ticket → Account)
|
||||
|
||||
---
|
||||
|
||||
### **How to Configure Column Types? (nvarchar, date, etc.)**
|
||||
#### **How to Configure Column Types? (nvarchar, date, etc.)**
|
||||
|
||||
You can **manually specify column types** using `.HasColumnType()`.
|
||||
|
||||
### **All Strings Should Be `nvarchar` with Specific Lengths**
|
||||
#### **All Strings Should Be `nvarchar` with Specific Lengths**
|
||||
|
||||
```csharp
|
||||
builder.Property(t => t.TicketNumber)
|
||||
@@ -99,22 +90,19 @@ builder.Property(t => t.TicketNumber)
|
||||
.HasColumnType("nvarchar(20)");
|
||||
```
|
||||
|
||||
### **Store Some DateTime Fields as SQL `DATE` Instead of `DATETIME2`**
|
||||
#### **Store Some DateTime Fields as SQL `DATE` Instead of `DATETIME2`**
|
||||
|
||||
```csharp
|
||||
builder.Property(t => t.PurchaseDate)
|
||||
.HasColumnType("date"); // Instead of default "datetime2"
|
||||
```
|
||||
|
||||
> 🚀 **Best Practice:** Always set **string lengths** to avoid `nvarchar(MAX)`, which hurts performance.
|
||||
|
||||
---
|
||||
|
||||
### **How to Define Constraints? (Not Null, Length, etc.)**
|
||||
#### **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**
|
||||
##### **Example: Ticket Number Must Be Unique & Required**
|
||||
|
||||
```csharp
|
||||
builder.Property(t => t.TicketNumber)
|
||||
@@ -127,104 +115,14 @@ builder.HasIndex(t => t.TicketNumber)
|
||||
|
||||
---
|
||||
|
||||
### **Final Configuration File Example (TicketConfiguration.cs)**
|
||||
|
||||
Here’s a **complete** example of a configuration file:
|
||||
|
||||
```csharp
|
||||
public class TicketConfiguration : IEntityTypeConfiguration<Ticket>
|
||||
{
|
||||
public void Configure(EntityTypeBuilder<Ticket> builder)
|
||||
{
|
||||
builder.HasKey(t => t.Id);
|
||||
|
||||
builder.Property(t => t.TransportationId)
|
||||
.IsRequired();
|
||||
|
||||
builder.Property(t => t.SeatId)
|
||||
.IsRequired();
|
||||
|
||||
builder.Property(t => t.BuyerId)
|
||||
.IsRequired();
|
||||
|
||||
builder.Property(t => t.TravelerId)
|
||||
.IsRequired();
|
||||
|
||||
builder.Property(t => t.CreatedAt)
|
||||
.IsRequired();
|
||||
|
||||
builder.Property(t => t.CompanionId)
|
||||
.IsRequired(false);
|
||||
|
||||
builder.Property(t => t.TicketStatusId)
|
||||
.IsRequired();
|
||||
|
||||
builder.Property(t => t.SerialNumber)
|
||||
.IsRequired()
|
||||
.HasMaxLength(50)
|
||||
.IsUnicode(false);
|
||||
|
||||
builder.Property(t => t.Description)
|
||||
.HasMaxLength(200)
|
||||
.IsUnicode(false);
|
||||
|
||||
// Relationships
|
||||
builder.HasOne(t => t.Transportation)
|
||||
.WithMany(t => t.Tickets)
|
||||
.HasForeignKey(t => t.TransportationId)
|
||||
.OnDelete(DeleteBehavior.Restrict);
|
||||
|
||||
builder.HasOne(t => t.Seat)
|
||||
.WithMany(s => s.Tickets)
|
||||
.HasForeignKey(t => t.SeatId)
|
||||
.OnDelete(DeleteBehavior.Restrict);
|
||||
|
||||
builder.HasOne(t => t.Buyer)
|
||||
.WithMany(a => a.BoughtTickets)
|
||||
.HasForeignKey(t => t.BuyerId)
|
||||
.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);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### **Summary & Best Practices**
|
||||
|
||||
✅ **Store Configuration Files in:** `Infrastructure/Configurations`
|
||||
✅ **Define Foreign Keys:** Use `HasOne()` and `WithMany()`
|
||||
✅ **Explicitly Define Navigation Properties** if the names differ
|
||||
✅ **Column Types:** Use `.HasColumnType()` for `nvarchar`, `date`, etc.
|
||||
✅ **Constraints:** Use `.IsRequired()`, `.HasMaxLength()`, `.IsUnique()`
|
||||
|
||||
---
|
||||
|
||||
|
||||
|
||||
### **1. Join Tables with Multiple IDs**
|
||||
|
||||
#### **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.
|
||||
|
||||
#### **Example of a Join Table**
|
||||
|
||||
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**
|
||||
##### **Entities**
|
||||
|
||||
```csharp
|
||||
public class Student : Entity<long>
|
||||
@@ -249,7 +147,7 @@ public class StudentCourse
|
||||
}
|
||||
```
|
||||
|
||||
#### **Configuration for Join Table**
|
||||
##### **Configuration for Join Table**
|
||||
|
||||
You would configure the join table using the Fluent API:
|
||||
|
||||
@@ -273,139 +171,157 @@ public class StudentCourseConfiguration : IEntityTypeConfiguration<StudentCourse
|
||||
}
|
||||
```
|
||||
|
||||
#### **Key Points for Join Tables**
|
||||
### Final Configuration File Example (`TicketConfiguration.cs`)
|
||||
|
||||
- **Composite Primary Key**: The join table uses a composite key made up of both foreign keys.
|
||||
- **Navigation Properties**: This enables navigation from `Student` to `Course` and vice versa.
|
||||
|
||||
---
|
||||
|
||||
### **2. 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**
|
||||
Here’s an example of a configuration file:
|
||||
|
||||
```csharp
|
||||
public class SomeEntity
|
||||
public class TicketConfiguration : IEntityTypeConfiguration<Ticket>
|
||||
{
|
||||
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)
|
||||
public void Configure(EntityTypeBuilder<Ticket> builder)
|
||||
{
|
||||
modelBuilder.Entity<SomeEntity>(builder =>
|
||||
{
|
||||
builder.HasKey(e => e.Id);
|
||||
builder.Property(e => e.Id)
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasDefaultValueSql("NEWSEQUENTIALID()");
|
||||
});
|
||||
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
|
||||
## 🚧 Merge
|
||||
- [ ] Create a PR and merge the current branch with develop
|
||||
|
||||
# 2. Application DBContext and ConnectionString Configurations
|
||||
## 🚧 Branching (Application `DBContext` and Connection String Configurations)
|
||||
|
||||
## Preparation
|
||||
- [ ] Read the documentation:
|
||||
https://learn.microsoft.com/en-us/ef/core/modeling/
|
||||
- [ ] 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
|
||||
- [ ] Location: Infrastructure/ApplicationDbContext.cs
|
||||
- [ ] Inherits DbContext
|
||||
- [ ] Create the constructor like the code below
|
||||
- [ ] Add the Needed DbSets
|
||||
- [ ] Add the necessary DbSets
|
||||
- [ ] Override `OnModelCreating` and `OnConfiguring` as below
|
||||
|
||||
```csharp
|
||||
using AlibabaClone.Domain.Aggregates.AccountAggregates;
|
||||
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
|
||||
namespace AlibabaClone.Infrastructure
|
||||
public class ApplicationDBContext : DbContext
|
||||
{
|
||||
public class ApplicationDBContext : DbContext
|
||||
public ApplicationDBContext(DbContextOptions<ApplicationDBContext> options) : base(options)
|
||||
{
|
||||
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<Person> People { get; set; }
|
||||
public DbSet<Role> Roles { get; set; }
|
||||
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; }
|
||||
|
||||
//... Add other DbSets as well
|
||||
public DbSet<Company> Companies { get; set; }
|
||||
|
||||
protected override void OnModelCreating(ModelBuilder modelBuilder)
|
||||
{
|
||||
modelBuilder.ApplyConfigurationsFromAssembly(typeof(ApplicationDBContext).Assembly);
|
||||
base.OnModelCreating(modelBuilder);
|
||||
}
|
||||
public DbSet<City> Cities { get; set; }
|
||||
public DbSet<Location> Locations { get; set; }
|
||||
public DbSet<LocationType> LocationTypes{ get; set; }
|
||||
|
||||
protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
|
||||
{
|
||||
optionsBuilder.UseLazyLoadingProxies();
|
||||
}
|
||||
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. (They might have a type or something... search the web to make sure)
|
||||
|
||||
- [ ] Adjust the ConnectionString to meet your needs
|
||||
### 📌 **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;Trusted_Connection=True;TrustServerCertificate=True"
|
||||
"DefaultConnection": "Server=YOUR_SERVER;Database=YourDb;User Id=USERNAME;Password=PASSWORD;TrustServerCertificate=True;"
|
||||
}
|
||||
}
|
||||
```
|
||||
@@ -413,11 +329,11 @@ namespace AlibabaClone.Infrastructure
|
||||
```json
|
||||
{
|
||||
"ConnectionStrings": {
|
||||
"DefaultConnection": "Server=YOUR_SERVER;Database=YourDb;Integrated Security=TRUE;Trusted_Connection=True;TrustServerCertificate=True"
|
||||
}
|
||||
"DefaultConnection": "Server=YOUR_SERVER;Database=YourDb;Integrated Security=True;TrustServerCertificate=True;"
|
||||
}
|
||||
}
|
||||
```
|
||||
- [ ] Put this `appsettings.json` in **`gitignore`** if you think is needed
|
||||
- [ ] Put this `appsettings.json` in **`gitignore`**
|
||||
### **Registering EF Core in `Program.cs`**
|
||||
- [ ] Modify `Program.cs`
|
||||
|
||||
@@ -433,12 +349,11 @@ builder.Services.AddDbContext<AppDbContext>(options =>
|
||||
var app = builder.Build();
|
||||
app.Run();
|
||||
```
|
||||
## Merge
|
||||
|
||||
## 🚧 Merge
|
||||
- [ ] Create a PR and merge the current branch with develop
|
||||
---
|
||||
|
||||
# Migrations and Database Setup
|
||||
## Branching
|
||||
## 🚧Branching (Migrations and Database Setup)
|
||||
- [ ] Create the feature/migrations branch based on develop
|
||||
|
||||
## Using Package Manager Console
|
||||
@@ -452,7 +367,14 @@ Add-Migration InitialCreate
|
||||
```
|
||||
Update-Database
|
||||
```
|
||||
## Merge
|
||||
## 🚧Merge
|
||||
- [ ] Create a PR and merge the current branch with develop
|
||||
---
|
||||
|
||||
# 🧠 Hints & Notes
|
||||
# 🙌 Acknowledgements
|
||||
|
||||
- ChatGPT for snippet refinement and explanations
|
||||
# 🔍 References
|
||||
[[Session02 Additional Info]]
|
||||
|
||||
|
||||
|
||||
@@ -1,456 +0,0 @@
|
||||
|
||||
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.
|
||||
|
||||
Would you like guidance on handling **soft deletes** or additional fields in the join table? 😊
|
||||
|
||||
If you haven’t added a **Roles** navigation property in `Account`, then EF Core doesn’t automatically handle the join table for you. In that case, you **must** manually define and configure it.
|
||||
|
||||
---
|
||||
|
||||
## **Step 1: Update the Account and Role Entities**
|
||||
|
||||
You should define **navigation properties** in both `Account` and `Role` so EF Core understands the many-to-many relationship.
|
||||
|
||||
### **Modify Account Entity**
|
||||
|
||||
```csharp
|
||||
public class Account
|
||||
{
|
||||
public int Id { get; set; }
|
||||
public string Username { get; set; }
|
||||
|
||||
// Navigation Property
|
||||
public ICollection<Role> Roles { get; set; } = new List<Role>();
|
||||
}
|
||||
```
|
||||
|
||||
### **Modify Role Entity**
|
||||
|
||||
```csharp
|
||||
public class Role
|
||||
{
|
||||
public int Id { get; set; }
|
||||
public string Name { get; set; }
|
||||
|
||||
// Navigation Property
|
||||
public ICollection<Account> Accounts { get; set; } = new List<Account>();
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## **Step 2: Create the Join Entity (AccountRole)**
|
||||
|
||||
Since you manually manage the relationship, you **must** define an explicit join table.
|
||||
|
||||
```csharp
|
||||
public class AccountRole
|
||||
{
|
||||
public int AccountId { get; set; }
|
||||
public int RoleId { get; set; }
|
||||
|
||||
public Account Account { get; set; }
|
||||
public Role Role { get; set; }
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## **Step 3: Configure Many-to-Many Relationship in Fluent API**
|
||||
|
||||
Modify your `AccountConfiguration` (or wherever you configure EF Core).
|
||||
|
||||
```csharp
|
||||
public class AccountConfiguration : IEntityTypeConfiguration<Account>
|
||||
{
|
||||
public void Configure(EntityTypeBuilder<Account> builder)
|
||||
{
|
||||
builder.HasKey(a => a.Id);
|
||||
|
||||
builder.HasMany(a => a.Roles) // Define many-to-many
|
||||
.WithMany(r => r.Accounts) // Inverse property
|
||||
.UsingEntity<AccountRole>( // Define the join entity
|
||||
j => j.HasOne(ar => ar.Role)
|
||||
.WithMany()
|
||||
.HasForeignKey(ar => ar.RoleId),
|
||||
j => j.HasOne(ar => ar.Account)
|
||||
.WithMany()
|
||||
.HasForeignKey(ar => ar.AccountId),
|
||||
j =>
|
||||
{
|
||||
j.HasKey(ar => new { ar.AccountId, ar.RoleId }); // Composite PK
|
||||
});
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## **Now, EF Core Automatically Manages the Join Table**
|
||||
|
||||
Now, you **don’t** need to manually insert into `AccountRole`.
|
||||
Instead, just modify the `Roles` collection.
|
||||
|
||||
### **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 inserts into AccountRole
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## **When Should You Use This Approach?**
|
||||
|
||||
✅ If you want EF Core to manage `AccountRole` automatically.
|
||||
✅ If `AccountRole` doesn’t have extra fields.
|
||||
|
||||
❌ If `AccountRole` has extra properties (e.g., `DateAssigned`), then you **must** treat it as a full entity with its own repository.
|
||||
|
||||
Would you like me to explain how to handle additional fields in `AccountRole`? 😊
|
||||
|
||||
Since you already have a **separate configuration file for the join table**, you do **not** need to redefine the relationship in `AccountConfiguration` and `RoleConfiguration`.
|
||||
|
||||
---
|
||||
|
||||
## **How to Structure Configuration Files Properly?**
|
||||
|
||||
Since `AccountRole` is explicitly defined, it’s best to configure the relationship in its **own configuration class** and avoid repeating it in `AccountConfiguration` and `RoleConfiguration`.
|
||||
|
||||
### **1️⃣ Keep Relationship Configuration Only in `AccountRoleConfiguration`**
|
||||
|
||||
Your `AccountRoleConfiguration` should handle **the composite key** and **foreign keys**.
|
||||
|
||||
```csharp
|
||||
public class AccountRoleConfiguration : IEntityTypeConfiguration<AccountRole>
|
||||
{
|
||||
public void Configure(EntityTypeBuilder<AccountRole> builder)
|
||||
{
|
||||
builder.HasKey(ar => new { ar.AccountId, ar.RoleId }); // Composite PK
|
||||
|
||||
builder.HasOne(ar => ar.Account)
|
||||
.WithMany() // No need for a navigation collection in Account
|
||||
.HasForeignKey(ar => ar.AccountId)
|
||||
.OnDelete(DeleteBehavior.Cascade); // Optional: Cascade delete
|
||||
|
||||
builder.HasOne(ar => ar.Role)
|
||||
.WithMany() // No need for a navigation collection in Role
|
||||
.HasForeignKey(ar => ar.RoleId)
|
||||
.OnDelete(DeleteBehavior.Cascade);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### **2️⃣ Keep `AccountConfiguration` and `RoleConfiguration` Simple**
|
||||
|
||||
Since `AccountRoleConfiguration` already defines the relationship, you should **not repeat it** in `AccountConfiguration` or `RoleConfiguration`.
|
||||
|
||||
#### ✅ **Minimal `AccountConfiguration`**
|
||||
|
||||
```csharp
|
||||
public class AccountConfiguration : IEntityTypeConfiguration<Account>
|
||||
{
|
||||
public void Configure(EntityTypeBuilder<Account> builder)
|
||||
{
|
||||
builder.HasKey(a => a.Id);
|
||||
builder.Property(a => a.Username).IsRequired().HasMaxLength(100);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
#### ✅ **Minimal `RoleConfiguration`**
|
||||
|
||||
```csharp
|
||||
public class RoleConfiguration : IEntityTypeConfiguration<Role>
|
||||
{
|
||||
public void Configure(EntityTypeBuilder<Role> builder)
|
||||
{
|
||||
builder.HasKey(r => r.Id);
|
||||
builder.Property(r => r.Name).IsRequired().HasMaxLength(50);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## **Why Should You Keep It This Way?**
|
||||
|
||||
✅ **Separation of concerns** → Each entity’s configuration is responsible only for its properties.
|
||||
✅ **Avoid redundancy** → The relationship is defined once in `AccountRoleConfiguration`.
|
||||
✅ **Easier maintenance** → You only modify the join table configuration in one place.
|
||||
|
||||
---
|
||||
|
||||
## **Final Answer:**
|
||||
|
||||
✔ **Keep relationship logic only in `AccountRoleConfiguration`.**
|
||||
✔ **Do not repeat it in `AccountConfiguration` or `RoleConfiguration`.**
|
||||
|
||||
This setup ensures **clean, maintainable EF Core configurations**. 🚀
|
||||
Do you need further clarifications on cascading deletes or performance considerations? 😊
|
||||
|
||||
You're right! Since you're using an explicit **join entity (`AccountRole`)**, you need to ensure that `Account.Roles` and `Role.Accounts` properly map through `AccountRole`.
|
||||
|
||||
---
|
||||
|
||||
## **How to Fix the Configuration?**
|
||||
|
||||
Since you have `AccountRole` explicitly defined, you must **properly configure the navigation properties in `Account` and `Role`** and **adjust the Fluent API configuration** accordingly.
|
||||
|
||||
---
|
||||
|
||||
### **Step 1: Modify `Account` and `Role` Entities**
|
||||
|
||||
You need to **use a navigation property that maps through the join entity (`AccountRole`)**.
|
||||
|
||||
#### ✅ **Modify `Account`**
|
||||
|
||||
```csharp
|
||||
public class Account
|
||||
{
|
||||
public int Id { get; set; }
|
||||
public string Username { get; set; }
|
||||
|
||||
// Many-to-Many Relationship via AccountRole
|
||||
public ICollection<AccountRole> AccountRoles { get; set; } = new List<AccountRole>();
|
||||
|
||||
// Shortcut for accessing roles
|
||||
public ICollection<Role> Roles => AccountRoles.Select(ar => ar.Role).ToList();
|
||||
}
|
||||
```
|
||||
|
||||
#### ✅ **Modify `Role`**
|
||||
|
||||
```csharp
|
||||
public class Role
|
||||
{
|
||||
public int Id { get; set; }
|
||||
public string Name { get; set; }
|
||||
|
||||
// Many-to-Many Relationship via AccountRole
|
||||
public ICollection<AccountRole> AccountRoles { get; set; } = new List<AccountRole>();
|
||||
|
||||
// Shortcut for accessing accounts
|
||||
public ICollection<Account> Accounts => AccountRoles.Select(ar => ar.Account).ToList();
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### **Step 2: Modify `AccountRoleConfiguration`**
|
||||
|
||||
Since the relationship is managed through `AccountRole`, **you must explicitly configure it** in its configuration.
|
||||
|
||||
```csharp
|
||||
public class AccountRoleConfiguration : IEntityTypeConfiguration<AccountRole>
|
||||
{
|
||||
public void Configure(EntityTypeBuilder<AccountRole> builder)
|
||||
{
|
||||
builder.HasKey(ar => new { ar.AccountId, ar.RoleId }); // Composite PK
|
||||
|
||||
builder.HasOne(ar => ar.Account)
|
||||
.WithMany(a => a.AccountRoles) // Connect to AccountRoles in Account
|
||||
.HasForeignKey(ar => ar.AccountId)
|
||||
.OnDelete(DeleteBehavior.Cascade);
|
||||
|
||||
builder.HasOne(ar => ar.Role)
|
||||
.WithMany(r => r.AccountRoles) // Connect to AccountRoles in Role
|
||||
.HasForeignKey(ar => ar.RoleId)
|
||||
.OnDelete(DeleteBehavior.Cascade);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### **Step 3: Keep `AccountConfiguration` and `RoleConfiguration` Simple**
|
||||
|
||||
Since relationships are handled in `AccountRoleConfiguration`, **you do not need to redefine them here**.
|
||||
|
||||
#### ✅ **`AccountConfiguration`**
|
||||
|
||||
```csharp
|
||||
public class AccountConfiguration : IEntityTypeConfiguration<Account>
|
||||
{
|
||||
public void Configure(EntityTypeBuilder<Account> builder)
|
||||
{
|
||||
builder.HasKey(a => a.Id);
|
||||
builder.Property(a => a.Username).IsRequired().HasMaxLength(100);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
#### ✅ **`RoleConfiguration`**
|
||||
|
||||
```csharp
|
||||
public class RoleConfiguration : IEntityTypeConfiguration<Role>
|
||||
{
|
||||
public void Configure(EntityTypeBuilder<Role> builder)
|
||||
{
|
||||
builder.HasKey(r => r.Id);
|
||||
builder.Property(r => r.Name).IsRequired().HasMaxLength(50);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## **Step 4: How to Use This in Code?**
|
||||
|
||||
Since we explicitly defined the relationship using `AccountRole`, you need to **add/remove relationships using the `AccountRoles` collection** instead of `Roles` directly.
|
||||
|
||||
### **Adding a Role to an Account**
|
||||
|
||||
```csharp
|
||||
public async Task AssignRoleToAccountAsync(int accountId, int roleId)
|
||||
{
|
||||
var account = await _context.Accounts
|
||||
.Include(a => a.AccountRoles) // Load join table
|
||||
.FirstOrDefaultAsync(a => a.Id == accountId);
|
||||
|
||||
var role = await _context.Roles.FindAsync(roleId);
|
||||
|
||||
if (account != null && role != null)
|
||||
{
|
||||
account.AccountRoles.Add(new AccountRole { AccountId = accountId, RoleId = roleId });
|
||||
await _context.SaveChangesAsync();
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### **Removing a Role from an Account**
|
||||
|
||||
```csharp
|
||||
public async Task RemoveRoleFromAccountAsync(int accountId, int roleId)
|
||||
{
|
||||
var accountRole = await _context.Set<AccountRole>()
|
||||
.FirstOrDefaultAsync(ar => ar.AccountId == accountId && ar.RoleId == roleId);
|
||||
|
||||
if (accountRole != null)
|
||||
{
|
||||
_context.Set<AccountRole>().Remove(accountRole);
|
||||
await _context.SaveChangesAsync();
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## **Why This Approach?**
|
||||
|
||||
✅ **EF Core Works Correctly** → `AccountRoles` is explicitly mapped, so EF Core understands how to handle the many-to-many relationship.
|
||||
✅ **No Extra Queries Needed** → Instead of adding/removing roles directly from `Roles`, you modify `AccountRoles`, preventing unnecessary tracking issues.
|
||||
✅ **More Flexibility** → If you ever need to store extra properties (e.g., `DateAssigned`), you can add them to `AccountRole`.
|
||||
|
||||
Would you like to extend this solution to include additional properties in `AccountRole`? 😊
|
||||
Reference in New Issue
Block a user