|
|
在VS中ASP.NET core项目有两种启动方式:(launchSettings.json中设置commandName)
可能是为了避免混淆,最新版的ASP.NET core WebApi不支持使用Conventional的route配置模式。
所以只能使用特性。
core中不是让Controller继承自ApiController,而是要在其上声明[ApiController],表明这个Controller可以作为Route的Endpoint使用。
一旦声明了[ApiController](也可以不声明,但WebApi中不推荐,其作用详见:微软文档),就必须要声明[Route]特性:
演示:略[Route("[controller]")] public class WeatherForecastController : ControllerBase
[controller]代指当前的Controller名称,此处就是WeatherForecast。
其他和Framework无异。(演示:略)
所有controller继承的是:
ASP.NET WebApi的Controller继承的是ControllerBase。
在core中就彻底废弃了HttpContext.Current,但Request和Response相比WebApi FW就更人性化一些,更贴近ASP.NET的传统(MVC)一些。
演示:查看属性
一样可以:
注意Content()方法只能传入string参数(不再是泛型类型),所以如果要同时能返回IActionResult和自定义类型对象,咋办?
public /*???*/ Get(int id) { var rng = new Random(); if (rng.Next(100) % 2 == 0) { return BadRequest(); } else { return Enumerable.Range(1, 5).Select(index => new WeatherForecast
这就需要ActionResult<TValue>了:
public ActionResult<WeatherForecast[]> Get(int id)
转到定义:
public sealed class ActionResult<TValue> : IConvertToActionResult@想一想@:这怎么实现的呢?(复习:类型转换重载)
默认WebApi core返回的是JSON格式的数据。
如果需要返回其他格式,比如XML,
首先需要在Startup.cs的ConfigureServices()中添加一个能够将C#对象转换成XML格式的service:
services.AddControllers().AddXmlSerializerFormatters();然后,
[Produces("application/xml")]或者,如果要作用于整个项目的话,就在ConfigureServices()中:
services.AddMvcCore(opt => { opt.Filters.Add(new ProducesAttribute("application/xml")); })
core内置的依赖注入工具。(复习:Autofac)
由依赖注入机制生成的对象,在ASP.NET中被称为service。
需要在Startup.cs的ConfigureServices()中添加:
services.AddSingleton<IUserService, UserService>();
这个过程被称之为:注册。(演示:还有其他重载方法)
它告诉ASP.NET core项目:在使用依赖注入时,用UserService对象作为IUserService变量(或者参数)的引用。或者说,当需要一个IUserService变量时,给一个UserService对象。
我们需要一个UserController的有参构造函数:
public class UserController : ControllerBase { private IUserService service; public UserController(IUserService service) { this.service = service;
而且在Service中的一样可以注入Repository:
public class UserService : IUserService { private IUserRepository userRepository; public UserService(IUserRepository userRepository) { this.userRepository = userRepository;
这里的IUserRepository究竟使用哪个实例对象,一样需要注册/声明:
services.AddSingleton<IUserRepository, UserRepository>();
这是.NET core 1.1开始引入的新特性
主要是为了解决在startup.cs中注册所有service的问题,从而严重的破坏了传统“分层架构”不能跨层调用的原则。
反过来促使我们思考:严格的分层是否有必要?
-- 感慨:技术的“螺旋式进步,破浪式前进”
类似于async方法的蔓延:只要底层进行了注入,上层也得注入,如上示例。
#试一试#:假设我们只需要Repository对Service的依赖注入,不需要在Controller中注入Service,行不行?
可以直接new一个repository对象传入:
this.service = new UserService(new UserRepository());但这样就丧失了依赖注入(在startup.cs中设置)的灵活性了。(演示)
正确的做法是在UserController中:
private IUserService service; public UserController(IUserRepository repository) { this.service = new UserService(repository); }
通过构造函数注入!
AddXXX()后面的XXX:
准备额外的
public class ArticleService : IArticleService { private IUserRepository userRepository; public ArticleService(IUserRepository userRepository) { this.userRepository = userRepository;让ArticleService和UserService都依赖IUserRepository。
断点演示:所有的Service使用
ASP.NET core和Framework最大的不同(难点),就在于core项目中大量有意无意的Service(依赖注入)应用。
比如我们要在SRV层中获得cookie,就首先要获得HttpContext对象,.NET Framework类库项目可以引入System.Web.dll,通过HttpContext.Current获得。
但.NET core的类库项目,是做不到的。(演示)
只能通过依赖注入:
public class UserService : IUserService { private HttpContext context; public UserService(IHttpContextAccessor accessor) { this.context = accessor.HttpContext;IHttpContextAccessor首先需要在Startup.cs中注册
services.AddHttpContextAccessor();
PS:AddHttpContextAccessor()实际上也不过是封装了AddScoped()……
微软推荐:使用扩展方法Add{ServiceName}合并service注册。
比如连接字符串,写在appsettings.json文件中:
"ConnectionStrings": { "_17bang": "Data Source=(localdb)\\ProjectModels;Initial Catalog=17bang;Integrated Security=True;" }
需要通过IConfiguration对象获取。
如果是在startup.cs中,该对象已经通过属性注入:
public class Startup { public Startup(IConfiguration configuration) { Configuration = configuration; } public IConfiguration Configuration { get; }
然后就可以直接使用:
string connStr = Configuration.GetConnectionString("_17bang");
如果不介意在UI层直接引入EF的话,可以直接使用ASP.NET core的便捷方法AddDbContext():
services.AddDbContext<SqlDbContext>(options => { options.UseSqlServer(Configuration.GetConnectionString("_17bang"));或者,其他项目可以“依葫芦画瓢”,比如在ProdRepository类库项目中:
然后在SqlDbContext中注入:
public class SqlDbContext : DbContext { private IConfiguration _configuration; public SqlDbContext(IConfiguration configuration) { this._configuration = configuration;
但是,这也导致必须在调用SqlDbContext的地方注入依赖(依赖蔓延):
public class UserRepository : IUserRepository { private IConfiguration configuration; public UserRepository(IConfiguration configuration) { this.configuration = configuration;
最后,如果是其他项目(比如DbFactory)引用SqlDbContext的话,就需要:
IConfiguration config = new ConfigurationBuilder() .SetBasePath(Environment.CurrentDirectory) .AddJsonFile("appsettings.json") .Build();演示:项目根目录下的.json文件编译时会copy到Environment.CurrentDirectory
SqlDbContext context = new SqlDbContext(config);
注意这里面的一些“大坑”:
#体会#:类库架构API应符合人的惯常预期,别人连蒙带猜就能用起来的类库/架构,才是好类库/架构
基于Service生命周期,我们可以非常方便的实现。只需要将之前的:
public User GetByName(string username) { return new SqlDbContext(configuration).Set<User>()
改成:
private DbContext dbContext; public UserRepository(DbContext dbContext) { this.dbContext = sqlDbContext;
public User GetByName(string username) { return dbContext.Set<User>()然后,在startup.cs中注册SqlDbContext的时候,就确定他们的生命周期为scoped:
services.AddScoped<DbContext, SqlDbContext>();
PS:对标ASP.NET Framework的Module和Handler,
ASP.NET core更加的“暴露”,在Startup.cs的Configure()中已有中间件配置:
public void Configure(IApplicationBuilder app, IWebHostEnvironment env) { if (env.IsDevelopment()) { app.UseDeveloperExceptionPage(); } app.UseHttpsRedirection(); app.UseRouting();
所以简单的讲一讲。
理解:一个HTTP请求到达ASP.NET之后,会:
app.UseEndpoints(endpoints => { endpoints.MapControllers(); //Route的终点(Endpoint)为Controller });
和Filter类似,但(大部分)在Filter之前,比Filter适用范围更广,(如果自定义实现的话)不受时间节点限制。
我们开发中一般不会自定义的创建中间件,只是使用现成的中间件进行配置。配置时注意其顺序!
演示:
ASP.NET core中对cookie进行了重构:区分了ResponseCookie(生成)和RequsetCookie(获取)!
演示:生成/获取
Response.Cookies.Append("UserId", "986");注意:
这样,我们就能在浏览器中看到生成的cookie了(chrome:F12->Application)
我们还可以通过
自行指定cookie的过期时间、域名、路径、是否“机要”等:
Response.Cookies.Append("userId", "id=18", new CookieOptions { Expires = System.DateTimeOffset.Now.AddDays(1), Domain = ".17bang.ren", Path = "/Log", IsEssential = true });
PS:关于IsEssential:在ASP.NET core 3.1版本之前,为了避免网站利用cookie收集用户隐私,国外已普遍立法要求使用cookie前应经过用户同意。
所以,ASP.NET core通过在startup.cs的Configure()方法中:
app.UseCookiePolicy();强制要求cookie的使用必须先经过用户同意。但如果在生成某个cookie的时候指定IsEssential=true,就可以绕过这个限制。
使用Try模式:
bool hasCookie = Request.Cookies.TryGetValue("userId", out string userId);
session在ASP.NET core项目中的应用要麻烦一些。首先就需要
在startup.cs中配置:
app.UseSession();
services.AddSession();
services.AddDistributedMemoryCache();
可以在两个地方配置session的过期时间、使用的cookie等:
services.AddSession(option => { option.Cookie = new CookieBuilder { Name = "17bang"
app.UseSession(new SessionOptions { IdleTimeout = TimeSpan.FromSeconds(5),
ASP.NET core的session不能直接存放(自定义)对象了。
ISession定义的Set()方法
void Set(string key, byte[] value)参数value类型为byte[](@想一想@:为什么?便于序列化)
但这非常不便于开发人员,所以有了存放int和string的扩展方法:SetInt32()和SetString()
int? userId = HttpContext.Session.GetInt32("userId"); if (userId == null) { HttpContext.Session.SetInt32("userId", 18);
如果要在session中存放对象的话,需要先将对象序列化。
推荐使用Newtonsoft的JsonConvert:
if (string.IsNullOrEmpty(jsonUser)) { jsonUser = JsonConvert.SerializeObject(user); else { user = JsonConvert.DeserializeObject<Entities.User>(jsonUser);
需要异步Action,代码如下:
IFormCollection forms = await Request.ReadFormAsync(); IFormFile file = forms.Files[0]; using (var stream = System.IO.File.Create( Path.Combine(server.WebRootPath, file.FileName))) { file.CopyTo(stream);其中,server来自依赖注入:
public StudentController(IWebHostEnvironment server) { this.server = server; }
Postman演示
可以调用File()返回FileContentResult,同MVC,略。
唯一需要注意的是:因为是.NET core项目,所以需要引入:Assembly System.Drawing.Common
和MVC混用,Result相关内容在WebApi中自动忽略。
多了一个Resource filters:authorization之后,Action之前,可
可以继承/实现异步的Filter:
public class AddHeaderFilter : IAsyncActionFilter { public async Task OnActionExecutionAsync( ActionExecutingContext context, ActionExecutionDelegate next) { //ActionExecuting await next(); //ActionExecuted
全局注册在Startup.cs的ConfigureServices()中,比如:
services.AddControllers(opt=> { opt.Filters.Add(typeof(AddHeaderFilter)); })
如果我们的Filter中需要Service,咋办?如果使用构造函数注入的话:
public class AddHeaderFilter : ActionFilterAttribute, IAsyncActionFilter { private IUserService userService; public AddHeaderFilter(IUserService userService) { this.userService = userService;
使用AddHeaderFilter的时候,语法都搞不定:
[AddHeaderFilter(/*???*/)]
这时候只有使用:
[ServiceFilter(typeof(AddHeaderFilter))]
[TypeFilter(typeof(AddHeaderFilter), Arguments = new object[] { "yz", "17bang" })]
两者的区别:
services.AddControllers(opt => { opt.Filters.Add(typeof(AddHeaderFilter));TypeFilter不需要,因为TypeFilter的实例生成依赖于:Microsoft.Extensions.DependencyInjection.ObjectFactory.
ASP.NET core提供了两个缓存接口,一般通过Service的方式使用:
首先需要在Startup.cs的ConfigureServices()中配置:
services.AddMemoryCache();
然后,在Controller的构造函数中注入:
private IMemoryCache cache; public UserController(IMemoryCache cache) { this.cache = cache;
断点演示:_cache的类型
接下来就可以直接使用_cache对象了,它有两个实例方法:
if (!cache.TryGetValue("time", out DateTime time)) { cache.Set("user", DateTime.Now);另外还有一个扩展方法:GetOrCreate(),可以一次性的完成上述逻辑:先从cache中获取,获取不到就获取且存放到缓存:
_cache.GetOrCreate(cacheEntry, c => DateTime.Now);
var cacheEntryOptions = new MemoryCacheEntryOptions() .SetSlidingExpiration(TimeSpan.FromSeconds(3)) .SetAbsoluteExpiration(DateTime.Now.AddMinutes(1));
注意:在ASP.NET core中,Absolute和Slide可以combine:
然后,将其传入Set()方法中:
cache.Set(cacheKey, cacheEntry, cacheEntryOptions);
ASP.NET core中不会自动清理缓存,所以我们有可能需要自行限制缓存的容量,详见:Use SetSize, Size, and SizeLimit to limit cache size
注意:不要把它和Framework的[OutputCache]相混淆,(*/ω\*)
ResponseCache仅仅是在Response Header中添加条目,本身并不进行缓存:
[ResponseCache(Duration = 50)]
就会生成:
Cache-Control: public, max-age=50
public可以变成:
你可以理解为:服务器“告诉/提示”客户端如何缓存数据到本地(及其“中间节点”)。
断点演示:每一次HTTP请求都会击中Action
还可以通过:
VaryByHeader = "User-Agent"
添加一个:
vary: User-Agent
VaryByQueryKeys = new string[] { "name" }
需要开启 Response Caching Middleware中间件。在StartUp.cs中设置:
services.AddResponseCaching();
app.UseResponseCaching();
多快好省!前端后端,线上线下,名师精讲
更多了解 加: