EFCore
EFcore介绍
//常用连接字符串
Data Source=DESKTOP-BOB3CAO;Initial Catalog={{}};Integrated Security=True;TrustServerCertificate=True
//临时插一句 这是在多DbContext情况下进行迁移的命名
dotnet ef migrations add InitialCreate --context MyIdentityContext
dotnet ef database update --context MyIdentityContext
ORM是负责从关系型数据库到C#对象的双向转换
EFCore则是微软官方提供给我们的一套不需要去考虑底层的ORM框架是模型驱动的,类似的框架还有dapper它则是数据库驱动的,即对于EFcore而言是对对象进行操作,而Dapper还是需要写sql语句
开发环境搭建
建立实体类,建立配置类,建立DbContext(需要写入DataSet集合,可以理解为所有表对应的实体类集合),生成数据库,编写业务代码
安装EFcoreTools进行迁移,迁移是分多步的Migration,也可以进行回滚(当然可以记录迁移信息类似git操作)Add-Migration Info
同步数据库信息Update-DataBase
如果想实现对数据库字段的配置,在其配置类对应的configuration中去对builder链式编程即可,如下
builder.ToTable("T_Books");//配置实体映射的表名
builder.Property(b => b.Title).HasMaxLength(200).IsRequired();//配置Title字段的最大长度为200,IsRequired并且不能为空
以下为必须重写在DbContext的函数
protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
{
base.OnConfiguring(optionsBuilder);
optionsBuilder.UseSqlServer("Data Source=DESKTOP-BOB3CAO;Initial Catalog=TestDB;Integrated Security=True;TrustServerCertificate=True");
//optionsBuilder.UseLoggerFactory(factory);
//optionsBuilder.LogTo(Console.WriteLine);
}
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
base.OnModelCreating(modelBuilder);
//应用实体类的配置
modelBuilder.ApplyConfigurationsFromAssembly(this.GetType().Assembly);
}
使用EFCore进行增删改查
当你想新增数据时,可以往我们的DbContext伪数据库的DBSet表中去增加对象,之后调用SaveChange进行同步数据库
tips:使用Linq查询集合时返回的对象一般是IQuery接口的待查询内容,需要手动.ToList()立刻完成查询并返回,但是查询单个就直接返回实体类!
var b1 = new Books
{
Title = "C#高级编程",
PubTime = DateTime.Now,
Price = 99.9,
Description = "一本关于C#高级编程的书籍"
};
var b2 = new Books
{
Title = "EF Core实战",
PubTime = DateTime.Now,
Price = 79.9,
Description = "一本关于EF Core实战的书籍"
};
var b3 = new Books
{
Title = "ASP.NET Core开发指南",
PubTime = DateTime.Now,
Price = 89.9,
Description = "一本关于ASP.NET Core开发的书籍"
};
using (var ctx = new TestDbContext())
{
ctx.MyBooks.Add(b1);
ctx.MyBooks.Add(b2);
ctx.MyBooks.Add(b3);
await ctx.SaveChangesAsync();
var re = ctx.MyBooks.Where(b => b.Price > 80);
foreach (var item in re)
{
Console.WriteLine($"书名:{item.Title},价格:{item.Price}");
}
}
对于查询操作,由于Dbset本身继承于IEnumerable接口,所以可以使用Linq操作
var result = ctx.MyBooks.FirstOrDefault(b => b.Title == "EF Core实战");
Console.WriteLine($"书名:{rre.Title},价格:{rre.Price}");
对于修改和删除都是使用Linq查询对应的实体对象,对对象进行相应操作
修改:
var re = ctx.MyBooks.FirstOrDefault(a=>a.Price==89.9); re.Title = "DecaASP.NET Core开发指南(第二版)_Decadede";
删除
using (var ctx = new TestDbContext())
{
var result = ctx.MyBooks.Where(b=>b.Price>80).ToList<Books>();
foreach (var item in result)
{
ctx.MyBooks.Remove(item);
}
await ctx.SaveChangesAsync();
}

约定大于配置
当你没有配置对应的继承IEntityTypeConfiguration类时,表名采用DbContext中对应的DbSet的属性名
数据列表的名字采用实体类属性的名字,类型将自动采用最兼容的类型
数据表列的可空性取决于对应实体类属性的可空性
名字为ID的属性默认为主键,如果主键使用int/short/long类型,将自动开启自增字段如果主键为Guid类型,则默认采用默认的Guid生成机制生成主键值
存在两种配置方式,FluentApi第一种就是建立一个继承接口的配置类在对应函数中去写配置
第二种就是使用特性
后者使用方便但把这个实体类强绑定当前数据库造成强烈耦合
主键
对于自增字段来说,不能人为给他赋值否则报错
Guid算法很牛,结合了当前网课MAC地址和当前时钟,导致生成的Guid值永远不会重复,但由于其一般不连续的特性无法作为索引使用,在MySql中主键必须是聚集索引会导致无法使用(就是去建立一个新表包含指针和当前字段的值按照顺序排列,假如一开始的id字段插入顺序是 1 7 9 2 6 那建立之后是1 2 6 7 9 假如我要找id=6的 没索引得一个个比对,有索引直接2分看中间 方便理解可以看成你往一个List<对象> 列表中去查找指定对象)
开始添加Phone实体00000000-0000-0000-0000-000000000000
开始添加Phone实体 数据库前f21c6198-c1ee-450a-fe54-08de145a25ee
添加Phone实体成功 数据库后f21c6198-c1ee-450a-fe54-08de145a25ee
说明Guid的赋值是有EFCore进行的而非数据库
实际上可以使用类似复合主键,GUid当成逻辑上的主键,自增列只是帮我们进行数据重排
迁移底层
Migration底层是由up和down方法,有点类似git的回滚和更新操作
数据库会存在一张表来记录所有的迁移记录!包含了数据库增加的内容方便efcore看的

反向工程
类似DbFirst就是先有数据库,再有实体类EFCore本身是基于ADO.NET去和数据库进行交互的,EF本身是把我们的C#代码翻译成了SQL语句在通过ADO.NET去执行操作
使用Linq查询时别使用不支持的封装函数在EF中查看对应的SQL语句,可以使用简单日志LogTo(Action)也可以使用标准日志的形式
private static ILoggerFactory factory = LoggerFactory.Create(b=>b.AddConsole()); optionsBuilder.UseLoggerFactory(factory);还有一种查询的SQL查看方法,返回对象是IQueryable的可以使用Console.WriteLine(p1.ToQueryString());EFCore中的迁移脚本是和数据库相关的,不能中在中途更换数据库时使用不属于他的迁移脚本
联表查询
实体关系,EFCore支持多实体关系操作
使用fluentApi进行配置

像这种一对多的关系配置都配置在一端,一对多你就在多的那端配置就行
多:
public void Configure(EntityTypeBuilder<Comment> builder)
{
builder.ToTable("T_Comments");
builder.HasOne<Article>(a => a.Article).WithMany(a => a.Comments).IsRequired();
}
一:
public void Configure(EntityTypeBuilder<Article> builder)
{
builder.ToTable("T_Articles");
builder.Property(a => a.Title).HasMaxLength(300).IsUnicode().IsRequired();
builder.Property(a => a.Content).IsUnicode().IsRequired();
}

注意,字段本身不包含外键,是ef自动生成的!
插入信息

//这里并没有往db中的comment添加数据,ef顺藤摸瓜更具他们两个类的关系,自动添加了评论
Article a1 = new Article() { Content = "Decade亚洲最帅", Title = "重大新闻!" };
var c1 = new Comment() { Content = "赞同2" };
var c2 = new Comment() { Content = "非常赞同2" };
a1.Comments.Add(c1);
a1.Comments.Add(c2);
db.Articles.Add(a1);
await db.SaveChangesAsync();
4.Include 会自动填充导航属性对应的集合,如上述中的CommentList注意:include包含的是需要被填充的导航属性但是查询好像不需要,只有当我们需要导航属性的值时才需要 5.我们可以手动添加字段并将其设置为外键 
HasForeignKey(a=>a.ArticleId);
public class User
{
public int Id { get; set; } // 主键
public string Name { get; set; }
}
public class Leave
{
public int Id { get; set; }
public int RequesterId { get; set; } // 外键 → User.Id
public int? ApproverId { get; set; } // 外键 → User.Id
// 导航属性 - EF Core 会自动通过外键查找并填充这些对象
public User Requester { get; set; } // 自动关联到 RequesterId 对应的 User
public User Approver { get; set; } // 自动关联到 ApproverId 对应的 User
}
单向导航&&关系配置在任意一方都可以
当出现一个基础表时(会被很多表引用),可以在WithMany里留空,并且在那个实体类中不进行声明集合对象
虽然说在1对多的时候进行配置在那一端都可以,但最推荐配置在多的一端s
自引用组织结构树
对于自引用表来说,务必将外键设置为可以null,对于一般外部表的引用来说,它们都是存在主键的,所以我们的外键列默认就是不为空

