您现在的位置是:网站首页> .NET Core

Asp.Net Core 技术收集

摘要

Asp.Net Core 技术收集


1.jpg


Asp.Net Core 入门

Asp.Net Core 3.1返回JsonResult时时间格式处理

在ASP.NET MVC中使用Jntemplate 

net core 模版引擎JNTemplate

.net core中关于System.Text.Json的使用

Asp.Net Core中完成拒绝访问功能

.Net Core利用反射动态加载DLL类库的方法

记一次Docker中部署Asp.Net Core 3.0的踩坑过程

Asp.Net Core Identity中基于角色授权

Asp.Net Core中的角色

扩展Asp.Net Core中的IdentityUser类

Asp.Net Core Identity 完成注册登录

Asp.Net Core 自定义验证属性

Asp.Net Core 客户端验证和远程验证

Asp.Net Core文件上传

.NetCore 使用Cookie

ASP.NET Core中间件和 ASP.NET HttpHandler HttpModule

ASP.NET Core中间件与HttpModule有何不同



Asp.Net Core 入门

Asp.Net Core 入门(一)——Program.cs做了什么

Asp.Net Core 入门(二)——StartUp

Asp.Net Core 入门(三) —— 自定义中间件

Asp.Net Core 入门(四)—— Model、View、Controller

Asp.Net Core 入门(五)—— 布局视图_Layout.cshtml

Asp.Net Core 入门(六)—— 路由

Asp.Net Core 入门(七)—— 安装Bootstrap

Asp.Net Core 入门(八)—— Taghelper

Asp.Net Core 入门(九)—— 环境变量 TagHelper

Asp.Net Core 入门(十)—— 模型绑定和验证

Asp.Net Core 进阶(一) —— 读取appsettings.json

Asp.Net Core 进阶(二) —— 集成Log4net

Asp.Net Core 进阶(三)—— IServiceCollection依赖注入容器和使用Autofac替换它

Asp.Net Core 进阶(四)—— 过滤器 Filters




Asp.Net Core 入门(一)——Program.cs做了什么

ASP.NET Core 是微软推出的一种全新的跨平台开源 .NET 框架,用于在 Windows、Mac 或 Linux 上生成基于云的新式 Web 应用程序。国内目前关于Asp.Net Core的书比较少,自己靠着阅读微软官方文档,源码和在52ABP梁老师的教程中慢慢的在一点点的积累Asp.Net Core的知识。因此希望将自己的学习记录下来,以此巩固和交流。


  闲话不多说,我们知道学习一门新的技术,最好的方法就是从它的启动入口进行跟踪,那么接下来我们先来看下Asp.Net Core的入口文件Program.cs。


  笔者使用的.Net SDK 是 2.2版本,所以下面的介绍都是基于2.2版本的。


    public class Program

    {

        public static void Main(string[] args)

        {

            CreateWebHostBuilder(args).Build().Run();

        }


        public static IWebHostBuilder CreateWebHostBuilder(string[] args) =>

            WebHost.CreateDefaultBuilder(args)

                .UseStartup<Startup>();

    }

  我们看到Program中Main函数很像我们之前的Console应用程序,那么它当中的CreateWebHostBuilder究竟做了什么事情呢?既然.Net Core是开源的,我们看源码就方便了很多,接下来就从源码来了解下它究竟做了什么工作。


     /// <summary>

        /// Initializes a new instance of the <see cref="WebHostBuilder"/> class with pre-configured defaults.

        /// </summary>

        /// <remarks>

        ///   The following defaults are applied to the returned <see cref="WebHostBuilder"/>:

        ///     use Kestrel as the web server and configure it using the application's configuration providers,

        ///     set the <see cref="IHostingEnvironment.ContentRootPath"/> to the result of <see cref="Directory.GetCurrentDirectory()"/>,

        ///     load <see cref="IConfiguration"/> from 'appsettings.json' and 'appsettings.[<see cref="IHostingEnvironment.EnvironmentName"/>].json',

        ///     load <see cref="IConfiguration"/> from User Secrets when <see cref="IHostingEnvironment.EnvironmentName"/> is 'Development' using the entry assembly,

        ///     load <see cref="IConfiguration"/> from environment variables,

        ///     load <see cref="IConfiguration"/> from supplied command line args,

        ///     configure the <see cref="ILoggerFactory"/> to log to the console and debug output,

        ///     and enable IIS integration.

        /// </remarks>

        /// <param name="args">The command line args.</param>

        /// <returns>The initialized <see cref="IWebHostBuilder"/>.</returns>

        public static IWebHostBuilder CreateDefaultBuilder(string[] args)

        {

            var builder = new WebHostBuilder();


            if (string.IsNullOrEmpty(builder.GetSetting(WebHostDefaults.ContentRootKey)))

            {

                builder.UseContentRoot(Directory.GetCurrentDirectory());

            }

            if (args != null)

            {

                builder.UseConfiguration(new ConfigurationBuilder().AddCommandLine(args).Build());

            }


            builder.ConfigureAppConfiguration((hostingContext, config) =>

            {

                var env = hostingContext.HostingEnvironment;


                config.AddJsonFile("appsettings.json", optional: true, reloadOnChange: true)

                      .AddJsonFile($"appsettings.{env.EnvironmentName}.json", optional: true, reloadOnChange: true);


                if (env.IsDevelopment())

                {

                    var appAssembly = Assembly.Load(new AssemblyName(env.ApplicationName));

                    if (appAssembly != null)

                    {

                        config.AddUserSecrets(appAssembly, optional: true);

                    }

                }


                config.AddEnvironmentVariables();


                if (args != null)

                {

                    config.AddCommandLine(args);

                }

            })

            .ConfigureLogging((hostingContext, logging) =>

            {

                logging.AddConfiguration(hostingContext.Configuration.GetSection("Logging"));

                logging.AddConsole();

                logging.AddDebug();

                logging.AddEventSourceLogger();

            }).

            UseDefaultServiceProvider((context, options) =>

            {

                options.ValidateScopes = context.HostingEnvironment.IsDevelopment();

            });


            ConfigureWebDefaults(builder);


            return builder;

        }

 internal static void ConfigureWebDefaults(IWebHostBuilder builder)

        {

            builder.UseKestrel((builderContext, options) =>

            {

                options.Configure(builderContext.Configuration.GetSection("Kestrel"));

            })

            .ConfigureServices((hostingContext, services) =>

            {

                // Fallback

                services.PostConfigure<HostFilteringOptions>(options =>

                {

                    if (options.AllowedHosts == null || options.AllowedHosts.Count == 0)

                    {

                        // "AllowedHosts": "localhost;127.0.0.1;[::1]"

                        var hosts = hostingContext.Configuration["AllowedHosts"]?.Split(new[] { ';' }, StringSplitOptions.RemoveEmptyEntries);

                        // Fall back to "*" to disable.

                        options.AllowedHosts = (hosts?.Length > 0 ? hosts : new[] { "*" });

                    }

                });

                // Change notification

                services.AddSingleton<IOptionsChangeTokenSource<HostFilteringOptions>>(

                            new ConfigurationChangeTokenSource<HostFilteringOptions>(hostingContext.Configuration));


                services.AddTransient<IStartupFilter, HostFilteringStartupFilter>();


                services.AddRouting();

            })

            .UseIIS()

            .UseIISIntegration();

        }

  从源码中我们可以看到,CreateDefaultBuilder执行的任务有:


  1、加载主机和应用程序的配置表信息


  2、配置日志记录


  3、设置Web服务器


  4、设置Asp.Net Core应用程序的托管形式。


  现在我们来看一下Asp.Net Core是如何加载配置的。


  访问配置信息可以通过 IConfiguration 接口进行访问,我们从源码也可以看到,Asp.Net Core加载配置的过程,它们之间是按照顺序如果有相同的配置会依次被覆盖:appsettings.json,appsettings{Environment}.json(应用程序配置文件) -->User Secrets(用户机密) --> Environment Variables(环境变量) --> Command-line arguments(命令行参数)。


  其中应用程序配置文件的{Environment}可以在项目的属性中调试进行配置,如果项目中不存在单独为某个环境配置的,采用appsettings.json。




  应用程序配置文件我们比较清楚,以前在.Net Framework我们有web.config,但是Asp.Net Core中出现了User Secrets用户机密,那么它有什么用呢,其实它可以保存我们比较敏感的配置信息,比如第三方的Key,像微信的Appkey之类的。那我们该如何添加User Secrets呢?我们需要在命令行的窗口下,切换到我们项目的执行文件夹下,即bin所在的那层目录,运行命令dotnet user-secrets set [KeyName] [KeyValue]




  接着我们就可以在VS项目中查看到该用户机密了


  


  Environment variables(环境变量)在 Properties文件夹下的launchSettings.json进行配置


{

  "iisSettings": {

    "windowsAuthentication": false,

    "anonymousAuthentication": true,

    "iisExpress": {

      "applicationUrl": "http://localhost:4084",

      "sslPort": 44338

    }

  },

  "profiles": {

    "IIS Express": {

      "commandName": "IISExpress",

      "launchBrowser": true,

      "environmentVariables": {

        "ASPNETCORE_ENVIRONMENT": "Development"

      }

    },

    "StudentManagement": {

      "commandName": "Project",

      "launchBrowser": true,

      "applicationUrl": "https://localhost:5001;http://localhost:5000",

      "environmentVariables": {

        "ASPNETCORE_ENVIRONMENT": "Development"

      }

    }

  }

}

  这里的profiles可以在VS调试运行的时候做选择




  而Command-line arguments(命令行参数)需要结合命令行窗口执行: dotnet run KeyName="KeyValue"。


  那么我们怎么获取配置信息呢? Asp.Net Core提供了ConfigureAppConfiguration 可以给我们调用获取配置,在StartUp文件中可以使用 _configuration["KeyName"] 来获取。


   最后,我们再看一下Asp.Net Core应用程序的托管形式,它有两种托管形式:进程内托管InProcess和进程外托管OutOfProcess。我们知道Asp.Net Core是可以自托管的,它默认托管形式就是InProcess。那么这两种方式的区别是什么呢?


  InProcess:配置进程内托管在项目.csproj文件中 <AspNetCoreHostingModel>InProcess</AspNetCoreHostingModel>,在InProcess托管情况下,CreateDefaultBuilder()方法调用UseIIS()方法并在IIS工作进程(w3wp.exe或iisexpress.exe)内托管应用程序,从性能角度,InProcess托管比OutOfProcess托管提供了更高的请求吞吐量。


  OutOfProcess:有2个Web服务器-内部Web服务器和外部Web服务器,内部Web服务器是Kestrel,托管进程是dotnet.exe;外部web服务器可以是iis,nginx,apache。


 


  现在我们知道了CreateDefaultBuilder做了什么工作了,那么在它之后调用了UseStartup<Startup>(),那Startup做了什么工作呢,我们下篇再来讨论。




Asp.Net Core 入门(二)——StartUp


上篇介绍了Program.cs中Main做了什么,这篇我们来讨论下Startup.cs它又做了什么呢?


  我们新建一个Asp.Net Core Mvc项目,先来开一下Startup的代码


    public class Startup

    {

        public Startup(IConfiguration configuration)

        {

            Configuration = configuration;

        }


        public IConfiguration Configuration { get; }


        // This method gets called by the runtime. Use this method to add services to the container.

        public void ConfigureServices(IServiceCollection services)

        {

            services.Configure<CookiePolicyOptions>(options =>

            {

                // This lambda determines whether user consent for non-essential cookies is needed for a given request.

                options.CheckConsentNeeded = context => true;

                options.MinimumSameSitePolicy = SameSiteMode.None;

            });



            services.AddMvc().SetCompatibilityVersion(CompatibilityVersion.Version_2_2);

        }


        // This method gets called by the runtime. Use this method to configure the HTTP request pipeline.

        public void Configure(IApplicationBuilder app, IHostingEnvironment env)

        {

            if (env.IsDevelopment())

            {

                app.UseDeveloperExceptionPage();

            }

            else

            {

                app.UseExceptionHandler("/Home/Error");

                // The default HSTS value is 30 days. You may want to change this for production scenarios, see https://aka.ms/aspnetcore-hsts.

                app.UseHsts();

            }


            app.UseHttpsRedirection();

            app.UseStaticFiles();

            app.UseCookiePolicy();


            app.UseMvc(routes =>

            {

                routes.MapRoute(

                    name: "default",

                    template: "{controller=Home}/{action=Index}/{id?}");

            });

        }

    }

  Startup包含两个方法,我们先来分析一下ConfigureServices。通过方法上面的注释可以看到,这个方法是被.Net运行时调用的,实际上Asp.Net Core提供了内置的IOC容器,此方法就是用来将我们自己的服务Service注入到Ioc容器当中。那么如何注入呢?当然是利用该方法的参数IServiceCollection进行注入。如果是我们自定义的Service,可以使用services.AddTransient<IStudentRepository,MockStudentRepository>()或其他的两种方式Singleton/Scoped来注入,具体区别看下面代码。如果是注入.Net Core自带的,一般是使用AddMvc,AddCors()。


      public void ConfigureServices(IServiceCollection services)

        {

            //单例注入,创建的对象在所有的地方所有的请求会话创建的都是相同

            services.AddSingleton<IStudentRepository, MockStudentRepository>();


            //瞬时注入,每次都创建一个新的对象

            services.AddTransient<IStudentRepository, MockStudentRepository>();


            //作用域注入,创建的对象在同一个请求会话时是相同的

            services.AddScoped<IStudentRepository, MockStudentRepository>();


            //注入MVC模块,包含MvcCore和常用第三方库的服务和方法

            services.AddMvc(); 


            //只包含核心的Mvc功能

            services.AddMvcCore();

        }

  接下来我们来看一下另一个方法Configure。它是用来配置Asp.Net Core Mvc的Http请求管道的。Asp.Net Core的http请求管道与我们之前的Asp.Net Mvc的请求管道有着很大的差别,它引入了中间件的概念,更像是一种责任链的模式。


  现在我们就先来分析一下,参数IHostingEnvironment env,顾名思义是web宿主的环境变量相关的。在我们的实际开发中,我们常常会将我们的环境分成:开发环境development,继承环境integration,测试环境testing,预发布环境staging,生产环境production。那这个环境是怎么配置的呢?实际上它是通过 ASPNETCORE_ENVIRONMENT 这个环境变量来配置运行时的环境的。


  一般我们的配置遵循:


  1、开发机器上,可以在launchingsetting.json文件中设置该环境变量


  2、在预发布环境staging或生产环境production下,尽量将该变量设置在操作系统中的环境变量里,因为上篇我们也讲到,Asp.Net Core启动读取变量配置的时候是会依次覆盖的。


  3、除了Asp.Net Core提供的development,staging,production外,我们可以定义其他的环境,通过调用 env.IsEnvironment("自定义的环境变量");来判断是否处于此环境。


  一般我们可以配置开发环境下显示错误的方式如下面代码。


if (env.IsDevelopment())

{

     DeveloperExceptionPageOptions developerExceptionPageOptions = new DeveloperExceptionPageOptions();

     developerExceptionPageOptions.SourceCodeLineCount = 50; //异常显示行数

     app.UseDeveloperExceptionPage();  //开发者异常页面

}

  在我们继续往下看代码之前,先来了解一下Asp.Net Core的中间件这个概念。


  中间件是组装到应用程序管道中以处理请求和响应的软件,Asp.Net Core自带了很多的中间件,可以看下下图展示的中间件,可以看到请求经过Logging -> StaticFiles -> MVC后处理完再从MVC -> StaticFiles -> Logging返回给调用方。




  其中MVC为终端中间件,终端中间件表示执行完这个中间件的时候就已经结束了,不会再往下调用其他的管道中间件了,这也是我们创建完项目后看到最后一个是app.UseMvc的原因。


  关于中间件我们需要注意的:


  1、中间件可以同时被访问和请求


  2、可以处理请求后,将请求传递给下一个中间件


  3、可以处理请求后,使管道短路


  4、可以处理传出响应


  5、中间件是按照添加的顺序执行的


   现在我们来具体分析下Configure里面是怎么配置管道的。IApplicationBuilder app 参数为我们提供了很多扩展方法,通过这些方法可以配置我们的中间件。首先我们来看Asp.Net Core提供的Use,Map,Run方法。


  Use方法可以使管道短路(即,可以不调用下一个中间件)。我们再Configure方法中调用app.Use方法,如果我们在该方法中不调用next()方法,那么请求到达该方法就结束返回了,不会再往下一个中间件执行,即后面的Run不会执行。


 


 app.Use(async (context, next) =>

            {

                context.Response.ContentType = "text/plain;charset=utf-8;"; //解决中文乱码

                await context.Response.WriteAsync("这是第一个Hello World");

                //await next(); //调用下一个中间件

            });


            app.Run(async (context) =>

            {

                //获取当前进程名

                await context.Response.WriteAsync( System.Diagnostics.Process.GetCurrentProcess().ProcessName);

            });



  如果我们放开了next(),则会继续往下执行




  再看一下下面的代码


            app.Use(async (context, next) =>

            {

                context.Response.ContentType = "text/plain;charset=utf-8;"; //解决中文乱码

                await context.Response.WriteAsync("Use1之前");

                await next(); //调用下一个中间件

                await context.Response.WriteAsync("Use1之后");

            });


            app.Use(async (context, next) =>

            {

                await context.Response.WriteAsync("Use2之前");

                await next(); //调用下一个中间件

                await context.Response.WriteAsync("Use2之后");

            });

        app.Run(async (context) =>

            {

                //获取当前进程名

                await context.Response.WriteAsync( System.Diagnostics.Process.GetCurrentProcess().ProcessName);

            });

 



  我们可以看到,请求过来后,以链式的方式执行: Use1之前 --> next --> Use2之前 --> next --> Run --> Use2之后 --> Use1之前。


  Run方法是一个约定, 并且一些中间件组件可能暴露在管道末端运行的Run [Middleware]方法,如果将Run放在了Configure里面,它也是终端中间件。


app.Run(async (context) =>

{

    context.Response.ContentType = "text/plain;charset=utf-8;"; //解决中文乱码

    //获取当前进程名

    await context.Response.WriteAsync("当前进程名:" + System.Diagnostics.Process.GetCurrentProcess().ProcessName);

});

  Map*扩展用作分支管道的约定。映射根据给定的请求路径的匹配来分支请求流水线,如果请求路径以给定路径开始,则执行分支。


      private static void HandleMapTest1(IApplicationBuilder app)

        {

            app.Run(async context =>

            {

                await context.Response.WriteAsync("Map Test 1");

            });

        }


        private static void HandleMapTest2(IApplicationBuilder app)

        {

            app.Run(async context =>

            {

                await context.Response.WriteAsync("Map Test 2");

            });

        }


  app.Map("/map1", HandleMapTest1);


  app.Map("/map2", HandleMapTest2);

 




 


   最后我们再来看一下Asp.Net Core提供的一些其他的中间件。


        /*自定义默认页(第一种方式)*/

            //DefaultFilesOptions defaultFilesOptions = new DefaultFilesOptions();

            //defaultFilesOptions.DefaultFileNames.Clear();

            //defaultFilesOptions.DefaultFileNames.Add("custom.html");  //自定义默认页

            //app.UseDefaultFiles(defaultFilesOptions);

            /**************************************************************************/


            /*自定义默认页(第二种方式)*/

            //FileServerOptions fileServerOptions = new FileServerOptions();

            //fileServerOptions.DefaultFilesOptions.DefaultFileNames.Clear();

            //fileServerOptions.DefaultFilesOptions.DefaultFileNames.Add("custom.html");

            //app.UseFileServer(fileServerOptions); //集合了UseStaticFiles,UseDefaultFiles,UseDirectoryBrowser

            /************************************************/


            //app.UseDefaultFiles(); //添加默认文件中间件,必须注册在UseStaticFiles前 index.html index.htm default.html default.htm


            app.UseStaticFiles(); //添加静态文件中间件

            //app.UseDirectoryBrowser();//暴露项目文件夹,不推荐使用


            app.UseMvcWithDefaultRoute();//在请求管道中添加带默认路由的MVC

 


   好了,这篇博文介绍了Startup文件做了哪些事情,其中最重要的就是中间件,那么下一篇就来聊聊Asp.Net Corez中的中间件吧。



