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
  1. ORM是负责从关系型数据库到C#对象的双向转换

  2. EFCore则是微软官方提供给我们的一套不需要去考虑底层的ORM框架是模型驱动的,类似的框架还有dapper它则是数据库驱动的,即对于EFcore而言是对对象进行操作,而Dapper还是需要写sql语句

开发环境搭建

  1. 建立实体类,建立配置类,建立DbContext(需要写入DataSet集合,可以理解为所有表对应的实体类集合),生成数据库,编写业务代码

  2. 安装EFcoreTools进行迁移,迁移是分多步的Migration,也可以进行回滚(当然可以记录迁移信息类似git操作)Add-Migration Info

  3. 同步数据库信息Update-DataBase

  4. 如果想实现对数据库字段的配置,在其配置类对应的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进行增删改查

  1. 当你想新增数据时,可以往我们的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}");
    }
}
  1. 对于查询操作,由于Dbset本身继承于IEnumerable接口,所以可以使用Linq操作

var result = ctx.MyBooks.FirstOrDefault(b => b.Title == "EF Core实战");
    Console.WriteLine($"书名:{rre.Title},价格:{rre.Price}");
  1. 对于修改和删除都是使用Linq查询对应的实体对象,对对象进行相应操作

  • 修改:

  • var re = ctx.MyBooks.FirstOrDefault(a=>a.Price==89.9); re.Title = "DecaASP.NET Core开发指南(第二版)_Decadede"; alt text

  • 删除

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();
}

alt text

约定大于配置

  1. 当你没有配置对应的继承IEntityTypeConfiguration类时,表名采用DbContext中对应的DbSet的属性名

  2. 数据列表的名字采用实体类属性的名字,类型将自动采用最兼容的类型

  3. 数据表列的可空性取决于对应实体类属性的可空性

  4. 名字为ID的属性默认为主键,如果主键使用int/short/long类型,将自动开启自增字段如果主键为Guid类型,则默认采用默认的Guid生成机制生成主键值

  5. 存在两种配置方式,FluentApi第一种就是建立一个继承接口的配置类在对应函数中去写配置alt text第二种就是使用特性alt text后者使用方便但把这个实体类强绑定当前数据库造成强烈耦合

主键

  1. 对于自增字段来说,不能人为给他赋值否则报错

  • 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进行的而非数据库

  1. 实际上可以使用类似复合主键,GUid当成逻辑上的主键,自增列只是帮我们进行数据重排

迁移底层

  1. Migration底层是由up和down方法,有点类似git的回滚和更新操作alt text数据库会存在一张表来记录所有的迁移记录!包含了数据库增加的内容方便efcore看的alt text alt text

  2. 反向工程 alt text类似DbFirst就是先有数据库,再有实体类

  3. EFCore本身是基于ADO.NET去和数据库进行交互的,EF本身是把我们的C#代码翻译成了SQL语句在通过ADO.NET去执行操作

  4. alt text使用Linq查询时别使用不支持的封装函数

  5. 在EF中查看对应的SQL语句,可以使用简单日志LogTo(Action)也可以使用标准日志的形式private static ILoggerFactory factory = LoggerFactory.Create(b=>b.AddConsole()); optionsBuilder.UseLoggerFactory(factory);还有一种查询的SQL查看方法,返回对象是IQueryable的可以使用Console.WriteLine(p1.ToQueryString());

  6. EFCore中的迁移脚本是和数据库相关的,不能中在中途更换数据库时使用不属于他的迁移脚本

联表查询

  1. 实体关系,EFCore支持多实体关系操作

  2. 使用fluentApi进行配置 alt text

像这种一对多的关系配置都配置在一端,一对多你就在多的那端配置就行

多:
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();
}

alt textalt text 注意,字段本身不包含外键,是ef自动生成的!

  1. 插入信息 alt text

//这里并没有往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.我们可以手动添加字段并将其设置为外键 alt text

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
}

单向导航&&关系配置在任意一方都可以

  1. 当出现一个基础表时(会被很多表引用),可以在WithMany里留空,并且在那个实体类中不进行声明集合对象

  2. 虽然说在1对多的时候进行配置在那一端都可以,但最推荐配置在多的一端s