一对一/多对多
一对一 必须显示声明外键,1对多默认就算在多的一方建立外键
多对对 存在中间表帮你映射
关于IQueryable与IEnumerable的区别
写在IEnumerable中的Linq方法是在客户端本地进行比较的(客户端评估)一切都在本地进行查询的,连接数据库时,不对sql语句进行多余操作
写在IQueryable中的Linq方法是在服务端进行比较的(服务端评估)是efcore转换成对于查询sql语句在服务端进行查询,不在本地内存中进行操作
关于IQueryable
可以更具具体业务进行Linq拼接
分页查询
using (MyDbContext db = new MyDbContext())
{
int sumIndex = db.Comments.Count();
if((pageIndex-1) * pageSize > sumIndex)
{
Console.WriteLine("越界");
return;
}
Console.WriteLine($"{sumIndex} {pageIndex - 1 * pageSize}");
List<Comment> cm = db.Comments.Skip((pageIndex-1) * pageSize).Take(pageSize).ToList();
for (int i = 0; i < pageSize; i++)
{
try
{
Console.WriteLine(cm[i].Name);
}
catch (Exception)
{
}
}
Console.WriteLine("总条数" + Math.Ceiling((sumIndex * 1.0 / pageSize)));
}
IQueryable底层


他默认是Reader形式的,也就是和数据库进行连接,数据分批次一次次传递
当然你可以直接Tolist Toarray直接加载到内存中
默认不支持同时开启多个DataReader,还是一一访问数据库安全,或者变成数组形式
对于EF中的异步方法
对于那些非终结方法(返回值是IQueryable的)由于,他们并没有立即去执行连接数据库,并不消耗I/O一般不阻塞线程
EF中执行SQL语句
db.Database.ExecuteSqlInterpolatedAsync(@$"insert into Articals(Name,Description) values('ttNew','Decade')");由于直接执行了,不需要savechanges(),EFCore非常牛逼,在执行sql语句的时候,如果你使用了内插值语法,他会把你的{}变成查询对象,而不是简单的字符串拼接,不会造成sql注入危机,自动进行参数化查询

包含实体的查询语句(在实体查询还不能少字段,他要赋值,只能单表查询)关于单引号,EF Core 会自动处理字符串参数的引号,由于在实体查询的返回值依旧是IQueryable对象所以是可以复用的,可以配合EFCore使用
如果你想执行原生的sql语句可以使用原生ADO
DbConnection dbc = db.Database.GetDbConnection();
using (var command = dbc.CreateCommand())
{
command.CommandText = "select * from Comments";
if (dbc.State != System.Data.ConnectionState.Open)
dbc.Open();
using (var reader = await command.ExecuteReaderAsync())
{
while (await reader.ReadAsync())
{
Console.WriteLine(reader.GetInt64(0));
}
}
}
总结:一般的查询Linq就足够了,非查询使用ExecuteSqlInterpolated,只针对现存实体使用FromSqlInterpolated,更复杂就用dapper ADO吧
EF如何知道数据变更了
当你从DBcontext中获取对象时,他会默认对你获取的对象进行跟踪,创建对应的快照,在你进行saveChange()之后进行对比
using (MyDbContext db = new MyDbContext())
{
var result = db.Articals.FirstOrDefault();
var re1 = db.Entry(result);
result.Name += "zjdawdw99";
var re2 = db.Entry(result);
Console.WriteLine(re1.State + " " + re1.DebugView.LongView);
Console.WriteLine(re2.State + " " + re2.DebugView.LongView);
}
在这里看你什么时候去获取Entry,他返回的是当前的结果,这个结果是不会变的,Entry只是一个方法,不是引用对象 2. 当然如果你只是想要查询不想更改可以使用var result = db.Articals.AsNoTracking().FirstOrDefault();降低内存占用
批量更新
是直接Update ### from / Delete ## from
await db.Comments.Where(o => o.Id > 2).ExecuteUpdateAsync(setter=>setter
.SetProperty(p=>p.Name,"hahaha"));
await db.Comments.Where(o => o.Id > 2).ExecuteDeleteAsync();
全局筛选器
这个东西可以理解为一种查询条件,只要你配置了就默认会添加在你的查询语句后面

添加全局筛选器是在对应表的Config中
using IncludeMySelf.Model;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Metadata.Builders;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace IncludeMySelf.Config
{
public class CommentConfig : IEntityTypeConfiguration<Comment>
{
public void Configure(EntityTypeBuilder<Comment> builder)
{
builder.HasOne<Artical>(o => o.CArtical).WithMany(o => o.Comments);
builder.HasQueryFilter(o=>o.IsDeleted==false);
}
}
}
主函数
using (MyDbContext db = new MyDbContext())
{
#region 软删除
var result = db.Comments.ToList();
for (int i = 0; i < result.Count; i++)
{
Console.WriteLine(result[i].IsDeleted);
}
#endregion
}
这样执行全体查询时就会进行自动过滤,当然是可以取消筛选器条件的
var result = db.Comments.IgnoreQueryFilters().Take(500).ToList();
以上这样写就能确保单次查询忽略条件限制
可能造成的性能问题: 使用 Price 索引:找到所有 Price > 100 的记录,然后逐条回表检查 IsDeleted 全表扫描:直接扫描整个表,同时检查两个条件
悲观并发控制
数据库锁和事务高度绑定
#region 悲观锁
using (var ts = db.Database.BeginTransaction())
{
Console.WriteLine("输入你的名称");
string name = Console.ReadLine();
var tarObj = db.houses.FromSqlInterpolated($"select * from T_House WITH (ROWLOCK,UPDLOCK) where id = 1 ").Single();
if (!String.IsNullOrEmpty(tarObj.Owner))
{
Console.WriteLine("exist");
}
else
{
Console.WriteLine("change");
tarObj.Owner = name;
}
Thread.Sleep(3000);
db.SaveChanges();
ts.Commit();
Console.ReadKey();
}
#endregion
关于阻塞:
有锁的话,会卡在数据库获取信息那一行
// 事务A
var objA = SELECT WITH (UPDLOCK) WHERE id=1; // 立即获取锁
// 事务B
var objB = SELECT WITH (UPDLOCK) WHERE id=1; // ❌ 在这里阻塞等待
// 事务A提交后 → 事务B才继续执行
意思就是说当第一个事务提交时,第二个事务会被阻塞在 var tarObj = db.houses.FromSqlInterpolated($"select * from T_House WITH (ROWLOCK,UPDLOCK) where id = 1 ").Single();一行,当第一个事务释放锁之后,才能继续执行
缺点就是如果出现大量并发执行的流程时,会造成大量数量阻塞,如果锁住一行数据所有修改请求都会被阻塞,容易造成死锁
乐观并发控制
乐观锁通过"轻量级的条件检查"替代"重量级的阻塞等待",在大多数现代应用场景中确实提供了更好的性能和用户体验!
进行配置如图所示原理则是在where中加入之前的旧值,当同一时间多个请求更改信息时,由于修改是要更具条件的,当第一条进行更新数值之后,其余的请求都不能找到对应的值进行修改,所以会丢失几次操作,但是不阻塞线程,当信息提交之后,下一次更改就会按照新的旧值进行设置
当你需要对一行数据多列进行更改,但又需要并发控制时,可以使用RowVersion,配置如图
,当你修改一行任意某列的值时,该列会进行自动更新,每次进行更新数据时都会把上一次的rowversion当作old值进行查找更改,当然你可以在除sqlserver数据库中使用GUID列进行替代效果一样
表达式树
ASP.Net.Core
一个基础的Controller
using Microsoft.AspNetCore.Mvc;
namespace AspDotNet.Controllers
{
//webapi项目
[ApiController]
//路由地址
[Route("api/[controller]")]
public class MyFirstController: ControllerBase
{
//请求协议
[HttpGet(Name ="TestFunc")]
public IEnumerable<string> All()
{
return new List<string>() {"day01","day02"};
}
}
}
对于幂等:幂等性 是一个数学和计算机科学概念,在 HTTP 协议中,它指的是: 同一个请求被执行一次与连续执行多次,对服务器资源的状态所产生的影响是完全相同的。 换句话说,客户端无论因为何种原因(如超时、网络抖动)重复发送了相同的请求,服务器端都应该能够处理这种重复,并保证资源最终处于一致的状态。
[Route("api/[controller]/[action]")] 这个对于路由来说就是先按照这个路径,在方法上的GetPost("name")是最终路径


