在我的文章《从零开始进行ABP项目开发——为什么从零开始搭建项》中,说明了为什么要从零开始搭建ABP项目,而不是从现有的模板入手。同样的原因也适用于其它技术的学习:从零开始,可以使我们了解各种内在的关联,知道很多设计结果的来龙去脉。我一直认为模板不是为入门选手学习而准备的,而是为熟练的工程师节省创建时间而准备的。现在我们开始从一个空项目一步一步创建Asp.Net Core应用。这里要解决的问题是创建一个最简单的项目,可以访问静态页面。
静态页面 首先,使用Visual Studio 2019 创建一个空的Web项目:
创建时选择空Web项目。这样所创建的项目中,只有Program.cs、StartUp.cs和appsettings.json这几个文件。运行这个项目,会启动浏览器,显示“Hello World”。
现在,我们要为项目增加静态html文件,首先创建目录wwwroot,因为所有的静态文件都需要放置在这个目录中。然后在这个目录中增加一个html文件index.html,在文件中随便写点什么:
1 2 3 4 5 6 7 8 9 10 <!DOCTYPE html> <html> <head> <meta charset="utf-8" /> <title></title> </head> <body> 你好 </body> </html>
然后修改StartUp:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Hosting; using Microsoft.AspNetCore.Http; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Hosting; namespace ZL.Workflow.WebEditor { public class Startup { // This method gets called by the runtime. Use this method to add services to the container. // For more information on how to configure your application, visit https://go.microsoft.com/fwlink/?LinkID=398940 public void ConfigureServices(IServiceCollection services) { } // This method gets called by the runtime. Use this method to configure the HTTP request pipeline. public void Configure(IApplicationBuilder app, IWebHostEnvironment env) { if (env.IsDevelopment()) { app.UseDeveloperExceptionPage(); } //增加静态内容 app.UseStaticFiles(); app.UseRouting(); app.UseEndpoints(endpoints => { endpoints.MapGet("/", async context => { await context.Response.WriteAsync("Hello World!"); }); }); } } }
我们只增加了app.UseStaticFiles(),说明我们需要应用可以访问静态文件。再次运行项目,可以在浏览器中访问index.html:
基本的功能已经有了,下一步我们将这个应用部署到IIS。
部署 现在我们部署已经创建好的简单应用。在Visual Studio 2019菜单中选择生成->发布,然后选择文件作为发布目标:
按“创建配置文件”按钮,创建一个发布配置:
按发布按钮,在指定的文件夹中生成了需要发布的文件:
这时可以直接运行文件夹中的exe文件,项目作为独立的应用运行:
在浏览器中可以访问http://localhost:5000
如果需要在IIS中托管运行,需要按照如下步骤进行。 (1)如果没有安装Asp.Net Core的托管服务,需要下载并安装,下载地址如下:https://www.microsoft.com/net/permalink/dotnetcore-current-windows-runtime-bundle-installer (2)创建一个应用程序池,将.NET CLR版本设置为无托管代码
(3)将生成的部署文件拷贝到需要部署的目录,比如TestWebSite:
(4)在IIS中创建网站或者应用,指向部署目录,并且设置应用程序池为(2)中创建的:
(5)在浏览器中可以访问这个应用了:
编写自定义中间件 在Startup的Configure中,可以使用UseEndPoints定义URL与处理程序之间的对应。MapPost可以映射POST方法的Url地址和处理函数,我们可以在这里编写简单的中间件代码,下面是简单的示例:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 public void Configure(IApplicationBuilder app, IWebHostEnvironment env) { if (env.IsDevelopment()) { app.UseDeveloperExceptionPage(); } //增加静态内容 app.UseStaticFiles(); app.UseRouting(); app.UseEndpoints(endpoints => { endpoints.MapGet("/", async context => { context.Response.Redirect("index.html"); }); endpoints.MapPost("/SaveGraph", async context => { var requ = context.Request; var xml = requ.Form["xml"]; var filename ="d:\\workflow\\temp\\"+ DateTime.Now.Ticks + ".xml"; System.IO.File.WriteAllText(filename, xml); await context.Response.WriteAsync(filename); }); endpoints.MapGet("/GetXml", async context => { var requ = context.Request; var xml = requ.Query["xml"]; var filename = "d:\\workflow\\temp\\" + xml; var content=System.IO.File.ReadAllText(filename); await context.Response.WriteAsync(content); }); }); }
这里,我们定义了Url”/SaveGraph”响应POST方法,用于接收页面发送的数据,使用context.Reqeust.Form[key]可以获取POST发送的数据。定义了“/GetXML”,用于根据xml的文件名,获取文件的内容,可以使用context.Request.Query[key]获取Url中的参数。
增加RazorPage 我们从零开始创建了一个Asp.Net Core的项目,在这个项目中增加了自定义的中间件和静态页面,现在我们要使用Asp.Net Core引入的轻量级的页面技术RazorPage创建简单的页面。在项目的Startup.cs中增加如下代码:
1、在ConfigrueServices中增加services.AddRazorPages():
1 2 3 4 public void ConfigureServices(IServiceCollection services) { services.AddRazorPages(); }
2、在Configure的app.UseEndpoints中增加,endpoints.MapRazorPages(),位置在自定义的路由后面:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 public void Configure(IApplicationBuilder app, IWebHostEnvironment env) { if (env.IsDevelopment()) { app.UseDeveloperExceptionPage(); } //增加静态内容 app.UseStaticFiles(); app.UseRouting(); app.UseEndpoints(endpoints => { endpoints.MapGet("/", async context => { context.Response.Redirect("index.html"); }); endpoints.MapRazorPages(); }); }
然后在项目中创建Pages文件夹,在这个文件夹中添加RazorPage:
RazorPage就已经添加好了,可以进行访问了。在创建的空的Asp.Net Core项目中,已经有了对RazorPage的支持,不需要增加新的依赖项,只需要在服务中增加RazorPage的支持,并在endpoints中定义映射就可以了。
增加MVC的支持 增加MVC的支持与增加RazorPage的支持类似,只要1、在ConfigureServices中增加对MVC的支持,2、增加endpoines的映射就可以,具体的代码如下:
1 2 3 4 5 public void ConfigureServices(IServiceCollection services) { services.AddRazorPages(); services.AddControllersWithViews(); }
··· 在endpoints.MapRazorPages();后面增加:
1 2 3 endpoints.MapControllerRoute( name: "default", pattern: "{controller=Home}/{action=Index}/{id?}");
然后在项目中增加Controllers文件夹和Views文件夹,然后在Controllers增加一个控制器MyPageController,在Views文件夹中创建MyPage文件夹,在这个文件夹中增加Razor视图Index.cshtml。
MVC的支持就增加完成了。
这里有一个问题,如果RazorPage中有与MVC路径相同的页面,系统如何处理?比如我们创建一个RazorPage,名称为MyPage,会怎么样呢?如果url中没有action,会访问RazorPage:
如果访问MVC页面需要增加index:
增加动态编译视图 Asp.Net Core缺省情况下会在发布时将视图编译到动态库,这样在发布后是不能编辑视图文件的。可是在实际项目中,我们经常需要在发布后编辑视图文件,这种情况下,我们需要启动视图的动态编译功能。
首先,通过NuGet安装 Microsoft.AspNetCore.Mvc.Razor.RuntimeCompilation,然后,在ConfigureServices中增加.AddRazorRuntimeCompilation():
1 2 3 4 5 6 7 8 public void ConfigureServices(IServiceCollection services) { services.AddRazorPages() .AddRazorRuntimeCompilation(); services.AddControllersWithViews() .AddRazorRuntimeCompilation(); }
最后,需要在项目文件中将CopyRazorGenerateFilesToPublishDirectory设置为true:
1 2 3 4 5 <PropertyGroup> <TargetFramework>netcoreapp3.1</TargetFramework> <CopyRazorGenerateFilesToPublishDirectory>true</CopyRazorGenerateFilesToPublishDirectory> </PropertyGroup>
这时,在执行发布后,在发布的目录中可以发现Views和Pages目录,分别保存RazorPage和MVC的视图。我们就可以在发布后编辑视图文件了。
配置文件 Asp.Net Framework的配置文件是web.config,Asp.Net Core中配置文件是appsettings.json。在StartUp中可以通过依赖注入获取IConfiguration对象,对配置项进行读取:
1 2 3 4 5 6 7 public class Startup { public Startup(IConfiguration configuration) { Configuration = configuration; } public IConfiguration Configuration { get; }
读取配置项很简单,比如在appsettings.json中有如下配置项:
1 2 3 4 5 6 7 8 9 10 11 { "Logging": { "LogLevel": { "Default": "Information", "Microsoft": "Warning", "Microsoft.Hosting.Lifetime": "Information" } }, "AllowedHosts": "*", "PlugIns": "RazorPlugin,RazorPlugin.Views.dll,MyRazor,MyRazor.Views.dll" }
通过下面的代码可以读取PlugIns:
1 var plugins=Configuration["PlugIns"];
读取上面的Logging中子项的信息也很简单,只要在层次之间使用冒号就可以:
1 var defaultLogLevel=Configuration["Logging:LogLevel:Default"];
IConfiguration 可以注入到RazorPageModel和MVC的Controller中,这样在RazorPage和MVC中可以获取配置项。配置项的键值大小写不敏感,ConnectionString和connectionstring是一样的。
视图文件 @{ Layout = “_Layout”; }
1 2 3 如果没有特殊定义,所有的页面使用_Layout布局页面。 在Shared目录中可以定义布局页面,布局页面中可以使用局部视图,比如:
@RenderBody()
1 _Header.cshtml和_Footer.cshtml也在Shared文件夹中,通过partial引用。<partial name="_Header" />等效于下面的代码:
@await Html.PartialAsync("_Header")
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 可以实现对局部视图的异步加载,原来的@Html.Partial("_Header")仍然可以使用,不过会有警告,提示可能出现阻塞。 ## 身份验证基础 使用Visual Studio 创建Asp.Net Core应用时,有身份验证的选项,缺省是“不进行身份验证”:  如果项目需要身份验证,可以在这里就进行设置,选择“更改”,会弹出可以选择的身份验证方式:  一般情况下,我们选择“个人用户账户”,这种情况下,应用内的用户账户会存储到应用数据库中。 我们看一下生成的项目结构:  与没有身份验证的项目相比,Areas目录中多了Identity,还多了Data目录,在这里可以看到Identity的数据库设置代码。使用NuGet包管理器,会发现自动安装了Identity的相关程序包:  原来,Identity作为Razor类库安装到项目中,除了必须暴露的部分(比如数据库初始化),其它部分包括界面,都被封装到Razor类库中,不会混杂在项目中。运行项目前,首先修改一下appsettings中的数据库连接,然后在NuGet控制台中更新数据库,然后就可以运行了。 ## 身份验证设置 现在我们研究一下身份验证在StartUp中的设置代码。在ConfigServices中有一些基本设置已经在项目创建时生成了:
public void ConfigureServices(IServiceCollection services)
{
services.AddDbContext
(options =>
options.UseSqlServer(
Configuration.GetConnectionString("DefaultConnection")));
services.AddDefaultIdentity<IdentityUser>(
options => { options.SignIn.RequireConfirmedAccount = false; }
)
.AddEntityFrameworkStores<ApplicationDbContext>();
1 2 这里需要注意的是 options.SignIn.RequireConfirmedAccount 设置项,缺省设置为true,这种情况下,新注册的用户需要进行确认才能完成注册,如果没有安装邮件系统,这个步骤无法完成,所以这里改为false。 上面的设置是一些基本设置,使用缺省设置注册用户时,会发现密码的强度要求非常高,但在开发时,我们不需要太高的密码强度,这可以通过设置进行改变,下面是我们修改后的设置:
services.Configure<IdentityOptions>(options =>
{
// Password settings.
options.Password.RequireDigit = false;
options.Password.RequireLowercase = false;
options.Password.RequireUppercase = false;
options.Password.RequireNonAlphanumeric = false;
options.Password.RequireDigit = false;
options.Password.RequiredLength = 1;
options.Password.RequiredUniqueChars = 1;
// Lockout settings.
options.Lockout.DefaultLockoutTimeSpan = TimeSpan.FromMinutes(15);
options.Lockout.MaxFailedAccessAttempts = 5;
options.Lockout.AllowedForNewUsers = true;
// User settings.
options.User.AllowedUserNameCharacters =
"abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789-._@+";
options.User.RequireUniqueEmail = false;
});
1 2 3 4 5 6 7 8 9 10 ## 现有项目中增加身份验证 很多情况下,我们一开始创建项目时没有选择使用身份验证,在项目进行一段时间后,希望增加身份验证,这时,可以使用添加模板项目的方式实现。在Visual Studio 2019的解决方案管理器中,选中项目,按右键,选择添加->新搭建基架的项目:  选择标识:  然后会弹出设置界面:  这里必须要设置的是数据上下文类,还需要设置一个用户类,设置完成后,点击“添加”,相关的代码会添加到项目中。 与从创建项目时就增加身份验证不同的是,Identity的设置没有添加到StartUp中,而是在独立的文件Area/Identity/IdentityHostingStartup.cs中,我们可以通过修改这个文件增加我们自己的设置:
public class IdentityHostingStartup : IHostingStartup { public void Configure(IWebHostBuilder builder) { builder.ConfigureServices((context, services) => { services.AddDbContext(options => options.UseSqlServer( context.Configuration.GetConnectionString(“ZLWorkflowWebEditorContextConnection”)));
services.AddDefaultIdentity<ZLWorkflowWebEditorUser>(options => options.SignIn.RequireConfirmedAccount = false)
.AddEntityFrameworkStores<ZLWorkflowWebEditorContext>();
services.Configure<IdentityOptions>(options =>
{
// Password settings.
options.Password.RequireDigit = false;
options.Password.RequireLowercase = false;
options.Password.RequireUppercase = false;
options.Password.RequireNonAlphanumeric = false;
options.Password.RequireDigit = false;
options.Password.RequiredLength = 1;
options.Password.RequiredUniqueChars = 1;
// Lockout settings.
options.Lockout.DefaultLockoutTimeSpan = TimeSpan.FromMinutes(15);
options.Lockout.MaxFailedAccessAttempts = 5;
options.Lockout.AllowedForNewUsers = true;
// User settings.
options.User.AllowedUserNameCharacters =
"abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789-._@+";
options.User.RequireUniqueEmail = false;
});
});
}
}
app.UseAuthentication();
app.UseAuthorization();
1 2 3 4 5 6 7 最后,需要修改appsettings中的数据库连接,并且在程序包控制台中执行Update-Database,生成需要的数据库结构。 ## 动态加载Application Parts 如果我们希望在现有的项目中增加新的部分,而不需要修改现有的项目,就可能用到Application Parts,这种技术可以使我们在多个项目中共享控制器视图和Razor页面。我们编写一个简单的例子说明如何使用。 首先创建一个Asp.Net Core Web 项目,作为宿主,我们需要将其它Razor类库作为插件,动态加载到项目中。我们需要增加一些代码来动态调入插件:
public void ConfigureServices(IServiceCollection services) { var mvcBuilders = services.AddMvc();
String basePath2 = System.IO.Path.GetDirectoryName(typeof(Program).Assembly.Location);
var plugins = Configuration["PlugIns"];
if (!string.IsNullOrEmpty(plugins))
{
var arr = plugins.Split(",".ToCharArray(), StringSplitOptions.RemoveEmptyEntries);
foreach (var plugin in arr)
{
var mypath = System.IO.Path.Combine(basePath2, plugin);
var myPlugin = System.Runtime.Loader.AssemblyLoadContext.Default
.LoadFromAssemblyPath(mypath);
mvcBuilders.AddApplicationPart(myPlugin);
}
}
}
1 这里,我们将插件名称保存在配置文件的PlugIns配置项中,多个插件用逗号隔开。如果插件中包含视图动态库,也需要包括进来。下面是配置文件的例子:
{ “Logging”: { “LogLevel”: { “Default”: “Information”, “Microsoft”: “Warning”, “Microsoft.Hosting.Lifetime”: “Information” } }, “AllowedHosts”: “*”, “PlugIns”: “RazorPlugin,RazorPlugin.Views.dll,MyRazor,MyRazor.Views.dll” }
1 2 3 4 5 6 7 8 9 10 11 12 这里包含两个插件RazorPlugin和MyRazor,以及它们各自的视图RazorPlugin.Views.dll和MyRazor.Views.dll。 这两个插件是两个Razor类库,包含控器、视图和RazorPage。如果是在开发环境,需要将输出路径设置为与宿主的输出路径一致,否则无法进行调试: {% asset_img "12.1.png" "alt text" %} 在部署时,只要简单将相关动态库拷贝到运行目录就可以了。 ## 使用ViewComponent创建可复用的组件 在以前使用Web Form时,感觉最有用的可以复用组件是用户控件(UserControl),控件可以包含业务逻辑,并且可以迭代调用。有些基于Web Form的应用,比如DotNetNuke,甚至可以说基本是使用用户控件构成的:只有一个Default.aspx作为入口,所有页面都是动态加载UserControl实现的。Asp.Net Core 中的ViewComponent,感觉很像以前的UserControl,我们可以将业务逻辑封装在ViewComponent中,独立进行发布,供不同的项目使用。 ViewComponent可以创建在Asp.Net Core Web应用项目中,也可以创建在Razor类库中,具体创建步骤如下: 1、首先要创建一个继承自ViewComponent的类,比如:
public class SayHelloViewComponent : ViewComponent
1 2 或者使用ViewComponent作为类名的后缀。也可以使用[ViewComponent]属性进行修饰,这三种方法都可以。 2、在这个类中,需要创建InvokeAsync方法,返回IViewComponentResult,这类似于控制器返回视图:
using System; using System.Collections.Generic; using System.Text; using System.Threading.Tasks;
namespace RazorPlugin.Components { public class SayHelloViewComponent : ViewComponent { public async Task InvokeAsync( string Name) {
var view = View("Default", Name);
return view;
}
}
}
1 2 3 4 5 3、定义ViewComponent的Razor页面部分,类似于MVC的视图,这个文件可以保存在如下位置: * /Views/{Controller Name}/Components/{View Component Name}/{View Name} * /Views/Shared/Components/{View Component Name}/{View Name} * /Pages/Shared/Components/{View Component Name}/{View Name} 建议将文件保存在/Views/Shared/Components/路径下。这个文件的结构于视图文件基本一样,比如:
@model string
Hello @Model
1 2 4、在视图、RazorPage或者其它ViewComponent中可以引用已定义的组件:
@await Component.InvokeAsync("SayHello", new { Name = "马大姐" })
```