Asp.Net Core 入门(三) —— 自定义中间件

上一篇我们讲了Startup文件,其中着重介绍了中间件,现在我们就来自定义我们自己的中间件吧。

  中间件通常封装在一个类中,并使用扩展方法进行暴露。它需要拥有一个类型为RequestDelegate的成员变量,通常定义为 private RequestDelegate _next ; 然后通过在构造函数中注入RequestDelegate,还需要有一个Invoke方法供Asp.Net Core调用。

  先看代码吧,我们定义了一个日志中间件

using Microsoft.AspNetCore.Http;

using Microsoft.Extensions.Logging;

using System;

using System.Collections.Generic;

using System.Globalization;

using System.Linq;

using System.Threading.Tasks;


namespace WebApplication1.MiddleWare

{

    /// <summary>

    /// 自定义日志中间件

    /// </summary>

    public class LoggerMiddleWare

    {

        private RequestDelegate _next;


        public LoggerMiddleWare(RequestDelegate next)

        {

            _next = next;

        }


        public async Task Invoke(HttpContext context)

        {

            context.Response.ContentType = "text/plain;charset=utf-8;";


            await context.Response.WriteAsync("this is my custom logger before;");


            // Call the next delegate/middleware in the pipeline

            await this._next(context);


            await context.Response.WriteAsync("this is my custom logger after;");

            

        }

    }

}

复制代码

  然后通过扩展方法将其暴露出来给ApplicationBuilder调用


复制代码

using Microsoft.AspNetCore.Builder;

using Microsoft.AspNetCore.Http;

using Microsoft.Extensions.Logging;

using System;

using System.Collections.Generic;

using System.Linq;

using System.Threading.Tasks;

using WebApplication1.MiddleWare;


namespace WebApplication1.Extension

{

    public static class LoggerMiddlerWareExtension

    {

        public static IApplicationBuilder UseLogger(this IApplicationBuilder builder)

        {

            return builder.UseMiddleware<LoggerMiddleWare>();

        }

    }

}

复制代码

在Startup文件中Configure方法添加如下语句


复制代码

   app.UseLogger();


   app.Run(async (context) =>

   {

       await context.Response.WriteAsync(

           $"Hello {CultureInfo.CurrentCulture.DisplayName}");

    });

运行结果

1.png

中间件应该遵循显式依赖原则,通过在其构造函数中暴露其依赖关系。 

  因为中间件是在应用程序启动时构建的,而不是每个请求,所以在每个请求期间,中间件构造函数使用的作用域生命周期服务不会与其他依赖注入类型共享。 如果需要在中间件和其他类型之间共享作用域服务,请将这些服务添加到Invoke方法的签名中。 Invoke方法可以接受由依赖注入填充的其他参数。



Asp.Net Core 入门(四)—— Model、View、Controller

和我们学习Asp.Net MVC一样,Asp.Net Core MVC的Model、View、Controller也和我们熟悉的Asp.Net MVC中的相似。不同的是我们在使用Asp.Net Core MVC的时候需要注入MVC。

  Asp.Net Core MVC注入 MVC 的方法有两种,一种是AddMvcCore(),它只是注入了MVC的一些核心的东西,不包括像返回的ViewResult、JsonResult等,如果要使用完整的MVC,需要使用另一种注入方式 AddMvc() 进行注入,AddMvc()里调用了AddMvcCore()。

  下面我们就来详细说说模型、视图、控制器。

  我们新建完一个MVC项目后,模板帮我们生成了Models、Views、Controllers三个文件夹,一般我们把新建的Model放在Models中,但实际项目上我们通常会新建一个项目放model。

  MVC 中的模型包含一组表示数据的类和管理该数据的逻辑。View和Controller之间可以通过Model来传递数据。 我们定义一个学生类,一个学生仓储接口还有一个该接口的实现。

 /// <summary>

    /// 学生模型

    /// </summary>

    public class Student

    {

        public int Id { get; set; }


        public string Name { get; set; }


        public string ClassName { get; set; }


        public string Email { get; set; }

    }

复制代码

  public interface IStudentRepository

    {

        Student GetStudent(int id);

    }

复制代码

 public class MockStudentRepository : IStudentRepository

    {

        private List<Student> _studentList;

        public MockStudentRepository()

        {

            _studentList = new List<Student>();

            _studentList.AddRange(new List<Student>() {

                new Student(){Id=1,Name="张三",ClassName="三年一班",Email="zhangshang@qq.com"},

                new Student(){Id=2,Name="李四",ClassName="三年一班",Email="zhangshang@qq.com"},

                new Student(){Id=3,Name="王五",ClassName="三年一班",Email="zhangshang@qq.com"},

                new Student(){Id=4,Name="赵六",ClassName="三年一班",Email="zhangshang@qq.com"},

                new Student(){Id=5,Name="钱七",ClassName="三年一班",Email="zhangshang@qq.com"},

            });

        }



        public Student GetStudent(int id)

        {

            return _studentList.Where(s => s.Id == id).FirstOrDefault();

        }

    }

复制代码

  新建一个HomeController,添加代码


复制代码

      private readonly IStudentRepository _studentRepository;

        //构造函数注入

        public HomeController(IStudentRepository studentRepository)

        {

            _studentRepository = studentRepository;

        }


        public IActionResult Index(int id)

        {

            return View(_studentRepository.GetStudent(id));

        }

复制代码

  添加视图Index.cshtml


复制代码

@model StudentManagement.Models.Student

@{

    ViewData["Title"] = "Index";

}


<div>

    @Model.Id

    @Model.Name

    @Model.ClassName

    @Model.Email

</div>

复制代码

  我们可以看到,Controller通过Repository获取到Student的数据后将其作为View的参数返回了,而在视图Index我们可以使用@model StudentManagement.Models.Student来引用模型。


  当然,将数据传递到视图还有其他的方式,这一点和我们在Asp.Net MVC的时候是一样的,我们可以总结一下:


  1、ViewData:弱类型的字典对象,使用string类型的键值对,运行时动态解析,没有智能感知,编译也没有检查类型。


  2、ViewBag:ViewData的包装器,都创建了一个弱类型的视图,使用动态属性来存储和查询数据。


  3、强类型视图:在视图中使用@model指令指定模型类型,使用@Model访问模型对象属性,提供编译类型检查和智能提示。


  需要注意的是,在我们HomeController的构造函数里,形参 IIStudentRepository 是怎么传入的呢?我们知道Asp.Net Core自带了依赖注入容器,在前面讲Startup文件时候知道ConfigureServices方法是用来注入服务的,所以我们需要将我们的 IIStudentRepository 注入进去,否则运行会抛异常。


services.AddTransient<IStudentRepository, MockStudentRepository>();

  在返回VIew的时候,上面例子我们传了一个model作为参数,实际上它也可以传递视图名称,在传递视图名称的时候可以使用绝对路径或相对路径,绝对路径必须指定.cshtml文件扩展名,相对路径不用带扩展名.cshtml。


   我们定义一个Detail视图,然后调用return View("Detail") 


  public ObjectResult Detail()

  {

      return new ObjectResult(_studentRepository.GetStudent(1));

  }

 return View("Detail"); 

  我们使用的是相对路径,那么.Net Core会执行下面的操作去查找视图


  1、在“/ Views / Home /”文件夹中查找,找到就返回,找不到继续往第2步找。


  2、在“/ Views / Shared /”文件夹中查找,找到就返回,找不到继续往第3步找。


  3、在“/ Pages / Shared /”文件夹中查找, 如果找到视图文件,则视图生成的 HTML 将发送回发出请求的客户端。如果找不到视图文件,将收到错误。


    The view 'Detail' was not found  

  

  最后,讲一下ViewModel模型视图的一个概念,很多时候我们的视图需要的数据往往比我们定义的Model能提供的数据要多,这时候就可以使用ViewModel了,在实际项目开发中,ViewModel也等于DTO(数据传输对象)。至于ViewModel的使用和Model一样,传递到View中进行显示。



Asp.Net Core 入门(五)—— 布局视图_Layout.cshtml

布局视图和我们在Asp.Net MVC一样,布局视图_Layout.cshtml使得所有视图保持一致的外观变得更加容易,因为我们只有一个要修改的布局视图文件,更改后将立即反映在整个应用程序的所有视图中。


  在 ASP.NET Core MVC 中,有一些视图文件,如布局的视图,_ViewStart.cshtml 和_ViewImports.cshtml 等其他.cshtml 文件的文件名以下划线开头,这些文件名中的前下划线表示这些文件不是直接面向浏览器。


  我们可以在单个应用程序中包含多个布局视图文件。比如一个布局视图文件服务为管理员用户,另外一个不同的布局视图文件服务于普通用户。


  我们一般将布局视图建在Views/Shared文件夹下,以_Layout.cshtml命名。


复制代码

<!DOCTYPE html>


<html>

<head>

    <meta name="viewport" content="width=device-width" />

    <title>@ViewBag.Title</title>

</head>

<body>

    <div>

        <!--@RenderBody()是注入视图特定内容的位置。例如,如果使用此布局视图呈现 index.chtml 视图,则会在我们 调用@RenderBody()方法 的位置注入 index.cshtml 视图内容 。-->

        @RenderBody()

    </div>


    @*@if (IsSectionDefined("Scripts"))

    {

        @RenderSection("Scripts");

    }*@


    @RenderSection("Scripts", false);


</body>

</html>

复制代码

  我们可以在Views/_ViewStart.cshtml指定启用哪个布局页,因为请求的时候会先找到_ViewStart.cshtml。


复制代码

@{

    Layout = "_Layout";

}


@if (User.IsInRole("Admin"))

{

    Layout = "_AdminLayout";

}

else

{

    Layout = "_NoAdminLayout";

}

复制代码

  同时,如果我们在很多页面都使用同一个命名空间,同一个model的话,我们可以在Views/_ViewImports.cshtml文件中添加共用的命名空间,model。


复制代码

@using StudentManagement.Models;

@using StudentManagement.ViewModels;


@*还支持以下指令*@

@*

    @addTagHelper

    @removeTagHelper

    @tagHelperPrefix

    @model

    @inherits

    @inject

*@

复制代码

  需要注意的是,_ViewStart和_ViewImports是支持分层的,除了将它放在 Views 文件夹中之外,我们还可以在 Views 文件夹的“Home”子文件夹中放置另一个_ViewImports,在文件 Home 的文件夹中的\_ViewImports将覆盖在 Shared 文件夹中的\_ViewImports文件指定的设置。



Asp.Net Core 入门(六)—— 路由

Asp.Net Core MVC的路由在Startup.cs文件中的Configure方法中进行配置,使其加入到Http请求管道中,如果不配置,那么我们所发送的请求无法得到象应。


  那么该怎么配置Asp.Net Core MVC的路由呢?通常是在Configure方法最后一行加入 app.UseMvcWithDefaultRoute(); 或者 app.UseMvc();


复制代码

app.UseMvcWithDefaultRoute(); //使用默认路由


app.UseMvc(routes =>

{

    routes.MapRoute("default", "{controller=Home}/{action=Index}/{id?}");

});

复制代码

  通过此配置使得我们在浏览器可以通过 https://localhost:44338/home/details 来访问到HomeController下的Details这个Action。


  上述路由配置是我们熟悉的传统路由,我们也可以通过属性路由来配置,那么属性路由的配置方式是什么样的呢?在Startp.cs文件Configure方法中,我们只使用app.UseMvc();然后在Controller的Action方法上通过特性Route来配置。


复制代码

        [Route("")]

        [Route("home")]

        [Route("/home/index")]

        public IActionResult Index(int id)

        {

            return Json(_studentRepository.GetStudent(id));

            //return Json(new { id = 1, name = "张三" });

        }


        [Route("Home/Details/{id?}")]

        public IActionResult Details(int? id)

        {

            Student model = _studentRepository.GetStudent(id??1);

            return View(model);

        }    

复制代码

  这样我们也可以通过浏览器访问




  值得提醒的是,使用属性路由 [Route("Home/Details")] 时,这个路由和我们的控制器名称是没有什么关系的,也就是说,如果我们在StudentController这个控制器下的Index Action上应用 [Route("Home/Details")] 配置,在浏览器上使用/home/details一样是可以访问到StudentController的details的,只是如果我们只是直接返回return View(),我们通过浏览器访问会抛出异常,原因是Asp.Net MVC 默认会去/Views/Student/ , /Views/Shared/ , /Pages/Shared/下查找Details.cshtml。解决这个问题也很简单,可以通过使用绝对路径来实现。


 return View("~/Views/Home/Details.cshtml",student);



   但是,我们发现,使用属性路由的时候,我们需要在每个Action上面添加Route特性,这样如果每个控制器都有很多Action,添加起来就很烦人和繁琐。当然,Asp.Net Core MVC也给我们提供了简写的方式,但依然很繁琐,下面先来看一下


复制代码

  [Route("[controller]/[action]")]

    public class HomeController : Controller

    {

        private readonly IStudentRepository _studentRepository;

        //构造函数注入

        public HomeController(IStudentRepository studentRepository)

        {

            _studentRepository = studentRepository;

        }


        [Route("")]

        [Route("~/")] //解决 http://localhost:44338/ 访问不了问题

        [Route("~/home")] //解决 http://localhost:44338/home 访问不了问题

        public IActionResult Index(int id)

        {

            return Json(_studentRepository.GetStudent(id));

        }


        [Route("{id?}")]

        public IActionResult Details(int? id)

        {

            Student model = _studentRepository.GetStudent(id??1);

            return View(model);

        }


        public ObjectResult detail()

        {

            return new ObjectResult(_studentRepository.GetStudent(1));

        }

    }

}

复制代码

  所以我们可以总结一下,在大多数情况下使用传统路由的配置会比较方便,这实际上也是在实际工作中用得比较多的情况,而属性路由相比传统路由有了更大的灵活性,在一些特殊情况上可以使用,更多的是和传统路由搭配。



Asp.Net Core 入门(七)—— 安装Bootstrap

我们使用 libman包管理器来安装,libman是微软推出的最新的包管理器,它是一个轻量级的客户端管理工具,可以从CDN下载客户端库和框架,它要求VS Studio必须在2017版本15.8或更高版本。

  我们先来看下怎么使用libman,找到项目的wwwroot,鼠标右键

1.png

2.png

点击安装,然后在项目中可以看到libman.json的文件,在输出-->显示输出来源--> 库管理器可以看到下载的进度。

现在我们来看下 libman.json 库管理器清单文件,provider 可以指定特定的包从哪里下载安装,libman.json使得对客户端库的安装和卸载变得非常简单,右键就可以选择卸载。

{

  "version": "1.0",

  "defaultProvider": "cdnjs",

  "libraries": [

        {

            "library": "twitter-bootstrap@4.3.1",

            "destination": "wwwroot/lib/twitter-bootstrap/"

        },

        {

            "provider": "unpkg",

            "library": "abp-web-resources@3.7.0",

            "destination": "wwwroot/lib/"

        }

  ]

}

在解决方案的项目中也可以选中libman.json文件后选择清空客户端库和还原客户端库。真的是非常方便。



Asp.Net Core 入门(八)—— Taghelper

Taghelper是一个服务端的组件,可以在Razor文件中创建和渲染HTML元素,类似于我们在Asp.Net MVC中使用的Html Taghelper。Asp.Net Core MVC内置的Tag Helper用于常见的任务,例如生成链接,创建表单,加载数据等。


  那么如何导入内置Tag Helpers呢?我们可以在项目的视图导入文件 Views/_ViewImports.cshtml 中引入。


@using StudentManagement.Models


@addTagHelper *,Microsoft.AspNetCore.Mvc.TagHelpers

  回顾一下以前在Asp.Net MVC中使用HtmlHelper,我们要使用<a>标签是这样做的


@Html.ActionLink("查看","details","home",new { id=1})

  而现在,使用TagHelper变得更方便,我们可以对比一下


<a asp-controller="home" asp-action="details" asp-route-id="1">查看</a>


@Html.ActionLink("查看","details","home",new { id=1})

生成的html也是一样的


复制代码

<!DOCTYPE html>

<html>

<head>

    <meta name="viewport" content="width=device-width" />

    <title></title>

</head>

<body>

    <div>

    <a href="/Home/Details/1">查看</a>

    <a href="/Home/Details/1">查看</a>

    </div>

</body>

</html>

复制代码

   那么为什么在Asp.Net Core MVC中要使用TagHelper呢?我们结合路由来看一下,假设路由开始是


app.UseMvc(routes =>

 {

     routes.MapRoute("default", "{controller=Home}/{action=Index}/{id?}");

 });

  如果我们使用下面的硬编码方式和TagHelper方式得到的效果是一样的


<a href="/home/index/1" >查看</a>

<a asp-controller="home" asp-action="details" asp-route-id="1">查看</a>

  但是如果路由发生了变化,变成了下面的形式


app.UseMvc(routes =>

{

    routes.MapRoute("default", "pre/{controller=Home}/{action=Index}/{id?}");

});

  那么硬编码的方式就无法访问了,而TagHelper方式依然能够正常访问,因为它是通过映射生成的,会自动加上pre/。


   接下来我们再看一下TagHelper里特殊的 Img tag helper。


<img asp-append-version="true" src="~/images/38.jpg" />

  Image TagHelper增强了<img>标签,为静态图像文件提供“缓存破坏行为”,它会生成唯一的散列值并将其附加到图片的URL中。此唯一字符串会提示浏览器从服务器重新加载图片,而不是从浏览器缓存重新加载。只有当磁盘上的文件发生更改时,它才会重新计算生成新的哈希值,缓存才会失效。


<img src="/images/38.jpg?v=ae5OTzlW663ZONSxqJlK_Eug8MyjukxUZsk0dwf3O9Y">



Asp.Net Core 入门(九)—— 环境变量 TagHelper

我们在之前讲Program.cs文件做了什么的时候,提到启动CreaeDefaultBuilder会获取环境变量来做一些判断之类的操作。那么我们的Taghelper也可以使用“ASPNETCORE_ENVIRONMENT"变量来设置在什么环境下加载什么库文件。可以通过environment标签来使用。


复制代码

    <environment include="Development">

        <link href="~/lib/twitter-bootstrap/css/bootstrap.css" rel="stylesheet" />

    </environment>


    <environment exclude="Staging,Production">

        <link href="~/lib/twitter-bootstrap/css/bootstrap.css" rel="stylesheet" />

    </environment>

