开发 .NET 应用用什么 IDE 最方便,当然是 Visual Studio! 这个巨无霸几乎把什么都整合进去了,什么设置,什么配置,什么包管理,一股脑都在里面。我们看到教学视频里教我们点这个,选这个,然后确认,搞定!如何我们初学入门,肯定一头雾水,为啥到这个菜单点这个,为啥要安装那个,而教学视频会说你去查手册。况且,Visual Studio 不是跨平台的,而我们开发的 Web 应用是跨平台的,这里有点小别扭。事实上,整个开发过程完全可以在 Linux 的环境里完成!我不说 Mac, 几乎99%用 Mac 的开发者可能不会选用 .NET 开发 Web 应用。
在 .NET 的宇宙中,我们建议用 Visual Studio Code 和 dotnet 命令行相结合来开发应用,可以用 Windows,也可以用 Linux 作为开发环境 。开发过程不只是单纯鼠标点击,至少会让我们点击之前了解需要安装什么包已经为什么这么操作的理由,加深对 .NET 的理解。随着 ASP.NET Core
的不断进化,我们会渐渐把很多工作转到命令行工具,习惯了命令行以后,我们一定会爱上这个一招制敌的工具。
创建 ASP.NET Core
项目
因为我们要用 Vistual Studio Code (VS Code) 开发项目,所以创建,构造,运行等都只能用命令行。
用命令行新建一个项目
我们用 dontnet new
命令来新建一个项目,配置文件和解决方案。
dotnet new --list
我们可以看到 .NET SDK 预安装的项目模板,我们选几个用到的模板了解一下:
模板名字 | 描述 |
---|---|
console | 创建命令行项目,基本在终端上输入输入信息 |
classlib | 创建类库,共享给其他项目使用 |
web | 用最少的代码创建 ASP.NET Core 项目 |
mvc | 用 MVC 框架创建 ASP.NEt Core 项目 |
webapp | 创建带 MVC 和 Razor Pages 的项目 |
webapi | 创建提供 APIs 的服务类项目 |
react | 包含客户端框架 React 的 ASP.NET Core 项目 |
blazorserver | 运用 Blazor Server 的项目 |
这些预定义模板是为了快速开发应用而生,后续我们要遵从模板预定义的配置和公约进行开发,才能发挥它们的功用。
在开始之前,我们还有了解几个配置模板:
- globaljson 这个模板指定项目要用到的 .NET 版本
- sln 解决方案,会生成文本文件
- gitignore 忽略一些不必要和git同步的文件
Listing 6-1 新建一个项目
dotnet new globaljson --sdk-version 8.0.400 --output FirstSolution/FirstProject
dotnet new web --no-https --output FirstSolution/FirstProject --framework net8.0
dotnet new sln -o FirstSolution
dotnet sln FirstSolution add FirstSolution/FirstProject
第一行新建一个配置文件 globaljson ,指定 SDK 版本和新建方案目录结构,在 FirstProject 里生成一个global.json;
第二行新建一个 Web 项目,不采用 https , 指定项目文件夹。如果不加最后一组配置,dotnet 会自动寻找最新版的 SDK 来生成项目;
Figure 6-1
第三行新建一个解决方案文件;
Figure 6-2
第四行把 Web 项目加入到解决方案里,如果用文本编辑器打开方案文件,可以看到类似下图
Figure 6-3
微软还提供了一个脚手架 scaffold 功能,让我们可以开发地更快一点。这个功能和 Visual Studio 有一样的特征:强大,但不适合不理解整个 .NET 的系统的新手。我们建议等熟悉开发框架后再用。
构造和运行项目
为了能构造和运行项目,我们先给项目添加一个 HTML 文件。
用 VS Code 打开 FirstProject 文件夹,在 FirstProject 文件夹里新建一个文件夹 wwwroot ,
在 VS Code 里右键点击左边树形文件夹列表的 wwwroot 文件夹,选择添加新文件,添加一个 HTML 文件 hello.html :
Listing 6-2
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8" />
<title></title>
</head>
<body>
<h2>HTML File in FirstProject</h2>
</body>
</html>
Listing 6-3 在 Program.cs
文件里添加使用静态文件的语句,用来显示 HTML 内容:
var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();
app.MapGet("/", () => "Hello World!");
app.UseStaticFiles(); //添加语句
app.Run();
Listing 6-4 修改 Properties 文件夹里的 launchSettings.json 文件的应用 Url 端口:
...
"iisExpress": {
"applicationUrl": "http://localhost:5000",
"sslPort": 0
}
...
"launchBrowser": true,
"applicationUrl": "http://localhost:5000",
"environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development"
}
...
在 VS Code 里按 Ctrl+~
打开终端,运行构建命令:
dotnet build
或者直接构建和运行项目:
dotnet run
编译器就开始构建,整合 HTTP 服务器,然后打开默认浏览器,地址栏里输入 http://localhost:5000/hello.html
显示页面。
.NET 还提供了一个热重载功能,任何代码变动,都会被自动重载并更新浏览器页面,无须每次都要按 Ctrl+c
停止应用,再运行 dotnet run
。
dotnet watch
如果实在是不习惯命令行,我们也可以再 VS Code 里构建和运行项目。选择菜单 Terminal > Run Build Task, 再点击 dotnet:build
,VS Code 就会编译项目。
接着选择 Run > Start Debugging (快捷键:F5),或者 Without Debugging (快捷键:Ctrl+F5),运行项目。
包管理
大部分项目除了标准的 ASP.NET Core
软件包以外,还需要其他附加特性,就会用到第三方开发的软件包。
管理 NuGet 包
我们用 dotnet add package
命令来添加新的 NuGet 包,比如:
dotnet add package Microsoft.EntityFrameworkCore.SqlServer
用来加入最新的实体框架核心 SQL Server 数据库支持模块。
用 dotnet remove package
移除已安装的包。
用 dotnet list package
来列出项目中已安装的包。
管理工具包
最常用的工具包是实体数据库管理工具包 dotnet-ef
,用 dotnet tool
命令来管理。
dotnet tool uninstall --global dotnet-ef
dotnet tool install --global dotnet-ef
–global 参数表示这个工具包是全局包,可以给任何 .NET 项目使用。
管理客户端包
客户端包主要包括图像处理,CSS 样式框架,JavaScript 文件等,这些软件包要用 LibMan 工具来管理。而这个工具要用 dotnet tool
来管理。工具管理工具。
dotnet tool uninstall --global Microsoft.Web.LibraryManager.Cli
dotnet tool install --global Microsoft.Web.LibraryManager.Cli
客户端包管理器要先初始化,再使用。
libman init -p cdnjs
-p 参数指定包仓库是 https://cdnjs.com
。初始化后,我们可以安装客户端软件包:
libman install bootstrap@5.3.3 -d wwwroot/lib/bootstrap
Debugging 项目
VS Code 提供了 Debugging 功能。我们是人类,我们出错,所以我们离不开这个功能。
移动鼠标到某一行语句的开头行号前面,鼠标变手形,点击暗红色圆点,圆点变成了红色,表示添加断点;我们就在这句语句的位置设置了一个断点。如果再点击这个红色断点,那么就取消了这个断点。按 F5 以调试模式运行项目,如果顶端有弹窗,选择 C# ,那么应用运行到这个语句时会暂停,任务栏的 VS Code 应用图标开始闪烁;点开 VS Code 后,就会看到调试窗口。
在顶端有一条控制面板,上面有6个图标按钮,表示 继续,跨过(F10),步入(F11),步出(Shift+F11),重启,停止。我们用这些来控制调试中执行语句的行为。最好记住这些快捷键,鼠标点就比较麻烦。
左边一排小窗口,从上到下是:变量窗口,监视窗口,调用栈,断点列表。
变量窗口会显示断点方法里的本地变量,移动鼠标到变量会显示详细信息。
点击监视窗口右上加号,可以添加特定的变量,监视它的变化。
调用会显示在运行栈里的方法,最顶端是当前方法的文件信息。
断点列表可以让我们在这里切换或者转到指定的断点。
VS Code 必备插件
工欲善其事,必先利其器。开发 Web 应用,VS Code 必装以下插件:
- .NET Install tool: 安装和管理不同版本的 SDK 和运行时
- C#:C#基本语言支持
- C# Dev Kit: 微软官方C#工具箱,包含单元测试模块
- IntelliCode for C# Dev Kit: AI 开发助手
- REST Client: REST 客户端,PostMan基本功能
必备 C# 技术
先准备一个新项目,来逐步学习必备技术。
dotnet new globaljson --sdk-version 6.0.400 --output EssentialTech
dotnet new web --no-https --output EssentialTech --framework net8.0
dotnet new sln -o EssentialTech
dotnet sln EssentialTech add EssentialTech
和 Listing 6-4 一样修改应用运行端口。
添加 MVC 框架
运行 PowerShell,转到 EssentialTech 文件夹,输入 code .
启动 VS Code 。
Listing 6-5 点击打开 Program.cs 主运行文件,在文件里添加 MVC 代码:
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddControllersWithViews(); // 给 WebApplication 添加控制服务
var app = builder.Build();
// app.MapGet("/", () => "Hello World!");
app.MapDefaultControllerRoute(); // 添加默认路径映射
app.Run();
AddControllersWithViews() 是MvcServiceCollectionExtensions里的方法,为特定的服务集合类添加支持controllers的服务。
MapDefaultControllerRoute()为项目添加默认的默认路径把 URL 映射到对应的 > controller,如果没有指定 Controller 名字,那么 URL 将被映射到 HomeController。
添加应用组件
我们来添加合适的组件来演示必备技术。
添加数据模型
我们在项目文件里新建一个 Models 文件夹,在这个文件夹里创建一个新文件 Credit.cs,在 Credit.cs 文件里定义一个 Credit 类:
Listing 6-6 Credit.cs
using System.Runtime.InteropServices;
namespace EssentialTech.Models;
public class Credit
{
public string CourseName { get; set; }
public int? Credits { get; set; }
public static Credit[] GetCredits()
{
Credit aspNetCore = new() { CourseName = "ASP.NET Core", Credits = 4 };
Credit html5 = new() { CourseName = "HTML5", Credits = 2 };
Credit css3 = new() { CourseName = "CSS3", Credits = 3 };
return [aspNetCore, html5, css3, null];
}
}
VS Code 会在 CourseName 属性和 null 下面显示一条警告曲线,我们先不管它。
有了数据模型,我们要开始创建控制器。我们在项目文件夹新建一个 Controllers 文件夹,在里面新建一个文件 HomeController.cs ,内容为:
Listing 6-7 HomeController.cs
using Microsoft.AspNetCore.Mvc;
namespace EssentialTech.Controllers;
public class HomeController : Controller
{
public IActionResult Index()
{
return View();
}
}
这个是必须的类,继承了程序集 Microsoft.AspNetCore.Mvc
里定义的 Controller 类;缺省返回主页视图。ASP.NET Core
项目模板会隐式包含一些常用的程序集:
- System
- System.Collections.Generic
- System.IO
- System.Linq
- System.Net.Http
- System.Net.Http.Json
- System.Threading
- System.Threading.Tasks
- Microsoft.AspNetCore.Builder
- Microsoft.AspNetCore.Hosting
- Microsoft.AspNetCore.Http
- Microsoft.AspNetCore.Routing
- Microsoft.Extensions.Configuration
- Microsoft.Extensions.DependencyInjection
- Microsoft.Extensions.Hosting
- Microsoft.Extensions.Logging
ASP.NET Core
项目是默认空值状态分析的,所以 VS Code 会给可能出现空值的非空变量或属性发出警告,而 CourseName 是非空字符串,通常做法,我们会给这些变量或属性一个初始化默认值。
在 GetCredits() 方法的返回值的数组里有 null 空值,所以我们要给函数签名的返回值后面加 ? ,表示可能空值。
Listing 6-8 在Credit类里使用可能空值类型
using System.Runtime.InteropServices;
namespace EssentialTech.Models;
public class Credit
{
public string CourseName { get; set; } = string.Empty; // 默认空字符串
public int? Credits { get; set; }
public static Credit?[] GetCredits() // 可能空值操作符?
{
Credit aspNetCore = new() { CourseName = "ASP.NET Core", Credits = 4 };
Credit html5 = new() { CourseName = "HTML5", Credits = 2 };
Credit css3 = new() { CourseName = "CSS3", Credits = 3 };
return [aspNetCore, html5, css3, null];
}
}
接下来要提供主页视图给控制器调用。我们先在项目文件夹新建一个 Views 文件夹,再在 Views 文件夹里新建一个子文件夹 Home , 最后在里面新建一个新的 Razor View 文件 Index.cshtml ,内容如下:
Listing 6-9 Index.cshtml
@model IEnumerable<string>
@{
Layout = null;
}
<!DOCTYPE html>
<html>
<head>
<meta name="viewport" content="width=device-width" />
<title>EssentialTech</title>
</head>
<body>
<h1>EssentialTech</h1>
<ul>
@foreach (var item in Model)
{
<li>@item</li>
}
</ul>
</body>
</html>
因为我们还没有把数据绑定到视图里面,现在视图是空白页。我们现在要让 HomeController 类了解数据模型类,要在cs文件里添加程序集的引用。
Listing 6-10 修改HomeController.cs的内容如下
using EssentialTech.Models;
using Microsoft.AspNetCore.Mvc; // 数据模型程序集引用
namespace EssentialTech.Controllers;
public class HomeController : Controller
{
public IActionResult Index()
{
Credit?[] credits = Credit.GetCredits(); //返回空值可能
return View(new string[] {
$"Course: {credits[0]?.CourseName}, Credits: {credits[0]?.Credits}",
});
}
}
这里要用到字符串插值,就是把变量用花括号{}
限定后放入字符串,这个字符串的开头双引号外必须加上一个$
来表示要对字符串里的限定做值插入。
我们把 credits 列表的第一项用字符串插值插入课程名和学分,构建一个字符串列表作为视图的数据模型,传给 return Veiw() 里;再运行应用,那么主页就会显示课程名称是 ASP.NET Core
,学分是 4 ,说明数据已经被正确绑定了。
如果我们把这个语句 $"Course: {credits[0]?.CourseName}, Credits: {credits[0]?.Credits}",
中的[0] 改为[3],那么插入的 credit 将是那个空值 null ,那么浏览器页面会在课程名称和学分后面显示空白。这样呢,就不够直观,我们可以用空值合并运算符,提高应用的可读性。我们可以把这个语句改为:
$"Course: {credits[3]?.CourseName ?? "未知"}, Credits: {credits[3]?.Credits ?? 0}"
在应付数据库数据时,总是要应付空值,空值合并运算符 ??
和强制非空值操作符 !
一定要学会用。
有的时候,可能我们会不得已用到空值,但是又不行看到烦人的黄色空值可能警告,我们可以在cshtml的文件开头加入一行汇编取消警告:
@{ #pragma warning disable CS8602 }
使用扩展方法
当我们不能直接修改类的内容时,扩展方法是一个方便的途径给类添加新的功能。为了便于演示,我们先定义一个新的类,表示您已经选择过的所有课程。
Listing 6-11 在 Models 文件夹里新建文件 MyCourses.cs
namespace EssentialTech.Models;
public class MyCourseCredits
{
public IEnumerable<Credit?>? CourseCredits { get; set; }
}
我们假想用这个类来存放所有已经选中的这个学期的课程,如果我们需要知道这些课程总共多少学分,并且我们也不能修改这个类,那么我们就可以用扩展方法来增加新功能。
Listing 6-12 在 Models 文件夹里新建文件 CreditsExtensions.cs
namespace EssentialTech.Models;
public static class CreditsExtension
{
public static int TotalCredits(this MyCourseCredits myCourseCredits)
{
int total = 0;
if(myCourseCredits?.CourseCredits == null)
{
return total;
}
foreach (Credit? credit in myCourseCredits.CourseCredits)
{
total += credit?.Credits ?? 0;
}
return total;
}
}
TotalCredits 方法以 this 为第一个参数,表示这是一个扩展方法,同时第二款参数 MyCourseCredits 告诉 .NET 它要应用到哪一个类,这个扩展方法返回所选学分的总和。
我们修改 HomeController.cs 的内容验证一下这个扩张方法:
Listing 6-12 修改 HomeController.cs 内容为
using EssentialTech.Models;
using Microsoft.AspNetCore.Mvc;
namespace EssentialTech.Controllers;
public class HomeController : Controller
{
public IActionResult Index()
{
MyCourseCredits myCourseCredits = new()
{
CourseCredits = Credit.GetCredits()
};
return View(new string[] { $"Total credits: {myCourseCredits.TotalCredits()}" });
}
}
dotnet run 运行后,浏览器网页显示 Total credits: 9 ,看来扩展方法能正确运行了。我们也可以给 IEnumerable 接口加一个扩展方法。
Listing 6-13 修改 MyCourseCredits 类,实现 IEnumerable 接口
using System.Collections;
namespace EssentialTech.Models;
public class MyCourseCredits : IEnumerable<Credit?>
{
public IEnumerable<Credit?>? CourseCredits { get; set; }
public IEnumerator<Credit?> GetEnumerator()
{
return CourseCredits?.GetEnumerator() ?? Enumerable.Empty<Credit?>().GetEnumerator();
}
IEnumerator IEnumerable.GetEnumerator()
{
return GetEnumerator();
}
}
接下来我们更新我们的扩展方法,让它成为 IEnumerable 的扩展。
Listing 6-14 更新 CreditsExtensions.cs
namespace EssentialTech.Models;
public static class MyExtensionMethods
{
public static int TotalCredits(this IEnumerable<Credit?> Credits)
{
int total = 0;
foreach (Credit? credit in Credits)
{
total += credit?.Credits ?? 0;
}
return total;
}
}
因此,我们现在可以计算所有这个接口的学分总和。我们来看看效果:
Listing 6-15 更新 HomeController.cs 文件
using EssentialTech.Models;
using Microsoft.AspNetCore.Mvc;
namespace EssentialTech.Controllers;
public class HomeController : Controller
{
public IActionResult Index()
{
MyCourseCredits myCourseCredits = new()
{
CourseCredits = Credit.GetCredits()
};
Credit[] otherCredits =
[
new Credit { CourseName = "JavaScript", Credits = 3 },
new Credit { CourseName = "Java", Credits = 4 },
new Credit { CourseName = "Bootstrap", Credits = 2 }
];
int myTotalCredits = myCourseCredits.TotalCredits();
int otherTotalCredits = otherCredits.TotalCredits();
return View(new string[] { $"My TotalCredits: {myTotalCredits} ", $"Other TotalCredits: {otherTotalCredits}" });
}
}
我们还可以设计一个实现过滤功能的扩展方法,模糊查找包含某些词的课程。
Listing 6-16 更新 CreditsExtensions
namespace EssentialTech.Models;
public static class CreditsExtensions
{
public static int TotalCredits(this IEnumerable<Credit?> Credits)
{
int total = 0;
foreach (Credit? credit in Credits)
{
total += credit?.Credits ?? 0;
}
return total;
}
// 过滤功能
public static IEnumerable<Credit?> FilterByCourseName(this IEnumerable<Credit?> Credits, string courseName)
{
foreach (Credit? credit in Credits)
{
if (credit?.CourseName.IndexOf(courseName, StringComparison.OrdinalIgnoreCase) >= 0)
{
yield return credit; //把符合条件credit放到返回的 IEnumerable 接口里。
}
}
}
}
Listing 6-17 再修改 HomeController.cs 文件
using EssentialTech.Models;
using Microsoft.AspNetCore.Mvc;
namespace EssentialTech.Controllers;
public class HomeController : Controller
{
public IActionResult Index()
{
MyCourseCredits myCourseCredits = new()
{
CourseCredits = Credit.GetCredits()
};
Credit[] otherCredits =
[
new Credit { CourseName = "JavaScript", Credits = 3 },
new Credit { CourseName = "Java", Credits = 4 },
new Credit { CourseName = "Bootstrap", Credits = 2 }
];
int myTotalCredits = myCourseCredits.TotalCredits();
int aboutJavaTotalCredits = otherCredits.FilterByCourseName("Java").TotalCredits();
return View(new string[] { $"My TotalCredits: {myTotalCredits} ",
$"Other TotalCredits: {aboutJavaTotalCredits}" });
}
}
再运行应用,输出结果为:
- My TotalCredits: 9
- Has ‘Java’ Course TotalCredits: 7
在 JavaScript 中,我们经常把函数作为参数对象传给另一个函数。在 C# 中也可以这样做,机制也是和 JavaScript 类似 —— 用函数代理。我们把过滤的判断过程用函数代理来表达,让这个代理指向实际处理这个过程的函数。
Listing 6-18 在 CreditsExtensions.cs 里新增一个运用函数代理的方法
namespace EssentialTech.Models;
public static class CreditsExtensions
{
public static int TotalCredits(this IEnumerable<Credit?> Credits)
{
int total = 0;
foreach (Credit? credit in Credits)
{
total += credit?.Credits ?? 0;
}
return total;
}
public static IEnumerable<Credit?> FilterByCourseName(this IEnumerable<Credit?> Credits, string courseName)
{
foreach (Credit? credit in Credits)
{
if (credit?.CourseName.IndexOf(courseName, StringComparison.OrdinalIgnoreCase) >= 0)
{
yield return credit;
}
}
}
// 使用 Func<> 函数代理
public static IEnumerable<Credit?> Filter(this IEnumerable<Credit?> credits,
Func<Credit?, bool> filter)
{
foreach (Credit? credit in credits)
{
if (filter(credit))
{
yield return credit;
}
}
}
}
Func<Credit?, bool> 是 C# 标准代理的封装方法,返回实际方法回叫时的布尔结果:
public delegate TResult Func<in T, out TResult>(T arg);
我们需要在调用这个扩展方法的容器类 HomeController 里,添加一个新的过滤逻辑方法。
Listing 4-19 更新HomeController.cs的内容
using EssentialTech.Models;
using Microsoft.AspNetCore.Mvc;
namespace EssentialTech.Controllers;
public class HomeController : Controller
{
// 定义一个 Func<in T, out TResult> 代理指向的实际方法
static bool FilterByCredit(Credit? credit) => credit?.Credits > 2;
public IActionResult Index()
{
MyCourseCredits myCourseCredits = new()
{
CourseCredits = Credit.GetCredits()
};
Credit[] otherCredits =
[
new Credit { CourseName = "JavaScript", Credits = 3 },
new Credit { CourseName = "Java", Credits = 4 },
new Credit { CourseName = "Bootstrap", Credits = 2 }
];
int myTotalCredits = myCourseCredits.TotalCredits();
string filterWord = "Java";
int aboutJavaTotalCredits = otherCredits.FilterByCourseName(filterWord).TotalCredits();
//使用新的过滤
int myTotalCreditsMoreThan2 = myCourseCredits.Filter(FilterByCredit).TotalCredits();
return View(new string[] { $"My TotalCredits: {myTotalCredits} ",
$"Has '{filterWord}' Course TotalCredits: {aboutJavaTotalCredits}",
$"My TotalCredits More Than 2: {myTotalCreditsMoreThan2}"});
}
}
重新运行应用,浏览器页面会显示新的结果:
My TotalCredits: 9
Has 'Java' Course TotalCredits: 7
My TotalCredits More Than 2: 7
比较简洁的方式是直接用 Lambda 表达式把所有过滤改写成像这样:
int aboutJavaTotalCredits = otherCredits
.Filter(credit => credit?.CourseName.IndexOf(filterWord, StringComparison.OrdinalIgnoreCase) >= 0)
.TotalCredits();
int myTotalCreditsMoreThan2 = myCourseCredits
.Filter(credit => credit?.Credits > 2)
.TotalCredits();
这样是不是显得简洁多了。事实上,在实际开发中,我们根本不会用到这样的一个过程。目前为止,最优雅最简洁最有效率又最容易被别人读懂的是用 LINQ 的优化过的内建的 Where 从句。
int aboutJavaTotalCredits = otherCredits
.Where(credit => credit?.CourseName.IndexOf(filterWord, StringComparison.OrdinalIgnoreCase) >= 0)
.TotalCredits();
int myTotalCreditsMoreThan2 = myCourseCredits
.Where(credit => credit?.Credits > 2)
.TotalCredits();
在绝大多数场合能用 LINQ 就用 LINQ ,不建议自己写过滤等功能,这里只是为了解释扩展方法构造了这样的场景。
发表回复