Compare commits

..

4 Commits

Author SHA1 Message Date
2be5694a6f 整合 Entity Framework Core 與 Pomelo MySQL
此次變更新增必要的命名空間,設置資料庫連接字串,並透過 `AddDbContext` 註冊 `ApplicationDbContext`。配置 MySQL 伺服器版本及相關選項,包括字符集和重試機制。根據開發環境啟用詳細錯誤和敏感數據日誌記錄,並添加控制台日誌記錄的過濾器。最後,應用程式被構建並運行。
2025-09-26 16:40:14 +08:00
196a07ef9a 新增 ApplicationDbContext 並實現時間戳功能
新增 `ApplicationDbContext` 類別,繼承自 `DbContext`,並定義 `Departments`、`Roles` 和 `Accounts` 的 `DbSet` 屬性以管理實體。在 `OnModelCreating` 方法中應用相應的實體配置。覆寫 `SaveChanges` 和 `SaveChangesAsync` 方法,並新增 `ApplyTimestamps` 方法以自動設置實體的 `CreatedAt` 和 `UpdatedAt` 時間戳。
2025-09-26 16:36:56 +08:00
194609720a 新增實體配置以定義資料庫結構
這些變更為 `Account`、`Department` 和 `Role` 實體配置資料庫結構。每個配置類別實作了 `IEntityTypeConfiguration<T>` 介面,並在 `Configure` 方法中定義了主鍵、欄位名稱、必填性和最大長度等屬性。此外,為 `Account` 實體設置了索引和外鍵關聯,以確保資料的完整性和唯一性。
2025-09-26 16:31:56 +08:00
171a60089b 新增 Account、Department 和 Role 類別 2025-09-26 16:26:58 +08:00
8 changed files with 298 additions and 0 deletions

63
Configurations.cs Normal file
View File

@ -0,0 +1,63 @@
using System;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.EntityFrameworkCore;
using Models.Entities;
using Configurations;
public class ApplicationDbContext : DbContext
{
public ApplicationDbContext(DbContextOptions<ApplicationDbContext> options)
: base(options)
{
}
public DbSet<Department> Departments => Set<Department>();
public DbSet<Role> Roles => Set<Role>();
public DbSet<Account> Accounts => Set<Account>();
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
base.OnModelCreating(modelBuilder);
// ®M¥Î©Ò¦³ EntityTypeConfiguration
modelBuilder.ApplyConfiguration(new DepartmentConfiguration());
modelBuilder.ApplyConfiguration(new RoleConfiguration());
modelBuilder.ApplyConfiguration(new AccountConfiguration());
}
public override int SaveChanges()
{
ApplyTimestamps();
return base.SaveChanges();
}
public override Task<int> SaveChangesAsync(CancellationToken cancellationToken = default)
{
ApplyTimestamps();
return base.SaveChangesAsync(cancellationToken);
}
private void ApplyTimestamps()
{
var utcNow = DateTime.UtcNow;
foreach (var entry in ChangeTracker.Entries<BaseEntity>())
{
switch (entry.State)
{
case EntityState.Added:
entry.Entity.CreatedAt = utcNow;
entry.Entity.UpdatedAt = null;
break;
case EntityState.Modified:
// Á×§K­×§ï CreatedAt
entry.Property(e => e.CreatedAt).IsModified = false;
entry.Entity.UpdatedAt = utcNow;
break;
}
}
}
}

View File

@ -0,0 +1,89 @@
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Metadata.Builders;
using Models.Entities;
namespace Configurations
{
public class AccountConfiguration : IEntityTypeConfiguration<Account>
{
public void Configure(EntityTypeBuilder<Account> builder)
{
builder.ToTable("accounts");
builder.HasKey(a => a.AccountId);
builder.Property(a => a.AccountId)
.HasColumnName("account_id");
builder.Property(a => a.Name)
.HasColumnName("name")
.IsRequired()
.HasMaxLength(100);
builder.Property(a => a.Username)
.HasColumnName("username")
.IsRequired()
.HasMaxLength(50);
builder.Property(a => a.Password)
.HasColumnName("password")
.IsRequired()
.HasMaxLength(256);
builder.Property(a => a.Email)
.HasColumnName("email")
.IsRequired()
.HasMaxLength(254);
builder.Property(a => a.DepartmentId)
.HasColumnName("department_id")
.IsRequired();
builder.Property(a => a.RoleId)
.HasColumnName("role_id")
.IsRequired();
builder.Property(a => a.Status)
.HasColumnName("status")
.HasConversion<string>()
.IsRequired()
.HasMaxLength(20);
builder.Property(a => a.CreatedAt)
.HasColumnName("created_at")
.IsRequired();
builder.Property(a => a.UpdatedAt)
.HasColumnName("updated_at");
builder.Property(a => a.ModifiedBy)
.HasColumnName("modified_by");
// 唯一索引
builder.HasIndex(a => a.Username)
.IsUnique()
.HasDatabaseName("UX_accounts_username");
builder.HasIndex(a => a.Email)
.IsUnique()
.HasDatabaseName("UX_accounts_email");
// 關聯與刪除行為
builder.HasOne(a => a.Department)
.WithMany(d => d.Accounts)
.HasForeignKey(a => a.DepartmentId)
.OnDelete(DeleteBehavior.Restrict);
builder.HasOne(a => a.Role)
.WithMany(r => r.Accounts)
.HasForeignKey(a => a.RoleId)
.OnDelete(DeleteBehavior.Restrict);
// 自參照外鍵modified_by -> accounts.account_id (SET NULL)
builder.HasOne<Account>()
.WithMany()
.HasForeignKey(a => a.ModifiedBy)
.OnDelete(DeleteBehavior.SetNull);
}
}
}