复制代码

  include属性接受将单个环境环境名称以逗号分隔的形式生成列表。在<environment>tag helper上,还有exclude属性,当托管环境与exclude属性值中列出的环境名称不匹配时,将呈现标签中的内容。


  我们在实际开发过程中,可能开发环境用的是本地的库,生产环境用的是cdn,所以我们可以这么做


复制代码

    <environment include="Development">

        <link href="~/lib/twitter-bootstrap/css/bootstrap.css" rel="stylesheet" />

    </environment>


    <environment exclude="Development">

        <link href="https://stackpath.bootstrapcdn.com/bootstrap/4.3.1/css/bootstrap.min.css" rel="stylesheet" integrity="sha384-ggOyR0iXCbMQv3Xipma34MD+dH/1fQ784/j6cY/iJTQUOhcWr7x9JvoRxT2MZw1T" crossorigin="anonymous">

    </environment>

复制代码

  值得注意的是,<link>元素上的”integrity“属性,全称是SubResource Integrity(SRI),用于检查”子资源完整性“,它是一种安全功能,允许浏览器检查被检索的文件是否被恶意更改。


  最后总结一下使用环境变量 tag helper,它可以使得我们生产环境在引用的CDN访问失败的情况下,回退到我们指定的另一个源或者本地库。具体做法如下


复制代码

    <environment exclude="Development">

        <link href="https://stackpath.bootstrapcdn.com/bootstrap/4.3.1/css/bootstrap.min.css" rel="stylesheet" integrity="sha384-ggOyR0iXCbMQv3Xipma34MD+dH/1fQ784/j6cY/iJTQUOhcWr7x9JvoRxT2MZw1T" crossorigin="anonymous"

              asp-fallback-href="~/lib/twitter-bootstrap/css/bootstrap.css"

              asp-fallback-test-class="sr-only"

              asp-fallback-test-property="position"

              asp-fallback-test-value="absolute"

              asp-suppress-fallback-integrity="true"

              >

    </environment>

复制代码

  使用asp-fallback-href属性指定回退源。asp-fallback-test-class 判断失败时用到的class,asp-fallback-test-property 判断失败时用到的属性,asp-fallback-test-value 判断失败时用到的值。通过将”asp-suppress-fallback-integrity“属性设置为false,就可以关闭浏览器下载的文件完整性检查。



Asp.Net Core 入门(十)—— 模型绑定和验证

模型绑定时将Http请求中的数据映射到控制器操作方法上对应的参数,操作方法中的参数可以是简单类型,如整形,字符串等,也可以是复杂类型,如Product,Order等。


  Asp.Net Core MVC的模型绑定和Asp.Net MVC模型绑定相似,模型绑定将按下图指定的顺序查找来自http请求中的数据绑定到控制器操作方法对应的参数上。




  同时,Asp.Net MVC Core绑定模型的时候同样也会进行模型的校验。那么,我们怎么给模型添加校验呢,其实也和Asp.Net MVC差不多。


  首先我们在模型的属性上添加验证属性,Display属性为显示在页面上的该字段的信息。


复制代码

/// <summary>

/// 学生模型

/// </summary>

public class Student

{

    public int Id { get; set; }


    [Display(Name="姓名")]

    [Required(ErrorMessage ="请输入名字")]

    public string Name { get; set; }


    [Display(Name = "班级")]

    [Required(ErrorMessage ="请输入班级")]

    public ClassNameEnum? ClassName { get; set; }


    [Display(Name = "邮箱地址")]

    [Required(ErrorMessage ="请输入邮箱地址")]

    public string Email { get; set; }

}

复制代码

一般的属性校验有:


  Required      指定该字段是必填的


  Range       指定允许的最小值和最大值


  MinLength         指定字符串的最小长度


  MaxLength     指定字符串的最大长度


  Compare      比较模型的2个属性,例如比较Email和ComfirmEmail属性


  RegularExpression   正则表达式,验证提供的值是否与正则表达式指定的模式匹配


 


  其次,使用ModelState.IsValid属性验证属性是否绑定成功


复制代码

if (ModelState.IsValid)

{

    Student newStudent = _studentRepository.Add(student);


    return RedirectToAction("Details", new { id = newStudent.Id });

}

else

{

    return View(student);

}

复制代码

  最后,使用asp-validation-for和asp-validation-summary tag helper 来显示错误信息


复制代码

<div asp-validation-summary="All" class="text-danger"></div>


        <div class="form-group row">

            <label asp-for="Name" class="col-sm-2 col-form-label"></label>

            <div class="col-sm-10">

                <input asp-for="Name" class="form-control" placeholder="请输入名字" />

                <span asp-validation-for="Name" class="text-danger"></span>

            </div>

        </div>



        <div class="form-group row">

            <label asp-for="Email" class="col-sm-2 col-form-label"></label>

            <div class="col-sm-10">

                <input asp-for="Email" class="form-control" placeholder="请输入邮箱" />

                <span asp-validation-for="Email" class="text-danger"></span>

            </div>

        </div>



        <div class="form-group row">

            <label asp-for="ClassName" class="col-sm-2 col-form-label"></label>

            <div class="col-sm-10">

                <select asp-for="ClassName" asp-items="Html.GetEnumSelectList<ClassNameEnum>()">

                    <option value="" selected></option>

                </select>

                <span asp-validation-for="ClassName" class="text-danger"></span>

            </div>

        </div>

复制代码

  值得注意的是,在select标签的验证上,模型中有Required和无Required都会提示 The value '' is invalid.这是因为枚举ClassName里是int类型,而option里的value为“”,导致类型转化失败,我们可以在Student的ClassName设置为可空类型ClassNameEnum? 。



Asp.Net Core 进阶(一) —— 读取appsettings.json

我们以前在Asp.Net MVC中使用 System.Configuration.ConfigurationManager 来读取web.config文件。但是Asp.Net Core MVC已经没有web.config文件了,它的配置信息一般写在appsettings.json当中,那么我们怎么读取该文件呢?


  在Asp.Net Core MVC中使用 Microsoft.Extensions.Options.ConfigurationExtensions 包来读取appsettings.json。具体的操作如下:


  使用NuGet添加  Microsoft.Extensions.Options.ConfigurationExtensions  包到我们的项目当中,然后在appsettings.json中添加我们自己的一些配置信息


复制代码

{

  "Logging": {

    "LogLevel": {

      "Default": "Warning"

    }

  },

  "AllowedHosts": "*",

  "ConnectionStrings": {

    "OpenAuthDBContext": "Data Source=localhost;Initial Catalog=dbname;User=sa;Password=123"

  },

  "AppSetting": {

    "SSOPassport": "http://localhost:52789",

    "Version": "1.0", //如果为demo,则屏蔽Post请求

    "DbType": "SqlServer", //数据库类型:SqlServer/MySql

    "MessageUrl": "http://localhot:23124", //短信平台接口

    "MessageType": "CAD71325-0097-4052-9183-56F04EED0B31" //短信类型ID

  }

}

复制代码

  然后我们新建一个文件AppSetting


复制代码

    /// <summary>

    /// 配置项

    /// </summary>

    public class AppSetting

    {


        public AppSetting()

        {

            SSOPassport = "http://localhost:52789";  

            Version = "";

            DbType = Define.DBTYPE_SQLSERVER;

        }

        /// <summary>

        /// SSO地址

        /// </summary>

        public string SSOPassport { get; set; }


        /// <summary>

        /// 版本信息

        /// 如果为demo,则屏蔽Post请求

        /// </summary>

        public string Version { get; set; }


        /// <summary>

        /// 数据库类型 SqlServer、MySql

        /// </summary>

        public string DbType { get; set; }


        /// <summary>

        /// 短信平台接口Url

        /// </summary>

        public string MessageUrl { get; set; }


        /// <summary>

        /// 短信类型

        /// </summary>

        public string MessageType { get; set; }

    }

复制代码

  接着在Startup.cs文件的ConfigureServices方法中添加


services.AddOptions();

//映射配置文件

services.Configure<AppSetting>(Configuration.GetSection("AppSetting"));

  最后就可以在我们的Controller中使用了,通过IOption<AppSetting>来读取。


复制代码

private readonly IOptions<AppSetting> _setting;

 

public LoginController(IAuth authUtil,IOptions<AppSetting> setting)

{

       _authUtil = authUtil;

       _setting = setting;

}


public string GetCaptcha(string phone)

{

    string messageUrl = _setting.Value.MessageUrl;

    string messageType = _setting.Value.MessageType;


    if (_authUtil.GetCaptcha(phone, messageUrl, messageType))

    {

        return JsonHelper.Instance.Serialize(new { Code = 200, Message = "" });

    }

    return JsonHelper.Instance.Serialize(new { Code = 500, Message = "验证码获取失败,请稍后重试!" });

}

复制代码

  需要注意的是,通过IOption的方式不能在Startup.cs中读取appsettings.json文件,在Startup.cs中读取appsettings.json文件需要使用Microsoft.Extensions.Configuration的IConfiguration。


var dbType = ((ConfigurationSection) Configuration.GetSection("AppSetting:DbType")).Value;

 或者使用


var v = Configuration["ASPNETCORE_ENVIRONMENT"];

var d = Configuration["AppSetting:MessageUrl"];

 针对格式为下面的json,我么可以通过索引来获取


复制代码

{

    "Message": {

        "Message1": {

            "Name": ""

        },

        "Message2": {

            "Name": ""

        }

    }

}

复制代码

_configuration["Message:0:Name"]



Asp.Net Core 进阶(二) —— 集成Log4net

Asp.Net Core 支持适用于各种内置日志记录API,同时也支持其他第三方日志记录。在我们新建项目后,在Program 文件入口调用了CreateDefaultBuilder,该操作默认将添加以下日志记录提供程序:ConsoleLogger、DebugLogger、EventSourceLogger。


  在工作中笔者使用的最多的日志记录组件是log4net,接下来我们就来看一下在Asp.Net Core中怎么集成 log4net。


  首先我们需要添加 log4net 组件,通过Nuget安装 log4net 和 Microsoft.Extensions.Logging.Log4Net.AspNetCore。


  接着我们引入配置文件log4net.config,放在项目config文件夹下


复制代码

<?xml version="1.0" encoding="utf-8"?>

<log4net>

  <!-- Define some output appenders -->

  <appender name="rollingAppender" type="log4net.Appender.RollingFileAppender">

    <file value="log\log.txt" />


    <!--追加日志内容-->

    <appendToFile value="true" />


    <!--防止多线程时不能写Log,官方说线程非安全-->

    <lockingModel type="log4net.Appender.FileAppender+MinimalLock" />


    <!--可以为:Once|Size|Date|Composite-->

    <!--Composite为Size和Date的组合-->

    <rollingStyle value="Composite" />


    <!--当备份文件时,为文件名加的后缀-->

    <datePattern value="yyyyMMdd.TXT" />


    <!--日志最大个数,都是最新的-->

    <!--rollingStyle节点为Size时,只能有value个日志-->

    <!--rollingStyle节点为Composite时,每天有value个日志-->

    <maxSizeRollBackups value="20" />


    <!--可用的单位:KB|MB|GB-->

    <maximumFileSize value="3MB" />


    <!--置为true,当前最新日志文件名永远为file节中的名字-->

    <staticLogFileName value="true" />


    <!--输出级别在INFO和ERROR之间的日志-->

    <filter type="log4net.Filter.LevelRangeFilter">

      <param name="LevelMin" value="ALL" />

      <param name="LevelMax" value="FATAL" />

    </filter>


    <!--必须结合起来用,第一个只过滤出WARN,第二个拒绝其它其它日志输出-->

    <!--

        <filter type="log4net.Filter.LevelMatchFilter">

            <param name="LevelToMatch" value="WARN" />

        </filter>

        <filter type="log4net.Filter.DenyAllFilter" />-->


    <layout type="log4net.Layout.PatternLayout">

      <conversionPattern value="%date [%thread] %-5level %logger - %message%newline"/>

    </layout>

  </appender>



  <!-- levels: OFF > FATAL > ERROR > WARN > INFO > DEBUG  > ALL -->

  <root>

    <priority value="ALL"/>

    <level value="ALL"/>

    <appender-ref ref="rollingAppender" />

  </root>

</log4net>

复制代码

  然后在Program文件中进行配置日志,如果不需要使用ConsoleLogger,DebugLogger或EventSourceLogger,可以使用ClearProviders方法清空默认的日志记录组件,AddFilter是过滤掉系统自带的System,Microsoft开头的日志,LogLevel。具体做法参考以下代码


复制代码

public static IWebHostBuilder CreateWebHostBuilder(string[] args) =>

    WebHost.CreateDefaultBuilder(args)

    .ConfigureLogging((context, loggingBuilder) =>

    {

        //loggingBuilder.ClearProviders();

        loggingBuilder.AddFilter("System", LogLevel.Warning);

        loggingBuilder.AddFilter("Microsoft", LogLevel.Warning);//过滤掉系统自带的System,Microsoft开头的,级别在Warning以下的日志

        loggingBuilder.AddLog4Net("config/log4net.config"); //会读取appsettings.json的Logging:LogLevel:Default级别

    })

    .UseStartup<Startup>();

复制代码

  配置完log4net后,可以使用 ILoggerFactory 或者 ILogger<T> 来记录日志,具体做法如下,其中的ILoggerFactory可以替换成ILogger<Starup>。


复制代码

public void Configure(IApplicationBuilder app, IHostingEnvironment env,ILoggerFactory logFactory)

{

    //ILogger logger = logFactory.CreateLogger(typeof(Log4NetLogger));

    ILogger logger = logFactory.CreateLogger<Startup>();

    logger.LogError("this is the first error");

    logger.LogInformation("this is the first info");

    logger.LogDebug("this is the first debug");

    logger.LogWarning("this is the first warning");

    logger.LogInformation(System.Diagnostics.Process.GetCurrentProcess().ProcessName);

}

复制代码

  上述是在Startup文件中调用日志,同样的在Controller中也可以调用日志进行记录。


复制代码

private readonly IStudentRepository _studentRepository;


private readonly ILogger<HomeController> _logger;


//构造函数注入

public HomeController(IStudentRepository studentRepository,ILogger<HomeController> logger)

{

    _studentRepository = studentRepository;

    _logger = logger;

}

复制代码

  如果要在Program中创建日志,则需要从DI容器中获取ILogger实列


复制代码

public static void Main(string[] args)

{

  var host = CreateWebHostBuilder(args).Build();


  var logger = host.Services.GetRequiredService<ILogger<Program>>();

  logger.LogInformation("Seeded the database.");


  host.Run();

}



Asp.Net Core 进阶(三)—— IServiceCollection依赖注入容器和使用Autofac替换它

Asp.Net Core 提供了默认的依赖注入容器 IServiceCollection,它是一个轻量级的依赖注入容器,所以功能不多,只是提供了基础的一些功能,要实现AOP就有点麻烦,因此在实际工作当中,我们常常会使用第三方依赖注入容器替换掉Asp.Net Core自带的依赖注入容器。


  我们先来看下Asp.Net Core自带依赖注入容器IServiceCollection的主要用法,虽然说在工作中经常会被替换掉,但有些情况下使用该自带的就已经足够了,所以自然也就需要先了解它的使用方法。


  IServiceCollection依赖注入生命周期和其他大多数依赖注入容器一样,分为 瞬时生命周期、单例和请求单例。我们可以在Startup.cs文件中的ConfigureServices方法中直接使用它。这里我们单独把它拿出来看一下具体怎么使用,我们定义ITestService1,ITestService2,ITestService3,ITestService4以及他们的4个实现类。


复制代码

IServiceCollection container = new ServiceCollection();

container.AddTransient<ITestService1, TestService1>();//瞬时生命周期 

container.AddSingleton<ITestService2, TestService2>();//单例:全容器都是一个

container.AddScoped<ITestService3, TestService3>();//请求单例:一个请求作用域是一个实例

container.AddSingleton<ITestService4>(new TestService4());


var provider = container.BuildServiceProvider();

ITestService1 testService1 = provider.GetService<ITestService1>();

ITestService1 testService2 = provider.GetService<ITestService2>();

Console.WriteLine(object.ReferenceEquals(testService1, testService2));//输出 false


ITestService2 testService2_1 = provider.GetService<ITestService2>();

ITestService2 testService2_2 = provider.GetService<ITestService2>();

Console.WriteLine(object.ReferenceEquals(testService2_1, testService2_2));//输出 true


ITestService3 testService3_1 = provider.GetService<ITestService3>();

ITestService3 testService3_2 = provider.GetService<ITestService3>();

Console.WriteLine(object.ReferenceEquals(testService3_1, testService3_2));//输出 true


var scope1 = provider.CreateScope();

var scope2 = provider.CreateScope();

ITestService3 testService3_3 = scope1.ServiceProvider.GetService<ITestService3>();

ITestService3 testService3_4 = scope2.ServiceProvider.GetService<ITestService3>();

Console.WriteLine(object.ReferenceEquals(testService3_3, testService3_4)); //输出 false


ITestService4 testService4_1 = provider.GetService<ITestService4>();

ITestService4 testService4_2 = provider.GetService<ITestService4>();

Console.WriteLine(object.ReferenceEquals(testService4_1, testService4_2)); //输出 true

复制代码

   上述代码已经可以很好的阐述了IServiceCollection的用法,但是这些也只是基本的功能,接下来我们就来看下使用Autofac如何替换掉IServiceCollection。


   Autofac是一个Microsoft .NET的IoC容器。 它管理类与类之间的依赖关系,使得应用程序层级之间实现了解耦,不至于在应用程序变得越来越复杂的情况下难以修改。


   那么现在就一起来看看怎么使用Autofac来替换掉Asp.Net Core自带的依赖注入容器吧,首先先来介绍最常用也是被推荐使用的构造函数注入方式。在Autofac官方文档中有例子可参考。要使用Autofac替换IServiceCollection,我们需要在Startup.cs文件中将ConfigureServices方法的返回值从void修改为 IServiceProvider。


  在开始之前,我们需要先从Nuget下载安装Autofac,可以使用如下命令进行安装


Install-Package Autofac.Extensions.DependencyInjection -Version 4.4.0

  接下来随便定义两个测试接口和实现类


复制代码

 public interface ITestServiceA

    {

        void Show();

    }


    public interface ITestServiceB

    {

        void Show();

    }


    public class TestServiceA : ITestServiceA

    {

        public   void Show()

        {

            Console.WriteLine("This is TestServiceA....");

        }

    }


    public class TestServiceB : ITestServiceB

    {

        public void Show()

        {

            Console.WriteLine("This is TestServiceB....");

        }

    }

复制代码

  接下来我们修改Startup.cs的ConfigureServices方法


复制代码

// This method gets called by the runtime. Use this method to add services to the container.

public IServiceProvider ConfigureServices(IServiceCollection services)

{

    services.Configure<CookiePolicyOptions>(options =>

    {

        // This lambda determines whether user consent for non-essential cookies is needed for a given request.

        options.CheckConsentNeeded = context => true;

        options.MinimumSameSitePolicy = SameSiteMode.None;

    });

    services.AddSession();

    services.AddMvc().SetCompatibilityVersion(CompatibilityVersion.Version_2_2);


    var containerBuilder = new ContainerBuilder();


    containerBuilder.RegisterModule<AutofacInitModule>();


    // Populate 方法是最重要的,如果没有调用这个方法,则无法将注入到 IServiceCollection的内置对象填充到autofac中,像控制器注入,Log注入等,程序会报错。

    containerBuilder.Populate(services);


    var container = containerBuilder.Build();

    return new AutofacServiceProvider(container);

}

