Win10 安装 SQL Server 2017

安装

下载自然是在:https://msdn.itellyou.cn/

安装和旧版并没有太大的区别,因为我们只是用来测试,序列号位置可以直接选择 Developer,另外需要安装 Java 的运行环境:Java SE Runtime Environment 8 Downloads

需要注意的是如果我们勾选了机器学习安装过程会比较久,而且如果电脑没有网络需要下载机器学习包才能继续安装。

具体可以参考:实例的 SQL Server 数据库内分析的累积更新 CAB 下载

因为下载比较慢,我也上传了一份到了百度网盘:https://pan.baidu.com/s/1WkdpZK1qC8Ft4PaYEBGByQ 提取码:xybu

里面 1033 结尾是英文版,2052 对应的是中文版,可以针对自己安装的 SqlServer 版本选择下载,如果不对的话无法执行下一步操作。

安装的时候登录选项尽量选择混合登录,启用 sa 用户。

如果已经安装了旧版本,例如我还安装了 2012,建议实例名添加安装的服务器版本方便区分,例如我设置的是 MSSQLSERVER2017

另外安装完成我们还需要安装:下载 SQL Server Management Studio (SSMS)

远程连接

打开 SQL Server 配置管理器,如果安装了其他版本的数据库并且曾经启用过远程连接,在 SQL Server 网络配置 中禁用 TCP/IPNamed Pipes,并启用 MSSQLSERVER2017 中的 TCP/IPNamed Pipes

双击 MSSQLSERVER2017 中的 TCP/IP,找到 IP地址 中的 IPAllTCP 端口 修改为 1433

sqlserver1

调整网络配置以后,还需要到 SQL Server 服务中重启 SQL Server (MSSQLSERVER)SQL Server (MSSQLSERVER2017) 的服务。

正常情况下,我们在命令行中运行 netstat -ano 可以找到 1433 端口,并且该端口对应的 PID 与我们安装的 SQL Server 2017 的一致。

sqlserver2

这时正常我们就能使用 SSMS、Navicat 或 DBeaver 连接 SQL Server 2017 了:

sqlserver3

如果仍然不能访问可能需要设置一下入站规则,设置以后可以使用 telnet 测试端口是否可以连接。

sqlserver4

Win10 安装 PostgreSQL

下载安装

直接在官网下载需要版本的安装包:PostgreSQL Database Download

安装过程还是无聊的下一步,这里不再赘述。

启动

安装成功可以从开始菜单找到 pgAdmin,运行管理我们安装的数据库。

打开以后需要我们输入密码,输入我们安装时设置的数据库密码即可。

postgresql1

如果不习惯英文,可以在:Configure pgAdmin -> Miscellaneous -> User language 中选择中文。

选择数据库服务器,查看属性可以查看到我们当前数据库的基本信息:

postgresql2

远程连接

防火墙设置

Win10 默认会有防火墙限制端口访问,所以首先我们需要添加入站规则,允许其他电脑连接数据库服务器端口,例如我们安装的这台默认是 5432

首先我们找到防火墙的高级设置:运行 control 打开控制面板 -> Windows Defender 防火墙 -> 高级设置

添加新的入站规则:入站规则 -> 新建规则 -> 选择“端口” -> 选择“TCP” -> 选择“特定端口”并输入5432 -> 选择“允许连接” -> 配置文件默认全选 -> 名称建议使用“PostgreSQL”

postgresql3

如果 ping 命令无法使用,可以设置入站规则启用“文件和打印机共享(回显请求 - ICMPv4-In)”
postgresql5

配置完成以后,可以使用 telnet 测试是否可以连接这个端口,需要先到程序中启用 telnet 功能,服务器和客户端电脑都要启用。

postgresql4

客户端电脑命令行测试:

1
telnet 192.168.101.145 5432

允许远程 IP

配置了端口访问,PostgreSQL 仍然没有结束,默认情况下其只允许使用 127.0.0.1 这个IP来访问服务器,我们可以通过配置 pg_hba.conf 来解除客户端限制。

首先我们到安装目录找到这个文件,例如:C:\Program Files\PostgreSQL\11\data\pg_hba.conf

找到 IP 设置这一段:

1
2
# IPv4 local connections:
host all all 127.0.0.1/32 md5

添加我们需要连接数据库服务器的客户端的 IP:

1
2
3
# IPv4 local connections:
host all all 127.0.0.1/32 md5
host all all 192.168.100.143/32 md5

保存以后无需重启服务器,正常情况下使用 Navicat 或 DBeaver 进行测试即可正常连接。

使用 CCProxy 实现代理上网

因为公司电脑如果想要正常上网需要申请,而我有一台远程机需要联网而又懒得申请,便找了个代理软件来联网。

首先下载代理软件 CCProxyProxy Server Software for Windows

安装与配置

同样是傻瓜式的下一步,安装后启动即可,需要注意的是软件要安装在有外网的电脑上。

默认如果是英文界面,可以进行设置:Option -> Advance -> Language -> OK,建议设置以后重启一下。

注意设置界面“请选择本地局域网 IP 地址”可能识别是错误的,如果是错误的需要取消自动检测,选择正确的局域网 IP。

ccproxy1

设置保存以后建议重新启动代理软件。

远程机配置

打开 IE 浏览器,设置代理:Internet 选项 -> 连接 -> 局域网设置

ccproxy2

然后选择高级进行如下设置:

ccproxy3

需要注意的是端口要与我们设置的端口保持一致。

设置以后“没有意外的话”就可以正常上网了:

ccproxy4

Win10 安装 MySQL 8.0

最近在研究 FreeSql,需要装 MySQL 数据库做一些测试,刚好碰到一些小问题简单记录一下。

下载

直接官网下载最新版本,下载 msi 文件进行安装,安装过程就是一直下一步了,没什么好说的。

下载地址:https://dev.mysql.com/downloads/installer/

本地连接

安装完成可以运行 MySQL 8.0 Command Line Client 进行管理,输入安装时设置的 root 用户密码登录即可。

登录以后可以简单做一些查询:

1
2
3
show databases;
use mysql;
select * from user;

我是安装在局域网的远程机上,所以出现以下问题,这里做一个记录。

Error 1130

MySQL 8.0 Command Line Client 工具使用 root 用户登录数据库,执行以下内容:

1
2
3
4
5
6
7
8
9
use mysql;
--正常安装后该字段为 host
select 'host' from user where user='root';
--通配符 % 也可以指定具体的 IP 地址
update user set host = '%' where user ='root';
--刷新 MySQL 的系统权限相关表
flush privileges;
--重新查看 user 表是否修改成功
select 'host' from user where user='root';

运行 services.msc 找到 MySQL 8.0 的服务重启。

Error 2059

MySQL 8.0 之前的版本中加密规则为 mysql_native_password,而在MySQL 8.0 以后的加密规则为 caching_sha2_password,最直接的方案就是更新为旧版的加密规则。

因为服务重启,我们重新打开 MySQL 8.0 Command Line Client 登录用户 root,执行以下命令。

1
2
use mysql;
alter user 'root'@'%' identified with mysql_native_password by '密码';

当安装数据库版本为 8.0.19 时,使用 DBeaver 连接报错:

1
The server time zone value '�й���׼ʱ��' is unrecognized or represents more than one time zone. You must configure either the server or JDBC driver (via the serverTimezone configuration property) to use a more specifc time zone value if you want to utilize time zone support.

这时我们只需要调整服务器时区为 Asia/Shanghai 即可。

20200219093729

以上执行结束,重新使用 Navicat 或 DBeaver 测试连接应该可以正常使用了。

C# 中的枚举类型与标志枚举的使用

枚举类型(也称为枚举)提供了一种有效的方式来定义可能分配给变量的一组已命名整数常量。

例如,假设你需要定义一个变量,其值表示一周内的某一天。 该变量只会存储七个有意义的值。

若要定义这些值,可以使用枚举类型,该类型是使用 enum 关键字声明的。

1
2
enum Day { Sunday, Monday, Tuesday, Wednesday, Thursday, Friday, Saturday };
enum Month : byte { Jan, Feb, Mar, Apr, May, Jun, Jul, Aug, Sep, Oct, Nov, Dec };

枚举类型的转换

默认情况下枚举的基类为 int,可以使用冒号指定其基类为其他整数类型。

如上例中,如果我们需要转换为基类型,可以直接进行强制转换:

1
2
3
4
5
6
7
8
9
10
11
Day today = Day.Monday;
int dayNumber = (int)today;
Console.WriteLine("{0} is day number #{1}.", today, dayNumber);

Month thisMonth = Month.Dec;
byte monthNumber = (byte)thisMonth;
Console.WriteLine("{0} is month number #{1}.", thisMonth, monthNumber);

// Output:
// Monday is day number #1.
// Dec is month number #11.

而假如我们想指定其基类型为一个非整数类型,例如 string 类型,编译器会提示以下错误:

1
CS1008	应输入类型 byte、sbyte、short、ushort、int、uint、long 或 ulong

除可以将枚举类型转换为基类型以外,我们可以将基类型的数值直接赋值给枚举类型的值,或者直接将枚举的值与基类的值做比较等。

1
2
Day restDay = 0;
bool isRestDay = Day.Sunday == 0;

另外,我们还可以将枚举值转换为有语义的内容,或者将语义内容的结果转换为枚举类型,例如枚举值 Day.Sunday 与字符串 Sunday 的相互转换:

1
2
3
4
5
6
7
8
9
// Rest days for 996 employees
Day restDay = Day.Sunday;

Console.WriteLine($"The rest day for 996 employees is {restDay.ToString()}.");
Console.WriteLine($"996 employees can only rest on {restDay} is {(Day)Enum.Parse(typeof(Day), "Sunday") == restDay}.");

// Output:
// The rest day for 996 employees is Sunday.
// 996 employees can only rest on Sunday is True.

需要注意的是,进行类型转换时,不能像 int 等类型使用关键字调用,只能用 Enum 来调用。另外建议如果不能确定是否能转换成功时,还是使用 TryParse 方法比较好,而且该方法是一个泛型的方法,可以直接指定返回的枚举类型。

1
2
3
4
5
6
// The working day of 997 employees is every day.
string workDay = "Everyday";
Console.WriteLine($"997 employees can rest is {Enum.TryParse<Day>(workDay, out _)}.");

// Output:
// 997 employees can rest is False.

默认情况下枚举是从 0 开始,我们也可以指定任意一个位置的实际指向的基类型的数值是多少,例如:

1
enum Month : byte { Jan = 1, Feb, Mar, Apr, May, Jun, Jul, Aug, Sep, Oct, Nov, Dec }; 

标志枚举

标志枚举实际的翻译应该为 作为位标志的枚举类型,标志枚举只是因为个人学习后的习惯性简称。从 MSDN 上的名字可以很容易的了解到,这种枚举类型可以参与位运算,例如 ANDORNOTXOR

可能你会问这有什么好处呢,这个就从我们基础类库中一个简单的例子来学习。

NumberStyles

工作中我们可能会经常接触到需要将字符串转换成数值型或日期型,一般我们都会用 ParseTryParse 方法来进行转换。

有时我们可能有非常明确的要求,比如转换小数点数字、带有正负号的数字、科学计数法的数字、HEX 数字等等,而这时我们可以关注一下以下两个方法:

1
2
public static Double Parse(string s, NumberStyles style, IFormatProvider provider);
public static bool TryParse(string s, NumberStyles style, IFormatProvider provider, out Double result);

其中 NumberStyles 就是我们可以指定的样式,我们可以指定字符串包含哪些规则,我们才进行转换。