关于ActionResult
有时需要返回一些错误信息如404 400,成功信息200... 单靠一个准确的返回值是做不到的,所以我们需要IActionResult,这个接口实现了一个异步方法,当我们返回IActionResult时会自动调用那个异步方法,来实现具体的数据展现,反正目前理解就用ActionResult就行了,能返回我们的需求还能帮我们隐式转换 4.关于传参
默认你不设置url传参,参数默认从queryString中获取,当然你可以使用[from(name="") type varname]来指定参数从何而来
DotNetCore的依赖注入
1.示例: 我们的webapi项目是不需要我们自己去创建服务容器的和对应provider的,var builder = WebApplication.CreateBuilder(args);这个build帮助我们构建了, builder.Services.AddControllers(); 由于存在这一句,会注册我们的controller类又由于依赖注入有传染性,在控制器类中写的服务也会被依次注入
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
namespace AspDotNet.Controllers
{
[Route("api/[controller]/[action]")]
[ApiController]
public class TestDIController : ControllerBase
{
public readonly Service.Calculate _calculate;
public TestDIController(Service.Calculate cal)
{
_calculate = cal;
}
[HttpGet]
public int TestAdd(int a, int b)
{
return _calculate.Add(a, b);
}
}
}
个别情况,如在构建函数时会消耗大量时间操作
public Service.Calculate _calculate;
[HttpGet]
public int TestAdd([FromServices] Calculate c,int a, int b)
{
_calculate = c;
return _calculate.Add(a, b);
}
缓存
需要注意内存缓存的性能是最高的,没有业务必要不需要使用分布式缓存
客户端响应缓存
1.对于客户端缓存可以在返回时加上允许浏览器缓存的特性,当然浏览器也可以不遵守
[HttpGet]
[ResponseCache(Duration = 10)]
public DateTime GetNow()
{
return DateTime.Now;
}
服务端响应缓存
浏览器访问方法时先找本地缓存,没找到连服务器缓存,还是没有连具体方法
当浏览器的报文头写入no caching发送请求时,所有缓存均失效

内存缓存
内存缓存是和主程序在同一个进程中的,共享进程资源 ,这种缓存就不会被http报文头的no caching取消. 原理还是类似服务器缓存的,本地存在一组键值对,如果访问数据不存在,往数据库取数据,存在就直接返回
[HttpGet]
public async Task<ActionResult<Book>> GetBook(int id)
{
_logger.LogWarning("GetBook方法被调用,参数id={Id}", id);
var re = await _memoryCache.GetOrCreateAsync($"Book_{id}", async entry =>
{
//设置绝对时间
entry.AbsoluteExpirationRelativeToNow = TimeSpan.FromSeconds(50);
//设置滑动时间 如果生命周期内被访问,则重置时间
entry.SlidingExpiration = TimeSpan.FromSeconds(5);
//混合使用就是看谁先过期,过期直接失效
_logger.LogWarning("缓存未命中,正在从FIndBooks服务获取数据,id={Id}", id);
return await _findBooks.GetBooksAsync(id);
});
_logger.LogWarning("返回结果,id={Id}, title={Title}", re.id, re.title);
return re;
}
这里的$"Book_{id}"就是键值对的key属性 关于缓存信息不一致的问题
关于缓存穿透问题,当用户输入一个不存在的ID时,缓存中没有对应的数据,所以会到数据库进行查询,数据库同样返回null值,我们调用GetOrCreateAsync方法已经将 null也作为键值对进行缓存了

数据库雪崩问题,当大量缓存同时过期时,造成数据库访问急剧上升,可以设置一个浮动时间进行过期(均匀一下)

entry.AbsoluteExpirationRelativeToNow = TimeSpan.FromSeconds(Random.Shared.Next(10,15));
分布式缓存(redis)
由于内存缓存只存在于单台服务器中,其中的缓存数据并不通用,在大型项目中引入分布式缓存,所有web服务器去访问一台缓存服务器,这里使用redis为例


具体使用方法


配置完成的业务代码
#region 分布式缓存
var result = await _distributedCache.GetStringAsync($"{id}");
if(result == null)
{
//设置绝对缓存时间
var options = new DistributedCacheEntryOptions()
{
AbsoluteExpirationRelativeToNow = TimeSpan.FromSeconds(20)
};
_logger.LogInformation("缓存没有对应数据");
var book = await _findBooks.GetBooksAsync(id);
var bookJson = System.Text.Json.JsonSerializer.Serialize(book);
await _distributedCache.SetStringAsync($"{id}",bookJson, options);
return book;
}
else
{
_logger.LogInformation("缓存有对应数据");
return System.Text.Json.JsonSerializer.Deserialize<Book>(result);
}
#endregion

多层项目使用EFCore注意:
当EFCore的类库项目与当前的AspwebApi项目独立时,为了使得配置连接数据库的便捷,一般连接字符串不会写在类库文件中
类库文件
namespace EFcoreBooks
{
public class MyDbContext:DbContext
{
//需要怎加一个包含回调的ctor,这个会在web项目进行配置option
public MyDbContext(DbContextOptions<MyDbContext> options):base(options)
{
}
public DbSet<Book> Books { get; set; }
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
base.OnModelCreating(modelBuilder);
modelBuilder.ApplyConfigurationsFromAssembly(typeof(MyDbContext).Assembly);
}
protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
{
//这里没有配置具体连接的string
base.OnConfiguring(optionsBuilder);
}
}
}
web项目
//这里我们通过依赖注入的形式去获取我们的Dbcontext实例对象
builder.Services.AddDbContext<EFcoreBooks.MyDbContext>(options =>
{
var opt = builder.Configuration.GetSection("conString").Value;
options.UseSqlServer(opt);
});
控制器
private readonly EFcoreBooks.MyDbContext _dbContext;
//直接在构造函数注入即可
public TestDbController(EFcoreBooks.MyDbContext dbContext)
{
_dbContext = dbContext;
}
最神人的一点,关于Migration
migration需要用到运行时环境,所以我们不得不在类库文件中建立一个返回Dbcontext的方法,用于获取连接数据库信息
namespace EFcoreBooks
{
//这个类用于在设计时创建DbContext实例,比如在使用EF Core工具进行迁移时
//不会在生产环境运行
internal class MyDbcontextDesingnFactory : IDesignTimeDbContextFactory<MyDbContext>
{
public MyDbContext CreateDbContext(string[] args)
{
DbContextOptionsBuilder<MyDbContext> optionsBuilder = new DbContextOptionsBuilder<MyDbContext>();
optionsBuilder.UseSqlServer("Data Source=DESKTOP-BOB3CAO;Initial Catalog=AspDB;Integrated Security=True;TrustServerCertificate=True");
return new MyDbContext(optionsBuilder.Options);
}
}
}
//反正它要一个Dbcontext才能进行迁移操作


关于Filter
IAsyncExceptionFilter全局异常处理器
我的理解它作为一个全局异常监听器,实现了在业务中try catch的功能,完成切片编程,书写日志,异常处理剥离出去 具体写法
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.Filters;
namespace 多层EFcore
{
public class MyExceptionFilter : IAsyncExceptionFilter
{
private readonly IWebHostEnvironment _myEnv;
public MyExceptionFilter(IWebHostEnvironment webInfo)
{
_myEnv = webInfo;
}
public Task OnExceptionAsync(ExceptionContext context)
{
//代表异常已处理,防止被其他中间件再次处理
//context.ExceptionHandled = true;
//返回json格式的错误信息
//context.Result = new Microsoft.AspNetCore.Mvc.JsonResult(new
//{
// code = 500,
// msg = "服务器异常,请联系管理员"
//});
//根据环境变具体的报错信息
//context.Exception
string mes;
if (_myEnv.IsDevelopment())
{
mes = context.Exception.ToString();
}
else
{
mes = "服务器异常,请联系管理员";
}
ObjectResult result = new ObjectResult(new { code=500,mesg=mes });
context.Result = result;
context.ExceptionHandled = true;
return Task.CompletedTask;
}
}
}
注册
builder.Services.Configure<MvcOptions>(o =>
{
o.Filters.Add<MyExceptionFilter>();
o.Filters.Add<ExceptionFilter>();
//这里执行顺序是颠倒的
});
当然它的优先级是不如try catch的
[HttpGet]
public ActionResult<string> TestException()
{
try
{
throw new Exception("测试异常过滤器");
}
catch
{
return Ok("lalala");
}
}
IAsyncActionFilter全局事件监听器
它和Exception的顺序不一样,是按照注册顺序执行的当 context.Result 被赋值后,ASP.NET Core 的过滤器管道会立即中断当前请求的处理,直接返回给客户端,不会执行后续的过滤器或Action方法。
示例
using Microsoft.AspNetCore.Mvc.Filters;
namespace 多层EFcore
{
public class MyActionFilter : IAsyncActionFilter
{
public async Task OnActionExecutionAsync(ActionExecutingContext context, ActionExecutionDelegate next)
{
//当存在多个过滤器时,按注册顺序执行 前部分1-2-3 执行Action 后部分 3-2-1
//他会把所以的前部分代码执行完毕之后再去执行实际的Action方法,最终执行完之后反过来执行后部分代码
Console.WriteLine("1这是MyActionFilter的前部分");
ActionExecutedContext executedContext = await next();
if(executedContext.Exception == null)
{
Console.WriteLine("1当前不存在异常");
}
else
{
Console.WriteLine("Error");
}
}
}
}
关于小案例 自动启用事务
using Microsoft.AspNetCore.Http.HttpResults;
using Microsoft.AspNetCore.Mvc.Controllers;
using Microsoft.AspNetCore.Mvc.Filters;
using System.Transactions;
namespace 多层EFcore
{
public class TransActionScopeFilter : IAsyncActionFilter
{
public async Task OnActionExecutionAsync(ActionExecutingContext context, ActionExecutionDelegate next)
{
bool isTx = false;
//context.ActionDescriptor Action的描述信息
//context.ActionArguments Action的参数信息
ControllerActionDescriptor ctrActionDescriptor = (ControllerActionDescriptor)context.ActionDescriptor;
if (ctrActionDescriptor != null)
{
//ctrActionDescriptor.MethodInfo 这里的methodinfo就是具体的action方法信息
bool isMarked = ctrActionDescriptor.MethodInfo.GetCustomAttributes(typeof(NotTransactionsAttribute),false).Any();
isTx = !isMarked;
}
if (isTx)
{
//TransactionScopeAsyncFlowOption.Enabled在异步方法中使用事务
using (TransactionScope tcs = new TransactionScope(TransactionScopeAsyncFlowOption.Enabled))
{
//相当于把Action方法包在里面执行了,既然抱在里面执行,那么就可以实现事务操作
var result = await next();
if(result.Exception == null)
{
tcs.Complete();
}
}
}
else
{
await next();
}
}
}
}
小案例 限流器
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.Filters;
using Microsoft.Extensions.Caching.Memory;
namespace 多层EFcore
{
public class RateLimitFilter : IAsyncActionFilter
{
private readonly IMemoryCache _memoryCache;
public RateLimitFilter(IMemoryCache chche)
{
_memoryCache = chche;
}
public async Task OnActionExecutionAsync(ActionExecutingContext context, ActionExecutionDelegate next)
{
string ip = context.HttpContext.Connection.RemoteIpAddress.ToString();
string cacheKey = $"lastTick_{ip}";
if (_memoryCache.Get<object>(cacheKey) == null)
{
_memoryCache.Set(cacheKey, new object(), TimeSpan.FromSeconds(1));
await next();
}
else
{
context.Result = new ObjectResult("请求过于频繁,请稍后再试") { StatusCode = 429 };
}
}
}
}
中间件



中间件类
using Microsoft.AspNetCore.Http;
using System.Text.Json;
using System.Text.Json.Serialization;
namespace MidWare
{
public class Program
{
public static void Main(string[] args)
{
var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();
app.Map("/test",async (pipeBuilder) =>
{
pipeBuilder.UseMiddleware<CheckMiddleWare>();
pipeBuilder.Run(async (context) =>
{
await context.Response.WriteAsync(JsonSerializer.Serialize(context.Items["data"]));
});
});
app.Run();
}
}
}
namespace MidWare
{
public class CheckMiddleWare
{
//下一个中间件
private readonly RequestDelegate _next;
public CheckMiddleWare(RequestDelegate next)
{
_next = next;
}
/// <summary>
/// 执行的中间件方法
/// </summary>
/// <param name="context">全管道通用的http全体信息</param>
/// <returns></returns>
public async Task InvokeAsync(HttpContext context)
{
string password = context.Request.Query["password"];
if( !string.IsNullOrEmpty(password))
{
if (context.Request.HasJsonContentType())
{
var stream = context.Request.BodyReader.AsStream();
dynamic? obj = await System.Text.Json.JsonSerializer.DeserializeAsync<dynamic>(stream);
context.Items["data"] = obj;
//继续执行下一个中间件
await _next.Invoke(context);
}
}
else
{
context.Response.StatusCode = 401;
}
}
}
}
参考规则
var app = builder.Build();
// 1. 全局中间件(所有请求)
app.UseExceptionHandler();
app.UseHttpsRedirection();
app.UseStaticFiles();
// 2. 特定路由分支
app.Map("/api", apiBuilder => { ... });
app.Map("/admin", adminBuilder => { ... });
app.Map("/signalr", signalRBuilder => { ... });
// 3. 回退处理(SPA 应用常用)
app.Run(async (context) =>
{
// 对于单页应用,返回 index.html
context.Response.ContentType = "text/html";
await context.Response.SendFileAsync("wwwroot/index.html");
});

关于Identity标识组件

该组件使用EFCore进行工作,你可以理解为微软帮我们做好了一个单一数据库,专门用于处理用户登录认证等问题
默认提供两个实体类,IdentityRole,IdentityUser,这里的long是主键,你可以继承它们对他们进行扩写,当然它们已经包含了一部分的基础信息如账号密码等
此外你的Dbcontext需要继承 IdentityDbContext<MyUser,MyRole,long> long代表验证实体表主键
接下来就算众多配置相关 直接照抄即可,下面的opt可以对登录信息账号规则进行配置
builder.Services.AddDataProtection();
builder.Services.AddIdentityCore<MyUser>(opt =>
{
opt.Password.RequiredLength = 4;
opt.Password.RequireNonAlphanumeric = false;
opt.Password.RequireUppercase = false;
opt.Password.RequireLowercase = false;
opt.Password.RequireDigit = false;
opt.Tokens.PasswordResetTokenProvider = TokenOptions.DefaultProvider;
opt.Tokens.EmailConfirmationTokenProvider = TokenOptions.DefaultProvider;
});
// 进行配置关系
IdentityBuilder identityBuilder = new IdentityBuilder(typeof(MyUser), typeof(MyRole), builder.Services);
identityBuilder.AddEntityFrameworkStores<MyDbContext>().AddDefaultTokenProviders().AddUserManager<UserManager<MyUser>>().AddRoleManager<RoleManager<MyRole>>();
其中 UserManager 和 RoleManager 提供了许多操作数据库相关查询认证操作,比我们自己手写方便多了
以下为一个简单的登录检测系统
public async Task<ActionResult<string>> Login(AccountLogin account)
{
MyUser findUser = await _userManager.FindByNameAsync(account.UserName);
if(findUser == null)
{
return BadRequest("User not found.");
}
if(await _userManager.IsLockedOutAsync(findUser))
{
return BadRequest("User is locked out.");
}
bool passwordValid = await _userManager.CheckPasswordAsync(findUser, account.PassWord);
if (!passwordValid)
{
await _userManager.AccessFailedAsync(findUser);
return BadRequest("Invalid password.");
}
return Ok("Login successful.");
}
关于重置Token(验证码)
[HttpGet]
public async Task<ActionResult<string>> ResetPassWordToken(string name)
{
MyUser findUser = await _userManager.FindByNameAsync(name);
if (findUser == null)
{
return BadRequest("User not found.");
}
string myChangeToken = await _userManager.GeneratePasswordResetTokenAsync(findUser);
return Ok(myChangeToken);
}
[HttpPost]
public async Task<ActionResult<string>> ReSetPassWord(string AccountName,string PassWord,string token)
{
MyUser findUser = await _userManager.FindByNameAsync(AccountName);
if (findUser == null)
{
return BadRequest("User not found.");
}
IdentityResult resetRe = await _userManager.ResetPasswordAsync(findUser, token, PassWord);
if (!resetRe.Succeeded)
{
return BadRequest(string.Join(",", resetRe.Errors.Select(e => e.Description)));
}
return Ok("Password reset successfully.");
}
这个token是系统更具安全令牌自动更新生成的,当密码变化等一系列操作进行时,会更改令牌的值,会和再次生成的token不一致,导致令牌失效
令牌的动态生成 Token = 算法(用户ID + 时间戳 + 安全戳)
每次调用 GeneratePasswordResetTokenAsync 都会重新计算,不是从存储中读取 安全优势 自动失效:密码修改后,所有未使用的重置链接立即失效
无状态:服务器不需要维护令牌状态
防重放:通常设计为一次性使用
JWT(Json Web Token)
为什么JWT比Session更好
首先Session为了面对分布式服务器时,需要一台中间服务器进行校验,本身它是存储在本地的一组键值对,不同服务器本地内存key自然不一样,客户端存储key和服务端进行对比,相同极为同一个用户一个典型的 JWT 长这样:
xxxxxxx.yyyyyyy.zzzzzzz 部分 用途 示例内容 Header(头部) 指定算法和类型 {"alg":"HS256","typ":"JWT"} Payload(负载) 存放声明(Claims),如用户 ID、过期时间等 {"sub":"1234567890","name":"Alice","exp":1600000000} Signature(签名) 保证前两部分不被篡改 HMACSHA256(Base64Url(Header) + "." + Base64Url(Payload), Secret)
3. 关于数据校验,是Token前两部分,即头部+payload+服务端key,结合算法算出第三部分的令牌,可以有效防止本地明文payload被更改
4. 吓哭了的配置
也没必要全部看懂,了解一下Claim中的内容是写入JWT的payload中的,由于payload的明文的最好不要写入重要信息 5. 吓哭了的解析
6. 当然我们的微软爹还是帮我们封装了
虽然我感觉还是很烦人
//配置系统直接拿数据
builder.Services.Configure<JWTconfig>(builder.Configuration.GetSection("JWT"));
//这里是添加一种认证方式
//下面的Bearer是一种令牌模式
//然后就获取key的字节数组,再由这个字节数组创建对应Token(此时还没生成,验证Token相关,配置规则),下面那一堆是认证相关
builder.Services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme).AddJwtBearer(x =>
{
var jwtSettings = builder.Configuration.GetSection("JWT").Get<JWTconfig>();
byte[] keyBytes = Encoding.UTF8.GetBytes(jwtSettings.Key);
var issuerSigningKey = new SymmetricSecurityKey(keyBytes);
x.TokenValidationParameters = new()
{
ValidateIssuer = false,
ValidateAudience = false,
ValidateLifetime = true,
ValidateIssuerSigningKey = true,
IssuerSigningKey = issuerSigningKey
};
});
app.UseAuthentication();记得在授权之前使用认证中间件
using Microsoft.AspNetCore.Mvc;
using Microsoft.EntityFrameworkCore.Metadata.Internal;
using Microsoft.Extensions.Options;
using System.IdentityModel.Tokens.Jwt;
using System.Security.Claims;
namespace MyIdentity.Controllers
{
[Route("api/[controller]/[action]")]
[ApiController]
public class JWTLoginController : Controller
{
private readonly DbContext _context;
private readonly UserManager<MyUser> _userManager;
private readonly RoleManager<MyRole> _roleManager;
private readonly IOptionsSnapshot<JWTconfig> optionsSnapshot;
public JWTLoginController(UserManager<MyUser> userManager, RoleManager<MyRole> roleManager, IOptionsSnapshot<JWTconfig> optionsSnapshot)
{
this.optionsSnapshot = optionsSnapshot;
_roleManager = roleManager;
_userManager = userManager;
}
[HttpPost]
public async Task<ActionResult<string>> GetUser(string name,string password)
{
MyUser u1 = await _userManager.FindByNameAsync(name);
if (u1 != null)
{
if(await _userManager.CheckPasswordAsync(u1, password))
{
if(!await _roleManager.RoleExistsAsync("Super"))
{
var roleM = await _roleManager.CreateAsync(new MyRole() { Name = "Super"});
}
await _userManager.AddToRoleAsync(u1, "Super");
List<Claim> claims = new List<Claim>();
claims.Add(new Claim(ClaimTypes.Email,"Decade_2568@outlook.com"));
claims.Add(new Claim(ClaimTypes.Name,name));
claims.Add(new Claim(ClaimTypes.Role, (await _userManager.GetRolesAsync(u1)).FirstOrDefault()));
Console.WriteLine((await _userManager.GetRolesAsync(u1)).FirstOrDefault());
string key = optionsSnapshot.Value.Key;
DateTime delayTime = DateTime.Now.AddSeconds(optionsSnapshot.Value.ExpireDate);
byte[] secBytes = Encoding.UTF8.GetBytes(key);
var secKey = new SymmetricSecurityKey(secBytes);
var credentials = new SigningCredentials(secKey, SecurityAlgorithms.HmacSha256Signature);
var tokenDescriptor = new JwtSecurityToken(claims: claims,
expires: delayTime, signingCredentials: credentials);
string jwt = new JwtSecurityTokenHandler().WriteToken(tokenDescriptor);
return Ok(jwt);
}
else
{
return BadRequest("User already exists, but password is incorrect.");
}
}
return BadRequest("User not found.");
}
}
}
通过HttpContext可以传递JWT,他会解析并且你可以直接获取明文json的内容
public ActionResult<string> GetInfo()
{
//从上下文获取对应请求所有内容
//取出用户访问时的ClaimType信息
Claim cl = this.User.FindFirst(ClaimTypes.Name);
Claim c2 = this.User.FindFirst(ClaimTypes.Email);
return Ok($"Hello, {cl?.Value} + {c2?.Value}");
}
8.在方法或者类前面加上[Authorize]或者类似[Authorize(Roles = "Role")],代表当前方法只能被正确的JWT登录访问,并且会查看角色数据(claims.Add(new Claim(ClaimTypes.Role, (await _userManager.GetRolesAsync(u1)).FirstOrDefault()));)也就是在生成时声明的
[Authorize(Roles = "admin")]
public ActionResult<string> GetSecretInfo()
{
return Ok("This is a secret info only for authorized admins.");
}