复制代码

  AutofacInitModule类是继承了Autofac.Module类的子类,我们可以重写Load方法进行Autofac的初始化注入,当然也可以直接写在Startup文件的ConfigureServices方法中,单独抽出来会显得不那么臃肿,优雅一些。


复制代码

using Autofac;


namespace WebApplication2

{

    internal class AutofacInitModule : Module

    {

        protected override void Load(ContainerBuilder builder)

        {

            builder.RegisterType<TestServiceA>().As<ITestServiceA>();

            builder.RegisterType<TestServiceB>().As<ITestServiceB>();


            // builder.RegisterType<TestServiceA>().As<ITestServiceA>().SingleInstance(); //单例

        }

    }

}

复制代码

  现在我们就可以在Controller中使用了


复制代码

public class HomeController : Controller

{

    private readonly ILogger<HomeController> _logger;

    private readonly ITestServiceA _serviceA;

    private readonly ITestServiceB _serviceB;



    public HomeController(ILogger<HomeController> logger, ITestServiceA serviceA, ITestServiceB serviceB)

    {

        this._logger = logger;

        this._serviceA = serviceA;

        this._serviceB = serviceB;

    }


    public IActionResult Index()

    {

        this._serviceA.Show();

        this._serviceB.Show();

        this._logger.LogWarning("this is logger....");

        return View();

    } 

}

复制代码

  运行程序,可以看到_logger、_serviceA、_serviceB都成功的被创建了。




  那么上述是使用的autofac的构造函数注入,虽然是最被推荐的也是最常用的注入方式,但是autofac也提供了属性注入的方式,下面我们也了解一下。


  修改Startup.cs的ConfigureServices方法和AutofaceInitModule类


复制代码

// This method gets called by the runtime. Use this method to add services to the container.

public IServiceProvider ConfigureServices(IServiceCollection services)

{

    services.Configure<CookiePolicyOptions>(options =>

    {

        // This lambda determines whether user consent for non-essential cookies is needed for a given request.

        options.CheckConsentNeeded = context => true;

        options.MinimumSameSitePolicy = SameSiteMode.None;

    });

    services.AddSession();

    services.AddMvc().SetCompatibilityVersion(CompatibilityVersion.Version_2_2).AddControllersAsServices();


    var containerBuilder = new ContainerBuilder();


    // Populate 方法是最重要的,如果没有调用这个方法,则无法将注入到 IServiceCollection的内置对象填充到autofac中,像控制器注入,Log注入等,程序会报错。

    containerBuilder.Populate(services);


    containerBuilder.RegisterModule<AutofacInitModule>();


    var container = containerBuilder.Build();

    return new AutofacServiceProvider(container);

}

复制代码

复制代码

using Autofac;

using Microsoft.AspNetCore.Mvc;

using System.Linq;


namespace WebApplication2

{

    internal class AutofacInitModule : Module

    {

        protected override void Load(ContainerBuilder builder)

        {

            //注册服务

            builder.RegisterType<TestServiceA>().As<ITestServiceA>().PropertiesAutowired();

            builder.RegisterType<TestServiceB>().As<ITestServiceB>().PropertiesAutowired();


            //注册所有控制器

            var controllersTypesInAssembly = typeof(Startup).Assembly.GetExportedTypes()

            .Where(type => typeof(ControllerBase).IsAssignableFrom(type)).ToArray();


            builder.RegisterTypes(controllersTypesInAssembly).PropertiesAutowired();


            // builder.RegisterType<TestServiceA>().As<ITestServiceA>().SingleInstance(); //单例

        }

    }

}

复制代码

需要注意的是 services.AddMvc().SetCompatibilityVersion(CompatibilityVersion.Version_2_2).AddControllersAsServices();和


containerBuilder.Populate(services);需要写在注入服务之前,属性注入才能成功。


复制代码

    public class HomeController : Controller

    {

        //private readonly ILogger<HomeController> _logger;

        //private readonly ITestServiceA _serviceA;

        //private readonly ITestServiceB _serviceB;


        public ILogger<HomeController> Logger { get; set; }

        public ITestServiceA ServiceA { get; set; }

        public ITestServiceB ServiceB { get; set; }


        //public HomeController(ILogger<HomeController> logger, ITestServiceA serviceA, ITestServiceB serviceB)

        //{

        //    this._logger = logger;

        //    this._serviceA = serviceA;

        //    this._serviceB = serviceB;

        //}


        public IActionResult Index()

        {

            //this._serviceA.Show();

            //this._serviceB.Show();

            //this._logger.LogWarning("this is logger....");


            Logger.LogWarning(ServiceA.Show());

            Logger.LogWarning(ServiceB.Show());


            return View();

        } 

    }

复制代码

  运行可以看到成功的结果




  最后,在一开始之前,我们提到Asp.Net Core自带的依赖注入容器在实现AOP的功能很麻烦,在工作中常常会替换成第三方的依赖注入容器,那么现在我们再来看一下autofac怎么实现AOP。


  Autofac实现AOP需要引入Autofac.Extras.DynamicProxy库,通过Nuget添加该库


Install-Package Autofac.Extras.DynamicProxy -Version 4.5.0

  接着为了实现AOP,我们定义如下类和接口


复制代码

using Autofac.Extras.DynamicProxy;

using Castle.DynamicProxy;

using Microsoft.Extensions.Logging;

using System;

using System.Collections.Generic;

using System.Linq;

using System.Threading.Tasks;


namespace WebApplication2

{

    public class AutofacTestAop : IInterceptor

    {

        public void Intercept(IInvocation invocation)

        {

            Console.WriteLine($"invocation.Methond={invocation.Method}, invocation.Arguments={string.Join(",", invocation.Arguments)}");


            invocation.Proceed(); //继续执行TestAop.Show


            Console.WriteLine($"Method {invocation.Method} Excuted");

        }

    }


    public interface ITestAop

    {

        void Show();

    }


    [Intercept(typeof(AutofacTestAop))]

    public class TestAop : ITestAop

    {

        private ILogger<TestAop> _logger;


        public TestAop(ILogger<TestAop> logger)

        {

            this._logger = logger;

        }


        public void Show()

        {

            this._logger.LogWarning("this is TestAop .....");

        }

    }

}

复制代码

  AutofacTestAop为实现了IInterceptor的类,它的Intercept方法就是用来做AOP的调用的。除了实现这个方法外,在需要添加AOP功能的类上需要添加特性 [Intercept(typeof(AutofacTestAop))] 。最后需要在AutofacInitModule中配置一下启用 EnableInterfaceInterceptors。


复制代码

using Autofac;

using Autofac.Extras.DynamicProxy;

using Microsoft.AspNetCore.Mvc;

using System.Linq;


namespace WebApplication2

{

    internal class AutofacInitModule : Module

    {

        protected override void Load(ContainerBuilder builder)

        {

            //注册服务

            builder.RegisterType<TestServiceA>().As<ITestServiceA>();

            builder.RegisterType<TestServiceB>().As<ITestServiceB>();


            builder.Register(c => new AutofacTestAop());

            builder.RegisterType<TestAop>().As<ITestAop>().EnableInterfaceInterceptors();


            // builder.RegisterType<TestServiceA>().As<ITestServiceA>().SingleInstance(); //单例

        }

    }

}

复制代码

  最后在HomeController中调用ITestAop的Show方法就会先执行AutofacTestAop里的Intercept方法了


Asp.Net Core 进阶(四)—— 过滤器 Filters

一、介绍  


  Asp.Net Core Filter 使得可以在请求处理管道的特定阶段的前后执行代码,我们可以创建自定义的 filter 用于处理横切关注点。 横切关注点的示例包括错误处理、缓存、配置、授权和日志记录。 filter 使得可以避免重复代码。


  Asp.Net Core 提供了5中过滤器类型,分别是:


  1、Authorization filters,授权过滤器是最先执行并且决定请求用户是否经过授权认证,如果请求未获授权,授权过滤器可以让管道短路。


  2、Resource filters,资源过滤器在Authorization filter执行完后执行,它有两个方法,OnResourceExecuting可以在filter管道的其余阶段之前运行代码,例如可以在模型绑定之前运行。OnResourceExecuted则可以在filter管道的其余阶段之后运行代码


  3、Action filters,操作过滤器可以在调用单个操作方法之前和之后立即运行代码,它可以用于处理传入某个操作的参数以及从该操作返回的结果。 需要注意的是,Aciton filter不可以在 Razor Pages 中使用。


  4、Exception filters,异常过滤器常常被用于在向响应正文写入任何内容之前,对未经处理的异常应用全局策略。


  5、Result filters,结果过滤器可以在执行单个操作结果之前和之后运行代码。 但是只有在方法成功执行时,它才会运行。


  至于它们在filter管道中的交互方式可以看下图。

1.png

    


二、实现


  Asp.Net Core提供了同步和异步两种filter接口定义,通过实现不同的接口可以实现同步或异步的过滤器,但是如果一个类同时实现了同步和异步的接口,Asp.Net Core的filter 管道只会调用异步的方法,即异步优先。具体的做法可以参考微软官方文档 https://docs.microsoft.com/en-us/aspnet/core/mvc/controllers/filters?view=aspnetcore-2.2。


  以下是IActionFilter和IAsyncActionFilter的实现


复制代码

public class MySampleActionFilter : IActionFilter

{

    public void OnActionExecuting(ActionExecutingContext context)

    {

        // Do something before the action executes.

    }


    public void OnActionExecuted(ActionExecutedContext context)

    {

        // Do something after the action executes.

    }

}


public class SampleAsyncActionFilter : IAsyncActionFilter

{

    public async Task OnActionExecutionAsync(

        ActionExecutingContext context,

        ActionExecutionDelegate next)

    {

        // Do something before the action executes.


        // next() calls the action method.

        var resultContext = await next();

        // resultContext.Result is set.

        // Do something after the action executes.

    }

}

复制代码

  除了直接实现Filter接口,Asp.Net Core同时提供了一些基于特性的过滤器,我们可以继承相应的特性来实现自定义的过滤器。这些特性包括ActionFilterAttribute、ExceptionFilterAttribute、ResultFilterAttribute、FormatFilterAttribute、ServiceFilterAttribute、TypeFilterAttribute。


  下面是微软文档的一个例子,在Result Filter给响应添加Header。


复制代码

public class AddHeaderAttribute : ResultFilterAttribute

{

    private readonly string _name;

    private readonly string _value;


    public AddHeaderAttribute(string name, string value)

    {

        _name = name;

        _value = value;

    }


    public override void OnResultExecuting(ResultExecutingContext context)

    {

        context.HttpContext.Response.Headers.Add( _name, new string[] { _value });

        base.OnResultExecuting(context);

    }

}


[AddHeader("Author", "Joe Smith")]

public class SampleController : Controller

{

    public IActionResult Index()

    {

        return Content("Examine the headers using the F12 developer tools.");

    }


    [ShortCircuitingResourceFilter]

    public IActionResult SomeResource()

    {

        return Content("Successful access to resource - header is set.");

    }

}

复制代码

 


三、Filter 的作用域和执行顺序


  可以将我们自定义的filter添加到我们的代码中进行调用,添加的方式有三种,对应其三个作用域:在Action上添加特性、在Controller上添加特性和添加全局filter。


  下面先来看下添加全局filter的做法,我们知道,在Asp.Net MVC中,filter都是在控制器实例化之后才能生效的,其Filter应该是确定的,不能被注入参数,但是到了Asp.Net Core,全局注册是在ConfigureServices方法中完成的,它可以被注入参数。


复制代码

public void ConfigureServices(IServiceCollection services)

{

    services.Configure<CookiePolicyOptions>(options =>

    {

        // This lambda determines whether user consent for non-essential cookies is needed for a given request.

        options.CheckConsentNeeded = context => true;

        options.MinimumSameSitePolicy = SameSiteMode.None;

    });



    services.AddMvc(options =>

    {

        options.Filters.Add(new AddHeaderAttribute("name", "Jesen"));

        options.Filters.Add(typeof(AddLogActionAttribute));

        options.Filters.Add(typeof(ExceptionHandlerAttribute));

    }).SetCompatibilityVersion(CompatibilityVersion.Version_2_2);

}

复制代码

  上述代码在addMvc中将我们自定义的 filter 添加到全局当中,这样对所有的控制器和action都产生了作用。而其他两种作用域的用法就是直接将特性添加到其上面。


复制代码

[AddHeader("", "")]

public class HomeController : Controller

{

    [AddLogAction]

    public IActionResult Index()

    {

        return View();

    }

}

复制代码

  那么这三种作用域的默认执行顺序是怎样的,下图很好的进行了展示。


1.png


  那么我们是否可以改变其默认执行顺序呢,答案当然是肯定的。我们可以通过实现 IOrderFilter来重写默认执行序列。 IOrderedFilter 公开了Order 属性来确定执行顺序,该属性优先于作用域。 具有较低的 Order 值的 filter 在具有较高的 Order 值的filter之前运行 before 代码,在具有较高的 Order 值的 filter 之后运行 after 代码。具体做法可以在使用构造函数参数时设置Order 属性。


[CustomFilter(Name = "Controller Level Attribute", Order=1)]

  在改变了Order属性后的执行顺序如下图所示

1.png



 四、依赖注入


  在前面添加filter事,我们在全局添加方式中知道了 filter 可以通过类型添加,也可以通过实例添加,通过实例添加,则该实例会被应用于所有的请求,按照类型添加则将激活该类型,这意味着它会为每个请求创建一个实例,依赖注入将注入所有构造函数的依赖项。


  但是如果通过特性添加到Controller或者Action上时,该 filter 不能由依赖注入提供构造函数依赖项,这是因为特性在添加时必须提供它的构造函数参数,这是由于特性的原理决定的。那是不是通过Controller或Action的特性不能有构造函数参数呢,肯定不是的,可以通过以下特性来获得依赖注入:ServiceFilterAttribute、TypeFilterAttribute和在特性上实现 IFilterFactory。


复制代码

namespace FilterDemo.Filter

{

    public class AddLogActionAttribute : ActionFilterAttribute

    {

        private readonly ILogger _logger;


        public AddLogActionAttribute(ILoggerFactory loggerFactory)

        {

            this._logger = loggerFactory.CreateLogger<AddLogActionAttribute>();

        }


        public override void OnActionExecuting(ActionExecutingContext context)

        {

            string controllerName = (string)context.RouteData.Values["controller"];

            string actionName = (string)context.RouteData.Values["action"];


            this._logger.LogInformation($"{controllerName}的{actionName}开始执行了...")

            base.OnActionExecuting(context);

        }



        public override void OnActionExecuted(ActionExecutedContext context)

        {

            base.OnActionExecuted(context);

        }

    }

}

复制代码

  上述代码我们定义了带Logger参数的AddLogActionAttribute Filter,接下来实现怎么在Controller或Action上使用,首先在Startup中添加注入


services.AddScoped<AddLogActionAttribute>();

  然后在Controller或Action上使用 ServiceFilter


[ServiceFilter(typeof(AddLogActionAttribute))]

public class HomeController : Controller

  TypeFilterAttribute与ServiceFilterAttribute类似,但不会直接从 DI 容器解析其类型。 它使用 Microsoft.Extensions.DependencyInjection.ObjectFactory 对类型进行实例化。因为不会直接从 DI 容器解析 TypeFilterAttribute 类型,所以使用 TypeFilterAttribute 引用的类型不需要注册在 DI 容器中。


 [TypeFilter(typeof(AddLogActionAttribute))]

 public IActionResult Index()

 {

      return View();

 }

五、Resource Filter  


  最后,我们来看一下Asp.Net Core不同于之前Asp.Net MVC的 ResourceFilter,在上述介绍中,我们知道了Resource Filter在Authorization Filter执行之后执行,然后才会去实例化控制器,那么Resource Filter 就比较适合用来做缓存,接下来我们自定义一个Resource Filter。


复制代码

using Microsoft.AspNetCore.Mvc;

using Microsoft.AspNetCore.Mvc.Filters;

using System;

using System.Collections.Generic;

using System.Linq;

using System.Threading.Tasks;


namespace FilterDemo.Filter

{

    public class CacheResourceFilterAttribute : Attribute, IResourceFilter

    {

        private static readonly Dictionary<string, object> _Cache = new Dictionary<string, object>();

        private string _cacheKey;

        /// <summary>

        /// 控制器实例化之前

        /// </summary>

        /// <param name="context"></param>

        public void OnResourceExecuting(ResourceExecutingContext context)

        {

            _cacheKey = context.HttpContext.Request.Path.ToString();

            if (_Cache.ContainsKey(_cacheKey))

            {

                var cachedValue = _Cache[_cacheKey] as ViewResult;

                if (cachedValue != null)

                {

                    context.Result = cachedValue; //设置该Result将是filter管道短路,阻止执行管道的其他阶段

                }

            }

        }


        /// <summary>

        /// 把请求都处理完后执行

        /// </summary>

        /// <param name="context"></param>

        public void OnResourceExecuted(ResourceExecutedContext context)

        {

            if (!String.IsNullOrEmpty(_cacheKey) &&

                !_Cache.ContainsKey(_cacheKey))

            {

                var result = context.Result as ViewResult;

                if (result != null)

                {

                    _Cache.Add(_cacheKey, result);

                }

            }

        }

    }

}

复制代码

  将其添加到HomeController的Index上


复制代码

[CacheResourceFilter]

[TypeFilter(typeof(AddLogActionAttribute))]

public IActionResult Index()

{

    return View();

}

复制代码

  运行可以发现第一次请求按照默认顺序执行,第二次请求会在Cache中查找该请求路径是否已经在Cache当中,存在则直接返回到Result,中断了请求进入管道的其他阶段。


Asp.Net Core 3.1返回JsonResult时时间格式处理

定义一个继承System.Text.Json.Serialization.JsonConverter的类,实现其Read 和 Write两个抽象方法


复制代码

    public class DateTimeConverter : JsonConverter<DateTime>

    {

        public string DateTimeFormat { get; set; } = "yyyy-MM-dd HH:mm:ss";


        public override DateTime Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) => DateTime.Parse(reader.GetString());


        public override void Write(Utf8JsonWriter writer, DateTime value, JsonSerializerOptions options) => writer.WriteStringValue(value.ToString(this.DateTimeFormat));

    }

复制代码

  然后在Startup中添加AddJsonOptions来设置时间的格式


复制代码

 services.AddMvc(options =>

            {

                options.EnableEndpointRouting = false;

                // 添加全局异常处理

                options.Filters.Add(typeof(GlobalExceptionFilter));

            }

            ).AddJsonOptions

            (

                option =>

                {

                    //统一设置JsonResult中的日期格式    

                    option.JsonSerializerOptions.Converters.Add(new DateTimeConverter());

                    //option.JsonSerializerOptions.Encoder = JavaScriptEncoder.Create(UnicodeRanges.All);

                }

            ).SetCompatibilityVersion(CompatibilityVersion.Version_3_0);

复制代码


在ASP.NET MVC中使用Jntemplate 

一、使用Jntemplate

打开HomeController.cs 添加如下代码

public IActionResult Jntemplate(string path)

{

    var t = Engine.LoadTemplate(path);

    t.Context.TempData = this.Data;

    var result = t.Render();

    return Content(result, "text/html");

}

Index Action我们也要稍微改造一样,打开Index Action,改造如下:

public IActionResult Index()