View File

@ -0,0 +1,24 @@
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Metadata.Builders;
using Models.Entities;
namespace Configurations
{
public class DepartmentConfiguration : IEntityTypeConfiguration<Department>
{
public void Configure(EntityTypeBuilder<Department> builder)
{
builder.ToTable("departments");
builder.HasKey(d => d.DepartmentId);
builder.Property(d => d.DepartmentId)
.HasColumnName("department_id");
builder.Property(d => d.DepartmentName)
.HasColumnName("department_name")
.IsRequired()
.HasMaxLength(100);
}
}
}

View File

@ -0,0 +1,24 @@
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Metadata.Builders;
using Models.Entities;
namespace Configurations
{
public class RoleConfiguration : IEntityTypeConfiguration<Role>
{
public void Configure(EntityTypeBuilder<Role> builder)
{
builder.ToTable("roles");
builder.HasKey(r => r.RoleId);
builder.Property(r => r.RoleId)
.HasColumnName("role_id");
builder.Property(r => r.RoleName)
.HasColumnName("role_name")
.IsRequired()
.HasMaxLength(50);
}
}
}

View File

@ -0,0 +1,28 @@
using System;
namespace Models.Entities
{
public class Account : BaseEntity
{
public int AccountId { get; set; }
public string Name { get; set; } = string.Empty;
public string Username { get; set; } = string.Empty;
public string Password { get; set; } = string.Empty;
public string Email { get; set; } = string.Empty;
public int DepartmentId { get; set; }
public int RoleId { get; set; }
public AccountStatus Status { get; set; }
public virtual Department? Department { get; set; }
public virtual Role? Role { get; set; }
}
public enum AccountStatus
{
Enabled,
Disabled,
Unverified
}
}

View File

@ -0,0 +1,12 @@
using System.Collections.Generic;
namespace Models.Entities
{
public class Department
{
public int DepartmentId { get; set; }
public string DepartmentName { get; set; } = string.Empty;
public virtual ICollection<Account> Accounts { get; set; } = new HashSet<Account>();
}
}

12
Models/Entities/Role.cs Normal file
View File

@ -0,0 +1,12 @@
using System.Collections.Generic;
namespace Models.Entities
{
public class Role
{
public int RoleId { get; set; }
public string RoleName { get; set; } = string.Empty;
public virtual ICollection<Account> Accounts { get; set; } = new HashSet<Account>();
}
}

46
Program.cs Normal file
View File

@ -0,0 +1,46 @@
using System;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Diagnostics;
using Microsoft.Extensions.Logging;
using Pomelo.EntityFrameworkCore.MySql.Infrastructure;
var builder = WebApplication.CreateBuilder(args);
// 讀取連線字串
var connectionString = builder.Configuration.GetConnectionString("DefaultConnection")
?? throw new InvalidOperationException("Connection string 'DefaultConnection' not found.");
// EF Core + Pomelo MySQL 註冊
builder.Services.AddDbContext<ApplicationDbContext>(options =>
{
var serverVersion = new MariaDbServerVersion(new Version(10, 6, 0));
options.UseMySql(connectionString, serverVersion, mySqlOptions =>
{
// 字元集設定
mySqlOptions.CharSet(CharSet.Utf8Mb4);
mySqlOptions.CharSetBehavior(CharSetBehavior.AppendToAllColumns);
// 可選:重試策略
mySqlOptions.EnableRetryOnFailure(5, TimeSpan.FromSeconds(10), null);
});
// 開發環境顯示更詳細的資料庫日誌
if (builder.Environment.IsDevelopment())
{
options.EnableDetailedErrors();
options.EnableSensitiveDataLogging();
options.LogTo(Console.WriteLine, LogLevel.Information, DbContextLoggerOptions.DefaultWithLocalTime);
}
});
// Logging只在開發環境加強輸出 EF Core 訊息)
if (builder.Environment.IsDevelopment())
{
builder.Logging.AddConsole()
.AddFilter("Microsoft.EntityFrameworkCore.Database.Command", LogLevel.Information)
.AddFilter("Microsoft.EntityFrameworkCore.Infrastructure", LogLevel.Information);
}
var app = builder.Build();
app.Run();