例如,我们用 TryParse 方法做一个测试,字符串 100.00 与字符串 1e-10,在什么使用什么样式时可以被转换成数字:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
string num = "100.00";
bool isNumber = double.TryParse(num, System.Globalization.NumberStyles.None, null, out _);
Console.WriteLine($"Number styles {System.Globalization.NumberStyles.None} {num} is number: {isNumber}");
isNumber = double.TryParse(num, System.Globalization.NumberStyles.AllowDecimalPoint, null, out _);
Console.WriteLine($"Number styles {System.Globalization.NumberStyles.AllowCurrencySymbol} {num} is number: {isNumber}");
isNumber = double.TryParse(num, System.Globalization.NumberStyles.AllowExponent, null, out _);
Console.WriteLine($"Number styles {System.Globalization.NumberStyles.AllowExponent} {num} is number: {isNumber}");

num = "1e-10";
isNumber = double.TryParse(num, System.Globalization.NumberStyles.None, null, out _);
Console.WriteLine($"Number styles {System.Globalization.NumberStyles.None} {num} is number: {isNumber}");
isNumber = double.TryParse(num, System.Globalization.NumberStyles.AllowDecimalPoint, null, out _);
Console.WriteLine($"Number styles {System.Globalization.NumberStyles.AllowCurrencySymbol} {num} is number: {isNumber}");
isNumber = double.TryParse(num, System.Globalization.NumberStyles.AllowExponent, null, out _);
Console.WriteLine($"Number styles {System.Globalization.NumberStyles.AllowExponent} {num} is number: {isNumber}");

// Output:
// Number styles None 100.00 is number: False
// Number styles AllowCurrencySymbol 100.00 is number: True
// Number styles AllowExponent 100.00 is number: False
// Number styles None 1e-10 is number: False
// Number styles AllowCurrencySymbol 1e-10 is number: False
// Number styles AllowExponent 1e-10 is number: True

参考以上代码的输出,当样式为 None 时,这两个字符串都不能识别为数字。而允许小数形式时,100.00 可以识别成数字但是 1e-10 不能。而如果使用科学计数法的形式时,100.00 不能识别成数字但是 1e-10 可以。而这样的控制还有很多,例如 HEX 样式或者带有正负号的数字等。

这样好像很人性化,我们可以控制哪些样式的字符串可以被识别为数字,但是如果我们将这些样式进行组合呢,例如我们允许带有正负号的小数,那么就即要使用 AllowDecimalPoint,又要使用 AllowLeadingSign,但是这个方法好像不能传入一个枚举类型的数组,那么正常情况下我们应该怎么做呢?

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
string num = "100";
bool isNumber = double.TryParse(num, System.Globalization.NumberStyles.AllowDecimalPoint | System.Globalization.NumberStyles.AllowLeadingSign, null, out _);
Console.WriteLine($"{num} is number: {isNumber}");
num = "100.00";
isNumber = double.TryParse(num, System.Globalization.NumberStyles.AllowDecimalPoint | System.Globalization.NumberStyles.AllowLeadingSign, null, out _);
Console.WriteLine($"{num} is number: {isNumber}");
num = "-100";
isNumber = double.TryParse(num, System.Globalization.NumberStyles.AllowDecimalPoint | System.Globalization.NumberStyles.AllowLeadingSign, null, out _);
Console.WriteLine($"{num} is number: {isNumber}");
num = "-100.00";
isNumber = double.TryParse(num, System.Globalization.NumberStyles.AllowDecimalPoint | System.Globalization.NumberStyles.AllowLeadingSign, null, out _);
Console.WriteLine($"{num} is number: {isNumber}");

// Output:
// 100 is number: True
// 100.00 is number: True
// -100 is number: True
// -100.00 is number: True

如上所示,我们可以使用 OR 操作符进行运算,其返回的值可以作为入参传入。

而如果我想将所有类型的格式都支持,这样写也会很繁琐,而枚举中我们可以将值进行组合,具体我们可以使用 F12 转到定义查看:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
[ComVisible(true)]
[Flags]
public enum NumberStyles
{
None = 0,
AllowLeadingWhite = 1,
AllowTrailingWhite = 2,
AllowLeadingSign = 4,
Integer = 7,
AllowTrailingSign = 8,
AllowParentheses = 16,
AllowDecimalPoint = 32,
AllowThousands = 64,
Number = 111,
AllowExponent = 128,
Float = 167,
AllowCurrencySymbol = 256,
Currency = 383,
Any = 511,
AllowHexSpecifier = 512,
HexNumber = 515
}

其可以写作:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
[ComVisible(true)]
[Flags]
public enum NumberStyles
{
None = 0,
AllowLeadingWhite = 1,
AllowTrailingWhite = 2,
AllowLeadingSign = 4,
Integer = AllowLeadingWhite | AllowTrailingWhite | AllowLeadingSign,
AllowTrailingSign = 8,
AllowParentheses = 16,
AllowDecimalPoint = 32,
AllowThousands = 64,
Number = Integer | AllowTrailingSign | AllowDecimalPoint | AllowThousands,
AllowExponent = 128,
Float = Integer | AllowDecimalPoint | AllowExponent,
AllowCurrencySymbol = 256,
Currency = Integer | AllowTrailingSign | AllowParentheses | AllowThousands | AllowCurrencySymbol,
Any = Integer | AllowTrailingSign | AllowParentheses | AllowThousands | AllowExponent | AllowCurrencySymbol,
AllowHexSpecifier = 512,
HexNumber = AllowLeadingWhite | AllowTrailingWhite | AllowHexSpecifier
}

为方便理解也可以写作:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
[ComVisible(true)]
[Flags]
public enum NumberStyles
{
None = 0x0, //000000000000
AllowLeadingWhite = 0x1, //000000000001
AllowTrailingWhite = 0x2, //000000000010
AllowLeadingSign = 0x4, //000000000100
Integer = 0x7, //000000000111
AllowTrailingSign = 0x8, //000000001000
AllowParentheses = 0x10, //000000010000
AllowDecimalPoint = 0x20, //000000100000
AllowThousands = 0x40, //000001000000
Number = 0x6f, //000001101111
AllowExponent = 0x80, //000010000000
Float = 0xa7, //000010100111
AllowCurrencySymbol = 0x100,//000100000000
Currency = 0x17f, //000101111111
Any = 0x1ff, //000111111111
AllowHexSpecifier = 0x200, //001000000000
HexNumber = 0x203 //001000000011
}

NumberStyles 外,我们在进行 DataTimeTimeSpan 转换时也可以使用 System.Globalization.DateTimeStylesSystem.Globalization.TimeSpanStyles

可以看到系统的标志枚举使用了 [Flags] 特性,而具体该类型的内容可以参考:FlagsAttribute Class

LogLevel

LogLevel 也就是输出日志的级别,在常见的日志框架中都可以见到,如果有使用过应该都知道我们是可以配置输出日志的级别,方便我们控制在开发环境或生产环境下输出的日志,而这是怎么实现的,可以参考以下例子:

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
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
/// <summary>
/// 日志输出方式
/// </summary>
[Flags]
public enum LogType
{
None = 0,
File = 1,
Console = 2,
All = 3
}

/// <summary>
/// 日志 严重性
/// </summary>
[Flags]
public enum LogLevel
{
None = 0,
Trace = 1,
Debug = 2,
Info = 4,
Warn = 8,
Error = 16,
Fatal = 32,
All = 63
}

/// <summary>
/// 日志
/// </summary>
public class Logger
{
/// <summary>
/// 设置日志记录等级
/// </summary>
public static LogLevel LogLevel { get; set; } = LogLevel.All;

/// <summary>
/// 日志输出方式
/// </summary>
public static LogType LogType { get; set; } = LogType.Console;

private static void LogToConsole(string message, ConsoleColor textColor)
{
Console.OutputEncoding = Encoding.UTF8;
Console.ForegroundColor = textColor;
Console.WriteLine(message);
Console.ResetColor();

Console.OutputEncoding = Encoding.Default;
}

private static void LogToConsole(string message, ConsoleColor textColor, ConsoleColor backColor)
{
Console.OutputEncoding = Encoding.UTF8;
Console.ForegroundColor = textColor;
Console.BackgroundColor = backColor;
Console.Write(message);
Console.ResetColor();
Console.WriteLine();

Console.OutputEncoding = Encoding.Default;
}

static void LogToConsole(LogLevel v, string message)
{
switch (v)
{
case LogLevel.Trace:
LogToConsole(message, ConsoleColor.Black, ConsoleColor.DarkYellow);
break;
case LogLevel.Debug:
LogToConsole(message, ConsoleColor.Cyan);
break;
case LogLevel.Info:
LogToConsole(message, ConsoleColor.Yellow);
break;
case LogLevel.Warn:
LogToConsole(message, ConsoleColor.Red);
break;
case LogLevel.Error:
LogToConsole(message, ConsoleColor.Gray, ConsoleColor.Red);
break;
case LogLevel.Fatal:
LogToConsole(message, ConsoleColor.Yellow, ConsoleColor.Red);
break;
}
}

private static readonly object _sync = new object();

static void LogToFile(LogLevel v, string message)
{
string directory = Path.Combine(Directory.GetCurrentDirectory(), "Logs");
string path = Path.Combine(directory, DateTime.Now.ToString("yyyy-MM-dd") + ".txt");
lock (_sync)
{
if (!Directory.Exists(directory))
{
Directory.CreateDirectory(directory);
}
File.AppendAllText(path, message, System.Text.Encoding.UTF8);
}
}
/// <summary></summary>
static void Log(LogLevel v, string message)
{
if ((v & LogLevel) == 0)
return;
if ((LogType & LogType.Console) != 0)
LogToConsole(v, $"[{DateTime.Now.ToString("HH:mm:ss")}] [{v.ToString()}] {message}");
if ((LogType & LogType.File) != 0)
LogToFile(v, $"[{DateTime.Now.ToString("HH:mm:ss")}] [{v.ToString()}] {message}\r\n");
LogEvent?.Invoke(v, message);
}

public static void Trace(string message) => Log(LogLevel.Trace, message);

public static void Debug(string message) => Log(LogLevel.Debug, message);

public static void Info(string message) => Log(LogLevel.Info, message);

public static void Warn(string message) => Log(LogLevel.Warn, message);

public static void Error(string message) => Log(LogLevel.Error, message);

public static void Fatal(string message) => Log(LogLevel.Fatal, message);

/// <summary>
/// 日志记录
/// </summary>
/// <param name="v">日志等级</param>
/// <param name="message">消息</param>
public delegate void LogAction(LogLevel v, string message);

/// <summary>
/// 日志记录事件
/// </summary>
public static event LogAction LogEvent;
}

以上例子简单实现了日志输出功能,其中 LogType 枚举是指定日志输出的类型,用来限定日志是输出到控制台还是文件,而 LogLevel 可以限定哪些类型的日志可以被输出。

默认情况下日志信息会输出到控制台,并且是将所有的日志都打印到控制台,下面简单测试一下默认情况下日志的打印输出以及我们控制打印输出的日志类型后再进行日志输出的效果:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
Console.WriteLine("*********** All ***********");
Logger.Trace(LogLevel.Trace.ToString());
Logger.Debug(LogLevel.Debug.ToString());
Logger.Info(LogLevel.Info.ToString());
Logger.Warn(LogLevel.Warn.ToString());
Logger.Error(LogLevel.Error.ToString());
Logger.Fatal(LogLevel.Fatal.ToString());

// 重新设定输出日志的级别 再次测试输出
Logger.LogLevel = LogLevel.All ^ LogLevel.Trace ^ LogLevel.Debug;
Console.WriteLine("*********** 排除Trace与Debug ***********");
Logger.Trace(LogLevel.Trace.ToString());
Logger.Debug(LogLevel.Debug.ToString());
Logger.Info(LogLevel.Info.ToString());
Logger.Warn(LogLevel.Warn.ToString());
Logger.Error(LogLevel.Error.ToString());
Logger.Fatal(LogLevel.Fatal.ToString());

Console.ReadKey();

enum

AgeUnit

AgeUnit 即年龄单位,在系统中我们可能需要存储一些“标签”信息,那么如果需要存储到数据库中,那么可能就需要设计一个明细表来存储或者加字段。