{

    this.Data.Set("Name", "Jntemplate");

    this.Data.Set("Now", DateTime.Now);

    return Jntemplate("Views/Home/Index.html");

}

新建一个视图文件Index.html,原来的Index.cshtml直接删除,并编辑内容如下:

${Name} web 应用Welcome to ${Name}!&copy;${Now.Year}

最后别忘记在 Startup配置一下引擎工作目录,打开Startup,在Configure中添加一句:Engine.Configure(c=>c.ResourceDirectories.Add(env.ContentRootPath));

二、使用JntemplateViewEngine

在上面我们虽然实现了jntemplate的使用,但是步骤相对繁琐,我们可以直接使用Jntemplate视图引擎来简化使用.


安装

首先新建一个asp.net mvc项目(参见上面的步骤)。然后我们先添加JntemplateViewEngine包, 点击项目 - 管理NUGET程序包,点击浏览,输入Jntemplate,安装好包JinianNet.AspNetCoreViewEngine.Jntemplate.


配置

打开 Startup在ConfigureServices方法中增加AddJntemplateViewEngine:

public void ConfigureServices(IServiceCollection services)

{

    //原来的代码

    services.AddJntemplateViewEngine();

}


如果你的视图文件按照默认习惯放在Views目录下,直接照上面的代码配置就可以了,如果是放在其它目录,则需要把目录添加到配置里面。

public void ConfigureServices(IServiceCollection services)

{

    //原来的代码

    services.AddJntemplateViewEngine((o) => {

        o.ViewLocationFormats.Add("/template/{1}/{0}.html");

    });

}



{0}为视图名,{1}为控制器名。


在Configure方法中增加UseJntemplate,如下如示

public void Configure(IApplicationBuilder app, IWebHostEnvironment env)

{

    //原来的代码

    //Use Jntemplate

    app.UseJntemplate(c =>

    {

        //在这里你也可以进行其它参数的配置,比如全局数据

        //这一句很重要,不然会找不到视图

        c.ContentRootPath = env.ContentRootPath;

    });

}

添加Action

打开HomeController.cs ,添加一个Index视图方法(或修改对应视图方法)如下:


例:


public IActionResult Index()

{

    this.Set("Name", "Jntemplate");

    this.Set("Now", DateTime.Now);

    return View();

}



添加视图Index.html

在Views\Home目录新建一个视图文件Index.html,内容如下(与第一节的一致):


${Name} web 应用Welcome to ${Name}!&copy;${Now.Year}


运行

按F5运行查看效果。


net core 模版引擎JNTemplate

一、新建项目:JNTemplateDemo


  <ItemGroup>

    <PackageReference Include="JinianNet.JNTemplate" Version="2.2.2" />

  </ItemGroup>

二、Program.cs


using JinianNet.JNTemplate;

using System;

 

namespace JNTemplateDemo

{

    class Program

    {

        static void Main(string[] args)

        {

            var templateContent = "$fun.Test(\"your input\") $name";

            var template = Engine.CreateTemplate(templateContent);

            template.Set("fun", new TestHelper());

            template.Set("name", "jntemplate");

            var result = template.Render();

            Console.WriteLine(result);

 

            Console.ReadKey();

        }

    }

    public class TestHelper

    {

        public string Test(string val)

        {

            return $"Good! {val}";

        }

    }

}

1.变量:

用于在模板中输出一个变量,该变量可以是任何对象。如:${var},可以简写为$var,其中var 为变量名,变量名只能是字母,下划线与数字的组合,且必须以字母开头。

例:

var template = Engine.CreateTemplate("$title!");

template.Set("title", "jntemplate");

template.Render(Console.Out);


2.属性与字段

用法: 用于访问对象属性或者字段,用法与c#类似,字段与属性必须是公开的(public),v2.0.0 中暂时不支持匿名对象的访问。如:${model.Name},可以简写为$model.Name.

例一:

var template = Engine.CreateTemplate("$model.Title!");

template.Set("model", new Site{ Title="jntemplate" });

template.Render(Console.Out);

如果访问静态属性或字段,需要通过template.SetStaticType(...)来指定静态对象类型。

例二:

var templateContent = "${DateTime.Now}";

var template = Engine.CreateTemplate(templateContent);

template.SetStaticType("DateTime", typeof(DateTime));

template.Render(Console.Out);


3.索引

用法:用于访问数组或者IList及其它支持索引的对象,用法与c#类似,如${arr[1]}

例:

var template = Engine.CreateTemplate("${arr[0]}");

template.SetStaticType("arr", new int[]{ 1,2,3});

template.Render(Console.Out);


4.函数方法

用法:用于调用对象实例方法,静态方法,或者委托。如:${func(p1,p2....) },可以简写为$func(p1,p2....)。

注意:方法必须是公开的(public),如果是私有的(private)则无法访问

例一(实例方法):

Class1类

public class Class1

{

    public int Add(int a,int b)

    {

        return a+b; 

    }

}


模板代码:

var template = Engine.CreateTemplate("$obj.Add(1,2)");

template.Set("obj", new Class1());

template.Render(Console.Out);


例二(静态方法):


var templateContent = "${string.Concat(\"str1\",\"str2\")}";

var template = Engine.CreateTemplate(templateContent);

template.SetStaticType("string", typeof(string));

template.Render(Console.Out);


例三(委托方法):


var template = Engine.CreateTemplate("$add(8,-2)");

template.Set<Func>("add", (x, y) =>

{

    return x + y;

});

template.Render(Console.Out);



5.逻辑判断(IF)

用法:用于处理代码逻辑,等同于c#里面的if与else if ,支持以下逻辑运算符:大于(>),小于(<),大于等于(>=),小于等于(<=),等于(==),不等于(!=),或者(||), 并且(&&)。

例一:

模板:demo.html

$if(id>0)

     编号大于零

$elif(id==0)

    编号等于零

$else

    编号小于零

$end


后台代码:

var template = Engine.LoadTemplate(@"c:\demo.html");

template.Set("id",15);

template.Render(Console.Out);

注意:else if 可以写作$elseif 也可以写作 $elif。标签必须以$end结束


6、列表迭代(Foreach)

用法:用来遍历输出一个列表,等同于c#中foreach,目标可以是数组或者实现了IEnumerable  接口的任意对象.

例一:

模板:demo.html

$foreach(model in list)${model.Title}$end


var template = Engine.LoadTemplate(@"c:\demo.html");

template.Set("list",new NewInfo[]{ ... });

template.Render(Console.Out);


$foreach(model in list) 也可以写作 $for(model in list) ,必须使用$end 结束标签。


7、模板引用

用法:用于引用某一个公共模板,有二种写法$load("路径")与$inclub("路径"):

load 引用并解析模板

inclub:引用模板(不解析),适用于不需要解析的文件,比如JS,CSS等

例:

$load("public/header.html")这是内容


8、总结

本文介绍了jntemplate常用的模板语法,包括变量,属性,方法,逻辑判断,迭代与模板引用这几大块,只要灵活组合使用,完全可以满足日常开发了。


部分不怎么常用的语法大家可以自行可以参考官方文档。



.net core中关于System.Text.Json的使用

在.Net Framework的时候序列化经常使用Newtonsoft.Json插件来使用,而在.Net Core中自带了System.Text.Json,号称性能更好,今天抽空就来捣鼓一下。


  使用起来其实也很简单,就是有些地方要注意,比如在我们的对象实体中有中文的话,直接序列化时中文会被转换成编码格式,时间格式序列化时会被转成默认的格式等。


  下面就直接上Demo的代码了


复制代码

using System;

using System.Text.Encodings.Web;

using System.Text.Json;

using System.Text.Json.Serialization;

using System.Text.Unicode;


namespace ConsoleApp4

{

    class Program

    {

        static void Main(string[] args)

        {

            Console.WriteLine("Hello World!");


            var user = new User{ 

                Id= 1,

                Name = "张三李四",

                Gender = Gender.Male,

                Email="Jesen@qq.com",

                CreatedTime = DateTime.Now

            };


            JsonSerializerOptions options = new JsonSerializerOptions();

            options.Encoder = JavaScriptEncoder.Create(UnicodeRanges.All); //解决中文序列化被编码的问题

            options.Converters.Add(new DateTimeConverter()); //解决时间格式序列化的问题


            var serializeString = JsonSerializer.Serialize(user, options);

            Console.WriteLine(serializeString);



            User obj = JsonSerializer.Deserialize<User>(serializeString,options);



            Console.ReadLine();

        }

    }


    class User

    {

        public int Id { get; set; }


        public string Name { get; set; }


        [JsonConverter(typeof(JsonStringEnumConverter))] //解决枚举序列化时被转成数值的问题

        public Gender Gender { get; set; }


        public string Email { get; set; }


        public DateTime CreatedTime { get; set; }

    }



    enum Gender

    {

        Male,


        FaleMale

    }



    public class DateTimeConverter : JsonConverter<DateTime>

    {

        public override DateTime Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)

        {

            return DateTime.Parse(reader.GetString());

        }


        public override void Write(Utf8JsonWriter writer, DateTime value, JsonSerializerOptions options)

        {

            writer.WriteStringValue(value.ToString("yyyy-MM-dd HH:mm:ss"));

        }

    }


    public class DateTimeNullableConverter : JsonConverter<DateTime?>

    {

        public override DateTime? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)

        {

            return string.IsNullOrEmpty(reader.GetString()) ? default(DateTime?) : DateTime.Parse(reader.GetString());

        }


        public override void Write(Utf8JsonWriter writer, DateTime? value, JsonSerializerOptions options)

        {

            writer.WriteStringValue(value?.ToString("yyyy-MM-dd HH:mm:ss"));

        }

    }

    

}

复制代码


Asp.Net Core中完成拒绝访问功能

很多时候如果用户没有某个菜单的操作权限的话在页面是不应该显示出来的。


复制代码

@if (SignInManager.IsSignedIn(User) && User.IsInRole("Admin"))

{

    <li class="nav-item dropdown">

        <a class="nav-link dropdown-toggle" href="#" id="navbarDropdownMenuLink"

           data-toggle="dropdown" aria-haspopup="true" aria-expanded="false">

            管理

        </a>

        <div class="dropdown-menu" aria-labelledby="navbarDropdownMenuLink">

            <a class="dropdown-item" asp-controller="Admin"

               asp-action="ListUsers">用户列表</a>

            <a class="dropdown-item" asp-controller="Admin"

               asp-action="ListRoles">角色列表</a>

        </div>

    </li>

}

复制代码

  如果通过Url来访问时,默认会跳转到一个Account/AccessDenied的拒绝页面,所以我们需要在Account控制器中定义一个AccessDenied方法,添加其视图。


复制代码

[HttpGet]

[AllowAnonymous]

public IActionResult AccessDenied()

{

    return View();

}

复制代码

<div class="text-center">

    <h1 class="text-danger">拒绝访问</h1>

    <h6 class="text-danger">您没有查看此资源的权限</h6>

    <img src="~/images/noaccess.png" style="height:300px; width:300px" />

</div>

  当然我们可以自定义拒绝跳转页面,那就是在startup中添加  options.AccessDeniedPath ,如下:


复制代码

services.ConfigureApplicationCookie(options =>

{

   options.AccessDeniedPath = "/Identity/Account/AccessDenied";

   //options.Cookie.Name = "YourAppCookieName";

    options.Cookie.HttpOnly = true;

    options.ExpireTimeSpan = TimeSpan.FromMinutes(60);

   //options.LoginPath = "/Identity/Account/Login";

    // ReturnUrlParameter requires 

    //using Microsoft.AspNetCore.Authentication.Cookies;

    options.ReturnUrlParameter = CookieAuthenticationDefaults.ReturnUrlParameter;

    options.SlidingExpiration = true;

});

复制代码


.Net Core利用反射动态加载DLL类库的方法

Sublevel sublevel = new Sublevel();

            //获取类型

            Type sublevelType = sublevel.GetType();

            //获取类型的方法列表

            //BindingFlags.NonPublic | BindingFlags.Instance | BindingFlags.Public 这个有一个注意点

            //实际上至少要有Instance(或Static)与Public(或NonPublic)标记。否则将不会获取任何方法。

            MethodInfo[] obj = sublevelType.GetMethods(BindingFlags.NonPublic | BindingFlags.Instance | BindingFlags.Public);

            //遍历所有的方法

            foreach (MethodInfo item in obj)

            {

                //返回方法的返回类型

                Console.Write(item.ReturnType.Name);

                //返回方法的名称

                Console.Write(" "+item.Name+"(");

                //获取方法的返回参数列表

                ParameterInfo[] parameters = item.GetParameters();

                foreach (var parameter in parameters)

                {

                    //参数类型名称

                    Console.Write(parameter.ParameterType.Name);

                    //参数名称

                    Console.Write(" "+parameter.Name+",");

                }

                Console.WriteLine(")");

                //过滤没用方法

                //1:查看是不是有参数的方法

                //2:查看这个方法的返回类型是不是我们想要的

                //3:查看这个方法的返回类型是不是我们想要的

                if (parameters.Any() && 

                    parameters[0].ParameterType == typeof(int) &&

                    item.ReturnType != typeof(void))

                {

                    //调用方法

                    object[] parametersObj = new object[] { 5 };

                    //调用实例方法

                    //第一个参数是我们的实体,后面是我们的参数(参数是一个数组,多个参数按照顺序来传递,没有参数可以为null)

                    //如果我们的方法是一个静态方法 ,这个参数可以为null (不是静态的就会报错)

                    Console.WriteLine(item.Invoke(model, parametersObj));

                }


               

            }

在.Net Framework时代,生成类库只需将类库项目编译好,然后拷贝到其他项目,即可引用或动态加载,相对来说,比较简单。但到了.Net Core时代,动态加载第三方类库,则稍微麻烦一些。

需要在该类库项目的.csproj文件中,在<PropertyGroup></PropertyGroup>标签中加入<EnableDynamicLoading>true</EnableDynamicLoading>标志,该属性将告诉编译器,该项目是动态加载的组件


NET Core3.0 反射异常“System.IO.FileNotFoundException:“Could not load file or assembly

原因为项目的.csproj文件 “<PlatformTarget>x86</PlatformTarget>”,删除这行标签或修改为“Any CPU”即可。



其中Context_Resolving(),是用于加载类库文件过程中,处理触发加载相关依赖文件的事件的方法,通过上述代码,可以保证将类库的完整地动态加载进程序,并且不会与其他动态加载类库项目发生程序集冲突的问题:比如A类库和B类库都有共同的依赖C,但两者的引用的C版本不同,通过AssemblyLoadContext可以保证A/B类库加载自己需要的版本。


using System;

using System.Collections.Generic;

using System.IO;

using System.Reflection;

using System.Runtime.Loader;

 

namespace LoadDLL

{

     /// <summary>

     /// 程序集加载器

     /// </summary>

     public class AssemblyLoader

     {

         private string _basePath;

         private AssemblyLoadContext context;

 

 

         public AssemblyLoader(string basePath)

         {

             _basePath = basePath;

         }

 

         public Type Load(string dllFileName, string typeName)

         {

                 context = new AssemblyLoadContext(dllFileName);

                 context.Resolving += Context_Resolving;

                 //需要绝对路径

                 string path = Path.Combine(_basePath, dllFileName);

                 if (File.Exists(path))

                 {

                     try

                     {

                         using (var stream = File.OpenRead(path))

                         {

                             Assembly assembly = context.LoadFromStream(stream);

                             Type type = assembly.GetType(typeName);

                             dicTypes.Add(typeName, type);

                             return type;

                         }

                     }

                     catch (Exception ex)

                     {

                         Console.WriteLine($"加载节点{dllFileName}-{typeName}发生异常:{ex.Message},{ex.StackTrace}");

                     }

 

                 }

                 else

                 {

                     Console.WriteLine($"节点动态库{dllFileName}不存在:{path}");

                 }            

             return null;

         }

 

         /// <summary>

         /// 加载依赖文件

         /// </summary>

         /// <param name="context"></param>

         /// <param name="assemblyName"></param>

         /// <returns></returns>

         private Assembly Context_Resolving(AssemblyLoadContext context, AssemblyName assemblyName)

         {

             string expectedPath = Path.Combine(_basePath, assemblyName.Name + ".dll"); ;

             if (File.Exists(expectedPath))

             {

                 try

                 {

                     using (var stream = File.OpenRead(expectedPath))

                     {

                         return context.LoadFromStream(stream);

                     }

                 }

                 catch (Exception ex)

                 {

                     Console.WriteLine($"加载节点{expectedPath}发生异常:{ex.Message},{ex.StackTrace}");

                 }

             }

             else

             {

                 Console.WriteLine($"依赖文件不存在:{expectedPath}");

             }

             return null;

         }

     }

 }



记一次Docker中部署Asp.Net Core 3.0的踩坑过程

