From 348a29fb0074202bfbde580445656d43ccfae01b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=A4=9C=E9=99=A8?= <3402439200@qq.com> Date: Sun, 7 Jun 2026 16:11:57 +0000 Subject: [PATCH 1/5] =?UTF-8?q?=E6=96=B0=E5=BB=BA=20=E9=99=88=E6=96=B9?= =?UTF-8?q?=E6=97=AD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- "\351\231\210\346\226\271\346\227\255/.keep" | 0 1 file changed, 0 insertions(+), 0 deletions(-) create mode 100644 "\351\231\210\346\226\271\346\227\255/.keep" diff --git "a/\351\231\210\346\226\271\346\227\255/.keep" "b/\351\231\210\346\226\271\346\227\255/.keep" new file mode 100644 index 0000000..e69de29 -- Gitee From 4a373aab5e4370a34cfbeb50d902d8606b896004 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=A4=9C=E9=99=A8?= <3402439200@qq.com> Date: Sun, 7 Jun 2026 16:14:24 +0000 Subject: [PATCH 2/5] =?UTF-8?q?=E4=BD=9C=E4=B8=9A?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: 夜陨 <3402439200@qq.com> --- ...\273\272webAPI\345\221\275\344\273\244.md" | 52 ++++++++++++ ...0\216dotnet-ef\345\267\245\345\205\267.md" | 80 +++++++++++++++++++ ...74\350\210\252\345\261\236\346\200\247.md" | 74 +++++++++++++++++ ...36\346\200\247\345\233\236\351\241\276.md" | 61 ++++++++++++++ 4 files changed, 267 insertions(+) create mode 100644 "\351\231\210\346\226\271\346\227\255/20260601-\346\220\255\345\273\272webAPI\345\221\275\344\273\244.md" create mode 100644 "\351\231\210\346\226\271\346\227\255/20260603-\350\243\205NuGet\345\214\205\344\270\216dotnet-ef\345\267\245\345\205\267.md" create mode 100644 "\351\231\210\346\226\271\346\227\255/20260604-\345\256\236\344\275\223\345\273\272\346\250\241\344\270\216\345\257\274\350\210\252\345\261\236\346\200\247.md" create mode 100644 "\351\231\210\346\226\271\346\227\255/20260605-\344\270\200\345\257\271\345\244\232\345\257\274\350\210\252\345\261\236\346\200\247\345\233\236\351\241\276.md" diff --git "a/\351\231\210\346\226\271\346\227\255/20260601-\346\220\255\345\273\272webAPI\345\221\275\344\273\244.md" "b/\351\231\210\346\226\271\346\227\255/20260601-\346\220\255\345\273\272webAPI\345\221\275\344\273\244.md" new file mode 100644 index 0000000..f0b2ae8 --- /dev/null +++ "b/\351\231\210\346\226\271\346\227\255/20260601-\346\220\255\345\273\272webAPI\345\221\275\344\273\244.md" @@ -0,0 +1,52 @@ +## 环境搭建与项目创建 + +### 一、创建 Web API 项目 + +使用 `dotnet` CLI 创建一个基于 Controller 的 Web API 项目: + +```bash +dotnet new webapi -n MyShopApi --use-controllers +``` + +**参数说明:** + +| 参数 | 说明 | +| --- | --- | +| `webapi` | 项目模板名称 | +| `-n MyShopApi` | 项目名(决定生成的目录名和默认命名空间) | +| `--use-controllers` | 使用 Controller-based 风格(而不是 Minimal API) | + +### 二、Program.cs(启动文件) + +.NET 6+ 采用**最小宿主模型**,所有启动逻辑集中在 `Program.cs`: + +```csharp +var builder = WebApplication.CreateBuilder(args); + +// 1. 注册服务到 DI 容器 +builder.Services.AddControllers(); +builder.Services.AddEndpointsApiExplorer(); +builder.Services.AddSwaggerGen(); + +var app = builder.Build(); + +// 2. 配置 HTTP 请求管道(中间件) +if (app.Environment.IsDevelopment()) +{ + app.UseSwagger(); + app.UseSwaggerUI(); +} + +app.UseHttpsRedirection(); +app.UseAuthorization(); +app.MapControllers(); + +app.Run(); +``` + +**结构说明:** + +- **`WebApplication.CreateBuilder(args)`** — 构建宿主,加载配置(如 `appsettings.json`) +- **`builder.Services`** — 用于注册服务(依赖注入容器) +- **`builder.Build()`** — 构建出 `WebApplication` 实例 +- **中间件顺序**:从开发环境的 Swagger 开始,依次经过 HTTPS 重定向、授权,最后映射到控制器路由 diff --git "a/\351\231\210\346\226\271\346\227\255/20260603-\350\243\205NuGet\345\214\205\344\270\216dotnet-ef\345\267\245\345\205\267.md" "b/\351\231\210\346\226\271\346\227\255/20260603-\350\243\205NuGet\345\214\205\344\270\216dotnet-ef\345\267\245\345\205\267.md" new file mode 100644 index 0000000..358a586 --- /dev/null +++ "b/\351\231\210\346\226\271\346\227\255/20260603-\350\243\205NuGet\345\214\205\344\270\216dotnet-ef\345\267\245\345\205\267.md" @@ -0,0 +1,80 @@ +## 安装 NuGet 包与 dotnet-ef 工具 + +### 一、安装两个 NuGet 包 + +在项目目录下执行: + +```bash +dotnet add package Microsoft.EntityFrameworkCore.Sqlite +dotnet add package Microsoft.EntityFrameworkCore.Design +``` + +| 包 | 作用 | +| --- | --- | +| `Microsoft.EntityFrameworkCore.Sqlite` | SQLite 数据库 provider,让 EF Core 能操作 SQLite | +| `Microsoft.EntityFrameworkCore.Design` | `dotnet ef` 迁移命令的运行时依赖 | + +### 二、安装 dotnet-ef 全局工具 + +```bash +dotnet tool install --global dotnet-ef +``` + +### 三、配置连接字符串 + +在项目根目录下约定数据库文件命名为 `myshop.db`。在 `appsettings.json` 中添加: + +```json +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.AspNetCore": "Warning" + } + }, + "AllowedHosts": "*", + "ConnectionStrings": { + "Default": "Data Source=myshop.db" + } +} +``` + +> `ConnectionStrings:Default` 是一个 JSON 路径,后续通过 `builder.Configuration.GetConnectionString("Default")` 读取。冒号是 JSON 路径的层级分隔符。 + +### 四、编写 AppDbContext + +新建 `Data/AppDbContext.cs`: + +```csharp +using Microsoft.EntityFrameworkCore; +using MyShopApi.Models; + +namespace MyShopApi.Data; + +// DbContext 代表一个数据库会话,封装了连接、实体变更跟踪、SQL 生成等能力 +public class AppDbContext : DbContext +{ + // 通过构造函数注入 DbContextOptions(包含连接字符串等配置) + public AppDbContext(DbContextOptions options) : base(options) { } + + // DbSet 代表数据库中的一张表,后续 CRUD 都通过它操作 + public DbSet Categories => Set(); + public DbSet Products => Set(); +} +``` + +### 五、在 Program.cs 中注册 DbContext + +```csharp +using Microsoft.EntityFrameworkCore; +using MyShopApi.Data; + +var builder = WebApplication.CreateBuilder(args); + +// 读取连接字符串,为 AppDbContext 配置 SQLite provider +builder.Services.AddDbContext(options => + options.UseSqlite(builder.Configuration.GetConnectionString("Default"))); + +builder.Services.AddControllers(); +// ... 其他原有配置保持不变 +``` diff --git "a/\351\231\210\346\226\271\346\227\255/20260604-\345\256\236\344\275\223\345\273\272\346\250\241\344\270\216\345\257\274\350\210\252\345\261\236\346\200\247.md" "b/\351\231\210\346\226\271\346\227\255/20260604-\345\256\236\344\275\223\345\273\272\346\250\241\344\270\216\345\257\274\350\210\252\345\261\236\346\200\247.md" new file mode 100644 index 0000000..3f41881 --- /dev/null +++ "b/\351\231\210\346\226\271\346\227\255/20260604-\345\256\236\344\275\223\345\273\272\346\250\241\344\270\216\345\257\274\350\210\252\345\261\236\346\200\247.md" @@ -0,0 +1,74 @@ +## 实体建模与导航属性 + +### 一、实体 vs DTO + +在开始建模前,先厘清两个关键概念的区别: + +| 概念 | 作用 | 对应关系 | +| --- | --- | --- | +| **实体(Entity)** | 和数据库表结构一一对应,供 `DbSet` 使用 | 数据库层 | +| **DTO(Data Transfer Object)** | 与 HTTP 接口的请求/响应 JSON 对应,给前端看 | 接口层 | + +本文档关注实体建模。 + +### 二、Category 实体(一对多的"一"端) + +`Models/Category.cs`: + +```csharp +using System.ComponentModel.DataAnnotations; + +namespace MyShopApi.Models; + +public class Category +{ + public int Id { get; set; } + + [Required, StringLength(50)] + public string Name { get; set; } = string.Empty; + + [StringLength(200)] + public string? Description { get; set; } + + // 导航属性:一对多的"一"端,集合类型 + // 通过 Product.Category 反向引用到当前 Category + public ICollection Products { get; set; } = new List(); +} +``` + +### 三、Product 实体(一对多的"多"端) + +`Models/Product.cs`: + +```csharp +using System.ComponentModel.DataAnnotations; + +namespace MyShopApi.Models; + +public class Product +{ + public int Id { get; set; } + + [Required, StringLength(100)] + public string Name { get; set; } = string.Empty; + + [Range(0, 1000000)] + public decimal Price { get; set; } + + [Range(0, int.MaxValue)] + public int Stock { get; set; } + + // 外键:按 EF Core 约定命名为 <导航属性名>Id 或 <类型名>Id 即可被自动识别 + public int CategoryId { get; set; } + + // 导航属性:指向所属分类("多"端的引用导航属性) + public Category? Category { get; set; } +} +``` + +### 四、导航属性总结 + +| 端 | 类型 | 示例 | +| --- | --- | --- | +| "一"端(父) | `ICollection` | `Category.Products` | +| "多"端(子) | 引用类型 + 外键属性 | `Product.Category` + `Product.CategoryId` | diff --git "a/\351\231\210\346\226\271\346\227\255/20260605-\344\270\200\345\257\271\345\244\232\345\257\274\350\210\252\345\261\236\346\200\247\345\233\236\351\241\276.md" "b/\351\231\210\346\226\271\346\227\255/20260605-\344\270\200\345\257\271\345\244\232\345\257\274\350\210\252\345\261\236\346\200\247\345\233\236\351\241\276.md" new file mode 100644 index 0000000..184214c --- /dev/null +++ "b/\351\231\210\346\226\271\346\227\255/20260605-\344\270\200\345\257\271\345\244\232\345\257\274\350\210\252\345\261\236\346\200\247\345\233\236\351\241\276.md" @@ -0,0 +1,61 @@ +## 一对多导航属性回顾 + +### 一、两端实体骨架 + +```csharp +public class Category +{ + public int Id { get; set; } + public string Name { get; set; } = string.Empty; + + // "一"端:集合导航属性 + public ICollection Products { get; set; } = new List(); +} + +public class Product +{ + public int Id { get; set; } + public string Name { get; set; } = string.Empty; + + // 外键 + public int CategoryId { get; set; } + + // "多"端:引用导航属性 + public Category? Category { get; set; } +} +``` + +### 二、在 DbContext 中显式声明关系 + +EF Core 能通过约定自动识别导航属性,但显式写出来更清晰,且可精细控制级联行为。在 `Data/AppDbContext.cs` 的 `OnModelCreating` 中: + +```csharp +modelBuilder.Entity() + .HasOne(p => p.Category) // Product 有一个 Category + .WithMany(c => c.Products) // Category 有多个 Product + .HasForeignKey(p => p.CategoryId) // 外键列是 CategoryId + .OnDelete(DeleteBehavior.Restrict); // 存在子记录时禁止删除父记录 +``` + +**`DeleteBehavior` 常见取值:** + +| 取值 | 行为 | +| --- | --- | +| `Cascade` | 删除父记录时,连带删除子记录 | +| `Restrict` | 存在子记录时禁止删除父记录(需先手动处理子记录) | +| `SetNull` | 删除父记录时,将子记录外键设为 NULL(要求外键属性可空) | + +### 三、查询时加载关联数据 + +查询时默认不会加载导航属性,需要主动声明: + +```csharp +// Include — 显式加载关联数据 +var categories = await db.Categories + .Include(c => c.Products) + .ToListAsync(); + +var products = await db.Products + .Include(p => p.Category) + .FirstOrDefaultAsync(p => p.Id == id); +``` -- Gitee From fb59f821affeeef34b8726c5475c4eb51eed7066 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=A4=9C=E9=99=A8?= <3402439200@qq.com> Date: Sun, 7 Jun 2026 16:14:46 +0000 Subject: [PATCH 3/5] =?UTF-8?q?=E5=88=A0=E9=99=A4=E6=96=87=E4=BB=B6=20?= =?UTF-8?q?=E9=99=88=E6=96=B9=E6=97=AD/.keep?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- "\351\231\210\346\226\271\346\227\255/.keep" | 0 1 file changed, 0 insertions(+), 0 deletions(-) delete mode 100644 "\351\231\210\346\226\271\346\227\255/.keep" diff --git "a/\351\231\210\346\226\271\346\227\255/.keep" "b/\351\231\210\346\226\271\346\227\255/.keep" deleted file mode 100644 index e69de29..0000000 -- Gitee From 56256fce9551c0faf0829caa7208322788725ea8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=A4=9C=E9=99=A8?= <3402439200@qq.com> Date: Sun, 14 Jun 2026 12:15:21 +0000 Subject: [PATCH 4/5] =?UTF-8?q?=E4=BD=9C=E4=B8=9A?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: 夜陨 <3402439200@qq.com> --- .../20260614.md" | 79 +++++++++++++++++++ 1 file changed, 79 insertions(+) create mode 100644 "\351\231\210\346\226\271\346\227\255/20260614.md" diff --git "a/\351\231\210\346\226\271\346\227\255/20260614.md" "b/\351\231\210\346\226\271\346\227\255/20260614.md" new file mode 100644 index 0000000..67f7a82 --- /dev/null +++ "b/\351\231\210\346\226\271\346\227\255/20260614.md" @@ -0,0 +1,79 @@ +## 笔记 + +### DTO + +统计 DTO = 专门用来返回「统计数据」的 DTO +它是一个数据类,只存统计结果,不做任何计算,专门用于 WebAPI 接口向前端返回统计信息。 + +```cs +namespace MyShopApi.Dtos; + +public class CategoryStatsDto +{ + public int CategoryId { get; set; } + public string CategoryName { get; set; } = string.Empty; + public int ProductCount { get; set; } + public decimal AveragePrice { get; set; } + public decimal MinPrice { get; set; } + public decimal MaxPrice { get; set; } + public int TotalStock { get; set; } +} +``` + +前端拿到某分类的统计信息: + +```json +{ + "code": 0, + "message": "success", + "data": { + "categoryId": 1, + "categoryName": "电子产品", + "productCount": 3, + "averagePrice": 3499.00, + "minPrice": 499.00, + "maxPrice": 5999.00, + "totalStock": 350 + } +} +``` + +### 查询 + +```cs +var stats = await _db.Products + .Where(p => p.CategoryId == id) + .GroupBy(p => p.CategoryId) + .Select(g => new + { + ProductCount = g.Count(), + AveragePrice = g.Average(p => p.Price), + MinPrice = g.Min(p => p.Price), + MaxPrice = g.Max(p => p.Price), + TotalStock = g.Sum(p => p.Stock) + }) + .FirstOrDefaultAsync(); +``` + +翻译成 SQL: + +```sql +SELECT + COUNT(*) AS ProductCount, + AVG(Price) AS AveragePrice, + MIN(Price) AS MinPrice, + MAX(Price) AS MaxPrice, + SUM(Stock) AS TotalStock +FROM Products +WHERE CategoryId = 1 +GROUP BY CategoryId; +``` + +**5 条 SQL → 1 条**。生产环境一定要这么写。 + +### 空集合处理 + +`Average()` / `Min()` / `Max()` 在空集合上抛 `InvalidOperationException`。两种处理: + +1. **前置 `Any()` 检查**(第一种写法) +2. **GroupBy 配 FirstOrDefault**(第二种写法,更优) \ No newline at end of file -- Gitee From cef330f2852666af1ce65566bfabc4ada1a7b1c4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=A4=9C=E9=99=A8?= <3402439200@qq.com> Date: Sun, 14 Jun 2026 12:19:40 +0000 Subject: [PATCH 5/5] =?UTF-8?q?update=20=20=E9=99=88=E6=96=B9=E6=97=AD/202?= =?UTF-8?q?60614.md.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: 夜陨 <3402439200@qq.com> --- .../20260608.md" | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename "\351\231\210\346\226\271\346\227\255/20260614.md" => "\351\231\210\346\226\271\346\227\255/20260608.md" (100%) diff --git "a/\351\231\210\346\226\271\346\227\255/20260614.md" "b/\351\231\210\346\226\271\346\227\255/20260608.md" similarity index 100% rename from "\351\231\210\346\226\271\346\227\255/20260614.md" rename to "\351\231\210\346\226\271\346\227\255/20260608.md" -- Gitee