但是如果这些信息如果比较固定,那么我们就可以用标志枚举的来进行存储,具体怎么使用这里不再赘述,可以参考我之前计算年龄的文章:年龄的计算问题 C# 篇

总结

一些很基础的数据类型,如果仔细研究一下 MSDN 上的说明,也可以开发出很多有意思并且很实用的用法,所以还是要多读文档啊!

参考:

C# 中的线程安全集合

前几天很久以前做的一个接口被反馈经常会报错崩溃,查看系统日志为某个字段为空的错误。

排查程序代码发现该类型是 List 集合,虽然程序启动时会给该变量初始化,但是因为会在多线程中访问这个集合,所以在多线程中访问 List 集合操作集合进行增删操作时,可能会由于 List 在“扩容”变成了一个空对象,而在此时访问就会出现上述问题。

解决方案当然也是很简单,因为 List 集合不是线程安全的集合,可以在操作集合的位置加锁。但是由于集合访问的位置较多,所以干脆调整代码使用 C# 自带的线程安全集合 ConcurrentBag

C#中的线程安全集合

System.Collections.Concurrent 命名空间提供多个线程安全集合类。当有多个线程并发访问集合时,应使用这些类代替 System.CollectionsSystem.Collections.Generic 命名空间中的对应类型。 但是,不保证通过扩展方法或通过显式接口实现访问集合对象是线程安全的,可能需要由调用方进行同步。

说明
BlockingCollection 为实现 IProducerConsumerCollection 的线程安全集合提供阻塞和限制功能。
ConcurrentBag 表示对象的线程安全的无序集合。
ConcurrentDictionary<TKey,TValue> 表示可由多个线程同时访问的键/值对的线程安全集合。
ConcurrentQueue 表示线程安全的先进先出 (FIFO) 集合。
ConcurrentStack 表示线程安全的后进先出 (LIFO) 集合。
OrderablePartitioner 表示将一个可排序数据源拆分成多个分区的特定方式。
Partitioner 提供针对数组、列表和可枚举项的常见分区策略。
Partitioner 表示将一个数据源拆分成多个分区的特定方式。
接口 说明
IProducerConsumerCollection 定义供制造者/使用者用来操作线程安全集合的方法。 此接口提供一个统一的表示(为生产者/消费者集合),从而更高级别抽象如 BlockingCollection 可以使用集合作为基础的存储机制。

参考:

文章迁移中

之前文章发布在一个 me 域名中,已经被腾讯云禁止访问了,而且这个域名没法备案,所以没办法只有做迁移。

虽然 halo 可以直接导入文章,但是之前附件都是保存在本地,而由于某些原因,这些文件都已经丢失了,而我本地没有备份。

好在目前整理发布出来的文章内容不是太多,才 40 余篇,但是中间配图丢失了就比较烦恼,特别是有些是示例源码的执行结果。

简单点的,我会想办法找到源码执行并将执行结果的截图补充回来,但是如果比较麻烦也只有删除这些图片了。

有点烦,但是这也是没有办法的办法,莫慢待

Expression 表达式目录树

简述表达式目录树

简单的表达式树实现以及声明方式

下面的代码分别是 Lambda 表达式与表达式目录树的 Lambda 表达方式:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
Console.WriteLine("***Lambda表达式***");
{
Func<int, int, int> func = (m, n) => (m + n) * 2;
var result = func(55, 66);
Console.WriteLine($"lambda:{result}");
}

Console.WriteLine("***Lambda表达式目录树***");
{
Expression<Func<int, int, int>> expression = (m, n) => (m + n) * 2;
// var func = expression.Compile();
// var result = func(55, 66);
var result = expression.Compile()(55, 66);
Console.WriteLine($"expression:{result}");
}

执行结果:

expression1

同样我们也可以拼装一个表达式目录树:

1
2
3
4
5
6
7
8
9
10
11
Console.WriteLine("***拼装表达式目录树***"); // 自己拼装表达式目录树
{
ParameterExpression parameterExpression1 = Expression.Parameter(typeof(int), "m");// 参数表达式
ParameterExpression parameterExpression2 = Expression.Parameter(typeof(int), "n");
ConstantExpression constantExpression = Expression.Constant(2);// 常量表达式
BinaryExpression binaryExpression1 = Expression.Add(parameterExpression1, parameterExpression2);// 二元表达式
BinaryExpression binaryExpression2 = Expression.Multiply(binaryExpression1, constantExpression);
Expression<Func<int, int, int>> expression = Expression.Lambda<Func<int, int, int>>(binaryExpression2, parameterExpression1, parameterExpression2);// 将表达式翻译生 lambda 表达式 并将参数表达式传入
var result = expression.Compile()(55, 66);
Console.WriteLine($"expression:{result}");
}

执行结果:

expression2

使用工具查看表达式目录树结构

首先安装 ExpressionTreeVisualizer 工具,将不同版本的 ExpressionTreeVisualizer.dll 文件放置到对应版本的 VS 调试工具目录下,比如 Visual Studio 2017 需要放置到:C:\Program Files (x86)\Microsoft Visual Studio\2017\Enterprise\Common7\Packages\Debugger\Visualizers目录下。

expression3

expression4

文件拷贝到指定目录后,需要重启 Visual Studio,重启后调试程序,表达式目录树的监视工具会有一个 Expression Tree Visualizer 的选项。

expression5

选择该工具进行查看,可以看到表达式目录树的结构。

expression6

我们通过该工具的目录树结构,对 Lambda 表达式目录树进行分拆。

表达式目录树过滤对象

实体 User 类:

1
2
3
4
5
6
7
8
9
10
11
12
13
using System;

namespace JohnSun.ExpressionTest.ConsoleApp
{
public class User
{
public int Id { get; set; }
public string Name { get; set; }
public string Email { get; set; }
public int? Age { get; set; }
public DateTime? CreateDate { get; set; }
}
}

使用 EF 查询:

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
List<User> users = new List<User>()
{
new User() { Id = 1, Name = "Kangkang", Email = "kangkang@qq.com", Age = 17 },
new User() { Id = 2, Name = "Jane", Email = "jane@yahoo.com", Age = 16 },
new User() { Id = 3, Name = "Mike", Email = "mike@google.com", Age = 17 },
new User() { Id = 4, Name = "John", Email = "john@outlook.com", Age = 19 },
new User() { Id = 5, Name = "Lili", Email = "lili@163.com", Age = 18 },
};
string name = "Kangkang";
string emailType = "@qq.com";
int? minAge = 15;
Console.WriteLine("***EF查询方式过滤数据***");
{
var entities = users.AsQueryable();
if (!string.IsNullOrEmpty(name))
{
entities = entities.Where(u => u.Name == name);
}
if (!string.IsNullOrEmpty(emailType))
{
entities = entities.Where(u => u.Email.EndsWith(emailType));
}
if (minAge.HasValue)
{
entities = entities.Where(u => u.Age.HasValue && u.Age >= minAge);
}
foreach (var user in entities)
{
Console.WriteLine($"Id:{user.Id} Name:{user.Name}");
}
}

拼装表达式目录树:

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
42
43
44
45
46
47
48
49
Console.WriteLine("***拼接表达式目录树过滤数据***");
{
ParameterExpression parameterExpression = Expression.Parameter(typeof(User), "u");
Expression<Func<User, bool>> expression = Expression.Lambda<Func<User, bool>>(Expression.Constant(true), parameterExpression);
if (!string.IsNullOrEmpty(name))
{
MemberExpression memberExpression = Expression.Property(parameterExpression, typeof(User).GetProperty("Name"));
ConstantExpression constantExpression = Expression.Constant(name);

// 使用 Equal 方法
BinaryExpression binaryExpression = Expression.Equal(memberExpression, constantExpression);
Expression<Func<User, bool>> expressionName = Expression.Lambda<Func<User, bool>>(binaryExpression, parameterExpression);
// // 使用 Call 方法
// MethodInfo methodInfo = typeof(string).GetMethod("Equals", new Type[] { typeof(string) });
// MethodCallExpression methodCallExpression = Expression.Call(memberExpression, methodInfo, constantExpression);
// Expression<Func<User, bool>> expressionName = Expression.Lambda<Func<User, bool>>(methodCallExpression, parameterExpression);

// 合并表达式目录树
expression = expression.And(expressionName);
}
if (!string.IsNullOrEmpty(emailType))
{
MemberExpression memberExpression = Expression.Property(parameterExpression, typeof(User).GetProperty("Email"));
ConstantExpression constantExpression = Expression.Constant(emailType);
// 使用 Call 方法
MethodInfo methodInfo = typeof(string).GetMethod("EndsWith", new Type[] { typeof(string) });
MethodCallExpression methodCallExpression = Expression.Call(memberExpression, methodInfo, constantExpression);
Expression<Func<User, bool>> expressionEmailType = Expression.Lambda<Func<User, bool>>(methodCallExpression, parameterExpression);

// 合并表达式目录树
expression = expression.And(expressionEmailType);
}
if (minAge.HasValue)
{
Expression<Func<User, bool>> expressionAge = Expression.Lambda<Func<User, bool>>(Expression.Property(Expression.Property(Expression.Parameter(typeof(User), "u"), typeof(User).GetProperty("Age")), typeof(int?).GetProperty("HasValue")), Expression.Parameter(typeof(User), "u"));
// 合并表达式目录树
expression = expression.And(expressionAge);

BinaryExpression binaryExpression = Expression.GreaterThanOrEqual(Expression.Property(Expression.Property(Expression.Parameter(typeof(User), "u"), typeof(User).GetProperty("Age")), typeof(int?).GetProperty("Value")), Expression.Constant(minAge, typeof(int)));
expressionAge = Expression.Lambda<Func<User, bool>>(binaryExpression, parameterExpression);
// 合并表达式目录树
expression = expression.And(expressionAge);
}

foreach (var user in users.AsQueryable().Where(expression))
{
Console.WriteLine($"Id:{user.Id} Name:{user.Name}");
}
}

上面拼装涉及到的合并表达式目录树:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
using System.Linq.Expressions;

namespace JohnSun.ExpressionTest.ConsoleApp
{
/// <summary>
/// 遍历表达式类型 当遇到参数类型表达式时 替换为我们自己定义的参数
/// </summary>
public class ExpressionVisitorExtend : ExpressionVisitor
{
public ParameterExpression Parameter { get; private set; }
public ExpressionVisitorExtend(ParameterExpression param)
{
this.Parameter = param;
}
public Expression Replace(Expression exp)
{
return this.Visit(exp);
}
protected override Expression VisitParameter(ParameterExpression node)
{
return this.Parameter;
}
}
}
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
42
43
44
45
46
47
48
using System;
using System.Linq.Expressions;

namespace JohnSun.ExpressionTest.ConsoleApp
{
public static class ExpressionExtend
{
public static Expression<Func<T, bool>> And<T>(this Expression<Func<T, bool>> expr1, Expression<Func<T, bool>> expr2)
{
if (expr1 == null)
return expr2;
else if (expr2 == null)
return expr1;
ParameterExpression parameterExpression = Expression.Parameter(typeof(T), "t");
ExpressionVisitorExtend visitor = new ExpressionVisitorExtend(parameterExpression);

var left = visitor.Replace(expr1.Body);
var right = visitor.Replace(expr2.Body);
var body = Expression.And(left, right);
return Expression.Lambda<Func<T, bool>>(body, parameterExpression);
}
public static Expression<Func<T, bool>> Or<T>(this Expression<Func<T, bool>> expr1, Expression<Func<T, bool>> expr2)
{
if (expr1 == null)
return expr2;
else if (expr2 == null)
return expr1;

ParameterExpression parameterExpression = Expression.Parameter(typeof(T), "t");
ExpressionVisitorExtend visitor = new ExpressionVisitorExtend(parameterExpression);

var left = visitor.Replace(expr1.Body);
var right = visitor.Replace(expr2.Body);
var body = Expression.Or(left, right);
return Expression.Lambda<Func<T, bool>>(body, parameterExpression);
}
public static Expression<Func<T, bool>> Not<T>(this Expression<Func<T, bool>> expr)
{
if (expr == null)
return null;

var candidateExpr = expr.Parameters[0];
var body = Expression.Not(expr.Body);

return Expression.Lambda<Func<T, bool>>(body, candidateExpr);
}
}
}

