基于dotnet的后端学习
基础 WebApplication
一个基础的 Web 后端程序如下所示,大体上可以分为三个阶段。
- 创建并配置 WebApplicationBuilder 阶段
- 构造并配置 WebApplication 阶段
- 运行 WebApplication 阶段
public class Program
{
public static void Main(string[] args)
{
// 创建 WebApplicationBuilder
var builder = WebApplication.CreateBuilder(args);
// 构造 WebApplication
var app = builder.Build();
// 对 WebApplication 进行端点映射
app.MapGet(
pattern: "/",
handler: () => "Hello World!"
);
// 启动 WebApplication
app.Run();
}
}
为什么构造 WebApplication 需要分为 创建 WebApplicationBuilder,然后再构造 WebApplication 两步?
因为这两个对象对应应用生命周期的两个性质完全不同的阶段
WebApplicationBuilder 是筹备阶段,这个阶段你可以准备进行 「登记注册」,读取系统配置,配置日志都是这个阶段需要做的内容。
WebApplication 是运行阶段,将之前登记的东西创建为真正的实体。
Build() 方法实际上做了三件事
- 根据 WebApplicationBuilder 中登记的服务清单,构建出真正的 DI(依赖注入)容器。
- 容器从此冻结,运行阶段不可再注册新服务。
- 注意:此时只构建了容器(一个"知道如何创建服务"的工厂),服务实例并未创建——
- 实例是在第一次被索取时才延迟创建的。
- 创建并配置好 Web 服务器(Kestrel)
- 把所有配置、日志、环境信息整合成一个完整的、可以处理请求的应用
为什么必须冻结?不能在 app 上继续注册服务吗?
- 线程安全。 运行阶段你的服务会同时处理成百上千个并发请求,DI 容器会被大量线程同时读取。如果容器随时能被修改,就需要昂贵的锁机制。"构建一次、之后只读"让运行期完全无锁,这是性能基础。
- 可校验。 Build() 时框架可以一次性检查依赖关系:A 依赖 B、B 依赖 C,链条是否完整、有没有循环依赖。如果允许随时注册,这种校验就没法做了。
- 概念清晰。 "准备材料"和"开火做菜"是两个阶段,混在一起的项目(比如运行到一半改配置、动态加服务)是无数线上事故的来源。框架用类型系统强制你分开:builder 上根本没有 MapGet 方法,app 上根本没有 AddSingleton 方法——你在哪个阶段就只能干哪个阶段的事,编译器帮你守门。
服务器是如何知道要如何处理不同的 Web 请求的?
核心是「路由表 + 路由模板匹配」。每次调用 MapGet 等方法,都会在应用内部的一张路由表(端点列表)里登记一条记录:HTTP 方法 + 路由模板 + 处理函数。
一个请求到达后的流程:
- Kestrel 接收请求(事件驱动,基于 epoll,空闲时不消耗 CPU)
- 请求进入中间件管线,路由中间件取出请求的 HTTP 方法和 URL
- 将 URL 按
/分段,与路由表中的「路由模板」逐段做模式匹配,同时把 {id} 这样的模板段提取为参数 - 找到匹配的端点后,框架做「模型绑定」:把路由参数、查询字符串、请求体(JSON) 自动转换为处理函数需要的 C# 参数
- 执行处理函数,把返回值自动序列化(对象 → JSON)写回响应
- 没有任何模板匹配时,返回 404
MapGet 是什么?还有别的类似的方法吗
MapGet 注册一个响应 HTTP GET 请求的端点。HTTP 的每个方法都有对应的 Map 方法:
| 方法 | 对应 HTTP 方法 | 典型用途 |
|---|---|---|
MapGet |
GET | 查询数据(不修改状态) |
MapPost |
POST | 创建数据 |
MapPut |
PUT | 整体更新数据 |
MapPatch |
PATCH | 局部更新数据 |
MapDelete |
DELETE | 删除数据 |
MapMethods |
自定义 | 指定任意 HTTP 方法 |
Map |
所有方法 | 不区分方法(较少用) |
同一 URL 的不同方法可以映射到不同处理函数,如 MapGet("/api/params", ...) 和 MapPost("/api/params", ...) 可以共存。
成为 CRUD BOY
public class Program
{
public static void Main(string[] args)
{
var db = new Dictionary<string, string>()
{
["MaxLevel"] = "60",
["ServerName"] = "CN-1"
};
// 创建 WebApplicationBuilder
var builder = WebApplication.CreateBuilder(args);
// 构造 WebApplication
var app = builder.Build();
// 对 WebApplication 进行端点映射
var apiGroup = app.MapGroup("/api");
apiGroup.MapGet(
pattern: "/params",
handler: () => db.Select(kv => new ParamItem(kv.Key, kv.Value))
);
apiGroup.MapGet(
pattern: "/params/{key}",
handler: (string key) =>
{
if (db.TryGetValue(key, out var value))
{
return Results.Ok(new ParamItem(key, value));
}
return Results.NotFound();
}
);
apiGroup.MapPost(
pattern: "/params",
handler: (ParamItem item) =>
{
if (db.ContainsKey(item.key))
{
return Results.Conflict($"Param {item.key} exist");
}
db[item.key] = item.value;
return Results.Created($"/api/params/{item.key}", item);
}
);
apiGroup.MapPut(
pattern: "/params/{key}",
handler: (string key, ParamItem item) =>
{
if (db.ContainsKey(key) is false)
{
return Results.NotFound();
}
db[item.key] = item.value;
return Results.Ok(new ParamItem(key, item.value));
}
);
apiGroup.MapDelete(
pattern: "/params/{key}",
handler: (string key) =>
{
if (db.ContainsKey(key) is false)
{
return Results.NotFound();
}
db.Remove(key);
return Results.NoContent();
}
);
// 启动 WebApplication
app.Run();
}
record ParamItem(string key, string value);
}
依赖注入 Server
接口
/// <summary>
/// 参数存储的抽象接口。
/// 只描述"能做什么",不关心"怎么实现"——内存、数据库、Redis 都可以是背后的实现。
/// </summary>
public interface IParamStore
{
/// <summary>获取全部参数</summary>
IReadOnlyCollection<ParamItem> GetAll();
/// <summary>尝试获取单个参数,存在返回 true 并通过 item 传出</summary>
bool TryGet(string key, out ParamItem? item);
/// <summary>新增参数,key 已存在时返回 false</summary>
bool Add(string key, string value);
/// <summary>更新参数,key 不存在时返回 false</summary>
bool Update(string key, string value);
/// <summary>删除参数,key 不存在时返回 false</summary>
bool Remove(string key);
}
接口实现
using System.Collections.Concurrent;
namespace ParamStore;
/// <summary>
/// 基于 ConcurrentDictionary 的内存实现。
/// 注意:这个类不知道 HTTP 的存在——它只负责存取数据。
/// 这样的好处:将来换成数据库实现时,调用方无感知。
/// </summary>
public class MemoryParamStore : IParamStore
{
// ConcurrentDictionary:线程安全字典,支持并发读写
private readonly ConcurrentDictionary<string, string> _store = new()
{
["MaxLevel"] = "60",
["ServerName"] = "CN-1"
};
public IReadOnlyCollection<ParamItem> GetAll()
{
return _store
.Select(kv => new ParamItem(kv.Key, kv.Value))
.ToList();
}
public bool TryGet(string key, out ParamItem? item)
{
if (_store.TryGetValue(key, out var value))
{
item = new ParamItem(key, value);
return true;
}
item = null;
return false;
}
public bool Add(string key, string value)
{
// TryAdd:key 不存在时才添加,原子操作,天然处理了并发冲突
return _store.TryAdd(key, value);
}
public bool Update(string key, string value)
{
// key 存在才允许更新(保持和改造前一致的语义:不存在的 key 返回 404)
if (!_store.ContainsKey(key))
{
return false;
}
_store[key] = value;
return true;
}
public bool Remove(string key)
{
// TryRemove:原子地"存在则删除",不存在返回 false
return _store.TryRemove(key, out _);
}
}
主逻辑
using ParamStore;
var builder = WebApplication.CreateBuilder(args);
// 注册服务:声明"谁要 IParamStore,就给它 MemoryParamStore,全应用共享一个实例"
builder.Services.AddSingleton<IParamStore, MemoryParamStore>();
var app = builder.Build();
// 注意每个端点的第一个参数 IParamStore store——
// 你没有 new,也没有传参,是容器在请求到来时自动注入的
app.MapGet("/api/params", (IParamStore store) =>
{
return store.GetAll();
});
app.MapGet("/api/params/{key}", (string key, IParamStore store) =>
{
if (store.TryGet(key, out var item))
{
return Results.Ok(item);
}
return Results.NotFound();
});
app.MapPost("/api/params", (ParamItem item, IParamStore store) =>
{
if (!store.Add(item.Key, item.Value))
{
return Results.Conflict($"参数 {item.Key} 已存在");
}
return Results.Created($"/api/params/{item.Key}", item);
});
app.MapPut("/api/params/{key}", (string key, ParamItem item, IParamStore store) =>
{
if (!store.Update(key, item.Value))
{
return Results.NotFound();
}
return Results.Ok(new ParamItem(key, item.Value));
});
app.MapDelete("/api/params/{key}", (string key, IParamStore store) =>
{
if (!store.Remove(key))
{
return Results.NotFound();
}
return Results.NoContent();
});
app.Run();