前言
.NET Core
是一个开源的模块化的Framework,不管是开发web或移动设备都在同一个Framework(.NET Core)
下运行,而且 .NET Core
也可在不同的操作系统上运行,包括Windows、linux、MacOS,实现了跨平台跨设备。
更棒的是.NET Core
在发布程序时不用事先安装Framework而是通过Nuget下载,这样在初次部署时就不用安装一个复杂而庞大Framework,而是按需下载。这种基于Nuget的按需加载铸就.NET Core
跨平台。
过去总是有人会说.Net
无法在linux上运行,java就可以。几乎一提到这个问题,就不可避免的引发Java和.Net
对比的口水战。
而.Net Core
的出现,以ASP.NET的跨平台版本出现在了我们的眼前,它顺应了开源大趋势,对.Net开发者是个喜事,也多了一个追求前沿技术的机会 。至少突破了操作系统的限制,不在局限于windows,让.NET
开发者和其它跨平台语言(如java,ruby)c开发者有了更多共同的话题。
楠木大叔,从事.NET
开发近十年,具有丰富的开发实战经验和带团队经验,经历了数十个实战项目中的洗礼。 深知技术的迭代迅速,深刻理解技术从业者的艰辛和不易。本教程致力于解决实际问题,以问题为导向,设置了实际工作的中的场景和案例,并逐一讲授解决方案和实战技巧。
1 .NET 生态
2016年微软发布了.NET Core 1.0
迄今已有好几年了,但是很多.NET
程序员也一定有以下疑问:
.NET Core
到底是不是.NET
的下一个版本?还是说只是.NET
支持跨平台的一个版本?作为传统的
.NET
开发者或者说开发的程序都是在WIndows环境下面工作的,有没有必要学习.NET Core
?.NET Core
和.NET Framework
有什么不同?在开发新的程序是应该怎么选择
.NET Core
和.NET Framework
?
从上面图中我们可以看到.net
主要分为三个部分.net FrameWork
,.net Core
,Xamarin
XAMARIN 主要用来构建APP的(包括IOS,Android Windows)主要用的是C#语言
.NET Framework这个是我们现在经常用的,用这个可以创建windows应用程序还有web applications ,现在你可以用它创建Winform ,UWP ,wpf 等等相关的应用程序 ,web 方面就是
Asp.net MVC
.NET Core 是微软推出的最新的开源的,跨平台的框架,用它可以创建的应用可以运行在MAC,Linux上 。 .net core 支持UWP 和
ASP.NET Core
。
.NET Standard
为什么要引入.NET Standard
?
.NET生态在发展的过程中长期都是.NET Framework
这条线,后面加入适用于 iOS、Android 和 Windows 的新式高性能应用程序开发的Xamarin,后续又增加了适用于 Windows、macOS 和 Linux 的.NET Core。于是.NET 生态
出现了“三足鼎立”的局面。
能不能让开发者们只需要掌握一种基础类库就可以适用于不同平台,换句话说写一份代码就可以在.NET Framework
,.NET Core
,Xamarin
都能运行?而.NET Standard
的出现就解决了这个问题。.NET Standard
背后的动机是在.NET生态系统中建立更大的一致性。
.NET Framework
老项目能够迁移到.NET Core
吗
我相信绝大数有一定资历的 .NET
程序员都已经在.NET Framework
项目中积累了大量的经验,那么这些项目能否直接迁移到.NET Core
中呢。注意,并不是所有的.net Framework
的代码都可以直接运行在.net core
上。
这是微软一直在做的事情,也是广大开发者的心声。
在 .NET 的整个历史记录中,它都尝试在版本之间以及 .NET 各个风格之间保持高级别的兼容性。 .NET Core 将继续坚守这个准则。 尽管可以将 .NET Core 视为独立于 .NET Framework 的新技术,但下面的两个因素使 .NET Core 无法脱离 .NET Framework:
有许多最初开发过或在继续开发 .NET Framework 应用程序的开发人员。 他们希望各个 .NET 实现中的行为保持一致。
.NET Standard 库项目允许开发人员创建面向 .NET Core 和 .NET Framework 共享的通用 API 的库。 开发人员希望用于 .NET Core 应用程序的库与用于 .NET Framework 应用程序的同一个库的行为相同
在希望保持各个 .NET 实现之间的兼容性的同时,开发人员还希望在各个 .NET Core 版本之间保持高级别的兼容性。 具体而言,为 .NET Core 早期版本编写的代码应在较高版本的 .NET Core 上无缝运行。 实际上,许多开发人员都希望新发布的 .NET Core 版本中的新 API 也应该与引入这些 API 的预发布版本兼容。
从 .NET Framework 迁移到 .NET Core
从微软官方的表述可以看到,依然存在影响兼容性的变更。对新手来说,是没有思想包袱的,但是对于老鸟,建议空杯心态,将 .NET Core 当作全新的技术来学。
2 .NET Core项目结构
结构介绍
打开解决方案对话框,展开所有的目录,我们可以看到如下结构
这是一个非常简洁的结构,也是 ASP.NET Core
最基本的目录结构,重点讲一下AskBot.Web项目下的 5 个目录和文件
Program.cs 和 Startup.cs 的区别在于 Program.cs 会调用 Startup.cs ,这个可以通过 Program.cs 中的代码看出来
WebHost.CreateDefaultBuilder(args).UseStartup<Startup>();
当然了,还有很多其它的文件,但这些文件不是 ASP.NET Core
的必要组成部分。
实际项目中结构
以上是基本结构,在实际项目中比较流行的项目分层架构为DDD(Domain-Driven-Design)模式。目前实际项目中比较流行前后端分离模式,所以架构这块根据实际情况来用。常规的大概如下:
3 .NET Core 运行机制
ASP.NET Core
应用程序是在 .NET Core 控制台程序下调用特定的库,这是ASP.NET Core
应用程序开发的根本变化。所有的ASP.NET托管库都是从Program开始执行,而不是由IIS托管。也就是说 .NET工具链可以同时用于.NET Core 控制台应用程序和ASP.NET Core
应用程序。
public class Program
{
public static void Main(string[] args)
{
var host = new WebHostBuilder()
.UseKestrel() //指定宿主程序为Kestrel
.UseStartup<Startup>()// 调用Startup.cs类下的Configure 和 ConfigureServices
.Build();
host.Run();
}
}
Starup
对于一个ASP.NET Core
程序而言,Startup 类是必须的。ASP.NET Core
在程序启动时会从Program类中开始执行,然后再找到UseStartup<Startup>
中找到配置的Startup的类,如果不指定Startup类会导致启动失败。
Startup 类:
- 可选择性地包括 ConfigureServices 方法以配置应用的服务 。 服务是一个提供应用功能的可重用组件。 在 ConfigureServices 中注册服务,并通过依赖关系注入 (DI) 或 ApplicationServices 在整个应用中使用服务 。
- 包括 Configure 方法以创建应用的请求处理管道。
在应用启动时,ASP.NET Core 运行时会调用 ConfigureServices 和 Configure:
public class Startup
{
public Startup(IConfiguration configuration)
{
Configuration = configuration;
}
public IConfiguration Configuration { get; }
public void ConfigureServices(IServiceCollection services)
{
services.AddRazorPages();
}
public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
if (env.IsDevelopment())
{
app.UseDeveloperExceptionPage();
}
else
{
app.UseExceptionHandler("/Error");
app.UseHsts();
}
app.UseHttpsRedirection();
app.UseStaticFiles();
app.UseRouting();
app.UseAuthorization();
app.UseEndpoints(endpoints =>
{
endpoints.MapRazorPages();
});
}
}
ConfigureServices 方法
ConfigureServices方法是应用程序运行时将服务添加到容器中,其实就是注册ASP.NET Core
中Configure方法、中间件及Controller等地方需要用到的依赖注入关系,用ASP.NET Core
项目模板的时候默认会将MVC的服务添加到容器中
ConfigureServices 方法:
- 可选。
- 在 Configure 方法配置应用服务之前,由主机调用。
- 其中按常规设置配置选项。
IServiceCollection 上有 Add{Service} 扩展方法。 例如,AddDbContext、AddDefaultIdentity、AddEntityFrameworkStores 和 AddRazorPages:
public class Startup
{
public Startup(IConfiguration configuration)
{
Configuration = configuration;
}
public IConfiguration Configuration { get; }
public void ConfigureServices(IServiceCollection services)
{
services.AddDbContext<ApplicationDbContext>(options =>
options.UseSqlServer(
Configuration.GetConnectionString("DefaultConnection")));
services.AddDefaultIdentity<IdentityUser>(
options => options.SignIn.RequireConfirmedAccount = true)
.AddEntityFrameworkStores<ApplicationDbContext>();
services.AddRazorPages();
}
将服务添加到服务容器,使其在应用和 Configure 方法中可用。
Configure
Configure 方法用于指定应用响应 HTTP 请求的方式。 可通过将中间件组件添加到 IApplicationBuilder 实例来配置请求管道。 Configure 方法可使用 IApplicationBuilder,但未在服务容器中注册。 托管创建 IApplicationBuilder 并将其直接传递到 Configure。
public class Startup
{
public Startup(IConfiguration configuration)
{
Configuration = configuration;
}
public IConfiguration Configuration { get; }
public void Configure(IApplicationBuilder app, IHostingEnvironment env)
{
if (env.IsDevelopment())
{
app.UseDeveloperExceptionPage();
app.UseBrowserLink();
}
else
{
app.UseExceptionHandler("/Home/Error");
}
app.UseStaticFiles();
app.UseMvc(routes =>
{
routes.MapRoute(
name: "default",
template: "{controller=Home}/{action=Index}/{id?}");
});
}
}
ASP.NET Core
是通过对IApplicationBuilder进行扩展来构建中间件的, 上面代码中每个use扩展方法都是将中间件添加到请求管道。也可以给Configure方法附加服务(如:IHostingEnvironment)这些服务在ConfigureServices方法中被初始化。
用ASP.NET Core
项目模板添加的应用程序,默认添加的几个中间件:
- UseStaticFiles 允许应用程序提供静态资源。
- UseMvc 将MVC添加到管道并允许配置路由。
4 自定义全局异常处理
4.1 Net Core中使用中间件方式
首先,创建一个中间件ExceptionMiddleware
public class ExceptionMiddleware
{
private readonly RequestDelegate next;
private IHostingEnvironment environment;
public ExceptionMiddleware(RequestDelegate next,IHostingEnvironment environment)
{
this.next = next;
this.environment = environment;
}
public async Task Invoke(HttpContext context)
{
try
{
await next.Invoke(context);
var features = context.Features;
}
catch (Exception e)
{
await HandleException(context, e);
}
}
private async Task HandleException(HttpContext context, Exception e)
{
context.Response.StatusCode = 500;
context.Response.ContentType = "text/json;charset=utf-8;";
string error = "";
if (environment.IsDevelopment())
{
var json = new { message = e.Message};
error = JsonConvert.SerializeObject(json);
}
else
error = "抱歉,出错了";
await context.Response.WriteAsync(error);
}
}
创建 HandleException(HttpContext context, Exception e) 处理异常,判断是 Development 环境下,输出详细的错误信息,非 Development 环境仅提示调用者“抱歉,出错了”,同时使用 NLog 组件将日志写入硬盘;同样,在 Startup.cs 中将 ExceptionMiddleware 加入管道中
//ExceptionMiddleware 加入管道
app.UseMiddleware<ExceptionMiddleware>();
启动调试,结果如下
{"message":"Attempted to divide by zero."}
统一封装冷异常处理方式和消息格式,对前端也很友好。
4.2 使用ExceptionFilter
前面提到,过滤器可以处理错误异常。这里可以实践一把。
新建一个.NET Core MVC
控制器(.net core WebAPI也类似)。
我在Test/Index Action方法中故意制造一个异常(我们知道在被除数不能为0).
public IActionResult Index()
{
int a = 0, b = 5;
var result = b/a;
}
在Visual Studio中调试报错了
我们深知,异常这样报错很不友好,于是我们用了万能的try-catch
public IActionResult Index()
{
try
{
int a = 0, b = 5;
var result = b / a;
} catch (Exception)
{
throw new ArgumentException("被除数不能为0", "a");
}
}
这样异常提示确实友好了,并且我们拦截了异常,甚至可以将异常记录到日志中。
但是每个方法都这样加会不会觉得很烦?有没有想过一劳永逸的办法。从架构层面应该这样思考。
在传统的 Asp.Net MVC
应用程序中,我们一般都使用服务过滤的方式去捕获和处理异常,这种方式 非常常见,而且可用性来说,体验也不错,幸运的是 Asp.Net Core
也完整的支持该方式。 新建一个全局异常过滤器GlobalExceptionFilter.cs,继承自IExceptionFilter。
public class GlobalExceptionFilter:Attribute, IExceptionFilter
{
private readonly IHostingEnvironment _hostingEnvironment;
private readonly IModelMetadataProvider _modelMetadataProvider;
public GlobalExceptionFilter(
IHostingEnvironment hostingEnvironment,
IModelMetadataProvider modelMetadataProvider)
{
_hostingEnvironment = hostingEnvironment;
_modelMetadataProvider = modelMetadataProvider;
}
/// <summary>
/// 发生异常进入
/// </summary>
/// <param name="context"></param>
public async void OnException(ExceptionContext context)
{
ContentResult result = new ContentResult
{
StatusCode = 500,
ContentType = "text/json;charset=utf-8;"
};
if (_hostingEnvironment.IsDevelopment())
{
var json = new { message = context.Exception.Message };
result.Content = JsonConvert.SerializeObject(json);
}
else
{
result.Content = "抱歉,出错了";
}
context.Result = result;
context.ExceptionHandled = true;
}
}
我们在startup.cs中进行中注入
// 将异常过滤器注入到容器中
services.AddScoped<GlobalExceptionFilter>();
然后在需要的控制器上加上特性**ServiceFilter(typeof(GlobalExceptionFilter))]
[ServiceFilter(typeof(GlobalExceptionFilter))]
public class TestController : Controller
启动程序,错误提示,页面只会显示单纯错误信息。
5 .NET Core过滤器(Filter)
干了多年开发越来越觉得,异常处理和定位的能力反映出开发者硬核能力。对每一次线bug排查的复盘和总结,能让你功力倍增。
在日常开发中,除了考虑代码的严谨性,还要重视日子的记录。
过滤器
通过使用ASP.NET Core
中的筛选器,可在请求处理管道中的特定阶段之前或之后运行代码。
内置过滤器处理任务,例如:
- 授权(防止用户访问未获授权的资源)。
- 响应缓存(对请求管道进行短路出路,以便返回缓存的响应)。
可以创建自定义过滤器,用于处理横切关注点。 横切关注点的示例包括错误处理、缓存、配置、授权和日志记录。 过滤器可以避免复制代码。 例如,错误处理异常过滤器可以合并错误处理。
过滤器的工作原理
过滤器在 ASP.NET Core
操作调用管道(有时称过滤器管道)内运行。 过滤器管道在 ASP.NET Core 选择了要执行的操作之后运行。
过滤器类型
熟悉.NET MVC
框架的同学应该知道,MVC也提供了5大过滤器供我们用来处理请求前后需要执行的代码。分别是授权过滤器(AuthenticationFilter),资源过滤器(resource-filters),操作过滤器(ActionFilter),异常过滤器(ExceptionFilter),结果过滤器(ResultFilter)。
每种过滤选器类型都过滤器管道中的不同阶段执行:
授权过滤器最先运行,用于确定是否已针对请求为用户授权。 如果请求未获授权,授权过滤器可以让管道短路。
资源过滤器:
- 授权后运行。
- OnResourceExecuting 在过滤器管道的其余阶段之前运行代码。 例如,OnResourceExecuting 在模型绑定之前运行代码。
- OnResourceExecuted 在管道的其余阶段完成之后运行代码。
操作过滤器:
- 在调用操作方法之前和之后立即运行代码。
- 可以更改传递到操作中的参数。
- 可以更改从操作返回的结果。
- 不可在 Razor Pages 中使用。
异常过滤器在向响应正文写入任何内容之前,对未经处理的异常应用全局策略。
结果过滤器在执行操作结果之前和之后立即运行代码。 仅当操作方法成功执行时,它们才会运行。 对于必须围绕视图或格式化程序的执行的逻辑,它们很有用。
下图展示过滤器类型在筛选器管道中的交互方式。
过滤器使用
在.net core 中,一般是在StartUp.cs的ConfigureServices方法中注册
// 将异常过滤器注入到容器中
services.AddScoped<GlobalExceptionFilter>();
6 总结
在日常工作中,我们应用dotNetcore的项目其实蛮多的,支持跨平台真的部署,无疑是给更多C# 程序员一个突破自身瓶颈,开阔视野,接纳多系统,多语言的机会。
更多教程可以查看《dotnet跨平台》。