注意: EF 中使用 Lambda 表达式目录树其实是语法糖,通过 ILSpy 或者 Reflactor 等反编译工具我们可以看到实际的代码,比如:

1
entities = entities.Where(u => u.Age.HasValue && u.Age >= minAge);

反编译后内容是:

1
2
ParameterExpression[] expressionArray4 = new ParameterExpression[] { expression };
queryable = queryable.Where<User>(Expression.Lambda<Func<User, bool>>(Expression.AndAlso(Expression.Property(Expression.Property(expression = Expression.Parameter(typeof(User), "u"), (MethodInfo) methodof(User.get_Age)), (MethodInfo) methodof(int?.get_HasValue, int?)), Expression.GreaterThanOrEqual(Expression.Property(expression, (MethodInfo) methodof(User.get_Age)), Expression.Field(Expression.Constant(class_, typeof(<>c__DisplayClass0_0)), fieldof(<>c__DisplayClass0_0.minAge)))), expressionArray4));

当然上面代码中有些变量被反编译到其他位置,这里不再一一写出,而且这些内容在 VS 并不能被编译通过,所以如果想通过反编译工具将 Lambda 表达式目录树反拆出拼装的语句,需要再“翻译”一下。

另外这些语法糖并不是所有的反编译工具都能识别到,比如我在一个版本的 ILSpay 中打开这个项目的代码,还是语法糖的形式,但是在 .NET Reflector 中就可以查看到反编译后的内容。

ILSpy 中查看到的反编译内容:

expression7

.NET Reflector 中查看的反编译内容:

expression8

更多内容可以查看文章:http://www.cnblogs.com/jesse2013/p/expressiontree-part1.html

类型转换

需求描述

日常工作中经常会有需求,将一个类型的属性和字段值赋值给另外一个类型,两个类型的结构基本一致。例如:

User.cs

1
2
3
4
5
6
7
8
public class User
{
public int Id { get; set; }
public string Name { get; set; }
public string Email { get; set; }
public int? Age { get; set; }
public DateTime? CreateDate { get; set; }
}

UserCopy.cs

1
2
3
4
5
6
public class UserCopy
{
public int Id { get; set; }
public string UserName { get; set; }
public string Email { get; set; }
}

实现方案

硬编码

简单粗暴的方式:

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
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;

namespace JohnSun.ExpressionTest.ConsoleApp
{
public class ObjectMapper
{
/// <summary>
/// 硬编码的类型转换方案
/// </summary>
/// <param name="user"></param>
/// <returns></returns>
public static UserCopy TransUserCopy(User user)
{
return new UserCopy()
{
Id = user.Id,
Name = user.Name,
Email = user.Email
};
}
}
}

调用:

1
2
3
4
5
6
7
User user = new User() { Id = 1, Name = "Kangkang", Email = "kangkang@qq.com", Age = 17, CreateDate = new DateTime(2015, 1, 1) };
Console.WriteLine("***硬编码做法***");
{
// 效率最高但是最不灵活 比如其他类型的转换需要新建方法 属性值变化或者属性值较多比较麻烦
UserCopy copy = ObjectMapper.TransUserCopy(user);
Console.WriteLine($"Id:{copy.Id} Name:{copy.Name} Email:{copy.Email}");
}

序列化

需要引用 Newtonsoft.Json

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
using Newtonsoft.Json;

namespace JohnSun.ExpressionTest.ConsoleApp
{
public class SerializeMapper
{
/// <summary>
/// 序列化方式实现类型转换
/// </summary>
/// <typeparam name="TIn"></typeparam>
/// <typeparam name="TOut"></typeparam>
/// <param name="tIn"></param>
/// <returns></returns>
public static TOut Trans<TIn, TOut>(TIn tIn)
{
return JsonConvert.DeserializeObject<TOut>(JsonConvert.SerializeObject(tIn));
}
}
}

调用:

1
2
3
4
5
6
User user = new User() { Id = 1, Name = "Kangkang", Email = "kangkang@qq.com", Age = 17, CreateDate = new DateTime(2015, 1, 1) };
Console.WriteLine("***序列化转换***");
{
UserCopy copy = SerializeMapper.Trans<User, UserCopy>(user);
Console.WriteLine($"Id:{copy.Id} Name:{copy.Name} Email:{copy.Email}");
}

反射

反射是常用做法:

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
42
43
44
45
46
using System;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;

namespace JohnSun.ExpressionTest.ConsoleApp
{
public class ReflectionMapper
{
/// <summary>
/// 反射方式实现类型转换
/// </summary>
/// <typeparam name="TIn"></typeparam>
/// <typeparam name="TOut"></typeparam>
/// <param name="tIn"></param>
/// <returns></returns>
public static TOut Trans<TIn, TOut>(TIn tIn)
{
List<FieldInfo> tInFields = typeof(TIn).GetFields().ToList();
List<PropertyInfo> tInProps = typeof(TIn).GetProperties().ToList();
List<FieldInfo> tOutFields = typeof(TOut).GetFields().ToList();
List<PropertyInfo> tOutProps = typeof(TOut).GetProperties().ToList();
TOut tOut = Activator.CreateInstance<TOut>();

foreach (var field in tOutFields)
{
var tempField = tInFields.Find(f => f.Name == field.Name && f.FieldType == field.FieldType);
if (tempField != null)
{
field.SetValue(tOut, tempField.GetValue(tIn));
}
}

foreach (var prop in tOutProps)
{
var tempProp = tInProps.Find(p => p.Name == prop.Name && p.PropertyType == prop.PropertyType);
if (tempProp != null)
{
prop.SetValue(tOut, tempProp.GetValue(tIn, null), null);
}
}

return tOut;
}
}
}

调用:

1
2
3
4
5
6
User user = new User() { Id = 1, Name = "Kangkang", Email = "kangkang@qq.com", Age = 17, CreateDate = new DateTime(2015, 1, 1) };
Console.WriteLine("***反射转换***");
{
UserCopy copy = ReflectionMapper.Trans<User, UserCopy>(user);
Console.WriteLine($"Id:{copy.Id} Name:{copy.Name} Email:{copy.Email}");
}

表达式目录树

首先需要理解一下表达式目录树如何实现转换:

1
2
3
4
5
6
7
User user = new User() { Id = 1, Name = "Kangkang", Email = "kangkang@qq.com", Age = 17, CreateDate = new DateTime(2015, 1, 1) };
Console.WriteLine("***Lambda表达式目录树模仿硬编码***");
{
Expression<Func<User, UserCopy>> expression = u => new UserCopy() { Id = u.Id, Name = u.Name, Email = u.Email };
UserCopy copy = expression.Compile()(user);
Console.WriteLine($"Id:{copy.Id} Name:{copy.Name} Email:{copy.Email}");
}

将 Lambda 表达式目录树进行拆解,观察是否有规律可循:

1
2
3
4
5
6
7
8
9
Console.WriteLine("***拼装表达式目录树模仿硬编码***");
{
// 可以通过反编译工具模仿 Lambda 表达式目录树
ParameterExpression parameterExpression = Expression.Parameter(typeof(User), "u");
MemberBinding[] bindings = new MemberBinding[] { Expression.Bind(typeof(UserCopy).GetProperty("Id"), Expression.Property(parameterExpression, typeof(User).GetProperty("Id"))), Expression.Bind(typeof(UserCopy).GetProperty("Name"), Expression.Property(parameterExpression, typeof(User).GetProperty("Name"))), Expression.Bind(typeof(UserCopy).GetProperty("Email"), Expression.Property(parameterExpression, typeof(User).GetProperty("Email"))) };
Expression<Func<User, UserCopy>> expression = Expression.Lambda<Func<User, UserCopy>>(Expression.MemberInit(Expression.New(typeof(UserCopy)), bindings), parameterExpression);
UserCopy copy = expression.Compile()(user);
Console.WriteLine($"Id:{copy.Id} Name:{copy.Name} Email:{copy.Email}");
}

结合反射组装表达式目录树:

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
Console.WriteLine("***通过反射拼接表达式目录树***");
{
ParameterExpression parameterExpression = Expression.Parameter(typeof(User), "u");
List<MemberBinding> bindings = new List<MemberBinding>();

List<FieldInfo> tUserFields = typeof(User).GetFields().ToList();
List<PropertyInfo> tUserProps = typeof(User).GetProperties().ToList();
List<FieldInfo> tCopyFields = typeof(UserCopy).GetFields().ToList();
List<PropertyInfo> tCopyProps = typeof(UserCopy).GetProperties().ToList();
foreach (var field in tCopyFields)
{
var tempField = tUserFields.Find(f => f.Name == field.Name && f.FieldType == field.FieldType);
if (tempField != null)
{
bindings.Add(Expression.Bind(field, Expression.Field(parameterExpression, tempField)));
}
}
foreach (var prop in tCopyProps)
{
var tempProp = tUserProps.Find(p => p.Name == prop.Name && p.PropertyType == prop.PropertyType);
if (tempProp != null)
{
bindings.Add(Expression.Bind(prop, Expression.Property(parameterExpression, tempProp)));
}
}

Expression<Func<User, UserCopy>> expression = Expression.Lambda<Func<User, UserCopy>>(Expression.MemberInit(Expression.New(typeof(UserCopy)), bindings), parameterExpression);
UserCopy copy = expression.Compile()(user);
Console.WriteLine($"Id:{copy.Id} Name:{copy.Name} Email:{copy.Email}");
}

表达式目录树编译后的委托类型可以缓存,提高程序效率:

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
42
43
44
45
46
47
48
using System;
using System.Collections.Generic;
using System.Linq;
using System.Linq.Expressions;
using System.Reflection;

namespace JohnSun.ExpressionTest.ConsoleApp
{
public class ExpressionMapper
{
private static Dictionary<string, Delegate> _Dic = new Dictionary<string, Delegate>();
public static TOut Trans<TIn, TOut>(TIn tIn)
{
string funcKey = $"{typeof(TIn).GetType().FullName}${typeof(TOut).FullName}";
if (!_Dic.ContainsKey(funcKey))
{
ParameterExpression parameterExpression = Expression.Parameter(typeof(TIn), "t");
List<MemberBinding> bindings = new List<MemberBinding>();

List<FieldInfo> tUserFields = typeof(TIn).GetFields().ToList();
List<PropertyInfo> tUserProps = typeof(TIn).GetProperties().ToList();
List<FieldInfo> tCopyFields = typeof(TOut).GetFields().ToList();
List<PropertyInfo> tCopyProps = typeof(TOut).GetProperties().ToList();
foreach (var field in tCopyFields)
{
var tempField = tUserFields.Find(f => f.Name == field.Name && f.FieldType == field.FieldType);
if (tempField != null)
{
bindings.Add(Expression.Bind(field, Expression.Field(parameterExpression, tempField)));
}
}
foreach (var prop in tCopyProps)
{
var tempProp = tUserProps.Find(p => p.Name == prop.Name && p.PropertyType == prop.PropertyType);
if (tempProp != null)
{
bindings.Add(Expression.Bind(prop, Expression.Property(parameterExpression, tempProp)));
}
}

Expression<Func<TIn, TOut>> expression = Expression.Lambda<Func<TIn, TOut>>(Expression.MemberInit(Expression.New(typeof(TOut)), bindings), parameterExpression);
_Dic[funcKey] = expression.Compile();
}

return ((Func<TIn, TOut>)_Dic[funcKey]).Invoke(tIn);
}
}
}

结合泛型部分学习到的泛型缓存知识,使用泛型作为缓存载体取代字典:

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
42
43
44
45
46
47
using System;
using System.Collections.Generic;
using System.Linq;
using System.Linq.Expressions;
using System.Reflection;

namespace JohnSun.ExpressionTest.ConsoleApp
{
public class ExpressionGenericMapper<TIn, TOut>
{
private static Func<TIn, TOut> _Func = null;
static ExpressionGenericMapper()
{
ParameterExpression parameterExpression = Expression.Parameter(typeof(TIn), "t");
List<MemberBinding> bindings = new List<MemberBinding>();

List<FieldInfo> tUserFields = typeof(TIn).GetFields().ToList();
List<PropertyInfo> tUserProps = typeof(TIn).GetProperties().ToList();
List<FieldInfo> tCopyFields = typeof(TOut).GetFields().ToList();
List<PropertyInfo> tCopyProps = typeof(TOut).GetProperties().ToList();
foreach (var field in tCopyFields)
{
var tempField = tUserFields.Find(f => f.Name == field.Name && f.FieldType == field.FieldType);
if (tempField != null)
{
bindings.Add(Expression.Bind(field, Expression.Field(parameterExpression, tempField)));
}
}
foreach (var prop in tCopyProps)
{
var tempProp = tUserProps.Find(p => p.Name == prop.Name && p.PropertyType == prop.PropertyType);
if (tempProp != null)
{
bindings.Add(Expression.Bind(prop, Expression.Property(parameterExpression, tempProp)));
}
}

Expression<Func<TIn, TOut>> expression = Expression.Lambda<Func<TIn, TOut>>(Expression.MemberInit(Expression.New(typeof(TOut)), bindings), parameterExpression);
_Func = expression.Compile();
}

public static TOut Trans(TIn tIn)
{
return _Func.Invoke(tIn);
}
}
}

调用:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
Console.WriteLine("***通过反射拼接表达式目录树(缓存扩展)***");
{
// 第一次调用生成对应类型转换的委托后面再调用直接使用委托转换 提高转换效率
UserCopy copy = ExpressionMapper.Trans<User, UserCopy>(user);
copy = ExpressionMapper.Trans<User, UserCopy>(user);
Console.WriteLine($"Id:{copy.Id} Name:{copy.Name} Email:{copy.Email}");
}
Console.WriteLine("***通过反射拼接表达式目录树(泛型缓存扩展)***");
{
// 泛型类由于类型不同 会生成一个全新的副本 实现缓存的效果
UserCopy copy = ExpressionGenericMapper<User, UserCopy>.Trans(user);
copy = ExpressionGenericMapper<User,UserCopy>.Trans(user);
Console.WriteLine($"Id:{copy.Id} Name:{copy.Name} Email:{copy.Email}");
}

测试不同方法进行类型转换的效率

测试部分代码:

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
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
long time = 0;
User user = new User() { Id = 1, Name = "Kangkang", Email = "kangkang@qq.com", Age = 17, CreateDate = new DateTime(2015, 1, 1) };
Console.WriteLine("***硬编码转换***");
{
Stopwatch watch = new Stopwatch();
int sum = 0;
watch.Start();
for (int i = 0; i < 1000000; i++)
{
UserCopy copy = ObjectMapper.TransUserCopy(user);
sum += copy.Id;
}
watch.Stop();
time = watch.ElapsedMilliseconds;
}
Console.WriteLine($"***硬编码转换 {time}ms***");
Console.WriteLine("***序列化转换***");
{
Stopwatch watch = new Stopwatch();
int sum = 0;
watch.Start();
for (int i = 0; i < 1000000; i++)
{
UserCopy copy = SerializeMapper.Trans<User, UserCopy>(user);
sum += copy.Id;
}
watch.Stop();
time = watch.ElapsedMilliseconds;
}
Console.WriteLine($"***序列化转换 {time}ms***");
Console.WriteLine("***反射转换***");
{
Stopwatch watch = new Stopwatch();
int sum = 0;
watch.Start();
for (int i = 0; i < 1000000; i++)
{
UserCopy copy = ReflectionMapper.Trans<User, UserCopy>(user);
sum += copy.Id;
}
watch.Stop();
time = watch.ElapsedMilliseconds;
}
Console.WriteLine($"***反射转换 {time}ms***");
Console.WriteLine("***表达式目录树结合缓存***");
{
Stopwatch watch = new Stopwatch();
int sum = 0;
watch.Start();
for (int i = 0; i < 1000000; i++)
{
UserCopy copy = ExpressionMapper.Trans<User, UserCopy>(user);
sum += copy.Id;
}
watch.Stop();
time = watch.ElapsedMilliseconds;
}
Console.WriteLine($"***表达式目录树结合缓存 {time}ms***");
Console.WriteLine("***表达式目录树结合泛型缓存***");
{
Stopwatch watch = new Stopwatch();
int sum = 0;
watch.Start();
for (int i = 0; i < 1000000; i++)
{
UserCopy copy = ExpressionGenericMapper<User,UserCopy>.Trans(user);
sum += copy.Id;
}
watch.Stop();
time = watch.ElapsedMilliseconds;
}
Console.WriteLine($"***表达式目录树结合泛型缓存 {time}ms***");

执行结果:

expression9

结合结果,明显可取的方案为表达式目录树结合泛型缓存进行转换,其耗时与硬编码最接近。

初步了解 MVC5

ASP.NET MVC 是一个适用于 WEB 应用程序的经典模型 Model-View-Controller 模式。相对于 Web Forms 一个单一的整体,ASP.NET MVC 是由连接在一起的各种代码层所组成。

Global.asax 文件

Global.asax 文件概述

Global.asax 这个文件包含全局应用程序事件的事件处理程序。它响应应用程序级别和会话级别事件的代码。

运行时, Global.asax 将被编译成一个动态生成的 .NET Framework 类,该类是从 HttpApplication 基类派生的。

因此在 Global.asax 中的代码可以访问 HttpApplication 类中所有的 public 或者 protected 的成员。

Global.asax 不被用户直接请求,但 Global.asax 中的代码会被自动执行来响应特定的应用程序事件。

Global.asax 是可选的,而且在一个 web 项目中是唯一的,它应该处于网站的根目录。

一个请求的完整处理过程

以下过程由 Internet Information Service(inetinfo.exe)—— IIS 执行:

  • 客户端发出请求
  • 验证请求
  • 给请求授权
  • 确定请求的缓存
  • 获取缓存状态
  • 在请求的处理程序执行前
  • http 处理程序执行请求 (asp.net 页面由 aspnet_wp.exe 执行)
  • 在请求的处理程序执行后
  • 释放请求状态
  • 更新请求缓存
  • 请求结束

Global.asax 中的事件

Global.asax 中的所有事件可以分成两种,一种是满足特定事件时才会被触发,一种是每次请求都会被按照顺序执行的事件。

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
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
using System;
using System.Collections.Generic;
using System.Linq;
using System.Web;
using System.Web.Mvc;
using System.Web.Optimization;
using System.Web.Routing;

namespace JohnSun.MVC5.Web
{
public class MvcApplication : System.Web.HttpApplication
{
protected void Application_Start(object sender, EventArgs e)
{
// 不是每次请求都调用
// 在Web应用程序的生命周期里就执行一次
// 在应用程序第一次启动和应用程序域创建事被调用
// 适合处理应用程序范围的初始化代码
AreaRegistration.RegisterAllAreas();// 注册 Area
FilterConfig.RegisterGlobalFilters(GlobalFilters.Filters); // 注册 Filter
RouteConfig.RegisterRoutes(RouteTable.Routes);// 注册 Route
BundleConfig.RegisterBundles(BundleTable.Bundles);// 注册 Bundle
}

void Application_End(object sender, EventArgs e)
{
// 不是每次请求都调用
// 在应用程序关闭时运行的代码,在最后一个HttpApplication销毁之后执行
// 比如 IIS 重启,文件更新,进程回收导致应用程序转换到另一个应用程序域
}

void Session_Start(object sender, EventArgs e)
{
// 不是每次请求都调用
// 会话开始时执行
}

void Session_End(object sender, EventArgs e)
{
// 不是每次请求都调用
// 会话结束或过期时执行
// 不管在代码中显式的清空 Session 或者 Session 超时自动过期,此方法都将被调用
}

void Application_Init(object sender, EventArgs e)
{
// 不是每次请求都调用
// 在每一个 HttpApplication 实例初始化的时候执行
}

void Application_Disposed(object sender, EventArgs e)
{
// 不是每次请求都调用
// 在应用程序被关闭一段时间之后,在 .net 垃圾回收器准备回收它占用的内存的时候被调用。
// 在每一个 HttpApplication 实例被销毁之前执行
}

void Application_Error(object sender, EventArgs e)
{
// 不是每次请求都调用
// 所有没有处理的错误都会导致这个方法的执行
}


/*********************************************************************/
// 每次请求都会按照顺序执行以下事件
/*********************************************************************/

void Application_BeginRequest(object sender, EventArgs e)
{
// 每次请求时第一个出发的事件,这个方法第一个执行
}

void Application_AuthenticateRequest(object sender, EventArgs e)
{
// 在执行验证前发生,这是创建验证逻辑的起点
}

void Application_AuthorizeRequest(object sender, EventArgs e)
{
// 当安全模块已经验证了当前用户的授权时执行
}

void Application_ResolveRequestCache(object sender, EventArgs e)
{
// 当 ASP.NET 完成授权事件以使缓存模块从缓存中为请求提供服务时发生,从而跳过处理程序(页面或者是 WebService)的执行。
// 这样做可以改善网站的性能,这个事件还可以用来判断正文是不是从 Cache 中得到的。
}

//------------------------------------------------------------------------
// 在这个时候,请求将被转交给合适程序。例如:web 窗体将被编译并完成实例化
//------------------------------------------------------------------------

void Application_AcquireRequestState(object sender, EventArgs e)
{
// 读取了 Session 所需的特定信息并且在把这些信息填充到 Session 之前执行
}

void Application_PreRequestHandlerExecute(object sender, EventArgs e)
{
// 在合适的处理程序执行请求前调用
// 这个时候,Session 就可以用了
}

//-------------------------------------------------
//在这个时候,页面代码将会被执行,页面呈现为 HTML
//-------------------------------------------------

void Application_PostRequestHandlerExecute(object sender, EventArgs e)
{
// 当处理程序完成对请求的处理后被调用。
}

void Application_ReleaseRequestState(object sender, EventArgs e)
{
// 释放请求状态
}

void Application_UpdateRequestCache(object sender, EventArgs e)
{
// 为了后续的请求,更新响应缓存时被调用
}

void Application_EndRequest(object sender, EventArgs e)
{
// EndRequest 是在响应 Request 时最后一个触发的事件
// 但在对象被释放或者从新建立以前,适合在这个时候清理代码
}

void Application_PreSendRequestHeaders(object sender, EventArgs e)
{
// 向客户端发送 Http 标头之前被调用
}

void Application_PreSendRequestContent(object sender, EventArgs e)
{
// 向客户端发送 Http 正文之前被调用
}
}
}

路由 Routing

ASP.NET MVC 不再是要依赖于物理页面,我们可以使用自己的语法自定义 URL,通过这些语法来指定资源和操作。语法通过 URL 模式集合表达,也称为路由。

路由是代表 URL 绝对路径的模式匹配字符串。所以路由可以是一个常量字符串,也可能包含一些占位符。

新建一个 ASP.NET MVC 项目,在 Global.asax 文件我们可以看到路由在这里注册,让程序在启动的时候得到处理。

1
RouteConfig.RegisterRoutes(RouteTable.Routes);//注册 Route

转到定义可以看到注册的规则:

1
2
3
4
5
6
7
8
9
10
11
12
13
public class RouteConfig
{
public static void RegisterRoutes(RouteCollection routes)
{
routes.IgnoreRoute("{resource}.axd/{*pathInfo}");

routes.MapRoute(
name: "Default",
url: "{controller}/{action}/{id}",
defaults: new { controller = "Home", action = "Index", id = UrlParameter.Optional }
);
}
}

注意:因为匹配路由是按照添加顺序去解析,所以基本的路由规则是从特殊到一般排列,否则可能导致解析异常或 404 Not Found。

具体配置参考文章:史上最全的 ASP.NET MVC 路由配置Attribute Routing in ASP.NET MVC 5

控制器 Controller

mvc1

ASP.NET MVC 会调用不同的控制器类(和其内部不同的操作方法)这取决于传入 URL。所使用的 ASP.NET MVC 的默认 URL 路由逻辑使用这样的格式来判定哪些代码以便调用:/[Controller]/[ActionName]/[Parameters]

项目建立以后有一个默认的控制器 \Controllers\HomeController.cs

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
public class HomeController : Controller
{
public ActionResult Index()
{
return View();
}

public ActionResult About()
{
ViewBag.Message = "Your application description page.";

return View();
}

public ActionResult Contact()
{
ViewBag.Message = "Your contact page.";

return View();
}
}

Controllers 文件夹右键 -> 添加 可以看到创建控制器的选项。

控制器每一个方法为一个 Action,每个 Action 对应的是项目内 \Views\[Controller]\[ActionName].cshml,在方法内右键,菜单有“添加视图”与“转到视图”选项。

ActionResult 返回值

(一)视图类型

返回视图:

1
2
3
4
public ActionResult Index()
{
return View();
}

返回分部视图:

1
2
3
4
public ActionResult ViewTest()
{
return PartialView();
}

注意: 以上均需要对 Action 添加视图文件,若无 Action 方法的视图文件也可以直接通过视图名称或视图路径指定视图文件,具体可以查看 View() 方法重载。

(二)文本类型

返回 JavaScript 脚本:

1
2
3
4
public ActionResult ContentJS()
{
return Content("alert('test js.');", "text/javascript");
}

返回 CSS 样式:

1
2
3
4
5
6
7
8
9
10
public ActionResult ContentCSS()
{
HttpCookie cookie = Request.Cookies["theme"] ?? new HttpCookie("theme", "default");
switch (cookie.Value)
{
case "Theme1": return Content("body{font-family: SimHei; font-size:1.2em}", "text/css");
case "Theme2": return Content("body{font-family: KaiTi; font-size:1.2em}", "text/css");
default: return Content("body{font-family: SimSong; font-size:1.2em}", "text/css");
}
}

(三)JSON 类型

1
2
3
4
public ActionResult JsonTest()
{
return Json(new { name = "Kangkang", country = "China", email = "kangkang@163.com" }, JsonRequestBehavior.AllowGet);
}

(四)图片多媒体类型

1
2
3
4
5
public ActionResult ImageTest(string id)
{
string path = Server.MapPath($"/images/{id}.gif");
return File(path, "image/gif");
}

(五)Javascript 脚本类型

1
2
3
4
public ActionResult JavaScriptTest()
{
return JavaScript("alert('test js.');");
}

(六)文件类型(下载)

1
2
3
4
5
6
public ActionResult FileTest(string id)
{
string fileName = "可爱.gif";// 客户端保存的文件名
string filePath = Server.MapPath($"/images/1.gif");// 路径
return File(new FileStream(filePath, FileMode.Open), "image/gif", fileName);
}

注意:

  • FileContentResult:是针对文件内容创建的 FileResult,它只是调用当前 HttpResponseOutputStream 属性的 Write 方法直接将表示文件内容的字节数组写入响应输出流。
  • FilePathResult:是一个根据物理文件路径创建 FileResult
  • FileStreamResult:允许我们通过一个用于读取文件内容的流来创建 FileResult

可以参考: 了解 ASP.NET MVC 几种 ActionResult 的本质:FileResult

(七)返回 null 或者 void

1
2
3
4
public ActionResult Empty()
{
return null;
}

(八)返回未经授权浏览状态

1
2
3
4
public ActionResult HttpUnauthorizedResult()
{
return new HttpUnauthorizedResult();
}

注意: 响应给客户端错误代码 401(未经授权浏览状态),如果程序启用了 Forms 验证,并且客户端没有任何身份票据,则会跳转到指定的登录页。

(九)页面跳转

1
2
3
4
5
public ActionResult Redirect()
{
// 直接返回指定的url地址
return Redirect("http://www.google.com");
}
  • RedirectToRouteResult:直接使用 Action Name 进行跳转,也可以加上 ControllerName 以及参数。
  • RedirectToActionResult:指定路由进行跳转。
1
2
3
4
5
6
7
8
9
public ActionResult RedirectActionResult()
{
return RedirectToAction("Index", "Home", new { id = 1, name = "Kangkang" });
}

public ActionResult RedirectResult()
{
return RedirectToRoute("Default", new { controller = "Home", action = "Index" });
}

总结: 这些返回类型的共同点,那便是对 Action 有一定的要求:

  • 必须是一个 public 方法
  • 必须是实例方法
  • 不能被重载
  • 必须返回 ActionResult 类型

视图 View

mvc2

Views 文件夹下常用文件种类

文件类型 扩展名 概述
HTML .htmlhtm 静态 html 文件;
在开发中通用性最高的页面;
属于 ASP.NET MVC 常用页面;
Razor 文件 .cshtml 动态 MVC Razor 文件;
ASP.NET MVC 中,属于常用文件;
采用 Razor 语法格式;
基本取代 .aspx 文件;
WebForm 文件 .aspx 动态 ASPX 文件;
属于 WebForm 架构文件;
ASP.NET MVC 中,也可以用该页面布局页面但不推荐;
ASP.NET MVC 中基本被 cshtml 文件取代
ASP 文件 .asp 传统 ASP 文件,目前已过时;
发展:asp=>aspx=>cshtml
对应架构:ASP=>ASP.NET WebForm=>ASP.NET MVC

分析:

  • ASP.NET MVC 页面基本被放在 Views 文件夹下;
  • 利用 ASP.NET MVC 模板生成框架,Views 文件夹下的默认页面为 .cshtml 页面;
  • ASP.NET MVC 默认页面为 Razor 格式的页面,因此默认页面为 .cshtml 页面;
  • ASP.NET MVC 中,支持 WebForm 页面,即 .aspx 页面;
  • ASP.NET MVC 中,支持静态 html 页面;

默认 Views 文件夹包含内容

文件夹名称 概述
Account 文件夹 包含用于注册并登录用户账户的页面
Home 文件夹 存储首页和 About 页面信息
Share 文件夹 存储控制器间分享的视图,如布局页面和模板页等
_ViewStart.cshtml 程序最开始执行的页面

分析:

  • 这里没添加 Account 控制器;
  • 默认约定:在 Controllers 新增一个控制器,就会默认地在 Views 文件夹下新增一个视图文件夹,用来存放该控制器添加的视图,如上图中增加 Home 控制器,在 Views 下就自动新增加 Home 文件,用来存放是 Home 控制器视图;

视图种类

(一)起始视图:_ViewStart.cshtml

1
2
3
4
5
6
7
8
@*@{
Layout = "~/Views/Shared/_Layout.cshtml";
}*@

<div>
<h2>这里是 _ViewStart.cshtml 视图</h2>
<p>原则上程序运行时,这个视图首先被运行,其他视图在这个视图之后被运行。</p>
</div>

浏览 http://127.0.0.1/Home/Index

mvc8

(二)布局视图:_Layout.cshtml

首先将 _ViewStart.cshtml 文件还原修改其指向的 _LayoutDemo.cshtml 文件

1
2
3
@{
Layout = "~/Views/Shared/_LayoutDemo.cshtml";
}

添加 _LayoutDemo.cshtml 文件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
@{
ViewBag.Title = "LayoutDemo";
}
<!DOCTYPE html>

<html>
<head>
<meta name="viewport" content="width=device-width" />
<title>@ViewBag.Title</title>
</head>
<body>
<h2>---如下内容来源为视图---</h2>
<div>
@RenderBody()
</div>
<h2>---如下内容为Footer区---</h2>
<div>
<footer>Copyright by John Sun.</footer>
</div>
</body>
</html>

重新浏览 http://127.0.0.1/Home/Index

mvc7

  • _Layout.cshtml 基本结构就是 HTML 基本结构(其实 .aspx.cshtml 结构,均是 html 结构);
  • _LayoutDemo.cshtml 文件一个后台代码:@RenderBody()@RenderBody() 表示视图体,此外还有 @RenderSection() 表示部分视图和节点;

(三)弱类型视图

Controller 向 View 传递少量数据,一般情况,我们可以归为两大类别:弱类别传递(ViewBag,ViewData,TempData)和强类别传递(强类型视图)。然而,在实际操作中,当涉及大量数据时,弱类别就显得不是那么方便,此时,一般采用强类型视图。强类型视图一般由三部分构成,即控制器层,视图层和模型层,三者之间调用关系可表示为:

mvc3

(1)ViewData 和 TempData

ViewData 只在当前 Action 中有效,生命周期和 View 相同;

TempData 的数据至多只能经过一次 Controller 传递,并且每个元素至多只能被访问一次,访问以后,自动被删除。

TempData 一般用于临时的缓存内容或抛出错误页面时传递错误信息,可以将 TempData 在使用之前存储到相应的 ViewData 中以备循环使用。

TempData 保存在 Session 中,Controller 每次执行请求的时候,会从 Session 中先获取 TempData,而后清除 Session,获取完 TempData 数据,虽然保存在内部字典对象中,但是其集合中的每个条目访问一次后就从字典表中删除。具体代码层面,TempData 获取过程是通过 SessionStateTempDataProvider.LoadTempData 方法从 ControllerContext 的 Session 中读取数据,而后清除 Session,故 TempData 只能跨 Controller 传递一次。

HomeController.cs

1
2
3
4
5
6
7
8
public ActionResult Index()
{
ViewBag.Message = "Welcome to ASP.NET MVC!";
ViewData["myName"] = "我的名字";
TempData["myAgeOne"] = "26岁";
TempData["myAgeTwo"] = "27岁";
return View();
}

Index.cshtml 文件

1
2
3
姓名:@ViewData["myName"]
<br />
年龄1:@TempData["myAgeOne"]

About.cshtml 文件

1
2
3
4
5
姓名:@ViewData["myName"]
<br />
年龄1:@TempData["myAgeOne"]
<br />
年龄2:@TempData["myAgeTwo"]

(2)ViewBag 和 ViewData

1
ViewBag.Name = ViewData["Name"];
ViewData ViewBag
Key/Value 集合 dynamic 类型对象
ASP.NET MVC 1 就有 ASP.NET MVC 3 才有
基于 ASP.NET 3.5 基于 ASP.NET 4.0.NET Framework
ViewData 较快 ViewBag 较慢
视图中查询数据需要转换合适的类型 视图中查询数据不需要类型转换
有一些类型转换代码 可读性好

(四)强类型视图

增加实体类 UserInfo.cs

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
using System;
using System.Collections.Generic;
using System.Linq;
using System.Web;

namespace JohnSun.MVC5.Web.Models
{
public class UserInfo
{
public int UserId { get; set; }
public string UserName { get; set; }
public string Country { get; set; }
public string Hobby { get; set; }
public string Email { get; set; }
}
}

HomeController.cs 增加 Action 方法

1
2
3
4
5
6
7
8
9
public ActionResult ModelTest()
{
return View(new List<UserInfo>
{
new UserInfo(){ UserId = 1, UserName = "Kangkang", Country = "China", Hobby = "Basketball", Email = "kangkang@163.com" },
new UserInfo(){ UserId = 2, UserName = "Mary", Country = "America", Hobby = "Football", Email = "mary@gmail.com" },
new UserInfo(){ UserId = 3, UserName = "Jane", Country = "Canada", Hobby = "Compute game", Email = "jane@yahoo.com" },
});
}