自引用组织结构树

  1. 对于自引用表来说,务必将外键设置为可以null,对于一般外部表的引用来说,它们都是存在主键的,所以我们的外键列默认就是不为空 alt text

一对一/多对多

  1. 一对一 必须显示声明外键,1对多默认就算在多的一方建立外键

  2. 多对对 存在中间表帮你映射

关于IQueryable与IEnumerable的区别

  1. 写在IEnumerable中的Linq方法是在客户端本地进行比较的(客户端评估)一切都在本地进行查询的,连接数据库时,不对sql语句进行多余操作

  2. 写在IQueryable中的Linq方法是在服务端进行比较的(服务端评估)是efcore转换成对于查询sql语句在服务端进行查询,不在本地内存中进行操作

关于IQueryable

alt text alt text 可以更具具体业务进行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底层

alt text alt textalt text

  1. 他默认是Reader形式的,也就是和数据库进行连接,数据分批次一次次传递

  2. 当然你可以直接Tolist Toarray直接加载到内存中

  3. 默认不支持同时开启多个DataReader,还是一一访问数据库安全,或者变成数组形式

对于EF中的异步方法

  1. 对于那些非终结方法(返回值是IQueryable的)由于,他们并没有立即去执行连接数据库,并不消耗I/O一般不阻塞线程

EF中执行SQL语句

  1. db.Database.ExecuteSqlInterpolatedAsync(@$"insert into Articals(Name,Description) values('ttNew','Decade')");由于直接执行了,不需要savechanges(),EFCore非常牛逼,在执行sql语句的时候,如果你使用了内插值语法,他会把你的{}变成查询对象,而不是简单的字符串拼接,不会造成sql注入危机,自动进行参数化查询alt text

  2. 包含实体的查询语句(在实体查询还不能少字段,他要赋值,只能单表查询)关于单引号,EF Core 会自动处理字符串参数的引号,由于在实体查询的返回值依旧是IQueryable对象所以是可以复用的,可以配合EFCore使用

  3. 如果你想执行原生的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如何知道数据变更了

  1. 当你从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();

全局筛选器

  1. 这个东西可以理解为一种查询条件,只要你配置了就默认会添加在你的查询语句后面alt text

  2. 添加全局筛选器是在对应表的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();
以上这样写就能确保单次查询忽略条件限制
  1. 可能造成的性能问题: 使用 Price 索引:找到所有 Price > 100 的记录,然后逐条回表检查 IsDeleted 全表扫描:直接扫描整个表,同时检查两个条件

悲观并发控制

  1. 数据库锁和事务高度绑定

#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才继续执行
  1. 意思就是说当第一个事务提交时,第二个事务会被阻塞在 var tarObj = db.houses.FromSqlInterpolated($"select * from T_House WITH (ROWLOCK,UPDLOCK) where id = 1 ").Single();一行,当第一个事务释放锁之后,才能继续执行

  2. 缺点就是如果出现大量并发执行的流程时,会造成大量数量阻塞,如果锁住一行数据所有修改请求都会被阻塞,容易造成死锁

乐观并发控制

  1. 乐观锁通过"轻量级的条件检查"替代"重量级的阻塞等待",在大多数现代应用场景中确实提供了更好的性能和用户体验!

  2. alt text进行配置如图所示

  3. 原理则是在where中加入之前的旧值,当同一时间多个请求更改信息时,由于修改是要更具条件的,当第一条进行更新数值之后,其余的请求都不能找到对应的值进行修改,所以会丢失几次操作,但是不阻塞线程,当信息提交之后,下一次更改就会按照新的旧值进行设置

  4. 当你需要对一行数据多列进行更改,但又需要并发控制时,可以使用RowVersion,配置如图alt text,当你修改一行任意某列的值时,该列会进行自动更新,每次进行更新数据时都会把上一次的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"};
        }
    }
}

  1. 对于幂等:幂等性 是一个数学和计算机科学概念,在 HTTP 协议中,它指的是: 同一个请求被执行一次与连续执行多次,对服务器资源的状态所产生的影响是完全相同的。 换句话说,客户端无论因为何种原因(如超时、网络抖动)重复发送了相同的请求,服务器端都应该能够处理这种重复,并保证资源最终处于一致的状态。

  2. [Route("api/[controller]/[action]")] 这个对于路由来说就是先按照这个路径,在方法上的GetPost("name")是最终路径 alt textalt text

  3. 关于ActionResultalt text 有时需要返回一些错误信息如404 400,成功信息200... 单靠一个准确的返回值是做不到的,所以我们需要IActionResult,这个接口实现了一个异步方法,当我们返回IActionResult时会自动调用那个异步方法,来实现具体的数据展现,反正目前理解就用ActionResult就行了,能返回我们的需求还能帮我们隐式转换 4.关于传参alt text 默认你不设置url传参,参数默认从queryString中获取,当然你可以使用[from(name="") type varname]来指定参数从何而来

