# NetCoreHappyNewYear **Repository Path**: mir2021/net-core-happy-new-year ## Basic Information - **Project Name**: NetCoreHappyNewYear - **Description**: No description available - **Primary Language**: C# - **License**: Not specified - **Default Branch**: master - **Homepage**: None - **GVP Project**: No ## Statistics - **Stars**: 0 - **Forks**: 2 - **Created**: 2023-04-18 - **Last Updated**: 2023-04-18 ## Categories & Tags **Categories**: Uncategorized **Tags**: None ## README # NetCoreHappyNewYear ## 性能评测网站 [评测网站](https://www.techempower.com/) [结果查询](https://www.techempower.com/benchmarks/) [C#之一起吹牛B](https://www.bilibili.com/video/BV1vb411V7u7?p=1) ## 介绍 [bilibili 视频讲解练习](https://www.bilibili.com/video/BV15J411v7v4?p=1) ## 主要内容 asp.net core / log / ioc / aop ## NLOG 1. 使用NuGet添加 NLog.Config和NLog.Web.AspNetCore两个包 2. 安装完 NLog.Config 后自动生成 NLog.config 文件 3. 配置该文件 4. 在Program.cs 中写入: ``` .ConfigureLogging(logging=> { logging.ClearProviders(); logging.SetMinimumLevel(LogLevel.Debug); }).UseNLog(); ``` 注意:配置文件的等级会覆盖Program的配置等级 ![配置截图](./pics/nlog1.png) ## 用命令行启动 1. 发布 2. 在publish 文件夹下:dotnet WebAppHappyNewYear.dll --urls="http://*:7711" --ip="127.0.0.1" --port=7711 ## AutoMapper 1. 安装Nuget ![Nuget截图](./pics/automapper1.png) 2. Startup 中加载 automapper ``` services.AddAutoMapper(AppDomain.CurrentDomain.GetAssemblies()); ``` 3. 建立对应映射 使用默认约定,会将Company 中的字段自动映射到 CompanyDto 中相同的字段上并且忽略 空 引用(如果 dto 中有个属性 company 中不存在,那么这个值就被忽略映射了) ``` public class EmployeePorfile : Profile { public EmployeePorfile() { CreateMap() .ForMember(dest => dest.Name, opt => opt.MapFrom(src => $"{src.FirstName} {src.LastName}")) .ForMember(dest => dest.GenderDisplay, opt => opt.MapFrom(src => src.Gender.ToString())) .ForMember(dest => dest.Age, opt => opt.MapFrom(src => DateTime.Now.Year - src.DateOfBirth.Year)); CreateMap(); CreateMap(); CreateMap(); } } ``` 4. 使用时注入 IMapper ``` public ControlController(ILogger logger, IStopCommandService service, IMapper mapper) var stopCommandEntity = _mapper.Map(stopCommandDto); ``` ## AutoFac 1. 使用NUGET 添加如下两个(DependencyInjection 就是DI容器,这里就用到AUTOFAC 的 IOC 功能) ![配置截图](./pics/autofac1.png) 在 ASP.NET Core 3+ 需要你直接指定一个service provider factory而不是把它加入进service collection。基本用法的代码如下: ``` public static IHostBuilder CreateHostBuilder(string[] args) => Host.CreateDefaultBuilder(args) .UseServiceProviderFactory(new AutofacServiceProviderFactory())//使用autofac的容器工厂替换系统默认的容器 .ConfigureWebHostDefaults(webBuilder => { webBuilder.UseStartup(); }); ``` 然后在Startup中增加一个方法ConfigureContainer ``` public void ConfigureContainer(ContainerBuilder builder) { // 视频内方式 builder.RegisterModule(); } ``` 添加单个服务引用,和serviceCollection 一样用 ``` public class CustomAutofacModule : Autofac.Module { protected override void Load(ContainerBuilder containerBuilder) { //单个服务注入 containerBuilder.RegisterType().As().SingleInstance();; containerBuilder.RegisterType().As(); containerBuilder.RegisterType().As(); containerBuilder.RegisterType().As(); containerBuilder.RegisterType().As(); } } ``` ``` public class CustomAutofacModule : Autofac.Module { protected override void Load(ContainerBuilder containerBuilder) { //程序集合注入 Assembly assembly = Assembly.GetExecutingAssembly(); var interfaces = Assembly.Load("WebInterface"); var services = Assembly.Load("WebServices"); containerBuilder.RegisterAssemblyTypes(services, interfaces) .Where(t => t.Name.EndsWith("Service")).AsImplementedInterfaces().PropertiesAutowired(); } } ``` 2. 这个包用来做autofac 的AOP 注入如果没装应该不影响注入容器的 ![配置截图](./pics/autofac3.png),代码如下: 自定义一个AOP方法,继承自 IInterceptor: ``` public class CustomAutofacAop : { public void Intercept(IInvocation invocation) { Console.WriteLine($"invocation.Method={invocation.Method}"); Console.WriteLine($"invocation.Arguments={string.Join(",",invocation.Arguments)}"); invocation.Proceed(); //继续执行 Console.WriteLine($"方法{invocation.Method}执行完成了"); } } ``` Module 注入时,将AOP方法申明输入,并在需要AOP的类上EnableInterfaceInterceptors ``` public class CustomAutofacModule : Autofac.Module { protected override void Load(ContainerBuilder containerBuilder) { //视频方法 var assembly = this.GetType().GetTypeInfo().Assembly; var builder = new ContainerBuilder(); var manager = new ApplicationPartManager(); manager.ApplicationParts.Add(new AssemblyPart(assembly)); manager.FeatureProviders.Add(new ControllerFeatureProvider()); var feature = new ControllerFeature(); manager.PopulateFeature(feature); builder.RegisterType().AsSelf().SingleInstance(); builder.RegisterTypes(feature.Controllers.Select(ti => ti.AsType()) .ToArray()).PropertiesAutowired(); //containerBuilder.RegisterType().PropertiesAutowired(); containerBuilder.Register(c => new CustomAutofacAop()); //aop 注入 containerBuilder.RegisterType().As().SingleInstance().PropertiesAutowired(); containerBuilder.RegisterType().As(); containerBuilder.RegisterType().As(); containerBuilder.RegisterType().As(); containerBuilder.RegisterType().As(); containerBuilder.RegisterType().As().EnableInterfaceInterceptors(); } } ``` 最后直接在类上加入AOP 属性标签 ``` [Intercept(typeof(CustomAutofacAop))] public class A : IA { public void Show(int id, string name) { Console.WriteLine($"This is {id} _ {name}"); } } ``` ## AOP : asp.net core filter ### AOP : asp.net core filter Authorization filter Resource filter Action filter Exception filter Result filter ![配置截图](./pics/autofac4.png) ### Exception filter #### 创建一个Customer Filter 继承自 ExceptionFilterAttribute ``` public class CustomExceptionFilterAttribute:ExceptionFilterAttribute { public override void OnException(ExceptionContext context) { if(!context.ExceptionHandled) { Console.WriteLine($"{context.HttpContext.Request.Path} {context.Exception.Message}"); context.Result = new JsonResult(new { Result = false, Msg="发生异常" }); ; context.ExceptionHandled = true; //context.HttpContext.Response.Redirect("/Home/Error"); } } } ``` 然后就可以在任何需要使用的地方进行ACTION 或者 CONTROLLER 级别的注册 ``` //controller 注册 //[CustomExceptionFilterAttribute] public class FourthController : Controller { //action 注册 //[CustomExceptionFilterAttribute] public IActionResult Index() { int a = 0; int b = 1; int c = 3; var r = c / a; return View(); } } ``` #### 或者 进行全局注册 ``` services.AddControllersWithViews(options=> { options.Filters.Add(typeof(CustomExceptionFilterAttribute)); }); ``` 全局注册后,就不必在任何地方加注解,即在程序的任何地方生效了。 但是有时为了可以使 logger 等就可以被自动注入进CustomExceptionFilterAttribute,换句话说,如果写的Filter 需要依赖其他的service 时,单纯的属性注册就无能为力了,所以需要使用使用全局注册、service 注册等方法. 此时CustomExceptionFilterAttribute 就能自动注入其他service 了: ``` public class CustomExceptionFilterAttribute:ExceptionFilterAttribute { private readonly ILogger _logger; public CustomExceptionFilterAttribute(ILogger logger) { _logger = logger; } public override void OnException(ExceptionContext context) { if(!context.ExceptionHandled) { Console.WriteLine($"{context.HttpContext.Request.Path} {context.Exception.Message}"); context.Result = new JsonResult(new { Result = false, Msg="发生异常" }); ; _logger.LogError(context.Exception.Message); context.ExceptionHandled = true; context.HttpContext.Response.Redirect("/Home/Error"); //HttpContext.Current.Response.Redirect("/Home/Index"); } } } ``` #### 或者 进行service filter 注册 ``` [ServiceFilter(typeof(CustomExceptionFilterAttribute))] public class FourthController : Controller { //action 注册 //[CustomExceptionFilterAttribute] public IActionResult Index() { int a = 0; int b = 1; int c = 3; var r = c / a; return View(); } public IActionResult Info() { int a = 0; int b = 1; int c = 3; var r = c / a; return View(); } } ``` 然后再将 CustomExceptionFilterAttribute 在service 中注册: ``` containerBuilder.RegisterType(); ``` 可以在ConfigureServices里注册,不过既然用了AUTOFAC,就在CustomAutofacModule里注册 #### 或者 进行TypeFileter 注册 ``` [TypeFilter(typeof(CustomExceptionFilterAttribute))] ``` TypeFilter 注册无需在service 中注册 CustomExceptionFilterAttribute #### 或者 进行 IFileterFactory 注册 首先创建 Filter 构建工厂,继承自 Attribute, IFilterFactory,继承Attribute是为了能让CustomFilterFactoryAttribute这个类作为属性标签标记在action 或者 controller 上,即[CustomFilterFactoryAttribute()] ``` public class CustomFilterFactoryAttribute : Attribute, IFilterFactory { private readonly Type _type; public CustomFilterFactoryAttribute(Type type) { _type = type; } public bool IsReusable => true; public IFilterMetadata CreateInstance(IServiceProvider serviceProvider) { return (IFilterMetadata)serviceProvider.GetRequiredService(_type); } } ``` 然后再将 CustomExceptionFilterAttribute 在service 中注册: ``` containerBuilder.RegisterType(); ``` 最后,在需要使用的地方加上属性标签,并且指明,如果发生异常,构建哪个ExceptionFilter,此时CustomExceptionFilterAttribute中就可以使用service collection中注册的服务了 ``` [CustomFilterFactoryAttribute(typeof(CustomExceptionFilterAttribute))] public class FourthController : Controller ``` #### 总结 如果一个Filter不需要依赖其他service ,就直接创建并继承对应的filter ::ExceptionFilterAttribute, ActionFilterAtribute等 然后直接注解使用 如果需要依赖其他的service,除了全局注册外,其他注册还需要在service collection中加入这个filter #### 执行顺序 1. 创建一个 actionfilter,为了能让这个filter 将来能做依赖注入,必须继承 IFilterMetadata,此处查看.netcore 源码可以返现,所有serviceProvicer的返回值都是IFilterMetadata,所以依赖注入必须继承IFilterMetadata ``` public class CustomActionFilterAttribute : Attribute, IActionFilter, IFilterMetadata { private readonly ILogger _logger; public CustomActionFilterAttribute(ILogger logger) { _logger = logger; } public void OnActionExecuted(ActionExecutedContext context) { Console.WriteLine($"{nameof(CustomActionFilterAttribute)} {context.HttpContext.Request} OnActionExecuted "); _logger.LogInformation("OnActionExecuted"); } public void OnActionExecuting(ActionExecutingContext context) { Console.WriteLine($"{nameof(CustomActionFilterAttribute)} {context.HttpContext.Request} OnActionExecuting "); _logger.LogInformation("OnActionExecuting"); } } ``` 由于依赖了logger, 直接标记不行,所以使用 ifacotry 注册方式: ``` [CustomFilterFactoryAttribute(typeof(CustomActionFilterAttribute))] ``` 如果不需要注入logger ``` public class CustomActionFilter2Attribute : Attribute, IActionFilter, IFilterMetadata { public void OnActionExecuted(ActionExecutedContext context) { Console.WriteLine($"{nameof(CustomActionFilterAttribute)} {context.HttpContext.Request} OnActionExecuted "); } public void OnActionExecuting(ActionExecutingContext context) { Console.WriteLine($"{nameof(CustomActionFilterAttribute)} {context.HttpContext.Request} OnActionExecuting "); } } ``` 可以直接标记 2. 分别在action controller 全局上注册,其执行顺序为: global=>controller=>action excuting=>方法主体=>atcion excuted=>controller=>global =>exception!!!!!!!! ### 面向切面编程总结 |中间键|Filter|AutoFac| |:----:|:----:|:----:| |任何请求第一步到达|属于MVC流程|可以深入业务逻辑层| |不仅仅为网站服务|MVC 流程外做不了|可以在方法内部插入方法| |一般做提前过滤,鉴权,压缩缓存等|比如:404不能捕获| | |没有Controller信息,不适合做业务逻辑| | | |数据过滤,路由等不要求数据细节的|处理具体HTTP请求时,针对CONTROLLER级别|在数据访问层扩展时,针对Service级别| ### AOP advanced 1. 分别创建 ResourseFilter ResultFilter ActionFilter1 ActionFilter2 2. 注解后执行,给ActionFilter1=>Order-=1,ActionFilter1=>Order-=2 3. 执行效果如图: ![执行效果图](./pics/autofac5.png) + Filter 顺序 - IResourceFilter-OnExcuting 发生在控制器实例化之前 - IActionFilter-OnExcuting - Controller Excuting - IActionFilter-OnExcuted - IResultFilter-OnExcuting Result 是发生在视图替换环节 - IResultFilter-OnExcuted - IResourceFilter-OnExcuted **注:** 基于IResultFilter 的缓存,节省了控制器实例化和Action执行的时间,但是视图还是会重新执行 各类缓存: ![各类缓存](./pics/autofac6.png) ### Controller 缓存 写一个Resource Filter, 在Filter内存键值对缓存 ``` public class CustomCacheResourseFilterAttribute : Attribute, IResourceFilter, IFilterMetadata, IOrderedFilter { private static Dictionary CustomCacheResourseDictionary = new Dictionary(); public int Order { get; set; } public void OnResourceExecuted(ResourceExecutedContext context) { CustomCacheResourseDictionary.Add(context.HttpContext.Request.Path, context.Result); } public void OnResourceExecuting(ResourceExecutingContext context) { string key = context.HttpContext.Request.Path; if (CustomCacheResourseDictionary.ContainsKey(key)) { //Result 是MVC 的短路器,一旦 Result 被制定,那么 mvc 流程就结束 context.Result = CustomCacheResourseDictionary[key]; } } } ``` ### Service 缓存 使用 Autofac 创建一个Service 级别的注入: ``` public class CustomAutofacAop : IInterceptor { private static Dictionary CustomCacheInterceptorDictionary = new Dictionary(); public void Intercept(IInvocation invocation) { Console.WriteLine($"invocation.Method={invocation.Method}"); Console.WriteLine($"invocation.Arguments={string.Join(",", invocation.Arguments)}"); string key = $"{invocation.Method}_{string.Join(",", invocation.Arguments)}"; if (CustomCacheInterceptorDictionary.ContainsKey(key)) { invocation.ReturnValue = CustomCacheInterceptorDictionary[key]; } else { invocation.Proceed(); //继续执行 CustomCacheInterceptorDictionary.Add(key, invocation.ReturnValue); } Console.WriteLine($"方法{invocation.Method}执行完成了"); } } ``` ### 视图级别缓存: Cache 可以直接用ResponseCache 注解:此时是依靠客户端(即浏览器缓存的)缓存的,如果用不同浏览器打开,则没有缓存效果(对比下面的中间键缓存) ``` [ResponseCache(Duration =10)] public IActionResult CacheHtml() { base.ViewData["ControllerActionTime"] = DateTime.Now.ToString("yy-MM-dd HH:mm:ss fff"); Console.WriteLine("This is Fifth CacheHtml action"); _logger.LogInformation("This is Fifth CacheHtml action"); base.ViewData["ServiceActionTime"] = _ia.Plus(3, 4).ToString("yy-MM-dd HH:mm:ss fff"); return View(); } ``` ![备注](./pics/autofac7.png) ![备注](./pics/autofac8.png) ResponseCache:在响应请求时,添加一个ResponseHeader 来指导浏览器缓存结果 ``` //[ResponseCache(Duration =60)] public IActionResult Test_4() { //注释掉[ResponseCache] 用下面语句有同样效果 base.HttpContext.Response.Headers["Cache-Control"]="public,max-age=60"; base.ViewData["ControllerActionTime"] = DateTime.Now.ToString("yy-MM-dd HH:mm:ss fff"); Console.WriteLine("This is Fifth Test_4 action"); _logger.LogInformation("This is Fifth Test_4 action"); base.ViewData["ServiceActionTime"] = _ia.Plus(3, 4).ToString("yy-MM-dd HH:mm:ss fff"); return View(); } ``` #### 自定义: ResponseCache 本质就是在一个Action 执行完后加入一个HttpHeader,然后在对应地方加入注解就行了 ``` public class CustomCacheResultFilterAttribute : Attribute, IResultFilter, IFilterMetadata, IOrderedFilter { public int Duration { get; set; } public int Order => 0; public void OnResultExecuted(ResultExecutedContext context) { //此方法执行时,已经制定了 Response,此时不能再操作 Response, 否则会出错 Console.WriteLine($"{nameof(CustomCacheResultFilterAttribute)} OnResultExecuted"); } public void OnResultExecuting(ResultExecutingContext context) { context.HttpContext.Response.Headers["Cache-Control"] = $"public,max-age={Duration}"; } } ``` #### 中间件缓存 中间键缓存不会进入 MVC 流程 1. 在ConfigureService 中注册: services.AddResponseCaching(); 2. 在Configure 中申明中间键: app.UseResponseCaching(); 缓存要放在 app.UseRouting() 前面 3. 在对应地方注解缓存 :[ResponseCache(Duration = 60)],注意默认是客户端,服务端都缓存,可以用Location =ResponseCacheLocation.Client 指定在哪里缓存 ## Authentication 登陆验证 1. (可选)创建实体及数据库上下文 MyDbContext 2. (可选)生成迁移文件(add-migration),再迁移到数据库(update-database) 3. 添加登陆中间键,并用cookies ``` public void ConfigureServices(IServiceCollection services) { services.AddDbContext(options => { options.UseSqlServer(Configuration.GetConnectionString("default")); }); services.AddAuthentication(CookieAuthenticationDefaults.AuthenticationScheme) .AddCookie(option => { //设置如果没有登陆后,自动跳转的路径 option.LoginPath = "/user/login"; }); services.AddControllersWithViews(); } ``` 申明管道 ``` //登陆验证 app.UseAuthentication(); //授权验证 app.UseAuthorization(); ``` 4. 在对应controller上加入注解 ![注解截图](./pics/authorize1.png) 5. 设计LogInController 及其对应的view,并写授权controller ``` var loginUserEntity = _mapper.Map(user); if (await _loginService.CheckUser(loginUserEntity)) { //登陆授权 // claims 基本授权信息 var claims = new List() { new Claim(ClaimTypes.Name,user.Account) }; //身份Id: 由授权信息 和 授权信息类型创建 var claimsIdentity = new ClaimsIdentity(claims, CookieAuthenticationDefaults.AuthenticationScheme); //authorizention 会自动发送 token 给客户端,并生成 cookies ,默认生命周期是 session,即浏览器关闭后登陆信息丢失 await HttpContext.SignInAsync(CookieAuthenticationDefaults.AuthenticationScheme, new ClaimsPrincipal(claimsIdentity)); //设置过期时间 //await HttpContext.SignInAsync(CookieAuthenticationDefaults.AuthenticationScheme, // new ClaimsPrincipal(claimsIdentity), // new AuthenticationProperties // { // //是否持久化 // IsPersistent = true, // //过期时间 // ExpiresUtc = DateTimeOffset.Now.AddDays(1) // }); ; return Redirect(user.ReturnUrl); //if (string.IsNullOrEmpty(user.ReturnUrl)) //{ // return RedirectToAction("/"); //} //else //{ // return Redirect(user.ReturnUrl); //} } else { return RedirectToAction(nameof(SignIn)); } ``` 6. 登出 ``` public async Task Logout() { await HttpContext.SignOutAsync(); return RedirectToAction(nameof(SignIn)); } ``` ## ef core ### 数据库约束 约束是数据库用来确保数据满足业务规则的手段,不过在真正的企业开发中,除了主键约束这类具有强需求的约束,像外键约束,检查约束更多时候仅仅出现在数据库设计阶段,真实环境却很少应用,更多是放到程序逻辑中去进行处理。这也比较容易理解,约束会一定程度上降低数据库性能,有些规则直接在程序逻辑中处理就可以了,同时,也有可能在面对业务变更或是系统扩展时,数据库约束会使得处理不够方便。不过在我看来,数据库约束是保证数据准确性的最后一道防线,对于设计合理的系统,处于性能考虑数据库约束自然可有可无;不过若是面对关联关系较为复杂的系统,且对系统而言,数据的准确性完整性要高于性能要求,那么这些约束还是有必要的(否则,就会出现各种相对业务规则来说莫名其妙的脏数据,本人可是深有体会的。。)。总之,对于约束的选择无所谓合不合理,需要根据业务系统对于准确性和性能要求的侧重度来决定。 [参考博文](https://www.cnblogs.com/chengxiao/p/6032183.html) 在设计物理层面的主键时所遵循的一些原则: 1. 主键应当是对用户没有意义的。如果用户看到了一个表示多对多关系的连接表中的数据,并抱怨它没有什么用处,那就证明它的主键设计地很好。 2. 主键应该是单列的,以便提高连接和筛选操作的效率。 3. 永远也不要更新主键。实际上,因为主键除了惟一地标识一行之外,再没有其他的用途了,所以也就没有理由去对它更新。如果主键需要更新,则说明主键应对用户无意义的原则被违反了。 4. 主键不应包含动态变化的数据,如时间戳、创建时间列、修改时间列等。 5. 主键应当有计算机自动生成。如果由人来对主键的创建进行干预,就会使它带有除了惟一标识一行以外的意义。一旦越过这个界限,就可能产生认为修改主键的动机,这样,这种系统用来链接记录行、管理记录行的关键手段就会落入不了解数据库设计的人的手中。 (code first) 1. 创建Domain 项目存放实体类 2. 创建Data 项目并安装EF Core 如果只是Sql 就装这个(注意ef core 版本与其对应支持的 .net standard) ![Nuget](./pics/efcore1.png) 如果要使用迁移工具需要安装Tools 包, ![Nuget](./pics/efcore2.png) 3. 在Data 项目中创建 DbContext ``` public class DemoContext:DbContext { protected override void OnConfiguring(DbContextOptionsBuilder dbContextOptionsBuilder) { dbContextOptionsBuilder.UseSqlServer("Server=.;uid=sa;pwd=shris2020;database=EfCoreDemo;"); } public DbSet Clubs { get; set; } public DbSet Leagues { get; set; } public DbSet Players { get; set; } } ``` 4. 使用CodeFirst 初始化数据库 1) 定义/修改 Module (就是上面的Domain) 2) 创建Migration 文件(Migration文件是生成在Data项目中,但是它是不可执行的,所以需要个可执行程序 app 来直接这个文件) a. 打开包命令控制器(工具->Nuget包管理器->程序包管理器控制台) b. 选择DbContext 所在项目 ![如图](./pics/efcore3.png) c. 初始化Migration: "add-migration [文件名]" 如: add-migration inital 如果引用 DbContext 所在项目的的可启动项目(如APP EXE web 等)没有安装 Microsoft.EntityFrameWork.Design 会报错,去那个项目直接安装就好了 ![如图](./pics/efcore5.png) ![如图](./pics/efcore4.png) 此时生成 时间戳_inital 文件,里面up 方法用于生成数据库,down方法用于回滚数据库 snapshoot文件不可手动更改,它用来追踪数据库的变更 ![如图](./pics/efcore6.png) d. 生成数据库脚本(一般在生产环境中使用,这样交给DBA,他就可以直接用来生成数据库了) script-migration e. 一般开发环境下用 update-database 直接在数据库中生成数据库表: update-database -verbose(查看生产过程的参数明细) f. 如果代码中的数据库有变化(修改了Domain中的类),则再生成一个migration文件: add-migration chanePorperties g. 然后再更新数据库:update-database 3) 处理数据库中n:m关系 一般1:n 关系,ef 会自动根据导航属性生成,如: ``` public class League { public int Id { get; set; } [Required,MaxLength(100)] public string Name { get; set; } [Required] [MaxLength(50)] public string Country { get; set; } } public class Club { public Club() { Players = new List(); } public int Id { get; set; } public string Name { get; set; } public string City { get; set; } [Column(TypeName ="date")] public DateTime DateOfEstablishment { get; set; } public string History { get; set; } public League League { get; set; } public List Players { get; set; } } ``` League 和 Club 是 1:n的关系。 League 是 Club 的一个导航属性,Ef 在生成数据库表时,会将League 生成 LeagueId, 对应 League 的Id 当n:m关系时,Ef 就不能自动生成了,根据数据库设计理论,n:m关系就是表的乘积关系(笛卡尔集),所以利用一个中间表,其和两个表分别是1:m 和 1:n 关系: ``` public class GamePlayer { // 我觉得使用这个 id 作为主键,让其与Game 和 Player 关联比较好。 //public int Id { get; set; } //但是老师使用的是:GameId + PlayerId 作为联合主键 public int GameId { get; set; } public int PlayerId { get; set; } public Game Game { get; set; } public Player Player { get; set; } } ``` 然后在DbContex 中重写 OnModelCreating 方法,设置联合主键: ``` protected override void OnModelCreating(ModelBuilder modelBuilder) { //使用 fluent api 设置表 GamePlayer 的联合主键 modelBuilder.Entity().HasKey(x => new { x.GameId, x.PlayerId }); } ``` 最后再更新Migration 4)总结 1:1 关系时: 各自表除自己的属性外,建立一个对应关系的导航属性,以及对应关系表的Id 属性,并在ef 中指定那张表是主表: ``` public class Resume { public int Id { get; set; } public string Description { get; set; } //指明它与 Player 是1:1 关系,同时 Player 上也要建立它的导航属性和外键属性 public int PlayerId { get; set; } public Player Player { get; set; } } protected override void OnModelCreating(ModelBuilder modelBuilder) { //使用 fluant api 设置表 GamePlayer 的联合主键 modelBuilder.Entity().HasKey(x => new { x.GameId, x.PlayerId }); //制定1:1 关系表的主表,这里 HasForeignKey(x=>x.PlayerId) 确定 Resume 是主表 modelBuilder.Entity(). HasOne(x => x.Player). WithOne(x => x.Resume). HasForeignKey(x=>x.PlayerId); } ``` 1:n 关系时,可以在 1 的那张表中间建立一个 List 属性,也可以在n的那张表里建立 1 的那张表的导航属性,或者两边都建 n:m时,见上面介绍 最后项目中的表关系如图: ![如图](./pics/efcore7.png) 5. 在Console 项目中使用dbcontext 进行增删查改 要在控制台程序中使用log 需要安装Microsoft.Extensions.Logging 要在控制台中输出logging 需要安装Microsoft.Extensions.Logging.Console ![如图](./pics/efcore8.png) 然后在demodbcontext类中加入logger工厂 ``` public class DemoContext : DbContext { protected override void OnConfiguring(DbContextOptionsBuilder dbContextOptionsBuilder) { dbContextOptionsBuilder .UseLoggerFactory(ConsoleLoggerFacotry) .UseSqlServer("Server=.;uid=sa;pwd=shris2020;database=EfCoreDemo;"); } protected override void OnModelCreating(ModelBuilder modelBuilder) { //使用 fluant api 设置表 GamePlayer 的联合主键 modelBuilder.Entity().HasKey(x => new { x.GameId, x.PlayerId }); //制定1:1 关系表的主表,这里 HasForeignKey(x=>x.PlayerId) 确定 Resume 是主表 modelBuilder.Entity(). HasOne(x => x.Player). WithOne(x => x.Resume). HasForeignKey(x => x.PlayerId); } public DbSet Clubs { get; set; } public DbSet Leagues { get; set; } public DbSet Players { get; set; } public DbSet Resumes { get; set; } public static readonly ILoggerFactory ConsoleLoggerFacotry = LoggerFactory.Create(builder => { builder.AddFilter((categroy, level) => categroy == DbLoggerCategory.Database.Command.Name && level == LogLevel.Information).AddConsole(); }); } ``` 在控制台中插入数据 ``` static void Main(string[] args) { using var context = new DemoContext(); //添加一笔数据 //var serieA = new League //{ // Country="Italy", // Name="Serie A" //}; //context.Leagues.Add(serieA); //添加多笔数据 //var serieA1 = new League //{ // Country = "Italy", // Name = "Serie A1" //}; //var serieA2 = new League //{ // Country = "Italy", // Name = "Serie A2" //}; //context.Leagues.AddRange(serieA1, serieA2); //context.Leagues.AddRange(new List { serieA1,serieA2}); //添加不同类型数据 var serieA = context.Leagues.Single(x => x.Name=="Serie A"); var serieB = new League { Country = "Italy", Name = "Serie B" }; var serieC = new League { Country = "Italy", Name = "Serie C" }; var milan = new Club { Name = "AC Milan", City = "Milan", DateOfEstablishment = new DateTime(1899, 12, 16), League = serieA }; context.AddRange(serieB, serieC, milan); var count = context.SaveChanges(); Console.WriteLine(count); } ``` 在控制台中查询数据 ``` static void Main(string[] args) { using var context = new DemoContext(); var country = "Italy"; //这种使用参数传入(country) logger 输出是不会显示参数的,需要在logger中额外配置 EnableSensitiveDataLogging() var leagues = context.Leagues .Where(x => x.Country == country) .ToList(); //模糊查询-1 var leaguesLike1 = context.Leagues .Where(x => x.Country.Contains("e")) .ToList(); //模糊查询-2 var leaguesLike2 = context.Leagues .Where(x => EF.Functions.Like(x.Country,"%e%")) .ToList(); //这种情况,会在foreach中打开数据库连接,直到执行完毕后再关闭,当循环内执行时间过长时,会影响数据库效率 //foreach(var league in context.Leagues) //{ // //... //} //所以一般情况会使用数据库查询结构foreach foreach (var league in leagues) { Console.WriteLine(league.Name); } //ToList(), First(), FirsOrDefault() //Single(), SingleOrDefault(), Last(), LastOrDefault() //Count(), LongCount(), Min(), Max(), Average(), Sum() //Find() var first = context.Leagues.FirstOrDefault(x => EF.Functions.Like(x.Country, "%e%")); Console.WriteLine(first?.Name); //需要排序,不加 orderby(orderbydescending) 会抛 exception var last = context.Leagues.OrderByDescending(x=>x.Id).LastOrDefault(x => EF.Functions.Like(x.Country, "%e%")); Console.WriteLine(last?.Name); } ``` 删除数据 级联删除由DeleteBehavior的枚举值来设置: |行为名称|对内存中的依赖项/子项的影响|对数据库中的依赖项/子项的影响| |:---:|:---:|:---:| |Cascade|删除实体|删除实体| |ClientSetNull|外键属性设置为 null|无| |SetNull|外键属性设置为 null|外键属性设置为 null| |Restrict|无|无| + 如果实体在没有父项时不能存在,且希望 EF 负责自动删除子项,则使用“Cascade” 。 - 在没有父项时不能存在的实体通常使用必选关系,其中“Cascade” 是默认值。 + 如果实体可能有或可能没有父项,且希望 EF 负责为你将外键变为 null,则使用“ClientSetNull” - 在没有父项时可以存在的实体通常使用可选关系,其中“ClientSetNull” 是默认值。 + 如果希望数据库即使在未加载子实体时也尝试将 null 值传播到子外键,则使用“SetNull” 。 但是,请注意,数据库必须支持此操作,并且如此配置数据库可能会导致其他限制,实际上这通常会使此选项不适用。 这就是SetNull不是默认值的原因。 + 如果不希望 EF Core 始终自动删除实体或自动将外键变为 null,则使用“Restrict” 。 请注意,这要求使用代码手动同步子实体及其外键值,否则将引发约束异常 + 如果外键不可以为 null,则将外键值设置为 null 是无效的,会引发异常 不知道使用Annotation方式去解决,我还是使用了FluentApi去告知为级联删除。 ``` modelBuilder.Entity(e=> { e.ToTable("order"); e.HasData(orders); e.HasMany(e => e.NextOrders) .WithOne(e => e.PreviousOrder) .HasForeignKey(e => e.PreviousOrderId) .IsRequired(false).OnDelete(DeleteBehavior.Cascade); }); ``` ``` static void Main(string[] args) { using var context = new DemoContext(); //ef 只能删除被追踪的数据,所以必须先查出来 var milan = context.Clubs.Single(x => x.Name.Contains("AC")); //调用删除方法 context.Clubs.Remove(milan); //context.Remove(milan); //context.Clubs.RemoveRange(milan, milan); //context.RemoveRange(milan, milan); var count = context.SaveChanges(); Console.WriteLine(count); } ``` 修改数据 ``` static void Main(string[] args) { using var context = new DemoContext(); //首先数据必须被context 追踪才能修改 //var leauue = context.Leagues.First(); //leauue.Name += "~~"; //var count = context.SaveChanges(); //Console.WriteLine(count); //var leagues = context.Leagues.Skip(1).Take(3).ToList(); //foreach(var league in leagues) //{ // league.Name += "~~"; //} //var count = context.SaveChanges(); //Console.WriteLine(count); //大多数数据都是从前端传进来的,没法从数据库查询 var leagues = context.Leagues.AsNoTracking().Skip(1).Take(3).ToList(); //这里用 asnotracking 模拟 前端过来数据 foreach(var league in leagues) { league.Name += "++"; } context.Leagues.UpdateRange(leagues); var count = context.SaveChanges(); Console.WriteLine(count); } ``` 添加关系数据 ``` static void Main(string[] args) { using var context = new DemoContext(); //级联添加 //var serieA = context.Leagues.SingleOrDefault(x => x.Name.Contains("Serie A")); //var juventus = new Club //{ // League = serieA, // Name = "Juventus", // City = "Torino", // DateOfEstablishment = new DateTime(1897, 11, 1), // Players=new List // { // new Player // { // Name="C.Ronaldo", // DateOfBirthday=new DateTime(1985,2,5) // } // } //}; //context.Clubs.Add(juventus); //var count = context.SaveChanges(); //Console.WriteLine(count); //var juventus = context.Clubs.Single(x => x.Name == "Juventus"); //juventus.Players.Add(new Player //{ // Name="Gonzalo Higuain", // DateOfBirthday=new DateTime(1987,12,10) //}); //int count = context.SaveChanges(); //Console.WriteLine(count); // 使用 add remove update 都会追踪context变化,这里只是增加了一个球员, //却会同时 update club 的记录 然后再 insert 一个player //var juevntus = context.Clubs.Single(x => x.Name == "Juventus"); //juevntus.Players.Add(new Player //{ // Name="Matthijs de Ligt", // DateOfBirthday=new DateTime(1999,12,18) //}); //{ // using var newContext = new DemoContext(); // newContext.Clubs.Update(juevntus); // var count = newContext.SaveChanges(); // Console.WriteLine(count); //} //使用 attach 实现上面需求 // attach 为附加意思,发现clubs 中attach 对象是有主键的对象,所以不会修改club //但是发现club 中有一个 player 对象,没有主键,所以就执行新增操作 //var juevntus = context.Clubs.Single(x => x.Name == "Juventus"); //juevntus.Players.Add(new Player //{ // Name = "Miralem Pjanic", // DateOfBirthday = new DateTime(1999, 12, 18) //}); //{ // using var newContext = new DemoContext(); // newContext.Clubs.Attach(juevntus); // var count = newContext.SaveChanges(); // Console.WriteLine(count); //} //添加一个球员简历, 但是为什么球员那里的 ResumeId 不会变? var resume = new Resume { PlayerId=1, Description="..." }; context.Resumes.Add(resume); var count = context.SaveChanges(); Console.WriteLine(count); } ``` |方法|有主键|没主键| |:---:|:---:|:---:| |ADD|如果传入数据主键有值,则把数据添加到数据库中,
如果该表主键是自动生成的,但手动传入了主键,则报错|新增数据| |UPdate|如果传入值有主键,则修改该条记录|新增数据| |Attach|不会发生任何变化|新增数据| 加载关联数据 + 加载关联数据 - 预先加载,Eager loading - 显式加载,Explicit loading - 懒加载,Lazy loading 在EF 中context.ChangeTracker 用来管理变化追踪,所有实体(Entity)的变化都会加入到 Entries() 中来 ![快速监视](./pics/efcore9.png) Include 代表加载数据同时,把它的关联数据也一起带出来 ThenInclude 代表加载关联属性的关联数据 Entry(x) 加载一个数据,后面加 .collection 代表X 数据的集合关联属性 .refernce 代表x 数据的单个类型的关联属性 ``` static void Main(string[] args) { //预加载 using var context = new DemoContext(); //var clubs = context.Clubs // .Where(a=>a.Id>0) // .Include(b => b.League) // .Include(c=>c.Players) // .ThenInclude(d=>d.Resume) // .Include(e => e.Players) // .ThenInclude(f => f.GamePlayers) // .ThenInclude(g=>g.Game) // .ToList(); //context 无法追踪匿名类,只能追踪它识别的类,一下方法中select中的匿名类无法追踪 //但是匿名类中 Players 将被追踪 //var info = context.Clubs // .Where(x => x.Id > 0) // .Select(x => new // { // x.Id, // LeagueName = x.League.Name, // x.Name, // Players = x.Players // .Where(p => p.DateOfBirthday > new DateTime(1900, 1, 1)) // }).ToList(); //foreach (var data in info) //{ // foreach (var player in data.Players) // { // player.Name += "~"; // } //} //context.SaveChanges(); //查询一笔数据,这种方法无法应用与一笔集合数据的查询 var clb = context.Clubs.First(); //将这笔数据的集合类型的关联数据查询出来 context.Entry(clb).Collection(x => x.Players).Load(); //加上查询条件的加载 context.Entry(clb).Collection(x => x.Players) .Query().Where(x => x.DateOfBirthday > new DateTime(1990)).Load(); //将这笔数据的单个类型的关联数据查询出来 context.Entry(clb).Reference(x => x.League).Load(); //GamePlayer 表由于不在 DemoContext 的 DbSet 属性中,为了让其被追踪,使用如下方法: var gamePlayers = context.Set().Where(x => x.PlayerId > 0).ToList(); } ``` 修改关联数据 下面通过级联关系查出一个player 更新他的名字,缺更改了3条记录。(另外两条虽然没有变化,但是EF 还是会更新,影响了效率) ``` var game = context.Games.Include(x => x.GamePlayers) .ThenInclude(y => y.Player).First(); var firstPlayer = game.GamePlayers[0].Player; firstPlayer.Name += "$"; { using var newcontext = new DemoContext(); newcontext.Update(firstPlayer); var count = newcontext.SaveChanges(); Console.WriteLine(count); } ``` 这时可以直接更新player 表。 ``` var game = context.Games.Include(x => x.GamePlayers) .ThenInclude(y => y.Player).First(); var firstPlayer = game.GamePlayers[0].Player; firstPlayer.Name += "#"; context.Players.Update(firstPlayer); ``` 也可以: ``` var game = context.Games.Include(x => x.GamePlayers) .ThenInclude(y => y.Player).First(); var firstPlayer = game.GamePlayers[0].Player; firstPlayer.Name += "@"; { using var newcontext = new DemoContext(); newcontext.Entry(firstPlayer).State = EntityState.Modified; var cut = newcontext.SaveChanges(); Console.WriteLine(cut); } ``` 或者使用entry 修改 ``` //entry 方法 var game = context.Games.Include(x => x.GamePlayers) .ThenInclude(y => y.Player).First(); var firstPlayer = game.GamePlayers[0].Player; firstPlayer.Name += "@"; { using var newcontext = new DemoContext(); newcontext.Entry(firstPlayer).State = EntityState.Modified; var cut = newcontext.SaveChanges(); Console.WriteLine(cut); } ``` 删除多对多数据 ``` var game = context.Games.Include(x => x.GamePlayers).First(); var gameplayer = game.GamePlayers.Where(x => x.PlayerId == 4).First(); game.GamePlayers.Remove(gameplayer); ``` 修改多对多关系 先删除再添加 执行原生SQL 创建视图和存储过程: 生成一个新的没有修改的migration, 在up 方法中创建视图和存储过程,在down 方法中回滚,回滚的顺序应该和创建相反 然后执行 update-database ``` protected override void Up(MigrationBuilder migrationBuilder) { migrationBuilder.Sql(@" create view [dbo].[PlayserClubView] as select p.Id as PlayerId,p.name as PlayerName,c.Name as ClubName from [dbo].[Players] as p inner join [dbo].[Clubs] as c on p.ClubId=c.Id"); migrationBuilder.Sql(@" create procedure [dbo].[RemoveGamePlayerProcedure] @playerId int=0 as delete from [dbo].[GamePlayer] where [PlayerId]=@playerId return 0"); } protected override void Down(MigrationBuilder migrationBuilder) { //回滚顺序应该和up 顺序相反 migrationBuilder.Sql(@"drop procedure [dbo].[RemoveGamePlayerProcedure]"); migrationBuilder.Sql(@"drop view [dbo].[PlayserClubView]"); } ``` ## 在WEB 项目集成 1. 首先创建一个WEB API 项目,安装 Microsoft.EntityFrameworkCore.SqlServer 2. 修改DemoDbContext, 注释掉 OnConfiguration 改用构造函数注入参数: ``` public DemoContext(DbContextOptions options) :base (options) { } ``` 3. appsettings 中配置数据库连接 ``` "ConnectionStrings": { "LocalSqlServer": "Server=.;uid=sa;pwd=shris2020;database=EfCoreDemo;" }, ``` 4. starpup 注入DemoDbContext ``` services.AddDbContext(options=> { options.UseSqlServer(Configuration.GetConnectionString("LocalSqlServer")); }); ```