一、依赖注入DI
需要引入包 Microsoft.Extensions.DependencyInjection
1、基本使用
internal class Program
{
static void Main(string[] args)
{
IServiceCollection services = new ServiceCollection();//构造容器对象
services.AddSingleton<IClass, Class1>();//想容器中注入一个服务
using (ServiceProvider provider = services.BuildServiceProvider())
{
IClass ic = provider.GetRequiredService<IClass>();
ic.Name = "ccc";
Console.WriteLine(ic.Name);
}
}
}
public class Class1:IClass
{
public string Name { get; set; }
public int Age { get; set; }
}
public interface IClass
{
public string Name { get; set; }
}
internal class Program
{
static void Main(string[] args)
{
IServiceCollection services = new ServiceCollection();//构造容器对象
services.AddSingleton<IClass, Class1>();//想容器中注入一个服务
using (ServiceProvider provider = services.BuildServiceProvider())
{
using (IServiceScope scope = provider.CreateScope())
{
IClass ic1 = scope.ServiceProvider.GetRequiredService<IClass>();
ic1.Name = "cheng1";
Console.WriteLine(ic1.Name);//cheng1
IClass ic2 = scope.ServiceProvider.GetRequiredService<IClass>();
ic2.Name = "cheng2";
Console.WriteLine(ic2.Name);//cheng2,此时ic1和ic2的name都是cheng2
Console.WriteLine(ic1.Name);//cheng2
Console.WriteLine(Object.ReferenceEquals(ic1,ic2));//true,因为是在同一个scoped范围内的
}
}
}
}
public class Class1:IClass
{
public string Name { get; set; }
public int Age { get; set; }
}
public interface IClass
{
public string Name { get; set; }
}
2、三种生命周期
2.1、瞬时模式(Transient)
定义:在瞬时生命周期中,每次从依赖注入容器中请求服务时,都会创建一个新的服务实例。
使用场景:适用于轻量级的、无状态的服务,这些服务不需要在多个请求之间共享状态。
public void ConfigureServices(IServiceCollection services)
{
services.AddTransient<IMyTransientService, MyTransientService>();
}
2.2、作用域模式(Scoped)
定义:在作用域生命周期中,服务实例是在同一个作用域内共享的。对于Web应用程序,作用域通常与HTTP请求相关联,即每个HTTP请求都会创建一个新的作用域,并在请求结束时销毁该作用域及其中的所有服务实例。
使用场景:适用于需要在同一请求内共享状态的服务,如数据库上下文、httpcontext、会话状态等。
public void ConfigureServices(IServiceCollection services)
{
services.AddScoped<IMyScopedService, MyScopedService>();
}
2.3、单例模式(Singleton)
定义:在单例生命周期中,整个应用程序生命周期内只创建一个服务实例,并且该实例会在整个应用程序中共享。
使用场景:适用于那些在整个应用程序中需要保持不变的服务,如配置服务、日志记录器等。
public void ConfigureServices(IServiceCollection services)
{
services.AddSingleton<IMySingletonService, MySingletonService>();
}
3、注入方式
3.1、构造函数注入
构造函数注入是最常见和推荐的依赖注入方式。在这种方式中,依赖项作为参数传递给类的构造函数。这确保了类的依赖项在对象被创建时就已经被注入,并且对象在整个生命周期内都持有这些依赖项的引用。
public interface IMyDependency
{
void DoSomething();
}
public class MyDependency : IMyDependency
{
public void DoSomething()
{
// 实现逻辑
}
}
public class MyClass
{
private readonly IMyDependency _dependency;
public MyClass(IMyDependency dependency)
{
_dependency = dependency;
}
// 使用 _dependency 调用所需的方法
}
// 在Startup.cs或Program.cs中配置依赖注入
public void ConfigureServices(IServiceCollection services)
{
services.AddTransient<IMyDependency, MyDependency>();
}
3.2、方法注入
方法注入是一种较为少见的依赖注入方式,它通过在类的方法中注入依赖项来实现。这种方法通常用于需要动态地或基于特定条件来注入依赖项的场景。
public class MyClass
{
private IMyDependency _dependency;
public void SetDependency(IMyDependency dependency)
{
_dependency = dependency;
}
public void DoSomething()
{
// 使用 _dependency 调用所需的方法
}
}
// 实际上,在配置依赖注入时,你仍然需要在Startup.cs或Program.cs中注册服务
// 但在使用时,你需要手动调用SetDependency方法来注入依赖项
3.3、属性注入
属性注入是另一种依赖注入方式,它通过类的属性来注入依赖项。虽然属性注入在某些情况下很有用,但它不如构造函数注入那样受到推荐,因为它可能使类的依赖关系不那么明显,并且可能允许类的实例在完全初始化之前就被使用。
需要注意的是,在ASP.NET Core的默认依赖注入容器中,并不直接支持属性注入到控制器中,但可以通过其他方式(如中间件、视图等)实现属性注入,或者使用第三方库来支持。
public class MyClass
{
// 假设有某种方式支持属性注入
public IMyDependency Dependency { get; set; }
// 使用 Dependency 调用所需的方法
}
// 实际上,在ASP.NET Core中,你可能需要自定义实现或使用第三方库来支持属性注入
二、配置系统
1、基础用法
nuget包 Microsoft.Extensions.Configuration,Microsoft.Extensions.Configuration.Json
config.json文件如下,要属性设置为始终复制
{
"student1": {
"name": "kawa1",
"age": "18",
"address": {
"province": "henan",
"xian": "luyi"
}
},
"student2": "kawa2"
}
namespace Configuration_lianxi
{
internal class Program
{
static void Main(string[] args)
{
//optional表示文件是否可选,true表示文件不存在时,不会报错,否则反之。
//reloadonchange表示如果文件更改了是否重新加载配置
IConfigurationBuilder configurationBuilder = new ConfigurationBuilder()
.SetBasePath(Directory.GetCurrentDirectory())//SetBasePath方法设置了配置文件的基路径
.AddJsonFile("config.json",optional:false,reloadOnChange:true);//基路径下的config.json配置文件
IConfigurationRoot root = configurationBuilder.Build();
Console.WriteLine(root["student2"]);//kawa2
Console.WriteLine(root.GetSection("student2").Value);//kawa2
Console.WriteLine(root.GetSection("student1:age").Value);//18
//需要nuget包 Microsoft.Extensions.Configuration.Binder
Console.WriteLine(root.GetValue<int>("student1:age"));//18(int类型)
Class1 class1 = root.GetSection("student1:address").Get<Class1>();//绑定到Class1
Console.WriteLine(class1.province);//henan
}
}
class Class1
{
public string province { get; set; }
public string xian { get; set; }
}
}
在web api中也可以这样读取配置
namespace Configuration_lianxi2.Controllers
{
[Route("api/[controller]")]
[ApiController]
public class Test2Controller : ControllerBase
{
private readonly IConfiguration _configuration;
public Test2Controller(IConfiguration configuration)
{
this._configuration = configuration;
}
[HttpGet]
public string Method1()
{
string str1 = _configuration.GetSection("Student1:name").Value;//kawa1
return str1;
}
}
}
2、选项方式读取配置
使用选项方式读取配置是.NET Core中推荐的方式,因为他不仅和依赖注入机制结果得更好,而且它可以实现配置修改后自动刷新。
IOptions<T>:在配置改变后,我们不能读取新的值,必须重启程序。
IOptionsMonitor<T>:配置改变后,可以读到新的值。
IOptionsSnapshot<T>:配置改变后,可以读到新的值,与上者不同的是,上者在同一范围内会保持一致性。
(比如A,B代码都读取同一个配置,在A运行后B运行前更改了配置,使用IOptionsMonitor的话A读取的是旧值,B是新值,使用IOptionsSnapshot的话A,B都是旧值,只有再次进入这个范围才会是新值)
首先配置json,appsetting.json
{
"Logging": {
"LogLevel": {
"Default": "Information",
"Microsoft.AspNetCore": "Warning"
}
},
"AllowedHosts": "*",
"student1": {
"name": "kawa1",
"age": "18",
"address": {
"province": "henan",
"xian": "luyi"
}
},
"student2": "kawa2"
}
再定义实体类用于绑定配置
public class Student1
{
public string Name { get; set; }
public int age { get; set; }
public Address Address { get; set; }
}
public class Address
{
public string Province { get; set; }
public string xian { get; set; }
}
之后,在program.cs中注入服务
//选项配置
builder.Services.Configure<Student1>(builder.Configuration.GetSection("Student1"));
builder.Services.Configure<Address>(builder.Configuration.GetSection("student1:address"));
最后,新建控制器测试
namespace Configuration_lianxi2.Controllers
{
[Route("api/[controller]")]
[ApiController]
public class Test1Controller : ControllerBase
{
private readonly IOptionsSnapshot<Student1> _optionsSnapshot;
private readonly IOptionsSnapshot<Address> _optionsSnapshot1;
public Test1Controller(IOptionsSnapshot<Student1> optionsSnapshot, IOptionsSnapshot<Address> optionsSnapshot1)
{
_optionsSnapshot = optionsSnapshot;
_optionsSnapshot1 = optionsSnapshot1;
}
[HttpGet]
public IActionResult Method1()
{
ArrayList list1 = new();
string str1 = _optionsSnapshot.Value.Name;//kawa1
int str2 = _optionsSnapshot.Value.age;//18
string str3 = _optionsSnapshot1.Value.Province;//henan
return Ok(list1);
}
}
}
三、日志系统
1、基础
1.1、日志级别
Trace: 用于详细诊断信息,通常只在诊断问题时启用。
Debug: 用于调试信息,通常在开发过程中使用。
Information: 用于常规信息性消息,确认程序按预期工作。
Warning: 用于表示某些意外事件的提示,或者表明一些问题在不久的将来可能会发生(例如“磁盘空间低”)。程序仍然按预期工作。
Error: 用于严重错误,导致程序无法继续正常运行。
Critical: 用于非常严重的错误,表明程序本身可能无法继续运行。
2、NLog
特点:部署在本地文件,日志结构不明确
1.1、装包:NLog.Extensions.Logging
1.2、项目根目录下建配置文件:nlog.config(建议这个名字),设置为“如果较新则赋值”
<?xml version="1.0" encoding="utf-8" ?>
<nlog xmlns="http://www.nlog-project.org/schemas/NLog.xsd"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
<!--输出目标,往哪里输出-->
<targets>
<!--type:日志输出类型 File:文件 ColoredConsole:控制台-->
<!--fileName:日志存储的路径及名称-->
<!--layout:日志输出格式-->
<target name="log_file" xsi:type="File"
fileName="${basedir}/Logs/${shortdate}/${shortdate}.txt"
layout="${longdate} | ${event-properties:item=EventId_Id:whenEmpty=0} | ${uppercase:${level}} | ${logger} | ${message} ${exception:format=tostring}"
archiveFileName="${basedir}/archives/${shortdate}-{#####}.txt"
archiveAboveSize="102400"
archiveNumbering="Sequence"
concurrentWrites="true"
keepFileOpen="false" />
<!--<target name="console" xsi:type="ColoredConsole" layout="[${date:format=HH\:mm\:ss}]:${message} ${exception:format=message}" />-->
</targets>
<!--定义使用哪个target输出-->
<rules>
<logger name="*" minlevel="Trace" writeTo="log_file" />
<!--<logger name="*" minlevel="Debug" writeTo="console" />-->
</rules>
</nlog>
1.3、program.cs
builder.Logging.AddNLog("nlog.config");
var app = builder.Build();
1.4、新建个API用来测试
namespace NlogTest.Controllers
{
[Route("api/[controller]")]
[ApiController]
public class NlogTest1 : ControllerBase
{
private readonly ILogger<NlogTest1> logger;
public NlogTest1(ILogger<NlogTest1> logger)
{
this.logger = logger;
}
[HttpGet]
public IActionResult Test1()
{
logger.LogError("好像报错了");
return Ok("okle ");
}
}
}
结果:会在根目录下的bin文件下生成一个log文件夹
3、Serilog
包
- Serilog.AspNetCore:ASP.NET Core的集成包。
- Serilog.Sinks.File:用于将日志写入文件的包。
program.cs
// 配置Serilog
Log.Logger = new LoggerConfiguration()
.MinimumLevel.Debug() // 设置最小日志级别
.MinimumLevel.Override("Microsoft", LogEventLevel.Information) // 覆盖Microsoft的日志级别
.Enrich.FromLogContext() // 从日志上下文中丰富日志
.WriteTo.Console(new CompactJsonFormatter()) // 输出到控制台
.WriteTo.File("logs/myapp-.txt", rollingInterval: RollingInterval.Day) // 按天滚动写入文件
// 如果需要,可以添加更多接收器,如数据库等
.CreateLogger();
builder.Host.UseSerilog(); // 将Serilog注册为日志提供者
新建控制器方法测试
namespace Serilog_lianxi.Controllers
{
[Route("api/[controller]")]
[ApiController]
public class Test1Controller : ControllerBase
{
private readonly ILogger<Test1Controller> _logger;
public Test1Controller(ILogger<Test1Controller> logger)
{
_logger = logger;
}
[HttpGet]
public string Method1()
{
_logger.LogInformation("我来了");
return "猜猜我是谁";
}
}
}
四、缓存
缓存指的是在软件应用运行过程中,将一些数据生成副本直接进行存取,而不是从原始源(数据库,业务逻辑计算等)读取数据,减少生成内容所需的工作,从而显著提高应用的性能和可伸缩性。
1、缓存分为:响应缓存(客户端响应缓存、服务器端响应缓存)、内存缓存、分布式缓存
2、缓存命中:从缓存中获取到了要获取的数据
缓存命中率:多次请求中,命中的请求占全部请求的百分比
1、客户端响应缓存
不需要手动控制响应报文头的cache-control,只需要给需要缓存的控制器的操作方法添加ResponCache特性即可
namespace WebAPI0407_Cache.Controllers
{
[Route("api/[controller]")]
[ApiController]
public class TestController : ControllerBase
{
//浏览器端缓存时间60s
[HttpGet]
[ResponseCache(Duration = 60)]
public DateTime GetNow()
{
return DateTime.Now;
}
}
}
2、服务器端响应缓存
1、除了需要添加[ResponseCache(Duration = 60)]特性以外,还需要在program的app.MapControllers()之前添加app.UseResponseCaching()。
2、如果启用了CORS(跨域),要确保app.UseCors写在app.UseResponseCaching之前
3、内存缓存
1、内存缓存保存的是一系列的键值对,类似Dictionary。多个不同网站是运行在不同进程中的,所以不同网站的内存缓存是互不干扰的。网站重启或服务器故障,则清空内存缓存数据。nuget包是Microsoft.Extensions.Caching.Memory
2、使用:在program.cs的builder.Build()前添加builder.Services.AddMemoryCache();来把内存缓存相关服务注册到依赖注入容器中。
3、然后使用nuget包中的IMemoryCache接口。其中有一个经常用的异步方法GetOrCreateAsync
Task GetOrCreateAsync(this IMemoryCache cache, object key, Func> factory)
尝试获取缓存键为key的缓存值,方法的返回值为获取的缓存值。如果没有为缓存键key的缓存值,则调用factory指向的回调函数从数据源去获取数据,把获取到的数据作为返回值以及缓存值
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Caching.Memory;
namespace WebAPI0407_Cache.Controllers
{
[Route("api/[controller]")]
[ApiController]
public class TestController : ControllerBase
{
public readonly ILogger<TestController> _logger;
private readonly MyDbcontent _myDbcontent;
private readonly IMemoryCache _memoryCache;
public TestController(ILogger<TestController> logger, MyDbcontent myDbcontent, IMemoryCache memoryCache)
{
_logger = logger;
_myDbcontent = myDbcontent;
_memoryCache = memoryCache;
}
[HttpGet]
public async Task<Book[]> GetBooks()
{
_logger.LogInformation("开始执行GetBooks");
var items = await _memoryCache.GetOrCreateAsync("AllBooks", async (o) =>
{
_logger.LogInformation("从数据库中读取数据");
return await _myDbcontent.Books.ToArrayAsync();
});
_logger.LogInformation("把数据库返回个调用者");
return items;
}
}
}
4、通过设置缓存过期时间来解决缓存数据不一致的问题。
a1)、绝对过期时间:自设置缓存之后的指定时间过期
a2)、滑动过期时间:设置完时间后,如果在过期时间内被访问了,则重新计时过期时间。
过期时间通过GetOrCreateAsync中的IcacheEntry类型的参数来设置
b1)AbsoluteExpirationRelativeToNow用来设置绝对过期时间
[HttpGet]
public async Task<Book[]> GetBooks()
{
_logger.LogInformation("开始执行GetBooks");
var items = await _memoryCache.GetOrCreateAsync("AllBooks", async (o) =>
{
o.AbsoluteExpirationRelativeToNow = TimeSpan.FromSeconds(10);//AbsoluteExpirationRelativeToNow用来设置绝对过期时间
_logger.LogInformation("从数据库中读取数据");
return await _myDbcontent.Books.ToArrayAsync();
});
_logger.LogInformation("把数据库返回个调用者");
return items;
}
b2)SlidingExpiration用来设置滑动过期时间
[HttpGet]
public async Task<Book[]> GetBooks()
{
_logger.LogInformation("开始执行GetBooks");
var items = await _memoryCache.GetOrCreateAsync("AllBooks", async (o) =>
{
o.SlidingExpiration = TimeSpan.FromSeconds(10);//SlidingExpiration用来设置滑动过期时间
_logger.LogInformation("从数据库中读取数据");
return await _myDbcontent.Books.ToArrayAsync();
});
_logger.LogInformation("把数据库返回个调用者");
return items;
}
b3)绝对过期时间和滑动过期时间混合使用
[HttpGet]
public async Task<Book[]> GetBooks()
{
_logger.LogInformation("开始执行GetBooks");
var items = await _memoryCache.GetOrCreateAsync("AllBooks", async (o) =>
{
o.AbsoluteExpirationRelativeToNow = TimeSpan.FromSeconds(30);//AbsoluteExpirationRelativeToNow用来设置滑动过期时间
o.SlidingExpiration = TimeSpan.FromSeconds(10);//AbsoluteExpirationRelativeToNow用来设置滑动过期时间
_logger.LogInformation("从数据库中读取数据");
return await _myDbcontent.Books.ToArrayAsync();
});
_logger.LogInformation("把数据库返回个调用者");
return items;
}
5、缓存穿透问题的规避
缓存穿透问题指的是访问的资源不存在,数据为null,导致所有访问都会去查询数据库,从而增加数据库压力的问题。
下面这种写法会引起缓存穿透:
因为当数据库中不存在查询的数据时,变量b为null,再次访问,又会去访问数据库 。
解决办法:使用GetOrCreate的写法对数据进行缓存,不会导致缓存穿透,因为查询结果虽然为null,但也会被放入缓存中。
6、缓存雪崩问题
缓存项集中过期,引起请求在一段时间内同时访问数据库,导致数据库压力增大,这样的现象称为缓存雪崩。
解决缓存雪崩问题,可以在缓存的过期时间之上,增加一个随机的过期时间,避免在同一时间内大量缓存项同时失效。
4、Redis缓存
redis安装教程连接 Windows 安装Redis教程(图文详解)_下载使用redis_Redis可视化_配置Redis环境变量_redis-server redis.windows.conf-CSDN博客
1、包 Microsoft.Extensions.Caching.StackExchangeRedis
2、program.cs
//添加Redis缓存服务,最好放到配置文件中,这里省事了
builder.Services.AddStackExchangeRedisCache(option =>
{
option.Configuration = "localhost";
option.InstanceName = "webapi_kawa_";//添加前缀,避免缓存名称混乱
});
3、测试,引入依赖包中的IDistributedCache接口
namespace RedisCache_lianxi.Controllers
{
[Route("api/[controller]")]
[ApiController]
public class Test1Controller : ControllerBase
{
private readonly IDistributedCache _distributedCache;
public Test1Controller(IDistributedCache distributedCache)
{
_distributedCache = distributedCache;
}
[HttpGet]
public string Method1()
{
string str1 = _distributedCache.GetString("kawa1");
if(str1 == null)
{
_distributedCache.Set("kawa1", Encoding.UTF8.GetBytes(DateTime.Now.ToString()) , new DistributedCacheEntryOptions().SetAbsoluteExpiration(TimeSpan.FromMinutes(1)));
str1 = _distributedCache.GetString("kawa1");
}
return str1;//一分钟后缓存值才会刷新
}
}
}
Redis缓存可视化界面效果
注:缓存穿透等问题参考内存缓存