最近公司打算重构目前直销报单系统到微信小程序中,目前的系统只能在PC上面使用,这两年也搞过App端,但是由于人员流动和公司架构调整最后都不了了之,只留下一堆写了一半的接口。以前的接口依然是使用Asp.Net Framework实现的,而.Net的处境也很窘迫,很多.Neter也都转想Java或Pythod的怀抱了,自己也在学Java,但作为一个工作了6年的.Neter,要放弃.Net实在也有些不甘,或者说在转java的过程中对未来的不确定性让自己也犹豫了好久,直到.Net Core发展到3.0,我在部门对.Net Core进行了分享,这一次的接口重构也落到我的手上,那么无可厚非,我建议使用.Net Core来实现,从架构搭建到实现,我将尽力去做好它。


  周五的时候,架构搭的七七八八,实现了登录功能后,在我将其部署到开发服务器上时,在我从官网中(https://dotnet.microsoft.com/download/dotnet-core/3.0)下载完运行环境,在服务器上安装完IIS的捆包运行包,将项目部署到IIS上之后,项目正常运行,过了会运维同事告知其他原先的.Net FrameWork站点应用程序池全部停止了,重启后只要一访问就自动停止,无法访问,随后他即卸载了我刚安装的运行环境,原先的站点也能访问了,目前我也还没找出是什么原因导致的,在我自己的电脑上是完全没有问题的,不知道是什么环境导致这个问题,如在你看到这篇文章的时候知道这个情况,烦请告知一二,谢谢。


  好吧,我先不折腾IIS上的问题,决定将其部署到Docker好了。


  于是就开始了这次的踩坑历程了。


  首先我在项目中添加了Docker支持,VS2019自动在项目文件夹下生成了Dockerfile文件,如下


复制代码

FROM mcr.microsoft.com/dotnet/core/aspnet:3.0-buster-slim AS base

WORKDIR /app

EXPOSE 80


FROM mcr.microsoft.com/dotnet/core/sdk:3.0-buster AS build

WORKDIR /src

COPY ["src/H.Cms.Web.Api/H.Cms.Web.Api.csproj", "src/H.Cms.Web.Api/"]

COPY ["src/H.Cms.Infrastructure/H.Cms.Infrastructure.csproj", "src/H.Cms.Infrastructure/"]

COPY ["src/H.Cms.Core/H.Cms.Core.csproj", "src/H.Cms.Core/"]

COPY ["src/H.Cms.Application/H.Cms.Application.csproj", "src/H.Cms.Application/"]

COPY ["src/H.Cms.Web.Core/H.Cms.Web.Core.csproj", "src/H.Cms.Web.Core/"]

RUN dotnet restore "src/H.Cms.Web.Api/H.Cms.Web.Api.csproj"

COPY . .

WORKDIR "/src/src/H.Cms.Web.Api"

RUN dotnet build "H.Cms.Web.Api.csproj" -c Release -o /app/build


FROM build AS publish

RUN dotnet publish "H.Cms.Web.Api.csproj" -c Release -o /app/publish


FROM base AS final

WORKDIR /app

COPY --from=publish /app/publish .

ENTRYPOINT ["dotnet", "H.Cms.Web.Api.dll"]

复制代码

  接下来我将整个项目上传到我的虚拟机centos 7上,开始使用下面的pscp命令时,坑来了,文件夹名称不能有空格。


pscp -r F:/aspnet core/hcms root@192.168.143.122:/usr/soft

  好吧,无奈换成Secure Shell Client来上传,完成后,我使用putty登录我的虚拟机centos7,进入/root/source/HCms项目中,进入H.Cms.Web.Api目录中,使用docker build构建镜像时,提示错误了


docker build -t docker build -t jesen/hcms:1.0 .

  查看了下Dockerfile文件,好吧,Dockerfile文件从src开始,我把它拷贝到src同目录中,再次运行docker build ,这下果然,正常运行了,开始docker中不存在.netcore 3.0的环境,需要先下载安装,但是过程很缓慢,于是我去睡了个午觉,醒来后果然下好了,但是到了dotnet restore命令的时候,一直报api.nuget.org访问不了,天了噜,我从浏览器上直接访问没问题啊。这个问题花了我好久,最终我想说那我不在docker中restore了吧,我把项目发布出来在直接在docker中发布好吧,于是在VS中我选择了linux-x64的平台发布,欸,问题来了,也是报api.nuget.org访问不了,但这个时候有个ip了23.101.10.141,在 https://site.ip138.com中查询了一下,香港的ip,我想,最近香港暴力事件很严重,不是影响了吧,抱着试一试的心态,我通过修改host文件 ,添加了指向新加坡的IP,刷新DNS解析


104.215.155.1 api.nuget.org

ipconfig /flushdns

  重新在VS中发布,天了噜,真的发布成功了,好吧,那我就把虚拟机的/etc/hosts文件也添加这个IP解析吧,果不其然,在我再次运行docker build的时候,虽然等了一会,最后成功了,真的成功了。


  docker build 构建好镜像后,接下来就是要运行镜像,看看是否能正常在docker中运行了,运行docker run


docker run -d -p 8002:80 -v ~/docker/aspnetcore/hcms/conf/appsettings.json:/app/appsettings.json -name hcms jesen/hcms:v1.0

  开始我将挂载的appsettings.json文件写成了文件夹,报错了,没有注意到是这个问题,然后就用docker exec命令去查看docker容器里的hcms,发现没问题


docker exec -it hcms sh

  回来找完才知道是文件不存在,好吧,将~/docker/aspnetcore/hcms/conf/appsettings.json/目录删掉,新建appsettings.json文件,将配置信息也拷贝到该文件上,修改数据库链接IP地址,在主机上使用虚拟机的IP地址进行访问,页面正常加载。


1.png


 


   但是当我访问某个API时,报错了,我想应该是数据库链接IP访问不到,于是看了主机的防火墙,开着的,好吧,加了入站规则,放开1433端口,再次访问,正常了。

1.png



 


 


  以上就是将Asp.Net Core 3.0部署到docker上的过程,虽然花了一些时间,但也总算完美运行起来了,这让我在接下来的项目中勇敢的提倡使用.Net Core来开发增加了信心,加油!




Asp.Net Core Identity中基于角色授权

我们已经在之前介绍了简单的授权是在Controller或Action上添加属性Authorize来实现,那角色授权是在指定Authorize的同时指定Roles参数。


  我们来看看基于角色访问的三种方式:


  1、指定只有角色为Admin的用户才能访问 


[Authorize(Roles ="Admin") ]

  2、指定角色为Admin或User的用户才能问问


[Authorize(Roles ="Admin,User") ]

  3、同时拥有Admin角色和User角色才能访问


[Authorize(Roles ="Admin") ]

[Authorize(Roles ="User") ]

1.png



Asp.Net Core中的角色

在前面介绍中我们知道了Asp.Net Core Identity中创建用户使用到的类UserManager<IdentityUser>,同样的,创建角色我们需要使用RoleManager<IdentityRole>。


  接下来我们就来看看如何创建角色。


  先定义ViewModel


复制代码

namespace StudentManagement.ViewModels

{

    public class CreateRoleViewModel

    {

        [Required]

        [Display(Name = "角色")]

        public string RoleName { get; set; }

    }

}

复制代码

  创建AdminController


复制代码

namespace StudentManagement.Controllers

{

    public class AdminController : Controller

    {

       private readonly RoleManager<IdentityRole> roleManager;

    private readonly UserManager<ApplicationUser> userManager;

       public AdminController(RoleManager<IdentityRole> roleManager, UserManager<ApplicationUser> userManager)

        {

            this.roleManager = roleManager;

       this.userManager = userManager;


        }


        [HttpGet]

        public IActionResult CreateRole()

        {

            return View();

        }


        [HttpPost]

        public async Task<IActionResult> CreateRole(CreateRoleViewModel model)

        {

            if (ModelState.IsValid)

            {

                //我们只需要指定一个不重复的角色名称来创建新角色


                IdentityRole identityRole = new IdentityRole

                {

                    Name = model.RoleName

                };


                //将角色保存在AspNetRoles表中


                IdentityResult result = await roleManager.CreateAsync(identityRole);


                if (result.Succeeded)

                {

                    return RedirectToAction("ListRoles", "Admin");

                }


                foreach (IdentityError error in result.Errors)

                {

                    ModelState.AddModelError("", error.Description);

                }

            }


            return View(model);

        }

}

复制代码

  创建视图


复制代码

@model  CreateRoleViewModel


@{

    ViewBag.Title = "创建新角色";

}


<form asp-action="CreateRole" method="post" class="mt-3">

    <div asp-validation-summary="All" class="text-danger">

    </div>

    <div class="form-group row">

        <label asp-for="RoleName" class="col-sm-2 col-form-label"></label>

        <div class="col-sm-10">

            <input asp-for="RoleName" class="form-control" placeholder="Name">

            <span asp-validation-for="RoleName" class="text-danger"></span>

        </div>

    </div>


    <div class="form-group row">

        <div class="col-sm-10">

            <button type="submit" class="btn btn-primary" style="width:auto">

               创建角色

            </button>

        </div>

    </div>

</form>

复制代码

  创建完角色,我们需要显示它,在AdminController中添加下面的ListRoles方法


复制代码

[HttpGet]

public IActionResult ListRoles()

{

    var roles = roleManager.Roles;

    return View(roles);

}

复制代码

  新建ListRoles视图,既然有列表,那么就应该存在编辑和删除角色的功能


复制代码

@model IEnumerable<IdentityRole>


@{

    ViewBag.Title = "角色列表";

}


<h1>所有角色列表</h1>


@if (Model.Any())

{

    <a class="btn btn-primary mb-3" style="width:auto" asp-action="CreateRole"

       asp-controller="admin">添加新角色</a>


    foreach (var role in Model)

    {

        <div class="card mb-3">

            <div class="card-header">

                角色Id : @role.Id

            </div>

            <div class="card-body">

                <h5 class="card-title">@role.Name</h5>

            </div>

            <div class="card-footer">


                <form method="post" asp-action="DeleteUser" asp-route-id="@role.Id">

                    <a asp-controller="Admin" asp-action="EditRole"

                       asp-route-id="@role.Id" class="btn btn-primary">

                        编辑

                    </a>


                    <span id="confirmDeleteSpan_@role.Id" style="display:none">

                        <span>你确定你要删除?</span>

                        <button type="submit" class="btn btn-danger">是</button>

                        <a href="#" class="btn btn-primary"

                           onclick="confirmDelete('@role.Id', false)">否</a>

                    </span>


                    <span id="deleteSpan_@role.Id">

                        <a href="#" class="btn btn-danger"

                           onclick="confirmDelete('@role.Id', true)">删除</a>

                    </span>

                </form>



            </div>

        </div>

    }

}

else

{

    <div class="card">

        <div class="card-header">

            尚未创建任何角色

        </div>

        <div class="card-body">

            <h5 class="card-title">

                点击下面的按钮创建角色

            </h5>

            <a class="btn btn-primary" style="width:auto"

               asp-controller="admin" asp-action="CreateRole">

                创建角色

            </a>

        </div>

    </div>

}

复制代码

复制代码

[HttpPost]

public async Task<IActionResult> DeleteRole(string id)

{

    var role = await roleManager.FindByIdAsync(id);


    if (role == null)

    {

        ViewBag.ErrorMessage = $"无法找到ID为{id}的角色信息";

        return View("NotFound");

    }

    else

    {

        var result = await roleManager.DeleteAsync(role);


        if (result.Succeeded)

        {

            return RedirectToAction("ListRoles");

        }


        foreach (var error in result.Errors)

        {

            ModelState.AddModelError("", error.Description);

        }


        return View("ListRoles");

    }

}

[HttpGet]

public async Task<IActionResult> EditRole(string id)

{

    //通过角色ID查找角色

    var role = await roleManager.FindByIdAsync(id);


    if (role == null)

    {

        ViewBag.ErrorMessage = $"角色Id={id}的信息不存在,请重试。";

        return View("NotFound");

    }


    var model = new EditRoleViewModel

    {

        Id = role.Id,

        RoleName = role.Name

    };


    // 查询所有的用户

    foreach (var user in userManager.Users)

    {

        //如果用户拥有此角色,请将用户名添加到

        // EditRoleViewModel模型中的Users属性中

        //然后将对象传递给视图显示到客户端

        if (await userManager.IsInRoleAsync(user, role.Name))

        {

            model.Users.Add(user.UserName);

        }

    }


    return View(model);

}


//此操作方法用于响应HttpPost的请求并接收EditRoleViewModel模型数据

[HttpPost]

public async Task<IActionResult> EditRole(EditRoleViewModel model)

{

    var role = await roleManager.FindByIdAsync(model.Id);


    if (role == null)

    {

        ViewBag.ErrorMessage = $"角色Id={model.Id}的信息不存在,请重试。";

        return View("NotFound");

    }

    else

    {

        role.Name = model.RoleName;


        //使用UpdateAsync更新角色

        var result = await roleManager.UpdateAsync(role);


        if (result.Succeeded)

        {

            return RedirectToAction("ListRoles");

        }


        foreach (var error in result.Errors)

        {

            ModelState.AddModelError("", error.Description);

        }


        return View(model);

    }

}

复制代码

   创建视图


复制代码

@model EditRoleViewModel


@{

    ViewBag.Title = "编辑角色";

}


    <h1>编辑角色</h1>


<form method="post" class="mt-3">

    <div class="form-group row">

        <label asp-for="Id" class="col-sm-2 col-form-label"></label>

        <div class="col-sm-10">

            <input asp-for="Id" disabled class="form-control">

        </div>

    </div>

    <div class="form-group row">

        <label asp-for="RoleName" class="col-sm-2 col-form-label"></label>

        <div class="col-sm-10">

            <input asp-for="RoleName" class="form-control">

            <span asp-validation-for="RoleName" class="text-danger"></span>

        </div>

    </div>


    <div asp-validation-summary="All" class="text-danger"></div>


    <div class="form-group row">

        <div class="col-sm-10">

            <button type="submit" class="btn btn-primary">更新</button>

            <a asp-action="ListRoles" class="btn btn-primary">取消</a>

        </div>

    </div>


    <div class="card">

        <div class="card-header">

            <h3>该角色中的用户</h3>

        </div>

        <div class="card-body">

            @if (Model.Users.Any())

            {

                foreach (var user in Model.Users)

                {

                    <h5 class="card-title">@user</h5>

                }

            }

            else

            {

                <h5 class="card-title">目前没有信息</h5>

            }

        </div>

        <div class="card-footer">

            <a asp-controller="Admin" asp-action="EditUsersInRole"

               asp-route-roleId="@Model.Id" class="btn btn-primary">

                添加或删除用户到角色中

            </a>

        </div>

    </div>

</form>

复制代码

  有了角色之后,我们需要为角色添加用户,所以先创建 EditRoleViewModel 和 UserRoleViewModel。


复制代码

public class EditRoleViewModel

{

    public EditRoleViewModel()

    {

        Users = new List<string>();

    }


    [Display(Name = "角色Id")]

    public string Id { get; set; }


    [Required(ErrorMessage = "角色名称是必填的")]

    [Display(Name ="角色名称")]

    public string RoleName { get; set; }


    public List<string> Users { get; set; }

}

复制代码

复制代码

public class UserRoleViewModel

{

    public string UserId { get; set; }

    public string UserName { get; set; }

    public bool IsSelected { get; set; }

}

复制代码

复制代码

@model List<UserRoleViewModel>


@{

    var roleId = ViewBag.roleId;

}


<form method="post">

    <div class="card">

        <div class="card-header">

            <h2>在此角色中添加或删除用户</h2>

        </div>

        <div class="card-body">

            @for (int i = 0; i < Model.Count; i++)

            {

                <div class="form-check m-1">

                    <input type="hidden" asp-for="@Model[i].UserId" />

                    <input type="hidden" asp-for="@Model[i].UserName" />

                    <input asp-for="@Model[i].IsSelected" class="form-check-input" />

                    <label class="form-check-label" asp-for="@Model[i].IsSelected">

                        @Model[i].UserName

                    </label>

                </div>

            }

        </div>

        <div class="card-footer">

            <input type="submit" value="更新" class="btn btn-primary"

                   style="width:auto" />

            <a asp-action="EditRole" asp-route-id="@roleId"

               class="btn btn-primary" style="width:auto">取消</a>

        </div>

    </div>

</form>

复制代码

  创建控制器的方法


复制代码

[HttpGet]

public async Task<IActionResult> EditUsersInRole(string roleId)

{

    ViewBag.roleId = roleId;


    var role = await roleManager.FindByIdAsync(roleId);


    if (role == null)

    {

        ViewBag.ErrorMessage = $"角色Id={roleId}的信息不存在,请重试。";

        return View("NotFound");

    }


    var model = new List<UserRoleViewModel>();


    foreach (var user in userManager.Users)

    {

        var userRoleViewModel = new UserRoleViewModel

        {

            UserId = user.Id,

            UserName = user.UserName

        };


        if (await userManager.IsInRoleAsync(user, role.Name))

        {

            userRoleViewModel.IsSelected = true;

        }

        else

        {

            userRoleViewModel.IsSelected = false;

        }


        model.Add(userRoleViewModel);

    }


    return View(model);

}



[HttpPost]

public async Task<IActionResult> EditUsersInRole(List<UserRoleViewModel> model, string roleId)

{

    var role = await roleManager.FindByIdAsync(roleId);


    if (role == null)

    {

        ViewBag.ErrorMessage = $"角色Id={roleId}的信息不存在,请重试。";

        return View("NotFound");

    }


    for (int i = 0; i < model.Count; i++)

    {

        var user = await userManager.FindByIdAsync(model[i].UserId);


        IdentityResult result = null;


        if (model[i].IsSelected && !(await userManager.IsInRoleAsync(user, role.Name)))

        {

            result = await userManager.AddToRoleAsync(user, role.Name);

        }

        else if (!model[i].IsSelected && await userManager.IsInRoleAsync(user, role.Name))

        {

            result = await userManager.RemoveFromRoleAsync(user, role.Name);

        }

        else

        {

            continue;

        }


        if (result.Succeeded)

        {

            if (i < (model.Count - 1))

                continue;

            else

                return RedirectToAction("EditRole", new { Id = roleId });

        }

    }


    return RedirectToAction("EditRole", new { Id = roleId });

}


扩展Asp.Net Core中的IdentityUser类

虽然Asp.Net Core.Identity提供了IdentityUser类,但是在有些情况下我们需要一些额外的用户信息,比如性别,年龄等,这时候就需要来扩展IdentityUser类以达到我们的需求。


复制代码

namespace Microsoft.AspNetCore.Identity

{

    //

    // 摘要:

    //     Represents a user in the identity system

    //

    // 类型参数:

    //   TKey:

    //     The type used for the primary key for the user.

    public class IdentityUser<TKey> where TKey : IEquatable<TKey>

    {

        //

        // 摘要:

        //     Initializes a new instance of Microsoft.AspNetCore.Identity.IdentityUser`1.

        public IdentityUser();

        //

        // 摘要:

        //     Initializes a new instance of Microsoft.AspNetCore.Identity.IdentityUser`1.

        //

        // 参数:

        //   userName:

        //     The user name.

        public IdentityUser(string userName);


        //

        // 摘要:

        //     Gets or sets the date and time, in UTC, when any user lockout ends.

        //

        // 言论:

        //     A value in the past means the user is not locked out.

        public virtual DateTimeOffset? LockoutEnd { get; set; }

        //

        // 摘要:

        //     Gets or sets a flag indicating if two factor authentication is enabled for this

        //     user.

        [PersonalData]

        public virtual bool TwoFactorEnabled { get; set; }

        //

        // 摘要:

        //     Gets or sets a flag indicating if a user has confirmed their telephone address.

        [PersonalData]

        public virtual bool PhoneNumberConfirmed { get; set; }

        //

        // 摘要:

        //     Gets or sets a telephone number for the user.

        [ProtectedPersonalData]

        public virtual string PhoneNumber { get; set; }

        //

        // 摘要:

        //     A random value that must change whenever a user is persisted to the store

        public virtual string ConcurrencyStamp { get; set; }

        //

        // 摘要:

        //     A random value that must change whenever a users credentials change (password

        //     changed, login removed)

        public virtual string SecurityStamp { get; set; }

        //

        // 摘要:

        //     Gets or sets a salted and hashed representation of the password for this user.

        public virtual string PasswordHash { get; set; }

        //

        // 摘要:

        //     Gets or sets a flag indicating if a user has confirmed their email address.

        [PersonalData]

        public virtual bool EmailConfirmed { get; set; }

        //

        // 摘要:

        //     Gets or sets the normalized email address for this user.

        public virtual string NormalizedEmail { get; set; }

        //

        // 摘要:

        //     Gets or sets the email address for this user.

        [ProtectedPersonalData]

        public virtual string Email { get; set; }

        //

        // 摘要:

        //     Gets or sets the normalized user name for this user.

        public virtual string NormalizedUserName { get; set; }

        //

        // 摘要:

        //     Gets or sets the user name for this user.

        [ProtectedPersonalData]

        public virtual string UserName { get; set; }

        //

        // 摘要:

        //     Gets or sets the primary key for this user.

        [PersonalData]

        public virtual TKey Id { get; set; }

        //

        // 摘要:

        //     Gets or sets a flag indicating if the user could be locked out.

        public virtual bool LockoutEnabled { get; set; }

        //

        // 摘要:

        //     Gets or sets the number of failed login attempts for the current user.

        public virtual int AccessFailedCount { get; set; }


        //

        // 摘要:

        //     Returns the username for this user.

        public override string ToString();

    }

}

复制代码

  接下来我们就来定义一个继承自IdentityUser类的ApplicationUser类。


public class ApplicationUser : IdentityUser

{

    public string City { get; set; }

}

  然后在AppDbContext中继承泛型 IdentityDbContext<ApplicationUser>类。并且在Startup中注入的时候把IdentityUser修改为ApplicationUser后,再做数据库迁移,如果之前已经迁移过,则需要替换项目中所有的IdentityUser为ApplicationUser。


复制代码

public class AppDbContext:IdentityDbContext<ApplicationUser>

{


    public AppDbContext(DbContextOptions<AppDbContext> options):base(options)

    {

    }


    public DbSet<Student> Students { get; set; }


    protected override void OnModelCreating(ModelBuilder modelBuilder)

    {

        base.OnModelCreating(modelBuilder);

        modelBuilder.Seed();

    }

}

复制代码

services.AddIdentity<ApplicationUser, IdentityRole>()

    .AddErrorDescriber<CustomIdentityErrorDescriber>()

    .AddEntityFrameworkStores<AppDbContext>();


Asp.Net Core Identity 完成注册登录

Identity是Asp.Net Core全新的一个用户管理系统,它是一个完善的全面的庞大的框架,提供的功能有:


创建、查询、更改、删除账户信息


验证和授权


密码重置


双重身份认证


支持扩展登录,如微软、Facebook、google、QQ、微信等


提供了一个丰富的API,并且这些API还可以进行大量的扩展


    接下来我们先来看下它的简单使用。首先在我们的DbContext中需要继承自IdentityDbContext。


复制代码

    public class AppDbContext:IdentityDbContext

    {


        public AppDbContext(DbContextOptions<AppDbContext> options):base(options)

        {

        }


        public DbSet<Student> Students { get; set; }

        protected override void OnModelCreating(ModelBuilder modelBuilder)

        {

            base.OnModelCreating(modelBuilder);

            modelBuilder.Seed();

        }

    }

复制代码

    然后在Startup中注入其依赖,IdentityUser和IdentityRole是Identity框架自带的两个类,将其绑定到我们定义的AppDbContext中。


services.AddIdentity<IdentityUser, IdentityRole>()

    .AddEntityFrameworkStores<AppDbContext>();

    最后需要添加中间件UseAuthentication。


复制代码

public void Configure(IApplicationBuilder app, IHostingEnvironment env)

{

    //如果环境是Development,调用 Developer Exception Page

    if (env.IsDevelopment())

    {

        app.UseDeveloperExceptionPage();

    }

    else

    {

        app.UseExceptionHandler("/Error");

    app.UseStatusCodePages();

    app.UseStatusCodePagesWithReExecute("/Error/{0}");

    }

    app.UseStaticFiles();


    app.UseAuthentication();


    app.UseMvc(routes =>

    {

        routes.MapRoute("default", "{controller=Home}/{action=Index}/{id?}");

    });

}

复制代码

    接下来我们就可以使用数据库迁移 Add-Migration 来添加迁移,然后update-database我们的数据库。


    可以在数据库中看到其生成的表。


1.png


 


 


 


    在完成数据迁移之后,我们再来看下Identity中如何完成用户的注册和登录。


    我们定义一个ViewModel,然后定义一个AccountController来完成我们的注册和登录功能。Asp.Net Core Identity为我们提供了UserManger来对用户进行增删改等操作,提供了SignInManager的SignInAsync来登录,SignOutAsync来退出,IsSignedIn来判断用户是否已登录等。


复制代码

public class RegisterViewModel

{

    [Required]

    [Display(Name = "邮箱地址")]

    [EmailAddress]

    public string Email { get; set; }


    [Required]

    [Display(Name = "密码")]

    [DataType(DataType.Password)]

    public string Password { get; set; }


    [DataType(DataType.Password)]

    [Display(Name = "确认密码")]

    [Compare("Password",

        ErrorMessage = "密码与确认密码不一致,请重新输入.")]

    public string ConfirmPassword { get; set; }

}

public class LoginViewModel

{


    [Required]

    [EmailAddress]

    public string Email { get; set; }


    [Required]

    [DataType(DataType.Password)]

    public string Password { get; set; }


    [Display(Name = "记住我")]

    public bool RememberMe { get; set; }

}

复制代码

复制代码

using Microsoft.AspNetCore.Authorization;

using Microsoft.AspNetCore.Identity;

using Microsoft.AspNetCore.Mvc;

using StudentManagement.ViewModels;

using System.Threading.Tasks;


namespace StudentManagement.Controllers

{

    public class AccountController:Controller

    {

        private UserManager<IdentityUser> userManager;

        private SignInManager<IdentityUser> signInManager;


        public AccountController(UserManager<IdentityUser> userManager,

           SignInManager<IdentityUser> signInManager)

        {

            this.userManager = userManager;

            this.signInManager = signInManager;

        }


        [HttpGet]

        public IActionResult Register()

        {

            return View();

        }


        [HttpPost]

        public async Task<IActionResult> Register(RegisterViewModel model)

        {

            if (ModelState.IsValid)

            {

                //将数据从RegisterViewModel复制到IdentityUser

                var user = new IdentityUser

                {

                    UserName = model.Email,

                    Email = model.Email

                };


                //将用户数据存储在AspNetUsers数据库表中

                var result = await userManager.CreateAsync(user, model.Password);

            

                //如果成功创建用户,则使用登录服务登录用户信息

                //并重定向到home econtroller的索引操作

                if (result.Succeeded)

                {

                    await signInManager.SignInAsync(user, isPersistent: false);

                    return RedirectToAction("index", "home");

                }


                //如果有任何错误,将它们添加到ModelState对象中

                //将由验证摘要标记助手显示到视图中

                foreach (var error in result.Errors)

                {

                    if (error.Code== "PasswordRequiresUpper")

                    {

                        error.Description = "密码必须至少有一个大写字母('A'-'Z')。";

                    }


                    //PasswordRequiresUpper

                    //Passwords must have at least one uppercase ('A'-'Z').

                    ModelState.AddModelError(string.Empty, error.Description);

                }

            }

            return View(model);

        }


        [HttpPost]

        public async Task<IActionResult> Logout()

        {

            await signInManager.SignOutAsync();

            return RedirectToAction("index", "home");

        }


        [HttpGet]

        [AllowAnonymous]

        public IActionResult Login()

        {

            return View();

        }


        [HttpPost]

        [AllowAnonymous]

        public async Task<IActionResult> Login(LoginViewModel model, string returnUrl)

        {

            if (ModelState.IsValid)

            {

                var result = await signInManager.PasswordSignInAsync(

                    model.Email, model.Password, model.RememberMe, false);

                if (result.Succeeded)

                {

                    if (!string.IsNullOrEmpty(returnUrl))

                    {

                        return Redirect(returnUrl);

                    }

                    else

                    {

                        return RedirectToAction("index", "home");

                    }

                }

                ModelState.AddModelError(string.Empty, "登录失败,请重试");

            }

            return View(model);

        }

    }

}

复制代码

复制代码

@model RegisterViewModel


@{

    ViewBag.Title = "用户注册";

}


<h1>用户注册</h1>

<div class="row">

    <div class="col-md-12">

        <form method="post">

            <div asp-validation-summary="All" class="text-danger"></div>

            <div class="form-group">

                <label asp-for="Email"></label>

                <input asp-for="Email" class="form-control" />

                <span asp-validation-for="Email" class="text-danger"></span>

            </div>

            <div class="form-group">

                <label asp-for="Password"></label>

                <input asp-for="Password" class="form-control" />

                <span asp-validation-for="Password" class="text-danger"></span>

            </div>

            <div class="form-group">

                <label asp-for="ConfirmPassword"></label>

                <input asp-for="ConfirmPassword" class="form-control" />

                <span asp-validation-for="ConfirmPassword" class="text-danger"></span>

            </div>

            <button type="submit" class="btn btn-primary">注册</button>

        </form>

    </div>

</div>

复制代码

复制代码

@model LoginViewModel


@{

    ViewBag.Title = "用户登录";

}


<h1>用户登录</h1>


<div class="row">

    <div class="col-md-12">

        <form method="post">

            <div asp-validation-summary="All" class="text-danger"></div>

            <div class="form-group">

                <label asp-for="Email"></label>

                <input asp-for="Email" class="form-control" />

                <span asp-validation-for="Email" class="text-danger"></span>

            </div>

            <div class="form-group">

                <label asp-for="Password"></label>

                <input asp-for="Password" class="form-control" />

                <span asp-validation-for="Password" class="text-danger"></span>

            </div>

            <div class="form-group">

                <div class="checkbox">

                    <label asp-for="RememberMe">

                        <input asp-for="RememberMe" />

                        @Html.DisplayNameFor(m => m.RememberMe)

                    </label>

                </div>

            </div>

            <button type="submit" class="btn btn-primary">登录</button>

        </form>

    </div>

</div>

复制代码

    在实际工作中,我们需要配置密码的复杂度来增强用户信息的安全性。而Asp.Net Core Identity也默认也提供了一套机制PasswordOptions,可以查看其源码。


https://github.com/aspnet/AspNetCore/blob/master/src/Identity/Extensions.Core/src/PasswordOptions.cs


    但是有时候我们需要自定义我们的密码校验模式,这时候可以在Startup中注入


复制代码

services.Configure<IdentityOptions>(options =>

{

    options.Password.RequiredLength = 6;

    options.Password.RequiredUniqueChars = 3;

    options.Password.RequireUppercase = false;

    options.Password.RequireLowercase = false;

    options.Password.RequireNonAlphanumeric = false;

});

复制代码

    同时,我们希望我们在注册的时候提示错误信息时使用中文显示,可以定义一个继承IdentityErrorDescriber的类。


复制代码

using Microsoft.AspNetCore.Identity;


namespace StudentManagement.Middleware

{

    public class CustomIdentityErrorDescriber : IdentityErrorDescriber

    {

        public override IdentityError DefaultError()

        {

            return new IdentityError { Code = nameof(DefaultError), Description = $"发生了未知的故障。" };

        }


        public override IdentityError ConcurrencyFailure()

        {

            return new IdentityError { Code = nameof(ConcurrencyFailure), Description = "乐观并发失败,对象已被修改。" };

        }


        public override IdentityError PasswordMismatch()

        {

            return new IdentityError { Code = nameof(PasswordMismatch), Description = "密码错误" };

        }


        public override IdentityError InvalidToken()

        {

            return new IdentityError { Code = nameof(InvalidToken), Description = "无效的令牌." };

        }


        public override IdentityError LoginAlreadyAssociated()

        {

            return new IdentityError { Code = nameof(LoginAlreadyAssociated), Description = "具有此登录的用户已经存在." };

        }


        public override IdentityError InvalidUserName(string userName)

        {

            return new IdentityError { Code = nameof(InvalidUserName), Description = $"用户名'{userName}'无效,只能包含字母或数字." };

        }


        public override IdentityError InvalidEmail(string email)

        {

            return new IdentityError { Code = nameof(InvalidEmail), Description = $"Email '{email}' is invalid." };

        }


        public override IdentityError DuplicateUserName(string userName)

        {

            return new IdentityError { Code = nameof(DuplicateUserName), Description = $"User Name '{userName}' is already taken." };

        }


        public override IdentityError DuplicateEmail(string email)

        {

            return new IdentityError { Code = nameof(DuplicateEmail), Description = $"Email '{email}' is already taken." };

        }


        public override IdentityError InvalidRoleName(string role)

        {

            return new IdentityError { Code = nameof(InvalidRoleName), Description = $"Role name '{role}' is invalid." };

        }


        public override IdentityError DuplicateRoleName(string role)

        {

            return new IdentityError { Code = nameof(DuplicateRoleName), Description = $"Role name '{role}' is already taken." };

        }


        public override IdentityError UserAlreadyHasPassword()

        {

            return new IdentityError { Code = nameof(UserAlreadyHasPassword), Description = "User already has a password set." };

        }


        public override IdentityError UserLockoutNotEnabled()

        {

            return new IdentityError { Code = nameof(UserLockoutNotEnabled), Description = "Lockout is not enabled for this user." };

        }


        public override IdentityError UserAlreadyInRole(string role)

        {

            return new IdentityError { Code = nameof(UserAlreadyInRole), Description = $"User already in role '{role}'." };

        }


        public override IdentityError UserNotInRole(string role)

        {

            return new IdentityError { Code = nameof(UserNotInRole), Description = $"User is not in role '{role}'." };

        }


        public override IdentityError PasswordTooShort(int length)

        {

            return new IdentityError { Code = nameof(PasswordTooShort), Description = $"密码必须至少是{length}字符." };

        }


        public override IdentityError PasswordRequiresNonAlphanumeric()

        {

            return new IdentityError { Code = nameof(PasswordRequiresNonAlphanumeric), Description = "密码必须至少有一个非字母数字字符."

            };

        }


        public override IdentityError PasswordRequiresDigit()

        {

            return new IdentityError { Code = nameof(PasswordRequiresDigit), Description = $"密码必须至少有一个数字('0'-'9')." };

        }



        public override IdentityError PasswordRequiresUniqueChars(int uniqueChars)

        {

            return new IdentityError { Code = nameof(PasswordRequiresUniqueChars), Description = $"密码必须使用至少不同的{uniqueChars}字符。" };

        }


        public override IdentityError PasswordRequiresLower()

        {

            return new IdentityError { Code = nameof(PasswordRequiresLower), Description = "密码必须至少有一个小写字母('a'-'z')." };

        }


        public override IdentityError PasswordRequiresUpper()

        {

            return new IdentityError { Code = nameof(PasswordRequiresUpper), Description = "密码必须至少有一个大写字母('A'-'Z')." };

        }

    }

}

复制代码

    最后需要在注入Identity的时候添加上这个类


services.AddIdentity<IdentityUser, IdentityRole>()

    .AddErrorDescriber<CustomIdentityErrorDescriber>()

    .AddEntityFrameworkStores<AppDbContext>();

    完成登录后,我们需要对访问资源进行授权,需要在controller或者action上使用Authorize属性来标记,也可以使用AllowAnonymous来允许匿名访问,在项目中使用授权需要引入中间件UseAuthentication


 app.UseAuthentication();

    但是如果项目中有很多controller需要添加Authorize属性,我们可以在startup中添加全局的授权,代码如下。


复制代码

services.AddMvc(config => {

    var policy = new AuthorizationPolicyBuilder()

                    .RequireAuthenticatedUser()

                    .Build();

    config.Filters.Add(new AuthorizeFilter(policy));

});

复制代码

    一般在用户登录成功后需要重定向到原始的 URL,这个通过请求参数中带returnUrl来实现,但是如果没有判断是否本地的Url时则会引发开放式重定向漏洞。


    解决开放式重定向漏洞的方式也很简单,就是在判断的时候添加Url.IsLocalUrl或者直接return LocalRedirect。


if (Url.IsLocalUrl(returnUrl))

{


}

return LocalRedirect(returnUrl);


Asp.Net Core 自定义验证属性

很多时候,在模型上的验证需要自己定义一些特定于我们需求的验证属性。所以这一篇我们就来介绍一下怎么自定义验证属性。


  我们来实现一个验证邮箱域名的自定义验证属性,当然,最重要的是需要定义一个继承自ValidationAttribute的类,然后在实现其IsValid方法。


复制代码

public class ValidEmailDomainAttribute : ValidationAttribute

{

    private readonly string allowedDomain;


    public ValidEmailDomainAttribute(string allowedDomain)

    {

        this.allowedDomain = allowedDomain;

    }


    public override bool IsValid(object value)

    {

        string[] strings = value.ToString().Split('@');

        return strings[1].ToUpper() == allowedDomain.ToUpper();

    }

}

复制代码

  然后就可以在我们的Model上面使用  ValidEmailDomain 属性来校验,如下:


 


复制代码

public class RegisterViewModel

{


    [Required]

    [Display(Name = "邮箱地址")]

    [EmailAddress]

    [Remote(action: "IsEmailInUse", controller: "Account")]

    [ValidEmailDomain(allowedDomain: "qq.com",

    ErrorMessage = "电子邮件的后缀必须是qq.com")]

    public string Email { get; set; }


    [Required]

    [Display(Name = "密码")]

    [DataType(DataType.Password)]

    public string Password { get; set; }


    [DataType(DataType.Password)]

    [Display(Name = "确认密码")]

    [Compare("Password",

        ErrorMessage = "密码与确认密码不一致,请重新输入.")]

    public string ConfirmPassword { get; set; }


    public string City { get; set; }

}

复制代码


Asp.Net Core 客户端验证和远程验证

我们先来看这样一个注册页面和它的后台Model


复制代码

@model RegisterViewModel


@{

    ViewBag.Title = "用户注册";

}


<h1>用户注册</h1>

<div class="row">

    <div class="col-md-12">

        <form method="post">

            <div asp-validation-summary="All" class="text-danger"></div>

            <div class="form-group">

                <label asp-for="Email"></label>

                <input asp-for="Email" class="form-control" />

                <span asp-validation-for="Email" class="text-danger"></span>

            </div>

            <div class="form-group">

                <label asp-for="Password"></label>

                <input asp-for="Password" class="form-control" />

                <span asp-validation-for="Password" class="text-danger"></span>

            </div>

            <div class="form-group">

                <label asp-for="ConfirmPassword"></label>

                <input asp-for="ConfirmPassword" class="form-control" />

                <span asp-validation-for="ConfirmPassword" class="text-danger"></span>

            </div>


            <div class="form-group">

                <label asp-for="City"></label>

                <input asp-for="City" class="form-control" />

            </div>


            <button type="submit" class="btn btn-primary">注册</button>

        </form>

    </div>

</div>

复制代码

复制代码

    public class RegisterViewModel

    {

        [Required]

        [Display(Name = "邮箱地址")]

        [EmailAddress]

        [Remote(action: "IsEmailInUse", controller: "Account")]public string Email { get; set; }


        [Required]

        [Display(Name = "密码")]

        [DataType(DataType.Password)]

        public string Password { get; set; }


        [DataType(DataType.Password)]

        [Display(Name = "确认密码")]

        [Compare("Password",

            ErrorMessage = "密码与确认密码不一致,请重新输入.")]

        public string ConfirmPassword { get; set; }




        public string City { get; set; }

    }

复制代码

  如果我们点击了注册,校验过程是发送了请求到服务器,在服务器端进行校验,这在某个程度上来说影响了用户体验和系统性能。所以我们需要在提交到服务器前先在客户端进行验证。那么怎么做呢,在Asp.Net Core上很简单。


  实现客户端校验只需要添加三个js库即可,分别是:


  <script src="~/lib/jquery/jquery.js"></script>

  <script src="~/lib/jquery-validate/jquery.validate.js"></script>

  <script src="~/lib/jquery-validation-unobtrusive/jquery.validate.unobtrusive.js">


  我们可以通过libman来很方便的添加:


复制代码

{

  "version": "1.0",

  "defaultProvider": "cdnjs",

  "libraries": [

    {

      "library": "twitter-bootstrap@4.3.1",

      "destination": "wwwroot/lib/twitter-bootstrap/"

    },

    {

      "library": "jquery@3.4.1",

      "destination": "wwwroot/lib/jquery/"

    },

    {

      "library": "jquery-validate@1.19.1",

      "destination": "wwwroot/lib/jquery-validate/"

    },

    {

      "library": "jquery-validation-unobtrusive@3.2.11",

      "destination": "wwwroot/lib/jquery-validation-unobtrusive/"

    }

  ]

复制代码

  而有些时候在注册的时候需要检测所填入的邮箱是否已经被使用,这通常是使用Ajax来进行验证,当然,在Asp.Net Core中,这个验证写法变得异常简单,同样的是需要前面三个js库,然后需要做的是在模型的Email字段上添加属性 


[Remote(action: "IsEmailInUse", controller: "Account")]

  然后创建对应的controller和action


复制代码

[AcceptVerbs("Get", "Post")]

[AllowAnonymous]

public async Task<IActionResult> IsEmailInUse(string email)

{

    var user = await userManager.FindByEmailAsync(email);


    if (user == null)

    {

        return Json(true);

    }

    else

    {

        return Json($"邮箱: {email} 已经被注册使用了。");

    }

}

复制代码

  这样就可以了。


Asp.Net Core文件上传

文件上传功能在实际开发中经常使用,在 .Net Core中,文件上传接收类型不再使用 HttpPostedFile 或 HttpFileCollection来接收,而是使用 IFormFile 或 IFormFileCollection来接收。


  下面看一个例子就明白怎么使用了,具体代码如下:


复制代码

<form enctype="multipart/form-data" asp-controller="home" asp-action="upload" method="post" class="form-horizontal">

  <div class="form-group">

    <label for="input" class="col-sm-2 control-label">头像</label>

    <div class="col-sm-5">

      <input type="file" name="input" id="input" class="custom-file-input"/>

      <label class="custom-file-label"></label>

    </div>

    <input type="submit" value="提交" />

  </div>

</form>



@section scripts{

    <script>

        $(document).ready(function () {

            $(".custom-file-input").on("change", function () {

                var fileName = $(this).val().split("\\").pop();

                $(this).next(".custom-file-label").html(fileName);

            })

        });

    </script>

}

复制代码

复制代码

using System;

using System.Collections.Generic;

using System.Diagnostics;

using System.Linq;

using System.Threading.Tasks;

using Microsoft.AspNetCore.Mvc;

using FileUpload.Models;

using Microsoft.AspNetCore.Http;

using Microsoft.AspNetCore.Hosting;

using System.IO;


namespace FileUpload.Controllers

{

    public class HomeController : Controller

    {

        private readonly IHostingEnvironment _hostingEnvironment;


        public HomeController(IHostingEnvironment hostingEnvironment)

        {

            _hostingEnvironment = hostingEnvironment;

        }


        public IActionResult Index()

        {

            return View();

        }


        [HttpPost]

        public IActionResult Upload(IFormFile input)

        {

            if (input == null) return BadRequest();


            string uploadsFolder = Path.Combine(_hostingEnvironment.WebRootPath, "images");

            string uniqueFileName = Guid.NewGuid().ToString() + "_" + input.FileName;


            string filePath = Path.Combine(uploadsFolder,uniqueFileName);

       

        using(FileStream fileStream = new FileStream(filePath,FileMode.Create)

        {


                input.CopyTo(fileStream);

       }



            return Ok();

        }

    }

}

复制代码

  多文件上传


复制代码

<form enctype="multipart/form-data" asp-controller="home" asp-action="upload" method="post" class="form-horizontal">

  <div class="form-group">

    <label for="input" class="col-sm-2 control-label">头像</label>

    <div class="col-sm-5">

      <input type="file" name="input" id="input" class="custom-file-input" multiple />

      <label class="custom-file-label"></label>

    </div>

    <input type="submit" value="提交" />

  </div>

</form>


@section scripts{

    <script>

        $(document).ready(function () {

            $(".custom-file-input").on("change", function () {


                var fileLabel = $(this).next(".coustom-file-lable");

                var files = $(this)[0].files;

                if (files.length > 1) {

                    fileLabel.html("你已选择了" + files.length + "个文件");

                } else {

                    fileLabel.html(files[0].name);

                }

            })

        });

    </script>

}

复制代码

复制代码

  [HttpPost]

  public IActionResult Upload(IFormFileCollection input)

  {

            if (input == null) return BadRequest();


            string uploadsFolder = Path.Combine(_hostingEnvironment.WebRootPath, "images");

            string uniqueFileName = Guid.NewGuid().ToString() + "_" + input[0].FileName;


            string filePath = Path.Combine(uploadsFolder,uniqueFileName);

        using(FileStream fileStream = new FileStream(filePath,FileMode.Create)

        {


                input[0].CopyTo(fileStream);

       }

       return Ok(); 

  }


.NetCore 使用Cookie

.NetCore 使用Cookie

1、首先我们在Startup下面的ConfigureServices中注册授权认证服务以及AddCookie

services.AddAuthentication(CookieAuthenticationDefaults.AuthenticationScheme)

            .AddCookie(opt => { opt.LoginPath = new PathString("/Home/Index/"); });

 


2、然后可以在需要使用cookie的地方添加

HttpContext.Response.Cookies.Append("getCookie", "setCookieValue");

 


3、然后我们需要使用的时候直接取出cookie的值

  

 var getCookie = "";

   HttpContext.Request.Cookies.TryGetValue("getCookie", out getCookie);

 


4、到这里都是可以成功的,然后我们清除浏览器缓存之后再来尝试这样来处理cookie。发现Append和TryGetValue都是没有值的。此时内心无比惆怅!

令我百思不得其解,Google了一些资料,发现一个解决方案,和Configure中的文件有关。。

原因是AspNetCore2.1 支持了2018年5月25号出台的 GDPR规范,该规范认为 cookie是用户的隐私数据,如果要使用的话,必须征得用户同意.我们可以把Configure中的

app.UseCookiePolicy();

 


这一行代码注释掉。然后我们再次试试,发现存取cookie都是正常的了。一切都变的那么舒适了...


在实际使用中我们可以对cookie做一定的整合,我们首先创建一个CookieHelper

  

 public class CookieHelper:Controller

      {

        /// <summary>

        /// 添加cookie缓存不设置过期时间

        /// </summary>

        /// <param name="key"></param>

        /// <param name="value"></param>

        public void AddCookie(string key, string value)

        {

            try

            {

                HttpContext.Response.Cookies.Append(key, value);

            }

            catch (Exception ex)

            {

                throw;

            }

        }

        /// <summary>

        /// 添加cookie缓存设置过期时间

        /// </summary>

        /// <param name="key"></param>

        /// <param name="value"></param>

        /// <param name="time"></param>

        public   void AddCookie(string key,string value,int time)

        {

            HttpContext.Response.Cookies.Append(key, value,new CookieOptions

            {

                Expires=DateTime.Now.AddMilliseconds(time)

            });

        }

        /// <summary>

        /// 删除cookie缓存

        /// </summary>

        /// <param name="key"></param>

        public void DeleteCookie(string key)

        {

            HttpContext.Response.Cookies.Delete(key);

        }

        /// <summary>

        /// 根据键获取对应的cookie

        /// </summary>

        /// <param name="key"></param>

        /// <returns></returns>

        public  string GetValue(string key)

        {

            var value = "";

                HttpContext.Request.Cookies.TryGetValue(key,out value);

            if (string.IsNullOrWhiteSpace(value))

            {

                value = string.Empty;

            }

            return value;

        }

    }

 


然后我们就可以在我们需要的控制器里面去继承

  

  public class HomeController : CookieHelper

    {

        public IActionResult Index()

        {

            AddCookie("getCookie", "这是一个测试存cookie");

            return View();

        }

    }

 

public class cookieController : CookieHelper

    {

       

        public IActionResult Index()

        {  

            ViewBag.cookie = GetValue("getCookie");

            return View();

        }

    }

 


这样我们就完成了一次在.Net Core 中cookie的存取了


ASP.NET Core中间件和 ASP.NET HttpHandler HttpModule

原文

.NET Core技术研究-中间件的由来和使用

 

  我们将原有ASP.NET应用升级到ASP.NET Core的过程中,会遇到一个新的概念:中间件。


  中间件是ASP.NET Core全新引入的概念。中间件是一种装配到应用管道中以处理请求和响应的软件。 每个组件:


选择是否将请求传递到管道中的下一个组件。

可在管道中的下一个组件前后执行工作。

  单独看以上中间件的定义,一个很直观的感觉:中间件是HTTP请求管道中的一层层的AOP扩展。


  在展开介绍中间件之前,我们先回顾一下ASP.NET中HttpHandler和HttpModule的处理方式。


 一、ASP.NET中HttpHandler和HttpModule


  先看一张图:


    


   上图中有两个概念HttpHandler和HttpModule,其中:


   HttpHandler用于处理具有给定文件名或扩展名的请求。比如上图中的.report类的请求,同时,任何一个HttpHandler都需要实现接口IHttpHandler,都需要在Web.Config配置文件中注册使用。


   HttpModule用于处理每个请求调用,比如上图中的Authorization Module,每个Http请求都会经过HttpModule的处理。通过HttpModule可以中断Http请求,可以自定义HttpResponse返回。同时,任何一个HttpModule都需要实现接口IHttpModule,都需要在Web.Config配置文件中注册使用。


  ASP.NET Core引入了中间件来实现上面2种Http请求处理扩展。ASP.NET Core中间件和 ASP.NET HttpHandler HttpModule有什么区别?


  二、ASP.NET Core中间件和 ASP.NET HttpHandler HttpModule的区别


  1. 中间件比HttpHandler、HttpModule更简单


"模块"、"处理程序"、" Global.asax.cs"、 "WEB.CONFIG" (IIS配置除外)和 "应用程序生命周期" 消失


中间件已使用HttpHandler HttpModule的角色


中间件使用代码而不是在 web.config 中进行配置


通过管道分支,可以将请求发送到特定的中间件,不仅可以基于 URL,还可以发送到请求标头、查询字符串等。


   2. 中间件类似于HttpModule     


处理每个请求调用


可以实现Http请求中间和继续


能够创建自定义的HttpResponse


   3. 中间件和HttpModule按不同的顺序处理


中间件的顺序取决于它们插入请求管道的顺序,而模块的顺序主要基于应用程序生命周期事件


中间件中Http响应的顺序与Http请求的顺序相反,而对于HttpModule,请求和响应的顺序是相同的。


      


 三、ASP.NET Core中间件的设计原理


   ASP.NET Core 请求管道包含一系列请求委托,依次调用。 下图演示了这一概念。 沿黑色箭头执行。


   


   每个请求委托(中间件)都可以在下一个请求委托(中间件)之前和之后执行操作。中间件中的异常处理委托应该在管道的早期被处理,这样就可以捕获在管道后期发生的异常。


   在Startup.Configure 方法中添加中间件组件的顺序定义了针对请求调用这些中间件的顺序,以及响应的相反顺序。 这个顺序对于安全性、性能和功能非常重要。


   看一段示例代码:

public void Configure(IApplicationBuilder app, IWebHostEnvironment env)

{

    if (env.IsDevelopment())

    {

        app.UseDeveloperExceptionPage();

        app.UseDatabaseErrorPage();

    }

    else

    {

        app.UseExceptionHandler("/Error");

        app.UseHsts();

    }

 

    app.UseHttpsRedirection();

    app.UseStaticFiles();

    // app.UseCookiePolicy();

 

    app.UseRouting();

    // app.UseRequestLocalization();

    // app.UseCors();

 

    app.UseAuthentication();

    app.UseAuthorization();

    // app.UseSession();

 

    app.UseEndpoints(endpoints =>

    {

        endpoints.MapRazorPages();

        endpoints.MapControllerRoute(

            name: "default",

            pattern: "{controller=Home}/{action=Index}/{id?}");

    });

}

上述代码中每个中间件扩展方法都通过 Microsoft.AspNetCore.Builder 命名空间在 IApplicationBuilder 上公开。

        app.Use***都是各种常用的内置中间件。比如:

      1. 异常处理类中间件。如上述代码中:

           当应用在开发环境中运行时:异常显示页中间件 (UseDeveloperExceptionPage) 报告应用运行时错误。数据库错误页中间件报告数据库运行时错误。(app.UseDatabaseErrorPage();)

           当应用在生产环境中运行时:异常处理程序中间件 (UseExceptionHandler) 捕获以下中间件中引发的异常。TTP 严格传输安全协议 (HSTS) 中间件 (UseHsts) 添加 Strict-Transport-Security 标头。

      2. HTTPS 重定向中间件 (UseHttpsRedirection) 将 HTTP 请求重定向到 HTTPS。

      3. 静态文件中间件 (UseStaticFiles) 返回静态文件,并简化进一步请求处理。

      4. Cookie 策略中间件 (UseCookiePolicy) 使应用符合欧盟一般数据保护条例 (GDPR) 规定。

      5. 用于路由请求的路由中间件 (UseRouting)。

      6. 身份验证中间件 (UseAuthentication) 尝试对用户进行身份验证,然后才会允许用户访问安全资源。

      7. 用于授权用户访问安全资源的授权中间件 (UseAuthorization)。

      8. 会话中间件 (UseSession) 建立和维护会话状态。 如果应用使用会话状态,请在 Cookie 策略中间件之后和 MVC 中间件之前调用会话中间件。

      9. 用于将 Razor Pages 终结点添加到请求管道的终结点路由中间件(带有 MapRazorPages 的 UseEndpoints)。

      10. 对于单页应用程序 (SPA),SPA 中间件 UseSpaStaticFiles 通常是中间件管道中的最后一个。 SPA 中间件处于最后的作用是:允许所有其他中间件首先响应匹配的请求。允许具有客户端侧路由的 SPA 针对服务器应用无法识别的所有路由运行。

       还有很多其他的内置中间件,可以参考链接:https://docs.microsoft.com/en-us/aspnet/core/fundamentals/middleware/?view=aspnetcore-3.0。如下图,

       

      了解了ASP.NET Core内置的中间件之后,我们可能需要自定义一些中间件,比如说原有的ASP.NET HttpModule和HttpHandler.

      接下来第四部分,我们继续示例:

 四、自定义中间件

   将已有HttpModule用自定义中间件实现

   先看一下原有HttpModule的一个实现:

/// <summary>

/// 自定义HTTP扩展模块

/// </summary>

public class CustomerHttpModule : IHttpModule

    {       

        public void Init(HttpApplication context)

        {

            context.BeginRequest += Context_BeginRequest;

        }

 

        private void Context_BeginRequest(object sender, EventArgs e)

        {           

            HttpApplication app = (HttpApplication)sender;

            // Do something           

        }

 

        public void Dispose()

        {

 

        }

}

迁移到中间件实现:

/// <summary>

/// 自定义中间件

/// </summary>

public class CustomerMiddleware

{

        private readonly RequestDelegate _next;

 

        public CustomerMiddleware(RequestDelegate next)

        {

            _next = next;

        }

 

        public async Task Invoke(HttpContext context)

        {

            // Do something with context near the beginning of request processing.               

            await _next.Invoke(context);

            // Clean up.

        }

}

同时增加IApplicationBuilder的一个扩展方法:

public static IApplicationBuilder UseCustomerMiddleware(this IApplicationBuilder builder)

{

    return builder.UseMiddleware<CustomerMiddleware>();

}

Startup中使用这个中间件:     

app.UseCustomerMiddlewares(); 

以上是对ASP.NET Core中中间件的技术由来整理和使用分享。



ASP.NET Core中间件与HttpModule有何不同

前言

在ASP.NET Core中最大的更改之一是对Http请求管道的更改,在ASP.NET中我们了解HttpHandler和HttpModule但是到现在这些已经被替换为中间件那么下面我们来看一下他们的不同处。


HttpHandler

Handlers处理基于扩展的特定请求,HttpHandlers作为进行运行,同时做到对ASP.NET响应请求。他是一个实现System.Web.IHttphandler接口的类。任何实现IHttpHandler接口的类都可以作为Http请求处理响应的目标程序。

它提供了对文件特定的扩展名处理传入请求,

ASP.NET框架提供了一些默认的Http处理程序,最常见的处理程序是处理.aspx文件。下面提供了一些默认的处理程序。


Handler Extension Description

Page Handler .aspx handle normal WebPages

User Control Handler .ascx handle Web user control pages

Web Service Handler .asmx handle Web service pages

Trace Handler trace.axd handle trace functionality

创建一个自定义HttpHandler


public class CustomHttpHandler:IHttpHandler

{

    

    public bool IsReusable

    {

        //指定是否可以重用处理程序

        get {return true;}

    }

    

    public void ProcessRequest(HttpContext context)

    {

        //TODO

        throw new NotImplementedException();

    }

}


在web.config中添加配置项


<!--IIS6或者IIS7经典模式-->  

  <system.web>  

    <httpHandlers>  

      <add name="mycustomhandler" path="*.aspx" verb="*" type="CustomHttpHandler"/>  

    </httpHandlers>  

  </system.web>  

   

<!--IIS7集成模式-->  

  <system.webServer>  

    <handlers>  

       <add name="mycustomhandler" path="*.aspx" verb="*" type="CustomHttpHandler"/>  

    </handlers>  

  </system.webServer>  

异步HttpHandlers

异步的话需要继承HttpTaskAsyncHandler类,HttpTaskAsyncHandler类实现了IHttpTaskAsyncHandler和IHttpHandler接口


public class CustomHttpHandlerAsync:HttpTaskAsyncHandler

{

    

    public override Task ProcessRequestAsync(HttpContext context)

    {


        throw new NotImplementedException();

    }

}

HttpModule

下面是来自MSDN


Modules are called before and after the handler executes. Modules enable developers to intercept, participate in, or modify each individual request. Modules implement the IHttpModule interface, which is located in the System.Web namespace.


HttpModule类似过滤器,它是一个基于事件的,在应用程序发起到结束的整个生命周期中访问事件


自定义一个HttpModule

public class CustomModule : IHttpModule

    {

        public void Dispose()

        {

            throw new NotImplementedException();

        }


        public void Init(HttpApplication context)

        {

            context.BeginRequest += new EventHandler(BeginRequest);

            context.EndRequest += new EventHandler(EndRequest);

        }

        void BeginRequest(object sender, EventArgs e)

        {

            ((HttpApplication)sender).Context.Response.Write("请求处理前");

        }


        void EndRequest(object sender, EventArgs e)

        {

            ((HttpApplication)sender).Context.Response.Write("请求处理结束后");

        }

    }



web.config中配置


<!--IIS6或者IIS7经典模式-->  

<system.web>  

    <httpModules>  

      <add name="mycustommodule" type="CustomModule"/>  

    </httpModules>  

  </system.web>  

<!--IIS7集成模式-->  

<system.webServer>  

    <modules>  

      <add name="mycustommodule" type="CustomModule"/>  

    </modules>  

</system.webServer>  

中间件

中间件可以视为集成到Http请求管道中的小型应用程序组件,它是ASP.NET中HttpModule和HttpHandler的结合,它可以处理身份验证、日志请求记录等。


中间件和HttpModule的相似处

中间件和HttpMoudle都是可以处理每个请求,同时可以配置进行返回我们自己的定义。


中间件和httpModule之间的区别

HttpModule 中间件

通过web.config或global.asax配置 在Startup文件中添加中间件

执行顺序无法控制,因为模块顺序主要是基于应用程序生命周期事件 可以控制执行内容和执行顺序按照添加顺序执行。

请求和响应执行顺序保持不变 响应中间件顺序与请求顺序相反

HttpModules可以附件特定应用程序事件的代码 中间件独立于这些事件

中间件示例

 public void Configure(IApplicationBuilder app, IWebHostEnvironment env)

  {

      if (env.IsDevelopment())

      {

          app.UseDeveloperExceptionPage();

      }


      app.UseHttpsRedirection();


      app.UseRouting();


      app.UseAuthorization();


      app.UseEndpoints(endpoints =>

      {

          endpoints.MapControllers();

      });

  }

在如上代码片段中我们有一些中间件的添加,同时也有中间件的顺序。

















































































Top