diff --git "a/\345\220\225\347\245\245\345\213\207/20260601.md" "b/\345\220\225\347\245\245\345\213\207/20260601.md" new file mode 100644 index 0000000000000000000000000000000000000000..8a2a47512f4060c9cbde3f655b48ffc925837b56 --- /dev/null +++ "b/\345\220\225\347\245\245\345\213\207/20260601.md" @@ -0,0 +1,23 @@ +## Step 1 — 创建控制台项目并运行 +```bash +# 新建一个文件夹 +mkdir hello-dotnet && cd hello-dotnet + +# 创建控制台项目 +dotnet new console -n HelloApp + +# 进入项目目录 +cd HelloApp + +# 运行项目 +dotnet run +``` + +## Step 2 — 在 VS Code 中调试 + + +- 1. 在 VS Code 中打开 `Program.cs` +- 2. 在 `Console.WriteLine("Hello, World!");` 这行左侧点击,添加断点(红色圆点) +- 3. 按 `F5` 启动调试 +- 4. 程序在断点处暂停,观察底部调试工具栏 +- 5. 按 `F10` 单步执行,按 `F5` 继续运行 \ No newline at end of file diff --git "a/\345\220\225\347\245\245\345\213\207/20260603.md" "b/\345\220\225\347\245\245\345\213\207/20260603.md" new file mode 100644 index 0000000000000000000000000000000000000000..9bd14dfd1ee1fc877ab274f453003c4174113651 --- /dev/null +++ "b/\345\220\225\347\245\245\345\213\207/20260603.md" @@ -0,0 +1,128 @@ +# 笔记 +- 手写路由/约定路由 +- # 创建 Web API 项目 +- dotnet new webapi -n MyShopApi --use-controllers +- C#双引号是字符串,单引号是字符(很少用到) +- 代码没什么问题,但报错用dotnet watch重跑一下 + +# 案例 + +## 文件1:实体类 `Models/Category.cs` + +```csharp +namespace MyShopApi.Models; + +public class Category +{ + public int Id { get; set; } + public string Name { get; set; } = string.Empty; + public string? Description { get; set; } +} +``` + +--- + +## 文件2:控制器 `Controllers/CategoriesController.cs` + +```csharp +using Microsoft.AspNetCore.Mvc; +using MyShopApi.Models; + +namespace MyShopApi.Controllers; + +[ApiController] +[Route("api/[controller]")] +public class CategoriesController : ControllerBase +{ + // 模拟内存数据库 + private static readonly List _data = new() + { + new Category { Id = 1, Name = "电子产品", Description = "手机、电脑、配件" }, + new Category { Id = 2, Name = "图书", Description = "技术、小说、教材" } + }; + + // GET /api/categories - 获取所有分类 + [HttpGet] + public ActionResult> GetAll() + { + return Ok(_data); + } + + // GET /api/categories/{id} - 根据ID获取单个分类 + [HttpGet("{id:int}")] + public ActionResult GetById(int id) + { + var category = _data.FirstOrDefault(c => c.Id == id); + if (category is null) + return NotFound(); + return Ok(category); + } + + // POST /api/categories - 新增分类 + [HttpPost] + public ActionResult Create([FromBody] Category input) + { + // 自动生成ID + input.Id = _data.Count == 0 ? 1 : _data.Max(c => c.Id) + 1; + _data.Add(input); + + // 返回201状态码,并在响应头中附带新资源的地址 + return CreatedAtAction(nameof(GetById), new { id = input.Id }, input); + } + + // PUT /api/categories/{id} - 修改分类 + [HttpPut("{id:int}")] + public IActionResult Update(int id, [FromBody] Category input) + { + var existing = _data.FirstOrDefault(c => c.Id == id); + if (existing is null) + return NotFound(); + + existing.Name = input.Name; + existing.Description = input.Description; + return NoContent(); // 204状态码,无响应体 + } + + // DELETE /api/categories/{id} - 删除分类 + [HttpDelete("{id:int}")] + public IActionResult Delete(int id) + { + var existing = _data.FirstOrDefault(c => c.Id == id); + if (existing is null) + return NotFound(); + + _data.Remove(existing); + return NoContent(); // 204状态码,无响应体 + } +} +``` + +### 功能覆盖清单 + +| 功能 | HTTP方法 | 路由 | 状态码 | +| -------- | -------- | ---------------------- | ----------- | +| 查询所有 | GET | `/api/categories` | 200 OK | +| 查询单个 | GET | `/api/categories/{id}` | 200 / 404 | +| 新增 | POST | `/api/categories` | 201 Created | +| 修改 | PUT | `/api/categories/{id}` | 204 / 404 | +| 删除 | DELETE | `/api/categories/{id}` | 204 / 404 | + +### 测试方法 + +1. **启动项目**:在项目根目录执行 `dotnet run` +2. **打开Swagger**:访问 `https://localhost:5001/swagger` +3. **逐个测试5个接口**: + - GET 列表 → 返回2条初始数据 + - GET id=1 → 返回电子产品 + - GET id=999 → 返回404 + - POST → 新增分类,返回201和Location头 + - PUT id=1 → 修改后返回204 + - DELETE id=2 → 删除后返回204 + +### 注意事项(防扣分) + +1. **内存数据特性**:每次重启服务,数据会重置为初始的两条记录(这是教程预期行为,不是bug) +2. **POST时不要传Id**:代码会自动生成Id,前端传的Id值会被覆盖 +3. **PUT必须传完整数据**:`Name` 和 `Description` 字段都需要传,否则会被清空 +4. **路由约束**:`{id:int}` 限制了id必须是整数,传入字母会返回404 + diff --git "a/\345\220\225\347\245\245\345\213\207/20260604.md" "b/\345\220\225\347\245\245\345\213\207/20260604.md" new file mode 100644 index 0000000000000000000000000000000000000000..f4c44e6ee513a010c77ced55b8c19c5d52fdb6ca --- /dev/null +++ "b/\345\220\225\347\245\245\345\213\207/20260604.md" @@ -0,0 +1,160 @@ +- dotnet add package Microsoft.EntityFrameworkCore.sqlite -v 8 +- dotnet add package Microsoft.EntityFrameworkCore.Design -v 8 +- dotnet tool install -g dotnet-ef +- 在appsettings.json: + "ConnectionStrings":{ + “sqlite":"data source=./db.db;" + } + + +## 写 AppDbContext + +新建 `Data/AppDbContext.cs`: + +```csharp +using Microsoft.EntityFrameworkCore; +using MyShopApi.Models; + +namespace MyShopApi.Data; + +// DbContext = 一个数据库会话,封装连接、跟踪实体变更、生成 SQL +public class AppDbContext : DbContext +{ + // 通过构造函数注入配置(连接字符串) + public AppDbContext(DbContextOptions opts) : base(opts) { } + + // DbSet 代表一张表;后续 CRUD 都通过它 + public DbSet Categories => Set(); + public DbSet Products => Set(); +} +``` + +`Set()` 等价于 `DbSet { get; set; }`,写法更紧凑,效果一致。 + + +## 在 Program.cs 注册 + +```csharp +using Microsoft.EntityFrameworkCore; +using MyShopApi.Data; + +var builder = WebApplication.CreateBuilder(args); + +// 把连接字符串读出来,给 AppDbContext 配置 SQLite provider +builder.Services.AddDbContext(options => + options.UseSqlite(builder.Configuration.GetConnectionString("sqlite")) +); + +builder.Services.AddControllers(); +// ... 其他原有配置不变 +``` + +> `builder.Configuration.GetConnectionString("Default")` 找的就是 `appsettings.json` 里 `ConnectionStrings:Default` 节点。**冒号是 JSON 路径分隔符**。 + + + + +- dotnet build + +- dotnet ef migrations add Init + +- dotnet ef database update + + +## Controllers/CategoriesController.c +```csharp +using Microsoft.AspNetCore.Mvc; +using ShoppingMall.Data; +using ShoppingMall.Model; + +namespace ShoppingMall.Controllers; + +/// +/// 商品分类(Category)相关接口。 +/// 路由:/Categories +/// +[ApiController] +[Route("[controller]")] +public class CategoriesController : ControllerBase +{ + private readonly AppDbContext _db; + + public CategoriesController(AppDbContext db) + { + _db = db; + } + + /// + /// 获取全部分类列表。 + /// + [HttpGet] + public IEnumerable Index() + { + return _db.Categories.ToList(); + } + + /// + /// 根据主键获取单个分类。 + /// + /// 分类主键 Id + /// 若未找到则返回一个空的 ,方便调用方判空 + [HttpGet("{id}")] + public Category GetById(int id) + { + // 查不到时回退到空对象,避免上游出现 NullReferenceException + return _db.Categories.Find(id) ?? new Category(); + } + + /// + /// 新增一个分类。 + /// + [HttpPost] + public Category Create(Category obj) + { + _db.Categories.Add(obj); + _db.SaveChanges(); + return obj; + } + + /// + /// 更新指定分类的名称与描述。 + /// + /// 要更新的分类 Id + /// 新的分类数据(仅使用 CateName 与 Description) + /// 更新后的分类;未找到时返回空对象 + [HttpPut("{id}")] + public Category Update(int id, Category obj) + { + var tmp = _db.Categories.Find(id); + if (tmp is null) + { + return new Category(); + } + tmp.CateName = obj.CateName; + tmp.Description = obj.Description; + _db.Categories.Update(tmp); + _db.SaveChanges(); + return tmp; + } + + /// + /// 删除指定分类。 + /// + /// 要删除的分类 Id + /// 被删除的分类对象;未找到时返回空对象 + [HttpDelete("{id}")] + public Category Del(int id) + { + var tmp = _db.Categories.Find(id); + if (tmp is null) + { + return new Category(); + } + _db.Categories.Remove(tmp); + _db.SaveChanges(); + return tmp; + } +} + +``` + diff --git "a/\345\220\225\347\245\245\345\213\207/20260605.md" "b/\345\220\225\347\245\245\345\213\207/20260605.md" new file mode 100644 index 0000000000000000000000000000000000000000..112a60db2c0335744a7cb84d7164ee8063648b84 --- /dev/null +++ "b/\345\220\225\347\245\245\345\213\207/20260605.md" @@ -0,0 +1,225 @@ +## 笔记 +```js +GET 列表、GET 详情 → 200 +POST → 201 +PUT → 200 或 204 +DELETE → 204 +资源不存在 → 404 +业务冲突(如删有商品的分类)→ 409 +```` + +- **DTO(Data Transfer Object)**就是为接口入参/出参单独定义的"数据传输模型"。 + +## 一对多关系 + +`Category` 和 `Product` 是经典的一对多: + +``` +Category (1) ──── (*) Product + "电子产品" - iPhone 15 + - 小米 14 + - 机械键盘 +``` + +## 练习 +```csharp +// ========== 1. Models/Category.cs ========== +using System.Collections.Generic; + +namespace MyShopApi.Models +{ + /// + /// 商品分类实体 - 一的一端 + /// + public class Category + { + public int Id { get; set; } + public string Name { get; set; } = string.Empty; + public string? Description { get; set; } + + // 一对多关系:一个分类下有多个商品 + // 集合导航属性 + public ICollection Products { get; set; } = new List(); + } +} +``` + +```csharp +// ========== 2. Models/Product.cs ========== +namespace MyShopApi.Models +{ + /// + /// 商品实体 - 多的一端 + /// + public class Product + { + public int Id { get; set; } + public string Name { get; set; } = string.Empty; + public decimal Price { get; set; } + public int Stock { get; set; } + + // 外键 + public int CategoryId { get; set; } + + // 引用导航属性 + public Category? Category { get; set; } + } +} +``` + +```csharp +// ========== 3. Data/AppDbContext.cs ========== +using Microsoft.EntityFrameworkCore; +using MyShopApi.Models; + +namespace MyShopApi.Data +{ + public class AppDbContext : DbContext + { + public AppDbContext(DbContextOptions options) : base(options) + { + } + + public DbSet Categories => Set(); + public DbSet Products => Set(); + + protected override void OnModelCreating(ModelBuilder modelBuilder) + { + base.OnModelCreating(modelBuilder); + + // ========== 配置一对多关系 ========== + modelBuilder.Entity() + .HasOne(p => p.Category) // Product 有一个 Category + .WithMany(c => c.Products) // Category 有多个 Product + .HasForeignKey(p => p.CategoryId) // 外键是 CategoryId + .OnDelete(DeleteBehavior.Restrict); // 有子记录时禁止删除父 + } + } +} +``` + +```csharp +// ========== 4. 演示导航属性使用 ========== +// 可以在控制器或测试代码中这样使用 + +using Microsoft.EntityFrameworkCore; +using MyShopApi.Data; + +/// +/// 演示一对多导航属性 +/// +public class NavigationDemo +{ + private readonly AppDbContext _db; + + public NavigationDemo(AppDbContext db) + { + _db = db; + } + + /// + /// 多 → 一:从商品拿到分类信息 + /// + public async Task GetProductWithCategory(int productId) + { + // 必须用 Include 加载导航属性,否则 Category 为 null + var product = await _db.Products + .Include(p => p.Category) // 关键!不加这行 Category 是 null + .FirstOrDefaultAsync(p => p.Id == productId); + + if (product != null) + { + // 获取商品所属分类名称 + var categoryName = product.Category?.Name ?? "无分类"; + Console.WriteLine($"商品:{product.Name},分类:{categoryName}"); + } + + return product; + } + + /// + /// 一 → 多:从分类拿到所有商品 + /// + public async Task GetCategoryWithProducts(int categoryId) + { + // Include 加载该分类下的所有商品 + var category = await _db.Categories + .Include(c => c.Products) // 加载导航属性 + .FirstOrDefaultAsync(c => c.Id == categoryId); + + if (category != null) + { + Console.WriteLine($"分类:{category.Name},商品数:{category.Products.Count}"); + foreach (var product in category.Products) + { + Console.WriteLine($" - {product.Name} ¥{product.Price}"); + } + } + + return category; + } + + /// + /// 不写 Include 的后果演示 + /// + public async Task GetProductWithoutInclude(int productId) + { + var product = await _db.Products + .FirstOrDefaultAsync(p => p.Id == productId); + + // ❌ product.Category 会是 null,因为没有 Include + // 这就是导航属性不生效的最常见原因 + Console.WriteLine($"不写 Include,Category 是:{product?.Category}"); + return product; + } +} +``` + +```csharp +// ========== 5. 迁移文件中的外键配置(参考) ========== +// 这是执行 dotnet ef migrations add Init 后自动生成的 +// 在 Migrations/xxx_Init.cs 中可以看到类似代码 + +/* +protected override void Up(MigrationBuilder migrationBuilder) +{ + // ... 创建 Categories 表 ... + + migrationBuilder.CreateTable( + name: "Products", + columns: table => new + { + Id = table.Column(type: "INTEGER", nullable: false) + .Annotation("Sqlite:Autoincrement", true), + Name = table.Column(type: "TEXT", nullable: false), + Price = table.Column(type: "TEXT", nullable: false), + Stock = table.Column(type: "INTEGER", nullable: false), + CategoryId = table.Column(type: "INTEGER", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_Products", x => x.Id); + // 👇 外键约束配置 + table.ForeignKey( + name: "FK_Products_Categories_CategoryId", + column: x => x.CategoryId, + principalTable: "Categories", + principalColumn: "Id", + onDelete: ReferentialAction.Restrict); // ← Restrict + }); +} +*/ +``` + +```csharp +// ========== 6. 快速验证查询 ========== +/* +在 SQLite 中验证外键存在: + +SELECT sql FROM sqlite_master WHERE type='table' AND name='Products'; + +应该能看到 FOREIGN KEY (CategoryId) REFERENCES Categories(Id) +*/ +``` + +--- \ No newline at end of file