增加视图 ModelTest.cshtml

1
2
3
4
5
6
7
8
9
10
@model IEnumerable<JohnSun.MVC5.Web.Models.UserInfo>
@{
ViewBag.Title = "ModelTest";
}

<h2>ModelTest</h2>
@foreach (var info in Model)
{
<p>@($"{info.UserId} {info.UserName} {info.Country}")</p>
}

运行效果:

mvc4

(五)分布页

我们在 /Views/Shared 文件夹下创建一个分布页 _PartialPageDemo.cshtml,并向该页面中添加一段代码:

1
<h1 style="color:red;">我是分布页</h1>

HomeController.cs 文件中增加返回视图的方法

1
2
3
4
public ActionResult TestPartialPage()
{
return PartialView();
}

在 /Views/Home 增加对应视图文件 TestPartialPage.cshtml

1
<h1 style="color:red;">我是供控制器调用的分布页</h1>

Index.cshtml 中调用

1
2
3
4
5
6
7
@{
ViewBag.Title = "Home Page";
}

@Html.Partial("~/Views/Shared/_PartialPageDemo.cshtml")
@Html.Partial("~/Views/Home/TestPartialPage.cshtml")
@Html.Action("TestPartialPage")

显示效果如下:

mvc5

调用分布页的几种方式:

  • @Html.Partial() 提供分布页路径
  • @Html.Action() 提供控制器返回视图的方法
  • 通过 Ajax 方式调用

区域 Area

ASP.NET MVC 有预定义的目录规则,框架根据这些目录规则去加载各种类。

在 MVC 单项目中,随着业务越来越复杂多样,我们会希望按照功能对代码按文件夹分门别类。

如果在默认的目录结构下业务混合,这样不方便管理和维护;如果另开新项目,又比较散乱。那么 MVC 有没有这样一种机制来相对独立这些模块呢?答案是肯定的,这就是 MVC 的 Area 区域技术,用来实现在一个 MVC 项目中组织和维护多个相对独立的模块。