DotNetCore的依赖注入

alt text 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);
 }

缓存

需要注意内存缓存的性能是最高的,没有业务必要不需要使用分布式缓存

客户端响应缓存

alt text alt text alt text 1.对于客户端缓存可以在返回时加上允许浏览器缓存的特性,当然浏览器也可以不遵守

        [HttpGet]
        [ResponseCache(Duration = 10)]
        public DateTime GetNow()
        {
            return DateTime.Now;
        }

服务端响应缓存

  1. 浏览器访问方法时先找本地缓存,没找到连服务器缓存,还是没有连具体方法

  2. 当浏览器的报文头写入no caching发送请求时,所有缓存均失效 alt text alt text

内存缓存

alt text 内存缓存是和主程序在同一个进程中的,共享进程资源 ,这种缓存就不会被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属性 关于缓存信息不一致的问题alt text

  1. 关于缓存穿透问题,当用户输入一个不存在的ID时,缓存中没有对应的数据,所以会到数据库进行查询,数据库同样返回null值,我们调用GetOrCreateAsync方法已经将 null也作为键值对进行缓存了 alt text

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

entry.AbsoluteExpirationRelativeToNow = TimeSpan.FromSeconds(Random.Shared.Next(10,15));

分布式缓存(redis)

  1. 由于内存缓存只存在于单台服务器中,其中的缓存数据并不通用,在大型项目中引入分布式缓存,所有web服务器去访问一台缓存服务器,这里使用redis为例alt textalt text

  2. 具体使用方法alt textalt text

配置完成的业务代码
#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

alt text

多层项目使用EFCore注意:

  1. 当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

  1. 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才能进行迁移操作

alt textalt text

关于Filter

IAsyncExceptionFilter全局异常处理器

  1. 我的理解它作为一个全局异常监听器,实现了在业务中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全局事件监听器

  1. 它和Exception的顺序不一样,是按照注册顺序执行的当 context.Result 被赋值后,ASP.NET Core 的过滤器管道会立即中断当前请求的处理,直接返回给客户端,不会执行后续的过滤器或Action方法。

  2. alt text 示例

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 };
            }
        }
    }
}

中间件

alt textalt textalt text

  1. 中间件类

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");
});

alt text alt text

关于Identity标识组件

alt text alt text

  1. 该组件使用EFCore进行工作,你可以理解为微软帮我们做好了一个单一数据库,专门用于处理用户登录认证等问题

  2. 默认提供两个实体类,IdentityRole,IdentityUser,这里的long是主键,你可以继承它们对他们进行扩写,当然它们已经包含了一部分的基础信息如账号密码等

  3. 此外你的Dbcontext需要继承 IdentityDbContext<MyUser,MyRole,long> long代表验证实体表主键

  4. 接下来就算众多配置相关 直接照抄即可,下面的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>>(); 
  1. 其中 UserManager 和 RoleManager 提供了许多操作数据库相关查询认证操作,比我们自己手写方便多了

  2. 以下为一个简单的登录检测系统

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.");
}
  1. 关于重置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.");
        }
  1. 这个token是系统更具安全令牌自动更新生成的,当密码变化等一系列操作进行时,会更改令牌的值,会和再次生成的token不一致,导致令牌失效

  2. 令牌的动态生成 Token = 算法(用户ID + 时间戳 + 安全戳)