关于后台运行,托管服务HostedService



我们的WebApi项目是依赖前端的请求到中间件处理对应函数在返回响应的,对于自身没法运行代码
为了解决例如定时导出数据库用户这种需求,.Net提供了BackgroundService,执行自定义命令的类可以继承这个
using Microsoft.AspNetCore.Identity;
namespace AspDotCore.Manager;
public class HostedService1 : BackgroundService
{
private readonly IServiceScope serviceScope;
public HostedService1(IServiceScopeFactory scopeFactory)
{
serviceScope = scopeFactory.CreateScope();
}
//这个函数就是在后台运行的代码
protected async override Task ExecuteAsync(CancellationToken stoppingToken)
{
var myre = serviceScope.ServiceProvider.GetRequiredService<UserManager<MyUser>>();
}
public override void Dispose()
{
serviceScope.Dispose();
base.Dispose();
}
}
由于这玩意本身就是要DI的所以不能注入非singleton服务,所以需要自己实现一个IServiceScope,注意要实现Dispose,这样创建的都具有当前实例的Scope了
builder.Services.AddHostedService<HostedService1>();
关于参数校验
.Net本身实现了使用特性在实体类中进行标注的形式来限制输入,但是和EFCore一样,破坏了类的单一性,并且没法自定义报错信息
我们使用第三方FluentValidation,配置很类似FluentApi 官网:https://docs.fluentvalidation.net/en/latest/
//一样的导入程序集
builder.Services.AddValidatorsFromAssemblyContaining<TestRecordvalidation>();
//对实体类的配置类
using AspDotCore.Model;
using FluentValidation;
namespace AspDotCore.Validation;
public class TestRecordvalidation : AbstractValidator<TestRecord>
{
public TestRecordvalidation()
{
//对于每一个配置都可以加上withMessage来指定错误信息
RuleFor(x => x.Username).NotNull();
RuleFor(x => x.Name).Length(0, 10);
RuleFor(x => x.Email).EmailAddress();
RuleFor(x => x.Age).InclusiveBetween(18, 60);
}
}
不得不评鉴的就是private IValidator _validator; 在控制器还是要validator对象,这样就能获取我们配置的规则了
ValidationResult result = await _validator.ValidateAsync(msg);
if (result.IsValid)
{
return Ok("Validation Passed");
}
else
{
return BadRequest(result);
}
关于WebSocket和SignalR
前端代码 需要安装npm install @microsoft/signalr
<template>
<div>
<input type="text" v-model="useState.userMessage" @keypress="textMesOnKeypress"/>
<div>
<ul v-for="(item, index) in useState.userList" :key="index">
<li>{{ item }}</li>
</ul>
</div>
</div>
</template>
<script setup>
import { ref,reactive,onMounted } from "vue";
import * as signalR from "@microsoft/signalr";
let connection;
const useState = reactive({
userMessage: '',
userList: []
});
const textMesOnKeypress = async (e) => {
if (e.key === 'Enter') {
if (connection && connection.state === signalR.HubConnectionState.Connected) {
//SendPublicMessageAsync 这个是服务端的HUB方法 useState.userMessage这个就是服务器端方法的参数
await connection.invoke("SendPublicMessageAsync", useState.userMessage);
useState.userMessage = '';
}
}
};
onMounted(async () => {
connection = new signalR.HubConnectionBuilder()
.withUrl("http://localhost:5001/MyHub") //这个地址是服务端的地址
.withAutomaticReconnect()
.build();
await connection.start();
//ReceiveMessage 这个是服务端返回的消息
connection.on("ReceiveMessage", (message) => {
useState.userList.push(message);
})});
</script>
<style lang="scss" scoped>
</style>
后端代码
using Microsoft.AspNetCore.SignalR;
namespace SignalR_WebSocket.WebSocket;
public class MyHub : Hub
{
public async Task SendPublicMessageAsync(string message)
{
string connectionId = Context.ConnectionId;
string messageToSend = $"{connectionId}: {message}";
//群发消息
await Clients.All.SendAsync("ReceiveMessage", messageToSend);
}
}
首先得了解WebSocket协议和Http协议是平级的,Http是单工通讯的无状态的,得客户端发送它才能进行响应,而WebSocket就是长连接通讯了双工的,服务器可以随时向客户端发送消息
我们把SignalR集成到ApsDotNet中,这样共用同一端口,方便连接使用
记得配置跨域和中间件,websocket连接是后面升级的,服务端获取到客户端的内容主动升级连接
string[] urls = new string[] { "http://localhost:5173" };
builder.Services.AddCors(option =>
{
option.AddDefaultPolicy(builde=>{builde.WithOrigins(urls).AllowAnyMethod().AllowAnyHeader().AllowCredentials();});
});
builder.Services.AddSignalR();
//中间件分配一下app.MapHub<MyHub>("/MyHub");
//客户端发送的
GET /chat HTTP/1.1
Host: server.example.com
Upgrade: websocket
Connection: Upgrade ← 关键:要求升级协议
Sec-WebSocket-Key: x3JJHMbDL1EzLkh9GBhXDw==
Sec-WebSocket-Version: 13

