如果在编写代码时,养成良好的习惯,严格遵守规范,代码的可复用性和可维护性就高很多。评价代码的质量,不是在需求明确且不发生变化的情况下能够运行就可以了,而是在发生变化的情况下是否仍然保持稳定。
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 | <!DOCTYPE html> |
然后修改StartUp:
1 |
|
我们只增加了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 | public void Configure(IApplicationBuilder app, IWebHostEnvironment env) |
这里,我们定义了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 | public void ConfigureServices(IServiceCollection services) |
2、在Configure的app.UseEndpoints中增加,endpoints.MapRazorPages(),位置在自定义的路由后面:
1 | public void Configure(IApplicationBuilder app, IWebHostEnvironment env) |
然后在项目中创建Pages文件夹,在这个文件夹中添加RazorPage:

RazorPage就已经添加好了,可以进行访问了。在创建的空的Asp.Net Core项目中,已经有了对RazorPage的支持,不需要增加新的依赖项,只需要在服务中增加RazorPage的支持,并在endpoints中定义映射就可以了。
增加MVC的支持
增加MVC的支持与增加RazorPage的支持类似,只要1、在ConfigureServices中增加对MVC的支持,2、增加endpoines的映射就可以,具体的代码如下:
1 | public void ConfigureServices(IServiceCollection services) |
···
在endpoints.MapRazorPages();后面增加:
1 | endpoints.MapControllerRoute( |
然后在项目中增加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 | public void ConfigureServices(IServiceCollection services) |
最后,需要在项目文件中将CopyRazorGenerateFilesToPublishDirectory设置为true:
1 |
|
这时,在执行发布后,在发布的目录中可以发现Views和Pages目录,分别保存RazorPage和MVC的视图。我们就可以在发布后编辑视图文件了。
配置文件
Asp.Net Framework的配置文件是web.config,Asp.Net Core中配置文件是appsettings.json。在StartUp中可以通过依赖注入获取IConfiguration对象,对配置项进行读取:
1 | public class Startup |
读取配置项很简单,比如在appsettings.json中有如下配置项:
1 | { |
通过下面的代码可以读取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 | 如果没有特殊定义,所有的页面使用_Layout布局页面。 |
1 | _Header.cshtml和_Footer.cshtml也在Shared文件夹中,通过partial引用。<partial name="_Header" />等效于下面的代码: |
1 | 可以实现对局部视图的异步加载,原来的@Html.Partial("_Header")仍然可以使用,不过会有警告,提示可能出现阻塞。 |
services.AddDefaultIdentity<IdentityUser>(
options => { options.SignIn.RequireConfirmedAccount = false; }
)
.AddEntityFrameworkStores<ApplicationDbContext>();
1 | 这里需要注意的是 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 | ## 现有项目中增加身份验证 |
public class IdentityHostingStartup : IHostingStartup
{
public void Configure(IWebHostBuilder builder)
{
builder.ConfigureServices((context, services) => {
services.AddDbContext
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 | 最后,需要修改appsettings中的数据库连接,并且在程序包控制台中执行Update-Database,生成需要的数据库结构。 |
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 | 这里包含两个插件RazorPlugin和MyRazor,以及它们各自的视图RazorPlugin.Views.dll和MyRazor.Views.dll。 |
public class SayHelloViewComponent : ViewComponent
1 | 或者使用ViewComponent作为类名的后缀。也可以使用[ViewComponent]属性进行修饰,这三种方法都可以。 |
using System;
using System.Collections.Generic;
using System.Text;
using System.Threading.Tasks;
namespace RazorPlugin.Components
{
public class SayHelloViewComponent : ViewComponent
{
public async Task
string Name)
{
var view = View("Default", Name);
return view;
}
}
}
1 | 3、定义ViewComponent的Razor页面部分,类似于MVC的视图,这个文件可以保存在如下位置: |
@model string
Hello @Model
1 | 4、在视图、RazorPage或者其它ViewComponent中可以引用已定义的组件: |
一个简单的流程图遍历算法实现
根据流程定义文件生成测试用例,每个分支一个用例,需要进行流程图遍历
C#几个有用的反射方法
介绍几个常用的反射方法
审批流程需求分析与建模
一个简单的场景
我们先看一个简单的审批流程,请假流程:
- 员工填写请假申请,包括请假天数,理由等,将请假申请提交给部门经理
- 部门经理审批,如果审批结束(批准或者不批准),流程结束,如果需要员工修改申请,退回给员工进行修改。
这里,我们简化了流程,没有将诸如审批结果通知员工等作为一个环节进行描述,不管这个流程是否合理,我们用它作为起点,开始我们的需求分析。
可以有多种方式描述上面的流程,顺序流程图、BPMN图、状态图等等,这些图都可以准确描述上面的流程,这里我们使用状态图进行描述:
从图中可以看出,流程有四个状态:“开始”、“请假申请”、“部门经理审批”和“结束”,状态之间有一个或者多个的条件转移,也就是当某个条件满足,一个状态可以转移到下一个状态。我们在需求分析阶段需要做的是,如何将这些驱动状态转移的动作模型化,为软件的设计提出具体的要求。
分析
流程所处的状态我们可以用字符串进行描述,但如何描述状态转移的条件呢? “提交申请”本身是一个动作,如果在计算机界面操作,可能是一个提交按钮,如果通过邮件发送申请,可能是接收邮件的一个消息触发,我们需要把这个动作模型化,可以将每个状态转换都设置为一个bool变量,如果这个变量为真,就进行状态转换。这个流程涉及四个转换:
- 开始->请假申请,可以定义变量IsStart,如果为真,则代表流程状态从“开始”转换到“请假申请”。
- 请假申请->部门经理审批,可以定义变量IsSubmit,如果为真,则代表流程状态从“请假申请”转换到“部门经理审批”。
- 部门经理审批->请假申请,可以定义变量NeedModify,如果为真,则代表流程状态从“部门经理审批”转换到“请假申请”。
- 部门经理审批->结束,可以定义变量IsFinished,如果为真,则代表流程状态从“部门经理审批”转换到“结束”。
从上面四个转换可以看出,每个“转换”包括起始状态、目标状态和转换状态变量三个属性,转换状态变量是由外部输入决定的。这时,我们可以建立需求分析模型了。
初步的分析模型
我们将流程的模型定义为ProcessModel,流程的实例定义为Process,每个ProcessModel中包含若干的StateNode,其中由一个开始节点(StartNode)和一个结束节点(EndNode),其它的节点为执行节点(ExecNode),节点之间通过转换链接(StateTransfer)连接在一起,每个转换链接包含一个转换条件(TransferCondition),如果转换条件成立,状态发生转移。我们可以用下面的类图表示这个分析模型:
分析模型的改进
这个模型似乎可以描述上面的场景了,我们手工创建一下这个模型的实例:
- 我们创建一个ProcessModel,名称为“请假管理”
- 这个ProcessModel中包括四个StateNode:StartNode,SubmitNode,ApproveNode和EndNode, 分别对应“开始”、“请假申请”、“部门经理审批”和“结束”。
- 这个ProcessModel种包括四个状态转移,名称为:StartToSubmit,SubmitToApprove,ApproveToEnd,ApproveToSubmit,分别对应上面所说的四种状态转移。这里的问题是TransferCondition的定义,在分析中,我们有四个变量,IsStart,IsSubmit,NeedModify,IsFinished,这四个变量应该在定义TransferCondition中出现,比如IstStart==true。现在的问题是我们的模型中没有这四个变量的位置,也就是说,模型中缺少转换时条件变量的描述。我们对模型进行一下修改,增加这部分内容。
我们增加了流程相关的变量,在这个变量表中,我们可以定义IsStart,IsSubmit,NeedModify和IsFinisshed,流程在运行时,可以从外部接收这些变量的值,并保存在变量表中。在StateTransfer中可以使用这些变量定义条件转换的状态,并根据当前的值判断是否进行状态转换。
在ProcessModel中还增加了三个方法:
- Init:初始化流程
- GetCurrentState:获取流程的当前状态
- Go: 传入变量,驱动流程。
分析模型的进一步验证
现在,我们用更复杂一些的请假流程来验证分析模型。
我们可以将上面的流程简化为下面的状态转移表,增加了一个传入变量请假时间(Days):
开始节点 | 结束节点 | 转移条件 |
---|---|---|
Start | Submit | IsStart=true |
Submit | ManagerApprove | Days<=3&&IsSubmit=true |
Submit | GMApprove | Days>3&&IsSubmit=true |
ManagerApprove | End | IsApproved=true |
ManagerApproved | Submit | NeedModify=true |
GMApprove | End | IsApproved=true |
GMApprove | Submit | NeedModify=true |
现在看分析模型可以符合目前的需求。
Dotnetnuke(DNN)升级模块打包
DNN模块升级后的打包方法