每次调用 GeneratePasswordResetTokenAsync 都会重新计算,不是从存储中读取 安全优势 自动失效:密码修改后,所有未使用的重置链接立即失效

无状态:服务器不需要维护令牌状态

防重放:通常设计为一次性使用

JWT(Json Web Token)

  1. 为什么JWT比Session更好 alt text 首先Session为了面对分布式服务器时,需要一台中间服务器进行校验,本身它是存储在本地的一组键值对,不同服务器本地内存key自然不一样,客户端存储key和服务端进行对比,相同极为同一个用户

  2. 一个典型的 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) alt text 3. 关于数据校验,是Token前两部分,即头部+payload+服务端key,结合算法算出第三部分的令牌,可以有效防止本地明文payload被更改 alt text 4. 吓哭了的配置 alt text 也没必要全部看懂,了解一下Claim中的内容是写入JWT的payload中的,由于payload的明文的最好不要写入重要信息 5. 吓哭了的解析 alt text 6. 当然我们的微软爹还是帮我们封装了 alt text 虽然我感觉还是很烦人

//配置系统直接拿数据
 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
     };

 });

alt text 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.");
        }
    }
}

  1. alt text通过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.");
}

alt text alt text alt text

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

  1. alt text

  2. alt text

  3. alt text

  4. 我们的WebApi项目是依赖前端的请求到中间件处理对应函数在返回响应的,对于自身没法运行代码

  5. 为了解决例如定时导出数据库用户这种需求,.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>();

关于参数校验

  1. .Net本身实现了使用特性在实体类中进行标注的形式来限制输入,但是和EFCore一样,破坏了类的单一性,并且没法自定义报错信息

  2. 我们使用第三方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

  1. 前端代码 需要安装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>
  1. 后端代码

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);
    }
}
  1. 首先得了解WebSocket协议和Http协议是平级的,Http是单工通讯的无状态的,得客户端发送它才能进行响应,而WebSocket就是长连接通讯了双工的,服务器可以随时向客户端发送消息

  2. 我们把SignalR集成到ApsDotNet中,这样共用同一端口,方便连接使用

  3. 记得配置跨域和中间件,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

alt text alt text alt text alt text alt text alt text

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);
        }
    }