SignalR身份认证
#region JWT令牌
builder.Services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme).AddJwtBearer(x =>
{
var jwtSettings = "dwafeasfgjeoiguq093-erie321oprk9-38ru932i4kreo[32lk]pod0wiod-0=213";
byte[] keyBytes = Encoding.UTF8.GetBytes(jwtSettings);
var issuerSigningKey = new SymmetricSecurityKey(keyBytes);
x.TokenValidationParameters = new()
{
ValidateIssuer = false,
ValidateAudience = false,
ValidateLifetime = true,
ValidateIssuerSigningKey = true,
IssuerSigningKey = issuerSigningKey
};
//为JWT提供Token 反正就是不支持报文头传递,就使用查询字符串
x.Events = new JwtBearerEvents()
{
OnMessageReceived = context =>
{
var accessToken = context.Request.Query["access_token"];
var path = context.Request.Path;
if (string.IsNullOrEmpty(accessToken) && path.StartsWithSegments("/MyHub"))
{
context.Token = accessToken;
}
return Task.CompletedTask;
}
};
});
先装axios吧 npm install axios
//给不同用户分发消息 前端
<template>
<div>
用户名:<input v-model="messages.userName" type="text" />
密码:<input v-model="messages.passWord" type="password" />
消息: <input v-model="messages.singleMes" type="text"/>
私聊对象Name<input v-model="messages.BeSendName" type="text"/>
<button @click="GetJWTToken">连接服务器</button>
<button @click="SendMesg" :disabled="!isConnected">发送消息</button>
<div>连接状态: {{ connectionStatus }}</div>
<ul>
<li v-for="(value, index) in messages.ServerMessages" :key="index">
{{ value }}
</li>
</ul>
</div>
</template>
<script setup>
import * as signalR from "@microsoft/signalr";
import { onMounted, ref, reactive, onBeforeUnmount } from 'vue';
import axios from 'axios';
let connection = null;
const isConnected = ref(false);
const connectionStatus = ref('未连接');
const messages = reactive({
ServerMessages: [],
singleMes:'',
userName: '',
passWord: '',
Token: '',
BeSendName:''
});
// 配置对象
const getOptions = () => ({
skipNegotiation: true,
transport: signalR.HttpTransportType.WebSockets,
accessTokenFactory: () => messages.Token
});
// 发送消息方法
const SendMesg = async () => {
if (!isConnected.value || !connection) {
alert("请先连接服务器");
return;
}
if (!messages.BeSendName || !messages.singleMes) {
alert("请输入私聊对象和消息");
return;
}
try {
await connection.invoke("SendToSingle", messages.BeSendName, messages.singleMes);
messages.singleMes = ''; // 清空消息输入框
} catch (error) {
console.error('发送消息失败:', error);
alert('发送消息失败: ' + error.message);
}
}
// 初始化SignalR连接
const initializeSignalRConnection = () => {
if (connection) {
connection.stop();
}
connection = new signalR.HubConnectionBuilder()
.withUrl("http://localhost:5100/MyHub", getOptions())
.withAutomaticReconnect()
.configureLogging(signalR.LogLevel.Information)
.build();
// 连接事件处理
connection.onreconnecting(() => {
connectionStatus.value = '重新连接中...';
isConnected.value = false;
});
connection.onreconnected(() => {
connectionStatus.value = '已重新连接';
isConnected.value = true;
});
connection.onclose(() => {
connectionStatus.value = '连接已断开';
isConnected.value = false;
});
// 接收消息处理
connection.on("ReceiveMessage", (message, sender) => {
messages.ServerMessages.push(`${message} 是 ${sender} 发来的`);
});
return connection;
};
// 登录方法
const GetJWTToken = async () => {
if (!messages.userName || !messages.passWord) {
alert("请输入用户名和密码");
return;
}
try {
const response = await axios.post('http://localhost:5100/api/TestJWT/Login', {
userName: messages.userName,
password: messages.passWord
});
messages.Token = response.data;
alert('登录成功,正在连接SignalR...');
// 初始化并启动SignalR连接
const connection = initializeSignalRConnection();
try {
await connection.start();
connectionStatus.value = '已连接';
isConnected.value = true;
alert('SignalR连接成功');
} catch (err) {
console.error('SignalR连接失败:', err);
connectionStatus.value = '连接失败';
alert('SignalR连接失败: ' + err.message);
}
} catch (error) {
console.error('登录失败:', error);
if (error.response) {
console.log('错误状态码:', error.response.status);
console.log('错误数据:', error.response.data);
alert(`登录失败: ${error.response.data?.message || '用户名或密码错误'}`);
} else if (error.request) {
console.log('无响应:', error.request);
alert('网络错误,请检查连接');
} else {
console.log('错误:', error.message);
alert('请求配置错误');
}
}
}
// 组件卸载时断开连接
onBeforeUnmount(() => {
if (connection) {
connection.stop();
}
});
</script>
<style scoped>
div {
padding: 20px;
}
input {
margin: 5px;
padding: 8px;
border: 1px solid #ccc;
border-radius: 4px;
}
button {
margin: 10px 5px;
padding: 10px 20px;
background-color: #4CAF50;
color: white;
border: none;
border-radius: 4px;
cursor: pointer;
}
button:hover {
background-color: #45a049;
}
button:disabled {
background-color: #ccc;
cursor: not-allowed;
}
ul {
list-style-type: none;
padding: 0;
}
li {
padding: 8px;
margin: 5px 0;
background-color: #f9f9f9;
border-left: 4px solid #4CAF50;
}
</style>
//hub
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Identity;
using Microsoft.AspNetCore.SignalR;
namespace AspDotCore.Hub;
[Authorize]
public class MyHub : Microsoft.AspNetCore.SignalR.Hub
{
private readonly UserManager<MyUser> _userManager;
public MyHub(UserManager<MyUser> userManager)
{
_userManager = userManager;
}
// 客户端调用这个方法发送消息
/// <summary>
///
/// </summary>
/// <param name="clientName">传入对象name</param>
/// <param name="message"></param>
public async Task SendToSingle(string clientName,string message)
{
//和发JWT的地方是有关系的,发的地方声明了Claim Identity属性他存储的key是那个
var target = await _userManager.FindByNameAsync(clientName);
var nowObj = Context.User.Identity.Name;
await Clients.User(target.Id.ToString()).SendAsync("ReceiveMessage", message, nowObj);
}
}
不一定对,但前端确实是这么写的,并且这样好理解 标准连接流程: 第一次请求(协商/Negotiate) 客户端通过 普通 HTTP POST 访问 /hub/negotiate 端点 服务器返回包含连接令牌的协商响应: JSON 复制 { "connectionId": "xxx", "connectionToken": "加密令牌", // 核心凭证 "availableTransports": ["WebSockets", "ServerSentEvents"] } 第二次请求(正式连接) 客户端使用获取的 connectionToken 和 connectionId 发起 WebSocket 连接(或其他传输方式): wss://server/hub?id=connectionId&connectionToken=xxx 此时才建立 SignalR 的持久双向通道
第一次(协商/Negotiate) POST http://localhost:5100/MyHub/negotiate 这是自动添加的 /negotiate 端点 使用普通 HTTP POST 请求 返回连接令牌等信息
第二次(正式连接) ws://localhost:5100/MyHub?id=xxx&access_token=yyy 路径仍然是 /MyHub,不是其他地址 协议升级为 WebSocket(ws:// 或 wss://) 自动附加 id 和 access_token 等查询参数 你的代码实际做了什么: .withUrl("http://localhost:5100/MyHub") 这个配置会同时用于两次请求: SignalR 客户端自动在协商阶段在 URL 后添加 /negotiate 在连接阶段保持原路径,仅修改协议和参数
服务器软件的具体"包办"事项 技术层面包办: 网络通信:TCP/IP握手、HTTP协议解析
并发管理:线程池、异步处理
资源管理:内存分配、连接池
安全防护:防DDoS、SQL注入防护
监控日志:性能监控、访问日志
DDD领域驱动设计

领域可以简单理解为当前所从事的具体内容
内容可以细分为子领域
领域之间的优先级也不同 核心领域 支撑领域 通用领域,当然这是随着你的业务来决定的

对于开发而言,首先应该有业务思维,把领域中对象提取进行建模,从业务角度看待开发
我们的项目应该开始于创建领域模型,而不是考虑如何设计数据库和编写代码。使用领域模型,我们可以一直用业务语言去描述和构建系统,而不是使用技术人员的语言

流水线代码

表达内容不要存在歧义导致理解问题

存在唯一标识符的对象一般看做实体,实体能够独立存在
值对象做为实体的一部分属性依赖存在

实现高内聚低耦合是关键,聚合就是一堆有整体和部分关系的实体组合,其中选择一个当聚合根与外部进行交互

领域服务就是聚合内部的业务逻辑,比如让一个实体属性变更,并不包含与另一个聚合接触的业务
第一步,准备业务操作所需要的数据,第二步,执行由一个或者多个领域模型做出的业务操作,这些操作会修改实体的状态,或者生成一些操作结果。第三步,把对实体的改变或者操作结果应用于外部系啄

充血模型&&实现值对象

首先为了保证实体的安全性,我们只允许内部更改成员属性
有参构造保证了,存在当前实例的时候对应字段不为null
比如密码的散列值,我们不需要实际使用它,但是在数据库必须存在这么个东西
只读属性很好理解
不需要存在数据库的字段

对于值对象
首先它是不存在标识符的,也是不能脱离实体单独存在的

我们对一些适合合体的属性最好把他们聚集起来,方便标识实体的一些内容
那钱来举例子Curreny这个类型包含int count数量和enum 币种两个内容,它们能确保实体能够正常无歧义的进行操作

//针对枚举类型 在数据中存储的内容,因为枚举本身就是int的变种所以在数据库存的也是整形类型
builder.Property(o => o.CurrentUser).HasConversion<string>();
//对于我们自定义类型进行处理,会把当前我们自定义对象的属性拿出来放到主表中,毕竟这是**从属实体类型**不是单独存在的
builder.OwnsOne(o=>o.GeoInfo);
当然你可以进一步对从属类型的字段信息进行配置
实现聚合

我的理解就是一个主要实体(聚合根)包含了关系紧密的相关实体,它们组成了一个聚合对象,在EFcore中能实现更好的一致性的事务

MediatR

//注册服务
services.AddMediatR(cfg => cfg.RegisterServicesFromAssembly(typeof(Startup).Assembly));
//传输接口
INotification
//调用接口
INotificationHandler<>
//实际发布事件
_mediator.Publish(new TransferData("not bad"));
关于RabbitMQ
由于我们要实现集成事件,也就是跨微服务的事件,那就不能使用传统的消息队列观察者了,我们此时需要一个中间服务,从A服务发送消息,B服务接受并处理消息流程,我们使用RabbitMQ来实现我们的需求

信道,连接到RabbitMQ存在一个物理的TCP连接,但是我们的服务可能是断断续续发布的,所以存在虚拟管道来连接
队列,一般只存在于消费者方,用于存放发送方发过来的字节数组,通过用户自定义的规则把交换机中的内容放到对应的管道中

使用RabbitMQ先安装RabbitMQ.Client https://www.nuget.org/packages/RabbitMQ.Client** 业务代码 生产者
static async Task Main(string[] args)
{
var factory = new ConnectionFactory();
factory.HostName = "127.0.0.1";//RabbitMQ服务器地址
string exchangeName = "exchange1";//交换机的名字
string eventName = "myEvent";// routingKey的值
using var conn = await factory.CreateConnectionAsync();
while(true)
{
string msg = DateTime.Now.TimeOfDay.ToString();//待发送消息
using (var channel = await conn.CreateChannelAsync())//创建信道
{
// var properties = channel.CreateBasicProperties();
// properties.DeliveryMode = 2;
await channel.ExchangeDeclareAsync(exchange: exchangeName, type: "direct");//声明交换机 有这个交换机就直接用,没有会自动创建一个
byte[] body = Encoding.UTF8.GetBytes(msg);
await channel.BasicPublishAsync(exchange: exchangeName,routingKey: eventName,
mandatory: true,body: body);//发布消息
}
Console.WriteLine("发布了消息:" + msg);
Thread.Sleep(1000);
}
}
消费者
static async Task Main(string[] args)
{
//对于这个该死的交换机,如果发消息时不存在对于的消息队列,它会把收到的消息当作垃圾进行处理,所以在消息发送之前必须要进行消息队列的创建
var factory = new ConnectionFactory();
factory.HostName = "127.0.0.1";
//factory.DispatchConsumersAsync = true;
string exchangeName = "exchange1";
string eventName = "myEvent";
using var conn = await factory.CreateConnectionAsync();
using var channel = await conn.CreateChannelAsync();
string queueName = "queue1";
await channel.ExchangeDeclareAsync(exchange: exchangeName,type: "direct");
await channel.QueueDeclareAsync(queue: queueName,durable: true,
exclusive: false,autoDelete: false,arguments: null);
//把当前队列名称和routKey绑定到指定交换机上,交换机来进行队列分发
await channel.QueueBindAsync(queue: queueName,
exchange: exchangeName,routingKey: eventName);
var consumer = new AsyncEventingBasicConsumer(channel);
//callback方法 执行入队列操作
consumer.ReceivedAsync += Consumer_Received;
//开始监听指定队列的消息 ack是确认消息,确认消息执行完毕
await channel.BasicConsumeAsync(queue: queueName, autoAck: false,consumer: consumer);
Console.ReadLine();
async Task Consumer_Received(object sender, BasicDeliverEventArgs args)
{
try
{
var bytes = args.Body.ToArray();
string msg = Encoding.UTF8.GetString(bytes);
Console.WriteLine(DateTime.Now + "收到了消息" + msg);
await channel.BasicAckAsync(args.DeliveryTag, multiple: false);
await Task.Delay(800);
}
catch (Exception ex)
{
await channel.BasicRejectAsync(args.DeliveryTag, true);//失败重发
Console.WriteLine("处理收到的消息出错"+ex);
}
}
}
洋葱架构
外层显式依赖领域层接口,领域层通过依赖注入(DI)使用外层实现,本质是依赖反转原则(DIP)的落地,咱们结合架构层级拆解清楚,再补实践细节: 先明确核心结论(对应你的表述) 层级 行为 本质 外层(基础设施 / 应用层) 显式依赖领域层接口 外层是「实现方」,必须知道领域层定义的「行为契约」(接口)才能落地实现 领域层(内层核心) 不依赖外层,仅通过 DI 使用实现 领域层是「调用方」,只认接口不认具体实现,实现由外层注册到 DI 容器供其使用 逐层拆解:为什么要这么设计? 洋葱架构从内到外分为「领域层(核心)→ 应用层 → 基础设施层」,依赖规则是「内层不依赖外层,外层依赖内层」,而接口是实现这一规则的关键:
领域层:定义接口(契约),不关心实现 领域层是业务核心,只聚焦「what(要做什么)」,不关心「how(怎么实现)」。它会定义业务所需的接口(比如 用户仓储接口 UserRepository、支付服务接口 PaymentService),这些接口是领域逻辑的「行为契约」,只声明方法签名,不写具体逻辑。
好了对于Domain层而言
不同聚合间的实体不要直接引用
应该标注对应标识符
在domain层应该实现 实体 值对象 聚合根 聚合根内对自身值对象和实体的操作方法 仓储接口(在领域服务调用) 一些自己的服务 领域服务(操作聚合内对象进行业务操作)
//user聚合根举例
using Domain.ValueObject;
using Zack.Commons;
namespace Domain.Entities;
public record User : IAggregateRoot
{
public Guid Id { get; init; }
public PhoneNumber PhoneNumber { get; private set; }
//不被访问
private string? passwordHash;
public UserAccessFail AccessFailed { get; private set; }
public User()
{
}
public User(PhoneNumber phoneNumber)
{
this.PhoneNumber = phoneNumber;
this.Id = Guid.NewGuid();
this.AccessFailed = new UserAccessFail(this);
}
public bool HasPassword()
{
return !string.IsNullOrEmpty(this.passwordHash);
}
public void ChangePassword(string newPassword)
{
if (newPassword.Length < 3)
{
throw new ArgumentException("Password must be at least 3 characters long.", nameof(newPassword));
}
this.passwordHash = HashHelper.ComputeMd5Hash(newPassword);
}
public bool CheckPassword(string password)
{
return this.passwordHash == HashHelper.ComputeMd5Hash(password);
}
public void ChangePhoneNumber(PhoneNumber newPhoneNumber)
{
this.PhoneNumber = newPhoneNumber;
}
}
//领域服务
using Domain.Entities;
using Domain.MyDefinEnum;
using Domain.ValueObject;
using MediatR;
namespace Domain;
//user聚合的领域服务 使用IUserRepository和ISmsCodeSender服务与聚合根对聚合内部进行业务逻辑处理
public class UserDomainService
{
private IUserRepository _userRepository;
private ISmsCodeSender _smsCodeSender;
public UserDomainService(IUserRepository userRepository, ISmsCodeSender smsCodeSender)
{
_userRepository = userRepository;
_smsCodeSender = smsCodeSender;
}
public void ResetAccesFail(User user)
{
user.AccessFailed.Reset();
}
public bool IsLockedOut(User user)
{
return user.AccessFailed.IsLocked();
}
public void Fail(User user)
{
user.AccessFailed.Fail();
}
public async Task<UserAccessResult> CheckPasswordAsync(PhoneNumber phoneNumber, string password)
{
UserAccessResult userAccessResult = new UserAccessResult();
User user = await _userRepository.FindUserAsync(phoneNumber);
if (user == null)
{
userAccessResult = UserAccessResult.PhoneNumberNotFound;
return userAccessResult;
}
else if (IsLockedOut(user))
{
userAccessResult = UserAccessResult.LockOut;
}
else if(user.HasPassword() == false)
{
userAccessResult = UserAccessResult.NoPassword;
}
else if(user.CheckPassword(password))
{
userAccessResult = UserAccessResult.OK;
}
else
{
userAccessResult = UserAccessResult.PasswordNotConfirmed;
return userAccessResult;
}
if (userAccessResult == UserAccessResult.OK)
{
user.AccessFailed.Reset();
}
else
{
user.AccessFailed.Fail();
}
await _userRepository.PublishEventAsync(new UserAccessResultevent(phoneNumber, userAccessResult));
return userAccessResult;
}
public async Task<CheckCodeResult> CheckCodeAsync(PhoneNumber phoneNumber, string code)
{
CheckCodeResult checkCodeResult = new CheckCodeResult();
User? user = await _userRepository.FindUserAsync(phoneNumber);
if (user == null)
{
checkCodeResult = CheckCodeResult.PhoneNumberNotFound;
return checkCodeResult;
}
else if(IsLockedOut(user))
{
return CheckCodeResult.LookOut;
}
string? phoneCode = await _userRepository.FindPhoneNumberCodeAsync(phoneNumber);
if (phoneCode == null)
{
return CheckCodeResult.CodeError;
}
if (phoneCode == code)
{
return CheckCodeResult.Ok;
}
else
{
Fail(user);
return CheckCodeResult.CodeError;
}
}
}
对于Infrastructure层而言
在基建层infrastructure 应该实现 防腐层 Dbcontxt 仓储repository
//基建层实现仓储接口
using Domain;
using Domain.Entities;
using Domain.ValueObject;
using MediatR;
using Microsoft.Extensions.Caching.Distributed;
namespace Infrastracture;
public class UserRepository : IUserRepository
{
private readonly UserDbContext _context;
private readonly IDistributedCache _distributedCache;
private readonly IMediator _mediator;
public UserRepository(UserDbContext context, IDistributedCache distributedCache, IMediator mediator)
{
_context = context;
_distributedCache = distributedCache;
_mediator = mediator;
}
public async Task<User?> FindUserAsync(PhoneNumber phoneNumber)
{
User? findUser = _context.Users.FirstOrDefault(u => u.PhoneNumber.ToString() == phoneNumber.ToString());
return findUser;
}
public async Task<User?> FindUserAsync(Guid userId)
{
User? findUser = _context.Users.FirstOrDefault(u => u.Id.ToString() == userId.ToString());
return findUser;
}
public async Task AddNewLoginHistory(PhoneNumber phoneNumber, string message)
{
User? user = await FindUserAsync(phoneNumber);
Guid? userId = null;
if (user != null)
{
userId = user.Id;
}
_context.UserLoginHistories.Add(new UserLoginHistory(userId, phoneNumber, message));
}
public async Task SavePhoneNumberCodeAsync(PhoneNumber phoneNumber, string code)
{
string key = $"PhoneNumber_{phoneNumber.regionCode}_{phoneNumber.number}";
await _distributedCache.SetStringAsync(key, code,new DistributedCacheEntryOptions()
{
AbsoluteExpirationRelativeToNow = TimeSpan.FromMinutes(5)
});
}
public Task<string?> FindPhoneNumberCodeAsync(PhoneNumber phoneNumber)
{
string key = $"PhoneNumber_{phoneNumber.regionCode}_{phoneNumber.number}";
string? code = _distributedCache.GetString(key);
_distributedCache.Remove(key);
return Task.FromResult(code);
}
public Task PublishEventAsync(UserAccessResultevent userAccessResultevent)
{
return _mediator.Publish(userAccessResultevent);
}
}
对于webapi应用层而言
实现工作单元事务一致性
使用Domain层的领域服务
对领域服务使用基建层的注入
监听事件
实现简单的CRUD,完成连接数据校验等应用服务
//特性 实现工作单元获取方法信息上的DbContext
namespace DDDProject_demo01;
[AttributeUsage(AttributeTargets.Method)]
public class UnitOfWorkAttribute : Attribute
{
public Type[] DbContextTypes { get; set; }
public UnitOfWorkAttribute(params Type[] dbContextTypes)
{
DbContextTypes = dbContextTypes;
}
}
//工作单元实现
using System.Reflection;
using Microsoft.AspNetCore.Http.HttpResults;
using Microsoft.AspNetCore.Mvc.Controllers;
using Microsoft.AspNetCore.Mvc.Filters;
using Microsoft.EntityFrameworkCore;
namespace DDDProject_demo01;
public class UnitOfWorkFilter : IAsyncActionFilter
{
//总结就是获取方法对象上的特性,特性中包含实现的Dbcontext类型,获取到类型后向DI容器要相同的实例对象
public async Task OnActionExecutionAsync(ActionExecutingContext context, ActionExecutionDelegate next)
{
// context.Result 被赋值后,ASP.NET Core 的过滤器管道会立即中断当前请求的处理,直接返回给客户端,不会执行后续的过滤器或Action方法。
// ActionExecutionDelegate这个是webapi委托
var result = await next();
if (result.Exception != null) //执行完成进行事务保存
{
return;
}
var actionDescriptor = context.ActionDescriptor as ControllerActionDescriptor;
if (actionDescriptor == null)
{
return;
}
//获取特性对象
var uwAttributes = actionDescriptor.MethodInfo.GetCustomAttribute<UnitOfWorkAttribute>();
if (uwAttributes == null)
{
return;
}
foreach (var dbContext in uwAttributes.DbContextTypes)
{
//从DI容器获取Context实例对象 RequestServices 获取DI服务 这里的dbContext是被注册的DI类型
var dbCtx = context.HttpContext.RequestServices.GetService(dbContext) as DbContext;
if (dbCtx != null)
{
await dbCtx.SaveChangesAsync();
}
}
}
}
//使用领域服务
using DDDProject_demo01.RequestEntity;
using Domain;
using Domain.MyDefinEnum;
using Infrastracture;
using Microsoft.AspNetCore.Mvc;
using Microsoft.EntityFrameworkCore;
namespace DDDProject_demo01.Controllers;
[ApiController]
[Route("api/[controller]/[action]")]
public class TestController : ControllerBase
{
private readonly UserDomainService _userDomainService;
public TestController(UserDomainService userDomainService)
{
this._userDomainService = userDomainService;
}
[HttpPost]
public async Task<ActionResult> LoginByPhoneAndPwd(LoginByPhoneAndRequest request)
{
if(request.password.Length<3)
return BadRequest("password is too short.");
var phoneNumber = request.phoneNumber;
var result = await _userDomainService.CheckPasswordAsync(phoneNumber, request.password);
switch (result)
{
case UserAccessResult.OK:
return Ok("Login successful");
case UserAccessResult.NoPassword:
case UserAccessResult.PasswordNotConfirmed:
case UserAccessResult.PhoneNumberNotFound:
return BadRequest("Wrong password");
case UserAccessResult.LockOut:
return BadRequest("Lock out");
default:
throw new NotImplementedException();
}
}
}
方面 领域服务 应用服务
位置 领域层(domain包) 应用层(application包)
职责 封装领域逻辑,业务规则 协调工作流,用例实现
业务逻辑 包含丰富的业务逻辑 不包含核心业务逻辑
技术关注 不关心技术实现 管理事务、安全、日志等
状态 无状态 无状态
依赖 只依赖领域对象和其他领域服务 依赖领域服务、仓储、外部服务
可测试性 纯业务逻辑,易于单元测试 需要模拟外部依赖
java
// Controller应该很薄,只做HTTP相关的事情
@RestController
@RequestMapping("/orders")
public class OrderController {
private final OrderApplicationService orderAppService; // 注入真正的应用服务
// ✅ 正确做法:Controller只做HTTP层的事情
@PostMapping
public ResponseEntity<OrderResponse> createOrder(
@RequestBody @Valid CreateOrderRequest request) {
// 1. 参数校验(使用JSR-303注解自动完成)
// 2. 调用应用服务
OrderResponse response = orderAppService.createOrder(request);
// 3. 返回HTTP响应
return ResponseEntity.created(URI.create("/orders/" + response.getId()))
.body(response);
}
}
// ✅ 应用服务在单独的类中
@Service
@Transactional
public class OrderApplicationService {
// 真正的应用服务逻辑在这里
public OrderResponse createOrder(CreateOrderRequest request) {
// 业务协调逻辑在这里
}
}
进行nginx配置

//因为在开发环境微服务的地址都一样,所以得区分端口
#进行反向代理,当客户端向nginx发送IdentityService请求时,自动向http://localhost:50402发送对应的请求
location /IdentityService/{
proxy_pass http://localhost:50402/;
#把客户端信息传给微服务端
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Real-PORT $remote_port;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}
当需要自己进行分层时,把一切都交给IOC容器,IOC会进行逐注册从Domain层->Infrastructure层->WebApi应用层 务必写上构建函数进行依赖注入,依赖你尽管写最后都是在应用层进行注册封装
猎奇的建造者设计模式
就是使用嵌入类对外部类的私有构造函数进行赋值
using System.Net.NetworkInformation;
namespace AIStreamingOutput;
public record TestPerson
{
private int age{get; init;}
private string name{get; init;}
private string gender{get; init;}
public override string ToString()
{
return age + " " + name + " " + gender;
}
private TestPerson()
{
}
public class PersonBuilder
{
private int age{get; set;}
private string name{get; set;}
private string gender{get; set;}
public PersonBuilder SetAge(int age)
{
this.age = age;
return this;
}
public PersonBuilder SetName(string name)
{
this.name = name;
return this;
}
public PersonBuilder SetGender(string gender)
{
this.gender = gender;
return this;
}
public TestPerson Build()
{
return new TestPerson()
{
age = age,
name = name,
gender = gender
};
}
}
}