create-asp.net-core-app-start-from-beginning

在我的文章《从零开始进行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应用时,有身份验证的选项,缺省是“不进行身份验证”:
![alt text](create-asp-net-core-app-start-from-beginning/9.1.png)
如果项目需要身份验证,可以在这里就进行设置,选择“更改”,会弹出可以选择的身份验证方式:
![alt text](create-asp-net-core-app-start-from-beginning/9.2.png)
一般情况下,我们选择“个人用户账户”,这种情况下,应用内的用户账户会存储到应用数据库中。

我们看一下生成的项目结构:
![alt text](create-asp-net-core-app-start-from-beginning/9.3.png)
与没有身份验证的项目相比,Areas目录中多了Identity,还多了Data目录,在这里可以看到Identity的数据库设置代码。使用NuGet包管理器,会发现自动安装了Identity的相关程序包:

![alt text](create-asp-net-core-app-start-from-beginning/9.4.png)

原来,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的解决方案管理器中,选中项目,按右键,选择添加->新搭建基架的项目:
![alt text](create-asp-net-core-app-start-from-beginning/11.1.png)
选择标识:
![alt text](create-asp-net-core-app-start-from-beginning/11.2.png)
然后会弹出设置界面:
![alt text](create-asp-net-core-app-start-from-beginning/11.3.png)
这里必须要设置的是数据上下文类,还需要设置一个用户类,设置完成后,点击“添加”,相关的代码会添加到项目中。

与从创建项目时就增加身份验证不同的是,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;
            });
        });
    }
}
1
还需要修改的是在StartUp中,增加:
        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 = "马大姐" }) ```