alt text 不一定对,但前端确实是这么写的,并且这样好理解 标准连接流程: 第一次请求(协商/Negotiate) 客户端通过 普通 HTTP POST 访问 /hub/negotiate 端点 服务器返回包含连接令牌的协商响应: JSON 复制 { "connectionId": "xxx", "connectionToken": "加密令牌", // 核心凭证 "availableTransports": ["WebSockets", "ServerSentEvents"] } 第二次请求(正式连接) 客户端使用获取的 connectionToken 和 connectionId 发起 WebSocket 连接(或其他传输方式): wss://server/hub?id=connectionId&connectionToken=xxx 此时才建立 SignalR 的持久双向通道

  1. 第一次(协商/Negotiate) POST http://localhost:5100/MyHub/negotiate 这是自动添加的 /negotiate 端点 使用普通 HTTP POST 请求 返回连接令牌等信息

  2. 第二次(正式连接) ws://localhost:5100/MyHub?id=xxx&access_token=yyy 路径仍然是 /MyHub,不是其他地址 协议升级为 WebSocket(ws:// 或 wss://) 自动附加 id 和 access_token 等查询参数 你的代码实际做了什么: .withUrl("http://localhost:5100/MyHub") 这个配置会同时用于两次请求: SignalR 客户端自动在协商阶段在 URL 后添加 /negotiate 在连接阶段保持原路径,仅修改协议和参数 alt text alt text alt text 服务器软件的具体"包办"事项 技术层面包办: 网络通信:TCP/IP握手、HTTP协议解析

并发管理:线程池、异步处理

资源管理:内存分配、连接池

安全防护:防DDoS、SQL注入防护

监控日志:性能监控、访问日志

DDD领域驱动设计

alt text

  1. 领域可以简单理解为当前所从事的具体内容

  2. 内容可以细分为子领域

  3. 领域之间的优先级也不同 核心领域 支撑领域 通用领域,当然这是随着你的业务来决定的 alt text

  4. 对于开发而言,首先应该有业务思维,把领域中对象提取进行建模,从业务角度看待开发

  5. 我们的项目应该开始于创建领域模型,而不是考虑如何设计数据库和编写代码。使用领域模型,我们可以一直用业务语言去描述和构建系统,而不是使用技术人员的语言 alt text

  6. 流水线代码 alt text alt text

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

  8. 存在唯一标识符的对象一般看做实体,实体能够独立存在

  9. 值对象做为实体的一部分属性依赖存在 alt text

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

  11. 领域服务就是聚合内部的业务逻辑,比如让一个实体属性变更,并不包含与另一个聚合接触的业务

  12. 第一步,准备业务操作所需要的数据,第二步,执行由一个或者多个领域模型做出的业务操作,这些操作会修改实体的状态,或者生成一些操作结果。第三步,把对实体的改变或者操作结果应用于外部系啄 alt text alt text alt text alt text

充血模型&&实现值对象

alt text

  1. 首先为了保证实体的安全性,我们只允许内部更改成员属性

  2. 有参构造保证了,存在当前实例的时候对应字段不为null

  3. 比如密码的散列值,我们不需要实际使用它,但是在数据库必须存在这么个东西

  4. 只读属性很好理解

  5. 不需要存在数据库的字段 alt text alt text alt text alt text alt text

对于值对象

  1. 首先它是不存在标识符的,也是不能脱离实体单独存在的 alt text

  2. 我们对一些适合合体的属性最好把他们聚集起来,方便标识实体的一些内容

  3. 那钱来举例子Curreny这个类型包含int count数量和enum 币种两个内容,它们能确保实体能够正常无歧义的进行操作

  4. alt text

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

alt text 当然你可以进一步对从属类型的字段信息进行配置

实现聚合

alt text

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

MediatR

alt text alt text alt text alt text

//注册服务
services.AddMediatR(cfg => cfg.RegisterServicesFromAssembly(typeof(Startup).Assembly));
//传输接口
INotification
//调用接口
INotificationHandler<>
//实际发布事件
_mediator.Publish(new TransferData("not bad"));

关于RabbitMQ

  1. 由于我们要实现集成事件,也就是跨微服务的事件,那就不能使用传统的消息队列观察者了,我们此时需要一个中间服务,从A服务发送消息,B服务接受并处理消息流程,我们使用RabbitMQ来实现我们的需求 alt text

  2. 信道,连接到RabbitMQ存在一个物理的TCP连接,但是我们的服务可能是断断续续发布的,所以存在虚拟管道来连接

  3. 队列,一般只存在于消费者方,用于存放发送方发过来的字节数组,通过用户自定义的规则把交换机中的内容放到对应的管道中 alt text

  4. 使用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 容器供其使用 逐层拆解:为什么要这么设计? 洋葱架构从内到外分为「领域层(核心)→ 应用层 → 基础设施层」,依赖规则是「内层不依赖外层,外层依赖内层」,而接口是实现这一规则的关键:

  1. 领域层:定义接口(契约),不关心实现 领域层是业务核心,只聚焦「what(要做什么)」,不关心「how(怎么实现)」。它会定义业务所需的接口(比如 用户仓储接口 UserRepository、支付服务接口 PaymentService),这些接口是领域逻辑的「行为契约」,只声明方法签名,不写具体逻辑。

好了对于Domain层而言

  1. 不同聚合间的实体不要直接引用

  2. 应该标注对应标识符

  3. 在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层而言

  1. 在基建层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应用层而言

  1. 实现工作单元事务一致性

  2. 使用Domain层的领域服务

  3. 对领域服务使用基建层的注入

  4. 监听事件

  5. 实现简单的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配置

alt text

//因为在开发环境微服务的地址都一样,所以得区分端口
#进行反向代理,当客户端向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
            };
        }
    }

    

}

不会做游戏