在 VS 中右键单击项目,在弹出的菜单中选择“添加 (A)”->“Area…”,在弹出的对话框中输入区域名称(遵守 C# 标示符命名规则)即可(比如输入 System),VS 将自动在根目录创建 Areas 文件夹,此文件夹下每个独立的 Area 一个文件夹,System 文件夹内也是一样的 Models、Controllers、Views 结构。

mvc6

唯一不同的是多了一个 SystemAreaRegistration.cs(区域注册类),用于向 MVC 框架注册路由等信息,Global.asax.cs 中会自动调用该类的 RegisterArea 方法。新建 Area 后 VS 自动创建相关目录结构,按需修改 SystemAreaRegistration 路由即可。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
using System.Web.Mvc;

namespace JohnSun.MVC5.Web.Areas.System
{
public class SystemAreaRegistration : AreaRegistration
{
public override string AreaName
{
get
{
return "System";
}
}

public override void RegisterArea(AreaRegistrationContext context)
{
context.MapRoute(
"System_default",
"System/{controller}/{action}/{id}",
new { action = "Index", id = UrlParameter.Optional }
);
}
}
}

注意: 当 Area 中控制器与 MVC 中控制器同名,比如 HomeController,此时访问首页会异常提示:找到多个与名为“Home”的控制器匹配的类型。如果为此请求({controller}/{action}/{id})提供服务的路由没有指定命名空间以搜索与此请求相匹配的控制器,则会发生这种情况。如果是这样,请通过调用带有 namespaces 参数的 MapRoute 方法的重载来注册此路由。

此时修改 RouteConfig.cs 文件中注册路由的方法,添加 HomeController 的命名空间指向即可。

1
2
3
4
5
6
routes.MapRoute(
name: "Default",
url: "{controller}/{action}/{id}",
defaults: new { controller = "Home", action = "Index", id = UrlParameter.Optional },
namespaces: new string[] { "JohnSun.MVC5.Web.Controllers" }
);

快速上手入门视频:微软的船新框架 ASP.NET Core - 10 分钟讲完 MVC 基础 by Anduin

TCP协议之服务端

之前介绍过串口通讯,相对来说串口通讯是比较简单的,不用关心另外一端的连接状态,只需要打开串口,监听接收事件即可。发送也很简单,一个Write方法控制。

但是串口的问题也很明显,我们无法得知另外一端是否可用,只能通过通讯协议里进行指定,设置心跳包或者指定接收后进行响应。打开监听简单,但是维持监听判断监听状态就相对麻烦一些。

对于开发来说,Socket通信(网口)要麻烦一些,因为需要了解TCP/UPD协议,还区分了服务端与客户端,但是硬件连入局域网就可以通信,所以使用Socket通信也是一种主流的解决方案。

关于基础的知识比如什么是Socket通信,什么是TCP协议,这里也不做展开,有兴趣的可以自己搜索相关资料了解。

Socket通信调试工具

个人使用较多的是TCP/UDP Socket 调试工具

tcpserver1

使用中可能存在的一些问题:

  1. 偶发关闭了另外一端,会导致调试工具异常;
  2. 编码默认本地字符集,所以经常导致中文乱码,需要收集16进制自行解析编码;
  3. 在收集数据的时候,接收区会添加接收的时间,解析时需要我们自己将这些字符替换掉;

当然这些基本也不会影响我们的调试工作。

单客户端

首先我们使用调试工具进行模拟,调试工具中创建一个TCP Server,监听端口根据自己需求设置。因为我们没有监听指定端口的需求,只是进行模拟测试,我们这里设置监听5555端口进行测试:

tcpserver11

如上图,创建的TCP Server自动启动了监听。

服务端创建成功,我们就需要创建一个客户端进行连接,来进行数据的通讯,同样的我们选择TCP Client进行创建,并连接到本机的5555端口:

tcpserver2

这时我们就可以点击TCP Client下打开的客户端向服务端发送消息,或选择TCP Server服务端下连接的客户端,向指定客户端发送消息:

tcpserver3

那么,我们应该怎样在程序中打开服务端,监听客户端请求呢?可以参考以下代码:

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
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
using System;
using System.Collections.Generic;
using System.Net;
using System.Net.Sockets;
using System.Threading.Tasks;

namespace SocketTest
{
class Program
{
static void Main(string[] args)
{
TcpServerTest.Test();
}
}

public static class TcpServerTest
{
public static void Test()
{
// 服务端地址默认本地
IPAddress address = IPAddress.Parse("127.0.0.1");
// 提醒用户输入要监听的端口
Console.Write("请输入您要监听的端口:");
string input = Console.ReadLine();
int port;
while (!int.TryParse(input, out port))
{
Console.Write("您输入的端口错误,请重新输入:");
input = Console.ReadLine();
}

try
{
// 创建并打开监听
TcpListener listener = new TcpListener(address, port);
listener.Start();


ConsoleLogger.Info($"启用服务端 {listener.Server.LocalEndPoint} 成功,等待客户端连接。");

// 等待客户端的连接请求
TcpClient client = listener.AcceptTcpClient();

ConsoleLogger.Info($"客户端连接成功:{client.Client.RemoteEndPoint.ToString()}");

// 建立与客户端的数据流
using NetworkStream stream = client.GetStream();
byte[] buffer = new byte[1024];//设置缓存长度
while (true)
{
try
{
int length = stream.Read(buffer, 0, buffer.Length);
if (length > 0)
{
ConsoleLogger.Info($"[{client.Client.RemoteEndPoint}]接收到数据: {Console.InputEncoding.GetString(buffer, 0, length)}");

// 回发接收到的消息给客户端
client.Client.Send(buffer, 0, length, SocketFlags.None);
}
else
{
ConsoleLogger.Info($"客户端[{client.Client.RemoteEndPoint}]关闭");
break;
}
}
catch (Exception exc)
{
ConsoleLogger.Error($"客户端[{client.Client.RemoteEndPoint}]引发未处理异常:{exc.Message}");
break;
}
}
}
catch (Exception exc)
{
ConsoleLogger.Error($"TCP服务端测试出现了未经处理的异常:{exc.Message}");
}
ConsoleLogger.Info("测试结束");
}
}

public class ConsoleLogger
{
private static readonly object sync = new object();
public static void Error(string message)
{
lock (sync)
{
ConsoleColor backgroundColor = Console.BackgroundColor;
ConsoleColor foregroundColor = Console.ForegroundColor;
Console.BackgroundColor = ConsoleColor.White;
Console.ForegroundColor = ConsoleColor.Red;
Console.WriteLine($"{DateTime.Now.ToString("HH:mm:ss")} Error: {message}");
Console.BackgroundColor = backgroundColor;
Console.ForegroundColor = foregroundColor;
}
}

public static void Info(string message)
{
lock (sync)
{
ConsoleColor backgroundColor = Console.BackgroundColor;
ConsoleColor foregroundColor = Console.ForegroundColor;
Console.BackgroundColor = ConsoleColor.Black;
Console.ForegroundColor = ConsoleColor.White;
Console.WriteLine($"{DateTime.Now.ToString("HH:mm:ss")} Info: {message}");
Console.BackgroundColor = backgroundColor;
Console.ForegroundColor = foregroundColor;
}
}
}
}

如上例子展示了一个服务端的创建与监听过程,连接成功后如果接收到客户端的消息,“复读”该消息发送给客户端。

我们可以进行测试,首先运行项目,在控制台创建服务端,并开启监听。服务端创建成功后,使用Socket的调试工具创建一个TCP客户端,与服务端进行通讯测试,结束后断开连接:

tcpserver6

多客户端连接

如上例子正如测试结果一样,只能接受一个客户端的连接,当该客户端连接成功后,其他客户端如果再申请与服务端进行连接,虽然不会被拒绝,但是发送消息将无法获得响应。

关于如何建立与多个客户端连接,这里有两种方案。

循环

使用while循环,配合AcceptTcpClient方法,不停的监听来自客户端的请求。

循环中,一旦客户端连接成功,开启一个线程处理服务端与客户端的数据交互。然后循环继续,等待下一个客户端连接。

这里我们将测试用例中的Test方法进行改写:

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
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
public static void Test()
{
// 服务端地址默认本地
IPAddress address = IPAddress.Parse("127.0.0.1");
// 提醒用户输入要监听的端口
Console.Write("请输入您要监听的端口:");
string input = Console.ReadLine();
int port;
while (!int.TryParse(input, out port))
{
Console.Write("您输入的端口错误,请重新输入:");
input = Console.ReadLine();
}

try
{
// 创建并打开监听
TcpListener listener = new TcpListener(address, port);
listener.Start();

ConsoleLogger.Info($"启用服务端 {listener.Server.LocalEndPoint} 成功,等待客户端连接。");

// 等待客户端的连接请求
while (true)
{
TcpClient client = listener.AcceptTcpClient();
ConsoleLogger.Info($"客户端连接成功:{client.Client.RemoteEndPoint.ToString()}");

// 建立与客户端的数据流
Task.Factory.StartNew(() =>
{
using NetworkStream stream = client.GetStream();
byte[] buffer = new byte[1024];//设置缓存长度
while (true)
{
try
{
int length = stream.Read(buffer, 0, buffer.Length);
if (length > 0)
{
ConsoleLogger.Info($"[{client.Client.RemoteEndPoint}]接收到数据: {Console.InputEncoding.GetString(buffer, 0, length)}");

// 回发接收到的消息给客户端
client.Client.Send(buffer, 0, length, SocketFlags.None);
}
else
{
ConsoleLogger.Info($"客户端[{client.Client.RemoteEndPoint}]关闭");
break;
}
}
catch (Exception exc)
{
ConsoleLogger.Error($"客户端[{client.Client.RemoteEndPoint}]引发未处理异常:{exc.Message}");
break;
}
}
});
}
}
catch (Exception exc)
{
ConsoleLogger.Error($"TCP服务端测试出现了未经处理的异常:{exc.Message}");
}
ConsoleLogger.Info("测试结束");
}

测试效果如下:

tcpserver7

递归

递归本质上和循环类似,其利用的是异步等待TCP客户端连接方法BeginAcceptTcpClient的回调。

我们可以在回调中,使用EndAcceptTcpClient来异步的接受传入的连接,并创建客户端实例。而创建成功后,我们可以继续调用回调函数,来异步的接收下一个客户端的连接请求。

下面例子将回调函数写为匿名函数,实际开发中也可以使用普通函数:

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
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
public static void Test()
{
// 服务端地址默认本地
IPAddress address = IPAddress.Parse("127.0.0.1");
// 提醒用户输入要监听的端口
Console.Write("请输入您要监听的端口:");
string input = Console.ReadLine();
int port;
while (!int.TryParse(input, out port))
{
Console.Write("您输入的端口错误,请重新输入:");
input = Console.ReadLine();
}

try
{
// 创建并打开监听
TcpListener listener = new TcpListener(address, port);
listener.Start();

ConsoleLogger.Info($"启用服务端 {listener.Server.LocalEndPoint} 成功,等待客户端连接。");

// 异步等待客户端的连接请求的回调
AsyncCallback callback = null;
callback = new AsyncCallback((asyncResult) =>
{
// 因为没有使用函数而是使用匿名方法 这里也可以直接用可以访问到的局部变量listener
if (asyncResult.AsyncState is TcpListener tcpListener)
{
TcpClient client = tcpListener.EndAcceptTcpClient(asyncResult);
ConsoleLogger.Info($"客户端连接成功:{client.Client.RemoteEndPoint.ToString()}");

// 建立与客户端的数据流
Task.Factory.StartNew(() =>
{
using NetworkStream stream = client.GetStream();
byte[] buffer = new byte[1024];//设置缓存长度
while (true)
{
try
{
int length = stream.Read(buffer, 0, buffer.Length);
if (length > 0)
{
ConsoleLogger.Info($"[{client.Client.RemoteEndPoint}]接收到数据: {Console.InputEncoding.GetString(buffer, 0, length)}");

// 回发接收到的消息给客户端
client.Client.Send(buffer, 0, length, SocketFlags.None);
}
else
{
ConsoleLogger.Info($"客户端[{client.Client.RemoteEndPoint}]关闭");
break;
}
}
catch (Exception exc)
{
ConsoleLogger.Error($"客户端[{client.Client.RemoteEndPoint}]引发未处理异常:{exc.Message}");
break;
}
}
});

tcpListener.BeginAcceptTcpClient(callback, tcpListener);
}
});

// 异步等待客户端的连接请求
listener.BeginAcceptTcpClient(callback, listener);

Console.ReadKey();//避免控制台退出
}
catch (Exception exc)
{
ConsoleLogger.Error($"TCP服务端测试出现了未经处理的异常:{exc.Message}");
}
ConsoleLogger.Info("测试结束");
}

测试效果如下:

tcpserver8

由上图可以看出两者差别并不大,主要的区别在于循环是一个同步方法,而递归使用的是异步方法。

向指定客户端发送消息

控制台中如果控制向客户端发送消息,肯定要允许我们输入,来客户端并可以输入需要发送的消息。

所以这里我们需要将等待客户端连接请求与提示输入放在两个线程中进行,这里我们直接使用多客户端中“递归”的例子进行改造(当然也可以使用循环的例子,开启一个线程将循环放到另外一个线程中执行)。

因为我们的测试方法是静态方法,所以首先我们需要定义一个静态集合变量来存储连接的客户端。

客户端连接成功后,我们需要将客户端存储到集合中,方便我们遍历,当客户端断开连接后,我们需要将客户端从该集合中移除。因为涉及到多线程,这里建议使用线程安全的集合,或者在操作集合的位置加锁。

调整后代码如下:

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
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Net;
using System.Net.Sockets;
using System.Text.RegularExpressions;
using System.Threading.Tasks;

namespace SocketTest
{
class Program
{
static void Main(string[] args)
{
TcpServerTest.Test();
}
}

public static class TcpServerTest
{
static readonly object sync = new object();
static List<TcpClient> clients = new List<TcpClient>();
public static void Test()
{
// 服务端地址默认本地
IPAddress address = IPAddress.Parse("127.0.0.1");
// 提醒用户输入要监听的端口
Console.Write("请输入您要监听的端口:");
string input = Console.ReadLine();
int port;
while (!int.TryParse(input, out port))
{
Console.Write("您输入的端口错误,请重新输入:");
input = Console.ReadLine();
}

try
{
// 创建并打开监听
TcpListener listener = new TcpListener(address, port);
listener.Start();

ConsoleLogger.Info($"启用服务端 {listener.Server.LocalEndPoint} 成功,等待客户端连接。");

// 异步等待客户端的连接请求的回调
AsyncCallback callback = null;
callback = new AsyncCallback((asyncResult) =>
{
// 因为没有使用函数而是使用匿名方法 这里也可以直接用可以访问到的局部变量listener
if (asyncResult.AsyncState is TcpListener tcpListener)
{
TcpClient client = tcpListener.EndAcceptTcpClient(asyncResult);
lock (sync)
{
clients.Add(client);
}
ConsoleLogger.Info($"客户端连接成功:{client.Client.RemoteEndPoint.ToString()}");

// 建立与客户端的数据流
Task.Factory.StartNew(() =>
{
using NetworkStream stream = client.GetStream();
byte[] buffer = new byte[1024];//设置缓存长度
while (true)
{
try
{
int length = stream.Read(buffer, 0, buffer.Length);
if (length > 0)
{
ConsoleLogger.Receive($"[{client.Client.RemoteEndPoint}]接收到数据: {Console.InputEncoding.GetString(buffer, 0, length)}");
}
else
{
ConsoleLogger.Info($"客户端[{client.Client.RemoteEndPoint}]关闭");
lock (sync)
{
if (clients.Contains(client))
{
clients.Remove(client);
}
}
break;
}
}
catch (Exception exc)
{
ConsoleLogger.Error($"客户端[{client.Client.RemoteEndPoint}]引发未处理异常:{exc.Message}");
lock (sync)
{
if (clients.Contains(client))
{
clients.Remove(client);
}
}
break;
}
}
});

tcpListener.BeginAcceptTcpClient(callback, tcpListener);
}
});

// 异步等待客户端的连接请求
listener.BeginAcceptTcpClient(callback, listener);

while (true)
{
ConsoleLogger.Prompt("请输入选择你要执行的操作:\r\nall:查看所有客户端列表\r\nall {message}:向所有客户端发送消息\r\nClientNo {message}:向指定客户端发送消息");
input = Console.ReadLine();
if (string.Equals(input, "all", StringComparison.OrdinalIgnoreCase))
{
lock (sync)
{
if (clients.Count == 0)
{
ConsoleLogger.Error($"当前还未与客户端建立连接。");
}
else
{
ConsoleLogger.Info($"当前共有{clients.Count}个客户端连接:");
for (int i = 0; i < clients.Count; i++)
{
ConsoleLogger.Info($"Client No {i}{clients[i].Client.RemoteEndPoint}");
}
}
}
}
else if (Regex.IsMatch(input, "^all (.+)$", RegexOptions.IgnoreCase))
{
Match match = Regex.Match(input, "^all (.+)$", RegexOptions.IgnoreCase);
string message = match.Groups[1].Value;

lock (sync)
{
if (clients.Count == 0)
{
ConsoleLogger.Error($"当前还未与客户端建立连接。");
}
else
{
for (int i = 0; i < clients.Count; i++)
{
try
{
ConsoleLogger.Send($"向客户端[{clients[i].Client.RemoteEndPoint}]发送消息:{message}");
clients[i].Client.Send(Console.InputEncoding.GetBytes(message));
}
catch (Exception exc)
{
ConsoleLogger.Error($"向客户端[{clients[i].Client.RemoteEndPoint}]发送消息失败:{exc.Message}");
}
}
}
}
}
else if (Regex.IsMatch(input, "^(\\d{1,7}) (.+)$", RegexOptions.IgnoreCase))
{
Match match = Regex.Match(input, "^(\\d{1,7}) (.+)$", RegexOptions.IgnoreCase);
int clientIndex = int.Parse(match.Groups[1].Value);
string message = match.Groups[2].Value;

lock (sync)
{
if (clients.Count == 0)
{
ConsoleLogger.Error($"当前还未与客户端建立连接。");
}
else
{
if (clients.Count > clientIndex)
{
try
{
ConsoleLogger.Send($"向客户端[{clients[clientIndex].Client.RemoteEndPoint}]发送消息:{message}");
clients[clientIndex].Client.Send(Console.InputEncoding.GetBytes(message));
}
catch (Exception exc)
{
ConsoleLogger.Error($"向客户端[{clients[clientIndex].Client.RemoteEndPoint}]发送消息失败:{exc.Message}");
}
}
else
{
ConsoleLogger.Error($"该索引对应的客户端不存在,可能已经被断开连接。");
}
}
}
}
else
{
ConsoleLogger.Error($"内容输入错误,请按照要求重新输入你要进行的操作。");
}
}
}
catch (Exception exc)
{
ConsoleLogger.Error($"TCP服务端测试出现了未经处理的异常:{exc.Message}");
}
ConsoleLogger.Info("测试结束");
}
}

public class ConsoleLogger
{
private static readonly object sync = new object();

public static void Send(string message)
{
lock (sync)
{
ConsoleColor backgroundColor = Console.BackgroundColor;
ConsoleColor foregroundColor = Console.ForegroundColor;
Console.BackgroundColor = ConsoleColor.Black;
Console.ForegroundColor = ConsoleColor.Yellow;
Console.WriteLine($"{DateTime.Now.ToString("HH:mm:ss")} Info: {message}");
Console.BackgroundColor = backgroundColor;
Console.ForegroundColor = foregroundColor;
}
}

public static void Receive(string message)
{
lock (sync)
{
ConsoleColor backgroundColor = Console.BackgroundColor;
ConsoleColor foregroundColor = Console.ForegroundColor;
Console.BackgroundColor = ConsoleColor.Black;
Console.ForegroundColor = ConsoleColor.Gray;
Console.WriteLine($"{DateTime.Now.ToString("HH:mm:ss")} Info: {message}");
Console.BackgroundColor = backgroundColor;
Console.ForegroundColor = foregroundColor;
}
}

public static void Prompt(string message)
{
lock (sync)
{
ConsoleColor backgroundColor = Console.BackgroundColor;
ConsoleColor foregroundColor = Console.ForegroundColor;
Console.BackgroundColor = ConsoleColor.Black;
Console.ForegroundColor = ConsoleColor.Green;
Console.WriteLine(message);
Console.BackgroundColor = backgroundColor;
Console.ForegroundColor = foregroundColor;
}
}

public static void Error(string message)
{
lock (sync)
{
ConsoleColor backgroundColor = Console.BackgroundColor;
ConsoleColor foregroundColor = Console.ForegroundColor;
Console.BackgroundColor = ConsoleColor.Black;
Console.ForegroundColor = ConsoleColor.Red;
Console.WriteLine($"{DateTime.Now.ToString("HH:mm:ss")} Error: {message}");
Console.BackgroundColor = backgroundColor;
Console.ForegroundColor = foregroundColor;
}
}

public static void Info(string message)
{
lock (sync)
{
ConsoleColor backgroundColor = Console.BackgroundColor;
ConsoleColor foregroundColor = Console.ForegroundColor;
Console.BackgroundColor = ConsoleColor.Black;
Console.ForegroundColor = ConsoleColor.White;
Console.WriteLine($"{DateTime.Now.ToString("HH:mm:ss")} Info: {message}");
Console.BackgroundColor = backgroundColor;
Console.ForegroundColor = foregroundColor;
}
}
}
}

测试效果如下:

tcpserver9