xiyuan技术圈

溪源的技术博客,技术的不断进取之路


  • Home

  • 技术

  • 随笔

  • 读书

  • 管理

  • 归档

一种在.NET Core中重构遗留代码的利器

Posted on 2020-10-17 | Edited on 2021-04-25 | In 技术

Golden Master Pattern :一种在.NET Core中重构遗留代码的利器

在软件开发领域中工作的任何人都将需要在旧代码中添加功能,这些功能可能是从先前的团队继承而来的,您需要对其进行紧急修复。

可以在文献中找到许多遗留代码的定义,我更喜欢的定义是:“通过遗留代码,我们指的是我们害怕改变的有利可图的代码”。

该定义包含两个基本概念:

  1. 该代码必须有利可图。如果不是这样,我们将无意对其进行更改。
  2. 它必须引起对修改它的恐惧,因为我们可以引入新的bug或依赖影子的东西。

在以下情况下,更容易出错:

  • 测试未涵盖该代码。
  • 代码不干净;不遵守单一责任原则。
  • 该代码的设计不正确,或者随着时间的流逝其结构变得不合理:对一段代码进行更改可能会产生一些副作用。
  • 您没有时间全面了解正在修改的内容。

测试是我们作为开发人员可用的强大武器。这些测试为我们提供了结果的安全性,并且是一种快速检测错误的方法。但是,我们如何测试未知的代码?构建单元测试套件将为我们提供有关该项目的深入知识,但它将使长时间保持高成本。如果我们无法测试细节,则可以使用Characterization Test,它是描述软件行为的测试。

在这种情况下起重要作用的模式是“ 黄金大师模式”。基本思想很简单:如果我们无法深入了解,我们需要一些有关整个执行过程的指标。我们捕获正确执行的输出(stdout,图像,日志文件等),这就是我们的Golden Master,可用于预期输出。如果当前执行的输出匹配,我们可以确信我们的更改没有引入新的错误。

为了展示Golden Master Pattern的用法,让我们从一个示例开始(完整的代码可以在此处找到)。我们公司开发了用于命令行的游戏,包括井字游戏(该游戏的实现从此处获取),我们的老板要求我们更改游戏以提供调整游戏板尺寸的能力。让我们看一下代码:

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
namespace Tris
{
public class Game
{
//making array and
//by default I am providing 0-9 where no use of zero
static char[] arr = { '0', '1', '2', '3', '4', '5', '6', '7', '8', '9' };
static int player = 1; //By default player 1 is set
static int choice; //This holds the choice at which position user want to mark
// The flag variable checks who has won if its value is 1 then someone has won the match if -1 then Match has Draw if 0 then match is still running
static int flag = 0;

public static void run()
{
do
{
Console.Clear();// whenever loop will be again start then screen will be clear
Console.WriteLine("Player1:X and Player2:O");
Console.WriteLine("\n");
if (player % 2 == 0)//checking the chance of the player
{
Console.WriteLine("Player 2 Chance");
}
else
{
Console.WriteLine("Player 1 Chance");
}

Console.WriteLine("\n");
Board();// calling the board Function
choice = int.Parse(Console.ReadLine());//Taking users choice

// checking that position where user want to run is marked (with X or O) or not
if (arr[choice] != 'X' && arr[choice] != 'O')
{
if (player % 2 == 0) //if chance is of player 2 then mark O else mark X
{
arr[choice] = 'O';
player++;
}
else
{
arr[choice] = 'X';
player++;
}
}
else //If there is any position where user wants to run and that is already marked then show message and load board again
{
Console.WriteLine("Sorry the row {0} is already marked with {1}", choice, arr[choice]);
Console.WriteLine("\n");
Console.WriteLine("Please wait 2 second board is loading again.....");
Thread.Sleep(2000);
}
flag = CheckWin();// calling of check win
} while (flag != 1 && flag != -1);// This loof will be run until all cell of the grid is not marked with X and O or some player is not winner

Console.Clear();// clearing the console

Board();// getting filled board again

if (flag == 1)// if flag value is 1 then someone has win or means who played marked last time which has win
{
Console.WriteLine("Player {0} has won", (player % 2) + 1);
}

else// if flag value is -1 the match will be drawn and no one is the winner
{
Console.WriteLine("Draw");
}

Console.ReadLine();
}

// Board method which creats board
private static void Board()
{
Console.WriteLine(" | | ");
Console.WriteLine(" {0} | {1} | {2}", arr[1], arr[2], arr[3]);
Console.WriteLine("_____|_____|_____ ");
Console.WriteLine(" | | ");
Console.WriteLine(" {0} | {1} | {2}", arr[4], arr[5], arr[6]);
Console.WriteLine("_____|_____|_____ ");
Console.WriteLine(" | | ");
Console.WriteLine(" {0} | {1} | {2}", arr[7], arr[8], arr[9]);
Console.WriteLine(" | | ");
}

private static int CheckWin()
{
#region Horzontal Winning Condtion
//Winning Condition For First Row
if (arr[1] == arr[2] && arr[2] == arr[3])
{
return 1;
}

//Winning Condition For Second Row
else if (arr[4] == arr[5] && arr[5] == arr[6])
{
return 1;
}

//Winning Condition For Third Row
else if (arr[6] == arr[7] && arr[7] == arr[8])
{
return 1;
}

#endregion

#region vertical Winning Condtion

//Winning Condition For First Column
else if (arr[1] == arr[4] && arr[4] == arr[7])
{
return 1;
}

//Winning Condition For Second Column
else if (arr[2] == arr[5] && arr[5] == arr[8])
{
return 1;
}

//Winning Condition For Third Column
else if (arr[3] == arr[6] && arr[6] == arr[9])
{
return 1;
}

#endregion

#region Diagonal Winning Condition
else if (arr[1] == arr[5] && arr[5] == arr[9])
{
return 1;
}

else if (arr[3] == arr[5] && arr[5] == arr[7])
{
return 1;
}

#endregion

#region Checking For Draw

// If all the cells or values filled with X or O then any player has won the match
else if (arr[1] != '1' && arr[2] != '2' && arr[3] != '3' && arr[4] != '4' && arr[5] != '5' && arr[6] != '6' && arr[7] != '7' && arr[8] != '8' && arr[9] != '9')
{
return -1;
}

#endregion

else
{
return 0;
}
}
}
}

快速阅读后,代码看上去很混乱,职责没有正确分开,变量名也没有意义。

经过准确的阅读后,我们可以找到游戏板,该游戏板存储在“ static char [] arr”中。向阵列添加新元素没有任何效果,因为该阵列直接在PrintBoard和CheckWin函数中访问。现在我们知道要调整游戏板的大小,必须更改大部分代码。

创建一个新项目并运行游戏:

1
2
3
4
5
6
7
class Program
{
static void Main(string[] args)
{
Game.run();
}
}

一旦我们印刷了棋盘,游戏就会要求用户输入。我们可以通过从文件中读取输入来实现自动化。

1
2
3
4
5
6
7
8
9
10
11
12
class Program
{
private const string InputPath = "input.txt";

public static void Main(string[] args)
{
var input = new StreamReader(new FileStream(InputPath, FileMode.Open));
Console.SetIn(input);
Game.run();
input.Close();
}
}

所有输入的集合太大,无法使用蛮力测试。我们可以做的就是对输入进行采样。为此,我们考虑井字游戏的最终得分:

  • 玩家1获胜
  • 玩家2获胜
  • 绘制图形

选择覆盖这三种情况的最低限度的测试集,在文本文件中编写路径,并在golendenMaster文件夹中收集结果:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
class Program
{
private const string InputFolderPath = "input/";
private const string OutputFolderPath = "goldenMaster/";

public static void Main(string[] args)
{
int i = 1;
foreach (var filePath in Directory.GetFiles(InputFolderPath)) {
var input = new StreamReader(new FileStream(filePath, FileMode.Open));
var output = new StreamWriter(new FileStream(OutputFolderPath + "output" + i.ToString() + ".txt" , FileMode.CreateNew));
Console.SetIn(input);
Console.SetOut(output);
Game.run();
input.Close();
output.Close();
i++;
}
}
}

这三个结果文件代表了我们的Golden Master,我们可以在此基础上进行一些特性测试:

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
[Test]
public void WinPlayerOne()
{
inputPath = InputFolderPath + "input1.txt";
outputPath = OutputFolderPath + "output.txt";
var goldenMasterOutput = GoldenMasterOutput + "output1.txt";

var input = new StreamReader(new FileStream(inputPath, FileMode.Open));
var output = new StreamWriter(new FileStream(outputPath, FileMode.CreateNew));
Console.SetIn(input);
Console.SetOut(output);

Game.run();

input.Close();
output.Close();

Assert.True(AreFileEquals(goldenMasterOutput, outputPath));
}

private bool AreFileEquals(string expectedPath, string actualPath)
{
byte[] bytes1 = Encoding.Convert(Encoding.ASCII, Encoding.ASCII, Encoding.ASCII.GetBytes(File.ReadAllText(expectedPath)));
byte[] bytes2 = Encoding.Convert(Encoding.ASCII, Encoding.ASCII, Encoding.ASCII.GetBytes(File.ReadAllText(actualPath)));

return bytes1.SequenceEqual(bytes2);
}

只要测试是绿色的,我们就可以重构而不必担心破坏某些东西。一种可能的结果可能是:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
public static void run()
{
char[] board = { '0', '1', '2', '3', '4', '5', '6', '7', '8', '9' };
int actualPlayer = 1;

while (CheckWin(board) == 0)
{
PrintPlayerChoise(actualPlayer);
PrintBoard(board);
var choice = ReadPlayerChoise();
if (isBoardCellAlreadyTaken(board[choice]))
{
PrintCellIsAlreadyMarketMessage(board[choice], choice);
continue;
}
board[choice] = GetPlayerMarker(actualPlayer);
actualPlayer = UpdatePlayer(actualPlayer);
}

PrintResult(board, actualPlayer);
}

从这段代码中可以看出Board的概念及其职责。让我们尝试在新的Board类中提取行为。新Board应能够:

  • 印刷板
  • 标记玩家的选择
  • 检查是否有赢家

使用TDD(更多详情,请阅读这篇文章)制定一个可调整大小的Board(发现测试的完整代码在这里和阶级的一个位置)。现在尝试将它们插入游戏中,并检查Golden Master是否保持绿色:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
private const int Boardsize = 3;

public static void run()
{
Board board = new Board(Boardsize);
int actualPlayer = 1;

while (board.CheckWin() == -1)
{
PrintPlayerChoise(actualPlayer);
Console.WriteLine(board.Print());
var choice = ReadPlayerChoise();
if (!board.UpdateBoard(actualPlayer, choice))
{
PrintCellIsAlreadyMarketMessage(board.GetCellValue(choice), choice);
continue;
}
actualPlayer = UpdatePlayer(actualPlayer);
}

PrintResult(board, actualPlayer);
}

此时,我们可以恢复标准输入/标准输出并从用户那里读取电路板的尺寸:

1
2
3
4
5
6
7
8
9
class Program
{
public static void Main(string[] args)
{
Console.WriteLine("Insert Diagonal dimension of Board: ");
var boardSize = int.Parse(Console.ReadLine());
Game.run(boardSize);
}
}

如您所见,多亏了Golden Master Pattern,我们能够控制遗留代码并进行重构,而无需担心。但是,所有闪闪发光的东西都不是金子:在“噪声输出”的情况下使用Golden Master可能会很困难,“噪声输出”对于执行无用,但会随时间(例如时间戳记,线程名等)而变化。在这种情况下,我们可以过滤输出并仅考虑重要部分。

我希望它在您下次重写旧项目时对您有用:毕竟,我们担心我们的代码失去控制!

编写更好的C#代码的技巧

Posted on 2020-10-08 | Edited on 2021-04-25 | In 技术

编写更好的C#代码的技巧

编者导语

本文来自https://www.pluralsight.com,原文包含以下三篇文章:

《编写更好的C#代码简介》https://www.pluralsight.com/guides/introduction-to-writing-better-csharp-code

《编写更好的C#代码的技巧》https://www.pluralsight.com/guides/tips-for-writing-better-c-code

《有关编写更好的C#代码的更多技巧》https://www.pluralsight.com/guides/more-tips-for-writing-better-csharp-code

虽然本文仅介绍了C#6.0语言特性,而现在最新的C#已经到了9.0,但这些内容已经仍然常读常新。

一、简介

C#已从C#5更改为C#6,为使项目更具可读性,基于最佳标准的实践也得到了发展,。

本指南系列的目的是帮助您为在团队环境中运行的C#项目和.NET Framework应用程序编写更简洁的代码。在团队环境下,编写好的代码对开发人员可能更容易,因为编写的代码将由团队中其他开发人员使用,管理和更新,而代码质量往往取决于您个人团队的“哲学”和开发人员的编码实践。在这种情况下,最好的方法是遵循编码团队的准则,并为应用程序项目中的C#程序添加设计和风格,以使它们对读者更好。请注意,C#编译器并不关心您放入代码中的风格。但我们以一种使C#应用程序对读者来说看起来更简单,更清洁,将更容易的方式更深入地进行编程,同时保持代码开发的性能和效率。

在阅读本指南之前,您应该了解以下几点:

  1. 第6版对C#的改进
  2. .NET框架中的LINQ
  3. TaskC#中的异步编程和对象
  4. 使用C#进行的不安全编程,使您无法正常的使用内存管理
不专注于性能

应该注意的是,我不会谈论改变程序性能,提高效率或减少程序运行所花费的时间。通过编写简洁的C#代码,您可以在几秒钟内提高程序性能,但是以下技巧并不能保证您的代码性能更好。

为什么要编写整洁的代码?

您编写代码,编译器编译时没有警告也没有错误,代码很好。但是,如果其他人想读出该代码怎么办?如果有人后来需要为您或您所在的公司升级代码,该怎么办?看下面的代码:

1
2
3
4
5
6
public static void Main(string[] args) {
int x = 0;
x = Console.Read();

Console.WriteLine(x * 1.5);
}

该程序运行良好,系统中没有错误,应用程序也可以正常工作。但是您能告诉我该程序在现实生活中做什么吗?以下是可以做出的一些假设:

  1. 它只是乘以价值
  2. 就像奖金一样,它正在增加价值
  3. 是个人银行存款总额的利率
  4. 等等。

哪一个是真实的?没有人会知道。在这种情况下,最好编写出良好的代码,并记住遵循编程的基础。看下面的代码:

1
2
3
4
5
6
public static void Main(string[] args) {
int salary = 0;
salary = Console.Read();

Console.WriteLine(salary * 1.5);
}

这比以前的代码有意义吗?我们可以很容易地说这个代码将增加薪水的价值。请注意,仅通过改进代码,我们就能确保其他人可以比以前更快地理解它。

在本指南中,我不会向您展示如何遵循最佳原则。相反,我将以您已有的知识为基础,并教您如何充分利用C#程序。我将重点介绍如何在应用程序中编写良好的C#逻辑,因此您将看到通过以这种方式和结构编写程序,可以从应用程序中获得很多好处。

因此,让我们开始吧。

对象初始化

C#是一种面向对象的编程语言。如果对象本身没有分块,那么写一组提示有什么好处?本节将重点介绍在前进并new Object()在应用程序中编写代码之前应考虑的事项。您必须了解如何创建C#类以及事物如何协作以在系统中启动一个小程序。

例如,看下面的代码:

1
2
3
4
5
6
class Person {
public int ID { get; set; }
public string Name { get; set; }
public DateTime DateOfBirth { get; set; }
public bool Gender { get; set; }
}

您可能想要创建默认情况下设置值的程序,或者让它们来自模型或诸如此源代码的任何其他面向数据库的数据源,这些程序简化了在对象时输入默认值的方式正在创建。

1
var person = new Person { ID = 1, Name = "Afzaal Ahmad Zeeshan", DateOfBirth = new DateTime(1995, 08, 29), Gender = true };

相反,请尝试通过以下方式编写相同的代码:

1
2
3
4
var person = new Person();
person.ID = 1;
person.Name = "Afzaal Ahmad Zeeshan";
// So on.

这里的代码没有明显的性能改进,但是可以真正提高代码的可读性。如果您喜欢缩进,请在这里查看:

1
2
3
4
5
6
7
var person = new Person
{
ID = 1,
Name = "Afzaal Ahmad Zeeshan",
DateOfBirth = new DateTime(1995, 08, 29),
Gender = true
};

这也有缩进,但是它为您的C#代码的可读性添加了更多的说明。尽管前面的代码可以实现相同的功能,但是建议的代码可以使代码更易读和简洁。

二、技巧

空检查

NullReferenceException当缺少初始化的对象再次抛出异常时,您是否曾经对感到恼火?在程序中进行空检查有很多好处,不仅可以提高可读性,而且可以确保程序不会由于内存问题而终止(例如,内存中不存在变量时)。这些可能与程序的安全性以及团队具有的良好UI和UX准则相抵触。大多数情况下,由于以下原因会引发空异常:

1
2
3
string name = null;

Console.WriteLine(name);

在大多数情况下,除非您解决此问题,否则编译器本身不会继续运行,但是如果您设法以某种方式诱使编译器认为变量具有值,但在运行时没有变量,则会出现空引用异常。为了克服这个问题,您可以执行以下操作:

1
2
3
4
5
6
7

string name = null;

// Try to enter the value, from somewhere
if(name != null) {
Console.WriteLine(name);
}

此安全检查将确保在调用此变量时该值可用。否则,它将影响您代码的路径。但是,在C#6中,还有另一种方法可以克服此错误。考虑以下情形:建立数据库,建立数据表,找到您的人员但找不到他们的就业详细信息。你能找到他们工作的公司吗?

1
var company = DbHelper.PeopleTable.Find(x => x.id == id).FirstOrDefault().EmploymentHistory.CompanyName; // Error

如果您这样做,将会出现错误,因为我们只能在这些值的列表中进行简单几步的对象筛选。然后我们将碰到一个空值,一切都丢失了。C#6提出了一种克服这些情况的新方法,方法是在值和字段可以为null的后面使用安全的导航运算符。?.。像这样:

1
var company = DbHelper?.PeopleTable?.Find(x => x.id == id)?.FirstOrDefault()?.EmploymentHistory?.CompanyName; // Works

如果前一个不为null,则此代码仅检查下一个值。如果先前的值为null,它将返回null并将null保存为的值company,而不是引发错误。将检查留给框架本身可以很方便,但是,尽管如此,您仍然必须在最后检查其余值是否为null。

1
2
3
4
5
var company = DbHelper.PeopleTable?.Find(x => x.id == id)?.FirstOrDefault()?.EmploymentHistory?.CompanyName;

if(company != null) {
// Final process
}

但是您明白了这一点,而不是编写代码并检查所有内容是否为空,而是可以执行简单的检查并执行程序中想要的操作和逻辑。否则,将需要try...catch包装器或多个if...else块来控制程序在系统中的导航方式。

异步编程模式

如果您正在使用C#5进行编程,那么您已经在使用async / await关键字为您的应用程序带来改进。如果不是这种情况,那么我建议您在应用程序的源代码中使用异步编程模式。这不仅可以提高对程序的响应速度,还可以提高应用程序的可读性。在源代码中具有异步模式的一些好处是:

  1. 代码路径开始变得更加有意义。如果有一个进程在后台开始运行,那么程序员可以了解程序应该在哪里。
  2. 应用程序挂起问题将消失。大多数与应用程序阻塞相关的问题直接来自代码。当UI线程无法更新UI时,用户会认为该应用程序正在挂起并且没有响应,而事实并非如此。异步方法确实可以帮上大忙。
  3. 基于Windows运行时的应用程序完全基于此方法。您将(并且必须是!)在您的Windows Runtime应用程序中使用这种方法来解决诸如挂起应用程序或不良的编程习惯之类的问题。

自从线程化以来,代码执行的并行化就已经存在。异步已经成为程序和应用程序的重要组成部分,因此您更应该考虑使用它。

C#字符串构建

字符串是当今应用程序的重要组成部分,构建字符串可能会花费很多时间,并且还会导致应用程序性能下降。您可以通过多种方式在C#程序中构建字符串。以下是其中几种方式:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
string str = ""; // Setting it to null would cause additional problems.

// Way 1
str = "Name: " + name + ", Age: " + age;

// Way 2
str = string.Format("Name: {0}, Age: {1}", name, age);

// Way 3
var builder = new StringBuilder();
builder.Append("Name: ");
builder.Append(name);
builder.Append(", Age: ");
builder.Append(age);
str = builder.ToString();

请注意,C#中的字符串是不可变的。这意味着,如果您尝试更新它们的值,则会重新创建它们,并从内存中删除以前的句柄。这就是为什么方式1看起来是最好的方式,但经过进一步思考,事实并非如此。最好的方法是方法3,它使您可以构建字符串而不必在内存中重新创建对象。同时,C#6引入了一种全新的方式在C#中构建字符串,该方式比您以前想象的要好得多。新的字符串插值运算符$为您提供了以最佳方式执行字符串构建的功能。字符串插值如下所示:

1
2
3
4
5
6
7
8
9
static void Main(string[] args)
{
// Just arbitrary variables
string name = "";
int age = 0;

// Our interest
string str = $"Name: {name}, Age: {age}";
}

只需一行代码,编译器就会自动将其转换为string.Format()版本。为了证明这一点,将详细说明此C#程序已生成的字节码,并向您展示如何自动更改语法以读取字符串格式。

1
2
3
4
5
6
7
8
9
10
11
12
IL_0000:  nop
IL_0001: ldstr ""
IL_0006: stloc.0 // name
IL_0007: ldc.i4.0
IL_0008: stloc.1 // age
IL_0009: ldstr "Name: {0}, Age: {1}"
IL_000E: ldloc.0 // name
IL_000F: ldloc.1 // age
IL_0010: box System.Int32
IL_0015: call System.String.Format
IL_001A: stloc.2 // str
IL_001B: ret

可以看出,这显示了如何将语法更改回我们已经看到的语法。有关IL_0009更多信息,请参见。当其他人正在读取程序时,这可以使您的程序外观更简洁,并且如果要构建的字符串较小,则可以提高性能。如果字符串较大,请使用StringBuilder。

三、更多技巧

遍历数据

如果不对一组数据进行循环和迭代,那么应用程序有什么用?在这种情况下,有时您将不得不查找值,查找节点,查找记录或对集合进行任何其他遍历。在这种情况下,您确实需要确保编写干净的代码,因为这是性能和可读性都非常重要且相互关联的领域。有了一些经验,我就克服了编写用于读取和遍历数据的错误代码的方式。这正是LINQ应该加入的地方,LINQ允许您编写使用最佳.NET框架为用户和客户提供最佳编码体验和最佳体验的程序。

以前,您可能已经做过以下一些事情:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
6
// A function to search for people
Person FindPerson(int id) {
var people = DbContext.GetPeople(); // Returns List<Person>

foreach (var person in people) {
if(person.ID == id) {
return person;
}
}

// No person found.
return null;
}

// Then do this
var person = FindPerson(123);

对于任何想接手您代码的人来说,这都是一段易读的代码。但是,使用C#中的LINQ查询可以使代码更加简单和整洁。您可以通过两种方式执行此操作。一个有点像SQL,另一个是通过Where在集合上使用该函数并传递我们的要求。

1
2
3
4
5
6
7
8
9
10
11
// A function to search for people
Person FindPerson(int id) {
var people = DbContext.GetPeople(); // Returns List<Person>

return (from person in people
where person.ID == id
select person).ToList().FirstOrDefault();
}

// Then do this
var person = FindPerson(123);

该代码看起来有点像SQL,可以增强代码的可读性和性能。该函数相似,但是,该Where函数的读取效果更好,并使所有迭代都针对.NET框架本身,而.NET框架将为应用程序提供最佳性能。

现在,让我们看看用相同的C#代码编写此查询的另一种方式:

1
2
3
4
5
6
7
8
9
// A function to search for people
Person FindPerson(int id) {
var people = DbContext.GetPeople(); // Returns List<Person>

return people.FirstOrDefault(x => x.ID == id);
}

// Then do this
var person = FindPerson(123);

请注意,null如果没有找到匹配项,则返回第一个代码。这段代码也做同样的事情。唯一的第一个代码更糟糕的是它必须对集合本身执行迭代。

该本地变量return person;将允许程序返回控件,但是如果数据位于最后一个位置会发生什么呢?此数据搜索算法的复杂度仍为O(n)。

避免unsafe上下文

在您必须亲自处理内存时,C#还支持手动内存管理。C#中的不安全上下文允许您操作内存,执行指针算术,在可能无法访问的内存位置读取和写入数据,等等。但是,.NET框架可以做很多事情来克服内存问题,延迟和磁盘上其他问题。这也使.NET框架完全无需实际执行任何内存管理,.NET框架将为您做到这一点。

使用不安全的上下文有很多好处,例如,当您要围绕本机C ++库编写包装器时。Emgu CV就是这样一个示例,您将在其中编写一些代码来处理如何管理本机代码,并以更简单的方式来处理内存中的错误。在这种情况下,您可以:

  1. 使用指针管理和指针算术。您不能在此上下文之外的任何地址上执行任何操作,这是.NET规则所处的位置。
  2. 使用内存管理来操作内存中的对象。
  3. 使用C ++风格的编程,这正是C#设计的目的。

这几乎没有好处,如果您应该在应用程序中考虑这一点,请明智地考虑。

关于Unsafe纯属个人观点

我还想指出,关于“不安全”的利弊,我所说的一切都是我个人的看法。我不经常在程序中使用unsafe上下文,因为没有理由不考虑在应用程序中使用上下文。但是,如果您的应用程序需要本机内存管理,则可以使用此上下文。

尽可能使用Lambda表达式

Lambda来自函数式编程领域,在C#中已广泛使用,从内联函数一直到C#6中的getter only属性。我将展示C#中的两种用法,它们构成的程序,不仅看起来更清爽,而且性能指标也更高。

为此,我将向您显示该C#代码的IL。我个人喜欢在许多领域使用lambda,尤其是当我不得不用C#编写内联函数时。自从可以使用此概念编写仅用于getter的属性以来,我一直在使用它们,并且我个人认为它比以前做同一件事的方法更好。

1.将Lambda用于内联函数

您应该知道一些C#编程的示例,使用这种写法的代码很多。

例如在应用程序中进行事件处理的情况下,对于事件处理,您可以像下面这样编写当前函数:

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

// Without lamdbas
myBtn.Click += Btn_Click;

public void Btn_Click (object sender, EventArgs e) {
// Code to handle the event
}

// With the help of lambdas
myBtn.Click += (sender, e) =>
{
// Code to handle the event.
}

请注意,编译器将自动将对象映射到其类型。这在许多方面都很方便,因为它允许您用C#编写仅与对象一起保留的内联函数,除非您也想在其他任何地方使用它们。但是,这种处理事件的方法有一个缺点:一旦附加了事件处理程序,便无法删除它。在C#中可以,-+。

但是由于我们没有删除事件的参考,因此只能使用单独的函数。但是,如果不必删除处理程序,则应始终考虑在程序中使用这种事件处理方式。

2.将Lambda用于仅Getter的属性

在C#中,有一个使用属性而不是字段的概念。您可以控制如何设置值以及如何从字段中捕获值。将其视为Java编程语言的getter和setter方法的替代方法(或类似方法)。唯一的区别是您不必在某个地方分别编写它们,它们直接写在字段本身的前面。然后,C#程序编译器将创建自己的后备字段,用于存储值。

基本上,您必须编写如下这样的属性:

1
public string Name { get; }

请注意,这些属性是恒定的,设置后就无法更改。它们是在构造函数中设置的,或者(从C#6开始)在它们的前面设置。像这样:

1
public string Name { get; } = "Afzaal Ahmad Zeeshan";

但是,由于我们已经知道这是一个常量字段,您不能修改它,那么为什么不创建一个简单的常量属性呢?事情变得有些棘手。甚至一个属性也必须由字段来备份。在这种情况下,这将为我们解决问题:

1
public string Name => "Afzaal Ahmad Zeeshan";

这等效于编写以下内容:

1
public string Name { get { return "Afzaal Ahmad Zeeshan"; } }

但是由于编译期将getter字段转换为常量字段,并且在必须调用此属性的时候才会在程序中使用该字段,因此性能要好得多。

最后的话

本指南系列的目的是使您了解一些使程序更易于阅读和更好执行的方法。C#编译器本身会尽最大努力提高代码的质量和效率,而这程序员带来便利,同时也将使程序更好地工作。

除了上面提到的方法,还有许多其他提高可读性的方法,其中许多方法适合公司团队协作的形式编写程序,因为大多数团队往往都要求程序员遵循自己的编程方法和方式。

一文看懂"async"和“await”关键词是如何简化了C#中多线程的开发过程

Posted on 2020-07-25 | Edited on 2021-04-25 | In 技术

实际应用程序中的async和await

一文看懂”async”和“await”关键词是如何简化了C#中多线程的开发过程

当我们使用需要长时间运行的方法(即,用于读取大文件或从网络下载大量资源)时,在同步的应用程序中,应用程序本身将停止运行,直到活动完成。在这些情况下,异步编程非常有用:它使我们能够并行执行不同任务,并在需要时等待其完成。

有这种方法编程许多不同的模型类型:APM(异步编程模型),基于事件(异步模型EAP),以及TAP,基于任务的(异步模型任务)。让我们看看如何使用关键字async和await在C#中实现第三个方法。

编写异步代码的主要问题之一是可维护性:实际上,这种编程方法会使代码复杂化。幸运的是,C#5引入了一种简化的方法,在该方法中,编译器运行由开发人员先前完成的艰巨任务,并且应用程序保留类似于同步代码的逻辑结构。

让我们举个例子。假设我们有一个.NET Core项目,我们应该在其中管理三个实体:Area,Company和Resource。

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
public class Area
{
public int Id { get; set; }

[Required]
[StringLength(255)]
public string Name { get; set; }
}

public class Company
{
public int Id { get; set; }

[Required]
[StringLength(255)]
public string Name { get; set; }
}

public class Resource
{
public int Id { get; set; }

[Required]
[StringLength(255)]
public string Name { get; set; }
}

现在假设我们应该使用Entity Framework Core将这些实体的值保存在数据库中。其DbContext是:

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
public class AppDbContext : DbContext
{
public DbSet<Area> Areas { get; set; }
public DbSet<Company> Companies { get; set; }
public DbSet<Resource> Resources { get; set; }
public AppDbContext(DbContextOptions<AppDbContext> options) : base(options) {}

override protected void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.Entity<Area> ().HasData(
new Area { Id = 1, Name = "Area1"},
new Area { Id = 2, Name = "Area2"},
new Area { Id = 3, Name = "Area3"},
new Area { Id = 4, Name = "Area4"},
new Area { Id = 5, Name = "Area5"});
modelBuilder.Entity<Company> ().HasData(
new Area { Id = 1, Name = "Company1"},
new Area { Id = 2, Name = "Company2"},
new Area { Id = 3, Name = "Company3"},
new Area { Id = 4, Name = "Company4"},
new Area { Id = 5, Name = "Company5"});
modelBuilder.Entity<Resource>().HasData(
new Area { Id = 1, Name = "Resource1"},
new Area { Id = 2, Name = "Resource2"},
new Area { Id = 3, Name = "Resource3"},
new Area { Id = 4, Name = "Resource4"},
new Area { Id = 5, Name = "Resource5"});
}
}

从代码中可以看到,我们插入了一些示例数据进行处理。现在假设我们要使用Controller API公开这些数据,既单独(针对每个实体),又使用将它们全部联接在一起的方法,并通过一次调用返回它们。

使用同步方法,Controller API 将是:

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
[ApiController]
[Route("[controller]")]
public class DataController : ControllerBase
{
private readonly AppDbContext db = null;

public DataController(AppDbContext db)
{
this.db = db;
}

public IActionResult Get()
{
var areas = this.GetAreas();
var companies = this.GetCompanies();
var resources = this.GetResources();
return Ok(new { areas = areas, companies = companies, resources = resources });
}

[Route("areas")]
public Area[] GetAreas()
{
return this.db.Areas.ToArray();
}

[Route("companies")]
public Company[] GetCompanies()
{
return this.db.Companies.ToArray();
}

[Route("resources")]
public Resource[] GetResources()
{
return this.db.Resources.ToArray();
}
}

Get()方法在其中调用返回单个结果的三个方法,并等待每个方法的执行完成后再传递到下一个结果。这三种方法互不相关,因此您无需等待其中一种方法的执行即可调用另一种方法。然后,您可以创建三个独立的任务以并行执行。
第一种方法可以基于该方法运行中的任务类队列指定的作业是在跑步线程池,并返回一个任务对象,它代表了这项工作。这样,方法可以在线程池的不同线程上同时运行:

1
2
3
4
5
6
7
8
public IActionResult Get()
{
var areas = Task.Run(() = > this.GetAreas());
var companies = Task.Run(() = > this.GetCompanies());
var resources = Task.Run(() = > this.GetResources());
Task.WhenAll(areas, companies, resources);
return Ok(new { areas = areas.Result, companies = companies.Result, resources = resources.Result });
}

Task的Result属性包含详细说明的结果。方法WhenAll允许暂停当前线程执行,直到所有Task完成。运行代码,我们可以注意到一个有趣的事情:调用中断,并启动以下异常:

AggregateException:发生一个或多个错误。(在上一个操作完成之前,第二个操作在此上下文上开始。这通常是由使用相同DbContext实例的不同线程引起的。有关如何避免DbContext线程问题的更多信息,请参见https://go.microsoft.com。 / fwlink /?linkid = 2097913。)

此错误消息告诉我们,方法在不同的线程上同时执行,但是由于它们使用与DbContext 相同的实例来连接数据库,
因此引发了异常,DbContext类无法确保线程安全的功能:我们可以轻松地绕过此问题,避免了.NET Core 的依赖项注入引擎创建单个实例,而我们为每种方法创建了单独的实例。作为示例,让我们看看方法GetAreas()会如何变化:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
public class DataController : ControllerBase
{
private readonly DbContextOptionsBuilder <AppDbContext> optionsBuilder = null;

public DataController(IConfiguration configuration)
{
this.optionsBuilder = new DbContextOptionsBuilder <AppDbContext> ()
.UseSqlite(configuration.GetConnectionString("DefaultConnection"));
}

[Route("areas")]
public Area[] GetAreas()
{
using(var db = new AppDbContext(this.optionsBuilder.Options))
{
return db.Areas.ToArray();
}
}
}

好吧,现在可以了。我们应该注意,EFCore提供了一些方法,例如,与方法ToArrayAsync一样,使用相同的DbContext进行异步调用,该方法从IQueryable 创建一个数组,该数组 异步枚举它。此方法返回*Task *,它是表示异步操作的活动。这样,我们不再需要使用Task.Run():

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public IActionResult Get()
{
var areas = this.GetAreas();
var companies = this.GetCompanies();
var resources = this.GetResources();
Task.WhenAll(areas, companies, resources);
return Ok(new { areas = areas.Result, companies = companies.Result, resources = resources.Result });
}

[Route("areas")]
public Task<Area[]> GetAreas()
{
return db.Areas.ToArrayAsync();
}

无论如何,Microsoft不能保证这些异步方法在每种情况下都能工作,因为DbContext尚未设计为线程安全的。您可以查询此链接以获取更多信息:https : //docs.microsoft.com/zh-cn/ef/core/querying/async

使用Entity Framework Core时,最佳实践是在启动另一个异步操作之前,为每个异步操作都拥有一个DbContext或等待每个异步操作完成。
当我们必须进行异步调用并返回结果时,这种最佳做法是可以的。但是,如果我们想在返回结果之前对结果进行一些操作,会发生什么?如果我们想向列表中添加元素怎么办?我们应该等待结果,添加元素,然后返回修改后的列表:

1
2
3
4
5
6
7
8
9
10
[Route("companies")]
public Task<Company[]> GetCompanies()
{
using (var db = new AppDbContext(this.optionsBuilder.Options))
{
var data = this.db.Companies.ToListAsync().Result;
data.Insert(0, new Company() { Id = 0, Name = "-"});
return data.ToArray();
}
}

不幸的是,该代码无法编译,因为data.ToArray()返回的是数组而不是Task。实际上,这里我们需要三个线程:主调用方(Get()),数据库查询(this.db.Companies.ToListAsync())和一个线程,该线程将一个值添加到列表中。我们有三种方法可以做到这一点:让我们用三种单一方法来查看它们。我们已经看到的第一个,可以使用Task.Run()方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
[Route("companies")]
public Task<Company[]> GetCompanies()
{
return Task.Run(() =>
{
using (var db = new AppDbContext(this.optionsBuilder.Options))
{
var data = db.Companies.ToList();
data.Insert(0, new Company() { Id = 0, Name = "-" });
return data.ToArray();
}
});
}

作为替代方案,我们可以使用方法ContinueWith(),该方法可以应用于任务,并且可以在上一个方法完成后立即指定要运行的新任务:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
[Route("resources")]
public Task <Resource[]> GetResources()
{
using (var db = new AppDbContext(this.optionsBuilder.Options))
{
return db.Resources.ToListAsync()
.ContinueWith(dataTask = >
{
var data = dataTask.Result;
dataTask.Result.Insert(0, new Resource() { Id = 0, Name = "-" });
return data.ToArray();
});
}
}

我们可以让编译器执行“垃圾代码”,并使用关键字async和await,这可以为我们创建Task:

1
2
3
4
5
6
7
8
9
10
[Route("areas")]
public async Task <Area[]> GetAreas()
{
using (var db = new AppDbContext(this.optionsBuilder.Options))
{
var data = await db.Areas.ToListAsync();
data.Insert(0, new Area() { Id = 0, Name = "-" });
return data.ToArray();
}
}

正如您在最后一种方法中看到的那样,代码更加简单,并且向我们隐藏了Task的创建,从而使我们可以异步返回。让我们想象一下一个场景,其中调用不止一个,并且这种方法如何使一切变得更加线性。

重构的副作用是方法GetAreas()已成为异步操作。这个事实意味着,当不同的请求到达此API时,分配给该请求的线程池的线程将被释放以供其他请求使用,直到DbContext终止数据提取为止。

我希望我能引起您足够的兴趣来深入分析该论点。在许多情况下,使用async和await非常方便,并且除了使代码更加简洁和线性外,还可以提高应用程序的一般性能。

可以在这里找到代码:https://github.com/fvastarella/Programmazione-asincrona-con-async-await

ASP.NET Core中的内存中缓存

Posted on 2020-07-21 | Edited on 2021-04-25 | In 技术

ASP.NET Core中的分布式缓存

在上一篇文章中,我解释了如何使用内存缓存在ASP.NET Core应用程序中管理缓存。如果您的应用程序托管在单个服务器上,则可以使用这种类型的缓存。

那.NET Core框架可以使用哪些工具在云中的分布式方案中进行缓存呢?

IDistributedCache接口

该框架提供以下接口:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public interface IDistributedCache
{
byte[] Get(string key);
Task <byte[]> GetAsync(string key);

void Set(string key, byte[] value, DistributedCacheEntryOptions options);
Task SetAsync(string key, byte[] value, DistributedCacheEntryOptions options);

void Refresh(string key);
Task RefreshAsync(string key);

void Remove(string key);
Task RemoveAsync(string key);

}

如果您还记得上一篇文章(IMemoryCache)中使用的接口,则可能会注意到一些相似之处,但甚至有很多区别,例如:

  1. 异步方法
  2. 刷新方法(无需请求数据即可更新缓存键)
  3. 基于字节而非基于对象的方法

到目前为止,Microsoft提供了3种实现:一种是内存中的,主要用于应用程序的开发阶段,第二种是在SQL Server上,最后一种是基于REDIS。
要使用基于REDIS的版本,必须安装NuGet软件包Microsoft.Extensions.Caching.Redis。

分布式内存缓存

该实现由框架提供,并将我们的数据保存在内存中。它不是完全分布式的缓存,因为数据是从应用程序实例保存在其所在的服务器上的。在某些情况下可能会有用:

  • 开发与测试
  • 在生产中使用单个服务器时,内存消耗不是问题。

无论如何,我们将使我们的应用程序仅在必要时才使用“真实的”分布式解决方案(用于保存的接口是相同的)。
要启用此IDistributedCache实现,您只需要在Startup类中注册它,如下所示:

?

1
`services.AddDistributedMemoryCache();`

分布式SQL Server缓存

此实现使分布式缓存可以将SQL Server数据库用作存储。有一些配置步骤。第一步包括创建用于保留数据的表。
有一个命令行命令工具,可通过简单的指令轻松实现

?

1
`dotnet sql-cache ``create` `<``connection` `string > <``schema` `> <``table` `>`

允许创建具有以下结构的表:

?

1
`CREATE` `TABLE` `[dbo].[CacheTable](``  ``[Id] [nvarchar](449) ``NOT` `NULL``,``  ``[Value] [varbinary](``max``) ``NOT` `NULL``,``  ``[ExpiresAtTime] [datetimeoffset](7) ``NOT` `NULL``,``  ``[SlidingExpirationInSeconds] [``bigint``] ``NULL``,``  ``[AbsoluteExpiration] [datetimeoffset](7) ``NULL``,`` ``CONSTRAINT` `[pk_Id] ``PRIMARY` `KEY` `CLUSTERED ``(``  ``[Id] ``ASC``)``WITH` `(PAD_INDEX = ``OFF``, STATISTICS_NORECOMPUTE = ``OFF``, ``  ``IGNORE_DUP_KEY = ``OFF``, ALLOW_ROW_LOCKS = ``ON``, ``  ``ALLOW_PAGE_LOCKS = ``ON``) ``ON` `[``PRIMARY``]``) ``ON` `[``PRIMARY``] TEXTIMAGE_ON [``PRIMARY``]` `CREATE` `NONCLUSTERED ``INDEX` `[Index_ExpiresAtTime] ``ON` `[dbo].[CacheTable]``(``  ``[ExpiresAtTime] ``ASC``)``WITH` `(PAD_INDEX = ``OFF``, STATISTICS_NORECOMPUTE = ``OFF``, ``  ``SORT_IN_TEMPDB = ``OFF``, DROP_EXISTING = ``OFF``, ``  ``ONLINE = ``OFF``, ALLOW_ROW_LOCKS = ``ON``, ``  ``ALLOW_PAGE_LOCKS = ``ON``) ``ON` `[``PRIMARY``]`

配置此实现的第二个也是最后一个步骤,即在我们的应用程序中进行注册。在Startup类的ConfigureServices方法内,我们添加以下代码块:

?

1
`services.AddDistributedSqlServerCache(o =>``{``  ``o.ConnectionString = Configuration[``"ConnectionStrings:Default"``];``  ``o.SchemaName = ``"dbo"``;``  ``o.TableName = ``"Cache"``;``});`

分布式Redis缓存

在分布式缓存场景中,Redis的使用非常广泛。Redis是一个快速存储的数据存储,它是开源的并且是键值类型。它提供的响应时间不到1毫秒,从而允许在各个领域中的每个实时应用程序每秒接收数百万个请求。
如果您的应用程序的基础结构基于Azure云,则可以使用服务Azure Redis缓存,并且可以从您的订阅中对其进行配置。

要在Redis上使用分布式缓存,必须安装软件包Microsoft.Extensions.Caching.Redis。
第一步是在Startup.class中配置服务?

1
`public` `void` `ConfigureServices(IServiceCollection services)``{`` ``// Add framework services.`` ``// ... altri servizi ...`` ` ` ``services.AddDistributedRedisCache(cfg => `` ``{``  ``cfg.Configuration = Configuration.GetConnectionString(``"redis"``);`` ``});``}`

如果使用的是Azure Redis缓存,则可以通过访问面板的“访问键”部分找到连接字符串。

img

如何使用IDistributedCache

接口IDistributedCache的使用特别简单:如果您已经阅读了有关接口IMemoryCache的使用的文章,那么您将在命令中找到几点。如果要在控制器中使用它,则需要首先将实例注入构造函数中,如下面的代码所示:

?

1
`public` `class` `HomeController : Controller``{``  ``private` `readonly` `IDistributedCache _cache;` `  ``public` `HomeController(IDistributedCache cache)``  ``{``    ``_cache = cache;``  ``}``  ``public` `async Task  Index()``  ``{``    ``await _cache.SetStringAsync(``"TestString"``, ``"TestValue"``);` `    ``var value = _cache.GetString(``"TestString"``);` `    ``return` `View();``  ``}``}`

如果让此代码运行,并在Index方法的最后一行插入一个断点,则会得到以下结果:

img

我们可以使用DistributedCacheEntryOptions类轻松检查缓存的持续时间。在下面的代码中,我们创建一个实例,将持续时间设置为一小时。

?

1
`public` `async Task  Index()``{``  ``var options = ``new` `DistributedCacheEntryOptions``  ``{``    ``AbsoluteExpiration = DateTime.Now.AddHours(1)``  ``};` `  ``await _cache.SetStringAsync(``"TestString"``, ``"TestValue"``, options);` `  ``var value = _cache.GetString(``"TestString"``);` `  ``return` `View();``}`

结论与建议

在我们的应用中使用哪种IDistributedCache实现的决定取决于某些因素。在Redis和SQL之间进行选择(我将内存实现仅保留在测试和开发中,所以我将其保留在外)应该基于对您可用的基础结构,性能要求和开发团队的经验进行选择。如果团队对Redis感到放心,这将是最佳选择。SQL实现仍然是一个很好的解决方案,但应记住,数据恢复将不会提供出色的性能,因此应谨慎选择要缓存的数据。

ASP.NET Core中的内存中缓存

Posted on 2020-07-20 | Edited on 2021-04-25 | In 技术

ASP.NET Core中的内存缓存

让我们看看如何通过缓存优化ASP.NET Core应用程序性能

我相信,在我们的工作中,每个人都收到来自客户的请求或来自我们应用程序用户的反馈,以提高响应速度。

如果在编写代码时仅使用最佳实践还不够,那么我们肯定需要使用缓存来微调我们的应用程序。

缓存包括将那些不经常更改的信息存储在某个地方。频率是我们应用程序的业务要求。

在本文中,我们将看到ASP.NET Core可用于缓存的内容。

IMemoryCache和IDistributedCache

这两个接口代表.NET Core中用于缓存的内置机制。您可能已经听说过的所有其他技术都是这两个接口的实现。在本文中,我们将详细介绍内存缓存,而在以后的文章中将研究分布式缓存。

在ASP.NET Core中启用内存中缓存

我们可以使用ConfigureServices方法将ASP.NET内存中缓存合并到应用程序中。您可以在Startup类中启用内存中缓存,如下面的代码片段所示。

1
2
3
4
5
public void ConfigureServices(IServiceCollection services)
{
services.AddMvc();
services.AddMemoryCache();
}

该AddMemoryCache方法允许我们注册IMemoryCache接口,如上面提到的,将被用于高速缓存的基础。下面我们看到框架中接口的定义:

1
2
3
4
5
6
public interface IMemoryCache : IDisposable
{
bool TryGetValue(object key, out object value);
ICacheEntry CreateEntry(object key);
void Remove(object key);
}

接口中提供的方法并不是唯一可用于缓存的方法:我们将在后面看到,存在各种扩展来丰富可用的API并极大地促进其使用。例如:

1
2
3
4
5
6
7
8
9
public static class CacheExtensions
{
public static TItem Get<titem>(this IMemoryCache cache, object key);

public static TItem Set<titem>(this IMemoryCache cache, object key, TItem value, MemoryCacheEntryOptions options);

public static bool TryGetValue<titem>(this IMemoryCache cache, object key, out TItem value);
...
}

注册后,该接口可以注入到我们要使用它的类构造函数中,如下所示:

1
2
3
4
5
private IMemoryCache cache;
public MyCacheController(IMemoryCache cache)
{
this.cache = cache;
}

在下面的部分中,我们将研究如何在ASP.NET Core中使用缓存API来存储和检索对象。

使用IMemoryCache存储和检索项目

要使用IMemoryCache接口写入对象,请使用Set ()方法,如以下代码片段所示。

1
2
3
4
5
6
[HttpGet]
public string Get()
{
cache.Set(“MyKey”, DateTime.Now.ToString());
return “This is a test method...”;
}

此方法接受两个参数,第一个是键,用于标识缓存的对象,第二个参数是我们要存储的值。要从缓存中检索对象,请使用Get ()方法,如下面的代码片段所示。

1
2
3
4
5
[HttpGet(“{key}”)]
public string Get(string key)
{
return cache.Get<string>(key);
}

如果我们不确定缓存中是否存在特定的密钥,则可以使用TryGetValue()方法进行帮助:它返回一个布尔值,指示所请求的密钥存在或不存在。

下面是如何使用TryGetValue修改Get()方法的方法。

1
2
3
4
5
6
7
8
9
10
11
[HttpGet(“{key}”)]
public string Get(string key)
{
string obj;
if (!cache.TryGetValue<string>(key, out obj))

obj = DateTime.Now.ToString();
cache.Set<string>(key, obj);

return obj;
}

可用的另一种方法是GetOrCreate()方法,该方法验证所需密钥的存在,否则,该方法将为您创建它。

1
2
3
4
5
6
7
8
[HttpGet(“{key}”)]
public string Get(string key)
{
return cache.GetOrCreate<string>(“key”,
cacheEntry => {
return DateTime.Now.ToString();
});
}

如何在ASP.NET Core中的缓存数据上设置过期策略

当我们使用IMemoryCache存储对象时,MemoryCacheEntryOptions类为我们提供了各种技术来管理缓存数据的到期时间。

我们可以指定一个固定的时间,在该时间之后某个特定的密钥将过期(绝对过期),或者如果在某个特定的时间(滑动过期)之后没有访问它,则它可以过期。此外,还可以使用Expiration Token在缓存的对象之间创建依赖关系。这里有一些例子

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
//absolute expiration using TimeSpan
_cache.Set("key", item, TimeSpan.FromDays(1));

//absolute expiration using DateTime
_cache.Set("key", item, new DateTime(2020, 1, 1));

//sliding expiration (evict if not accessed for 7 days)
_cache.Set("key", item, new MemoryCacheEntryOptions
{
SlidingExpiration = TimeSpan.FromDays(7)
});

//use both absolute and sliding expiration
_cache.Set("key", item, new MemoryCacheEntryOptions
{
AbsoluteExpirationRelativeToNow = TimeSpan.FromDays(30),
SlidingExpiration = TimeSpan.FromDays(7)
})

// This method adds a trigger to refresh the data from background
private void UpdateReset()
{
var mo = new MemoryCacheEntryOptions();
mo.RegisterPostEvictionCallback(RefreshAllPlacessCache_PostEvictionCallback);
mo.AddExpirationToken(new CancellationChangeToken(new CancellationTokenSource(TimeSpan.FromMinutes(35)).Token));
Cache.Set(CACHE_KEY_PLACES_RESET, DateTime.Now, mo);
}

// Method triggered by the cancellation token that triggers the PostEvictionCallBack
private async void RefreshAllPlacesCache_PostEvictionCallback(object key, object value, EvictionReason reason, object state)
{
// Regenerate a set of updated data
var places = await GetLongGeneratingData();
Cache.Set(CACHE_KEY_PLACES, places, TimeSpan.FromMinutes(40));

// Re-set the cache to be reloaded in 35min
UpdateReset();
}

缓存回调

MemoryCacheEntryOptions类提供的另一个有趣功能是允许我们注册回调的功能,当从缓存中删除一项时将执行该回调。

1
2
3
4
5
6
7
8
9
MemoryCacheEntryOptions cacheOption = new MemoryCacheEntryOptions()  
{
AbsoluteExpirationRelativeToNow = (DateTime.Now.AddMinutes(1) - DateTime.Now),
};
cacheOption.RegisterPostEvictionCallback(
(key, value, reason, substate) =>
{
Console.Write("Cache expired!");
});

缓存标签助手

到目前为止,我们已经看到.NET Core提供的API的使用,从而能够直接使用IMemoryCache接口手动在缓存中写入和读取项目。此接口还有其他实现,可能非常有用。例如,在Web环境中,如果我们使用.NET Core MVC框架,则可以使用helper cache tag存储部分页面。它非常简单易用:您可以将视图的一部分包装在缓存标签中以启用缓存:

1
2
3
<cache>
<p>Ora: @DateTime.Now</p>
</cache>

对于页面的每个后续请求(包含此标记),将从缓存中使用该段的正文。如果将其放在页面上并观察其输出,则可以轻松检查行为。当然,我们使用它的方式仅出于示例目的,但是当您尝试渲染需要大量资源的页面时,您可以欣赏它的功能。一个明显的缓存候选是视图组件调用

1
2
3
<cache expires-on="@TimeSpan.FromSeconds(600)">
@await Component.InvokeAsync("BlogPosts", new { tag = "popular" })
</cache>

在上一个代码段中,您还可以通过属性expires-on来了解如何管理缓存中对象的过期期限。还有其他两种选择:

  • 过期时间:将使用TimeSpan进行评估以指示一段时间,此后必须重新生成内容;
  • 过期滑动:还应使用指示闲置时间的TimeSpan。每次从缓存中读取内容时,都会将其删除延迟。

另一个可定制方面涉及配置缓存标准的可能性。我们可能需要根据一些变量来更新缓存的对象。一些要求是由覆盖变化逐下列属性:

  • 按路由变化:通过路由参数的名称(例如id)进行了增强,以指示在指示的属性更改时必须重新生成内容;
  • 因查询而异:当更改查询字符串键时,将生成并缓存内容;
  • 按用户不同:当我们显示已登录用户的特定数据时(例如,包含名称和照片的个人资料框),必须将其设置为true;
  • 按标题变化:如果我们使用HTTP请求标头显示语言内容,则根据HTTP请求标头来更改缓存,例如“ Accept-Language”。
  • cookie-variable-by-cookie:允许您根据cookie的内容更改缓存,我们必须指出其名称。

可以使用一个或多个按属性的属性来执行高级缓存策略,但是,有句著名的名言:“功能强大,责任重大 ”。

结论

使用内存缓存可以使您将数据存储在服务器的内存中,并通过删除对外部数据源的不必要请求来帮助我们提高应用程序性能。如我们所见,它非常易于使用。

我提醒您,当您的应用程序托管在多台服务器或云托管环境中时,不能使用这种方法。在下一篇文章中,我们将讨论分布式缓存。

下次见!

常见的C#异常及其修复方法

Posted on 2020-07-20 | Edited on 2021-04-25 | In 技术

常见的C#异常及其修复方法

如果您今天是依靠编写的软件来谋生,那么您可能至少对异常的概念很熟悉。

Jeff Atwood曾经称它们为“现代编程语言的基础”。异常是现代软件开发中常见且有用的结构,但有时它们也可能造成混乱。

那么什么是异常?更具体地说,C#异常的主要类型是什么,以及如何使用它们?

今天的帖子将回答上述问题以及更多问题。我们将从“异常”的简要定义开始,然后继续解释该结构的组成部分。最后,我们将概述最常见的C#异常以及如何处理它们。

让我们开始吧。

有什么异常?

异常是一种可用于处理错误的机制。就这么简单。在某些情况下,例如,C程序员将返回错误代码,而Java或C#程序员都很有可能引发异常。

异常表示执行流程的突然中断。一旦引发异常,执行就会停止。如果未处理异常,则应用程序崩溃。

但是,实际上您是如何做到的呢?您如何引发或捕获异常?所有这些甚至意味着什么?

这就是我们将在下一部分中详细介绍的内容。

C#异常剖析

现在,我们将简要介绍C#异常的情况。您将了解应该使用的主要关键字,这些关键字不仅可以捕获和处理异常,还可以抛出自己的异常。我们列表上的第一个是try关键字和块。

try

异常处理解剖的第一部分是try块。您可以使用它来尝试执行一些可能引发异常的代码。考虑以下代码摘录:

1
2
3
4
5
string content = string.Empty;
try
{
content = System.IO.File.ReadAllText(@"C:\file.txt");
}

上面的代码声明了一个变量,并为其分配了空字符串。然后,我们有了try块。块中的单行代码try使用该类中的ReadAllText静态方法System.IO.File。我们担心该路径表示的文件可能不存在,在这种情况下会引发异常。但是,当然,这还不够。我们的代码不执行任何处理异常的操作。它甚至目前还没有编译!这是来自编译器的消息:

1
Expected catch or finally

该消息是不言自明的。我们需要 catch或finally代码块,因为尝试处理异常然后忘记执行处理部分没有任何意义。让我们开始吧。

catch

catch代码块使我们能够实际处理异常。我们将使用更多代码扩展前面的示例。

看看这个:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
static void Main(string[] args)
{
string content = string.Empty;

try
{
Console.WriteLine("First message inside try block.");
Console.WriteLine("Second message inside try block.");
content = System.IO.File.ReadAllText(@"C:\file.txt");
Console.WriteLine("Third message inside try block. This shouldn't be printed.");
}
catch
{
Console.WriteLine("First message inside the catch block.");
Console.WriteLine("Second message inside the catch block.");
}

Console.WriteLine("Outside the try-catch.");
Console.ReadLine();
}

如果运行上面的代码,则应看到以下内容:

1
2
3
4
5
First message inside try block.
Second message inside try block.
First message inside the catch block.
Second message inside the catch block.
Outside try-catch.

该路径处的文件不存在,因此将引发System.FileNotFoundException。发生这种情况时,执行流程将立即中断。因此,将在try块中打印“第三条消息”的行。这不应该被打印。” 永远不会被执行。

执行流程由catch块执行。完成后,将控制权交还给main方法,然后打印Outside try-catch。并等待用户输入。

最后

在了解了try和之后catch,我们终于(没有双关语)准备好使用这个非常有用但经常被误解的异常处理机制的一部分。那么,什么是finally?

该finally块是一种确保将执行给定代码段的方式,无论是否引发异常。考虑下面的代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
try
{
content = System.IO.File.ReadAllText(path);
Console.WriteLine("If you're reading this, no exception was thrown.");
}
catch (System.IO.FileNotFoundException e)
{
Console.WriteLine("If you're reading this, an exception was thrown.");
Console.WriteLine("Message: " + e.Message);
}
finally
{
Console.WriteLine("This will be printed, no matter what.");
}

那仍然是相同的示例,但是现在要简单得多。注意最后的finally块。如果运行此代码,则应该看到以下消息:

1
2
3
4
If you're reading this, an exception was thrown.
Message: Could not find file 'C:\file.txt'.
This will be printed, no matter what.
Outside the try-catch.

现在让我们模拟文件的存在。注释掉试图从文件中读取的行,然后再次运行该应用程序。这是您现在应该看到的:

1
2
3
If you're reading this, no exception was thrown.
This will be printed, no matter what.
Outside the try-catch.

如您所见,在最近的场景中,没有引发异常,因此该catch块中没有执行任何行。另一方面,finally在两种情况下都执行了该块。

throw

当涉及到异常时,您将不会总是从别人那里处理它们。您也可以自己抛出异常。为此,您将使用throw关键字,然后是要引发的异常的类的实例化。以下代码举例说明了这一点:

1
2
3
4
5
6
7
public ProductService(IProductRepository repository)
{
if (repository == null)
throw new ArgumentNullException();

this.repository = repository;
}

常见的.NET异常

以下是常见的.NET异常列表:

System.NullReferenceException

这是最著名的(甚至是臭名昭著的)异常之一。当您尝试调用方法/属性/索引器/等时,抛出此异常。在包含空引用的变量(即,它不指向任何对象)。下面的代码将导致空引用异常:

1
2
Person p = people.Where(x => x.SSN == verifySsn).FirstOrDefault();
string name = p.Name;

在上面的示例中,我们过滤了将每个项目的SSN属性与verifySsnvariable变量进行比较的序列。然后,我们使用该FirstOrDefault()方法从序列中仅提取第一项。如果序列不产生任何项目,则它将返回该类型的默认值。由于Person是引用类型,因此其返回值为null。

在下一行,我们尝试Name在空引用上取消引用属性。繁荣!空引用异常。

这是通常不抛出也不捕获的异常。您不要扔它,因为它毫无意义。如果您想与代码的客户交流,给定方法不接受null作为其参数的有效值,则使用的正确异常是System.ArgumentNullException。

您如何“修复”此异常?简而言之,您必须对可为空性小心谨慎。如果您正在编写将由第三方使用的任何代码(即使这些第三方是您的同事),则请认真考虑是否接受空引用作为有效值。

无论您做出什么决定,都必须使该决定非常明确并记录在案。您可以通过抛出System.ArgumentNullException,例如,并在方法上使用XML文档标题来实现。

System.IndexOutOfRangeException

在应用程序代码通常不会抛出或捕获该异常的意义上,该异常与上一个异常类似。

那为什么呢?

好吧,当您尝试使用无效的索引值访问数组,列表或任何可索引序列中的元素时,将引发此异常。一个简单的例子:

1
2
3
4
5
public static void PrintUrlSufix(string url)
{
var parts = url.Split('.');
Console.WriteLine(parts[2]);
}

上面的代码显然希望使用www.acme.com格式的URL 。但是,如果只是获得acme.com怎么办?为此,如果得到Hakuna Matata怎么办?是的,没错:System.IndexOutOfBoundException是的。

您如何避免遇到此异常?永远不要把事情视为理所当然。永远不要仅仅假设数据将采用正确的格式。做您的尽职调查和检查的东西。

System.IO.IOException

此C#异常具有一个不言自明的名称。这正是您的想法:这是IO操作期间发生错误时引发的异常。与前两个异常不同,您可能会发现自己不时捕捉或抛出其中一个。

本IOException类实际上是一些更具体的异常,例如:

  • DirectoryNotFoundException
  • EndOfStreamException
  • FileNotFoundException
  • FileLoadException
  • PathTooLongException

有关的文档,IOException建议您尽可能使用更具体的异常,而不是更一般的异常。

System.Net.WebException

此异常与网络有关。如果使用可插拔协议访问网络时发生错误,则抛出该错误。处理此异常时,请记住验证该Response属性,该属性将包含远程主机返回的响应。

System.Data.SqlClient.SqlException

此异常与数据库(特别是SQL Server)有关。SQL Server返回错误或警告时将引发该错误。该类具有一个称为的属性Errors,该属性是一个包含SqlError该类的一个或多个实例的集合。依次包含有关发生的错误的详细信息。

System.StackOverflowException

当执行堆栈溢出时,抛出此异常,这通常意味着递归出错。该代码有太多的嵌套方法调用。事情就是这样:这个异常是无法捕获的-至少从.NET 2.0起就没有-这意味着当抛出该异常时,您几乎没有其他选择。默认情况下,您的过程将被终止。

您应该做的而不是捕获此异常的方法是编写代码,以防止它首先发生。

System.OutOfMemoryException

可以说这是最令人困惑的C#异常之一。网上有很多资源可以很好地阐明问题,但是我将在此处提供一个简短的版本。发生的情况是此异常不涉及可用的物理内存。

那么,什么时候抛出此异常?

您知道何时要停车吗,因为其他驾驶员未正确停车而不能停车吗?如果您只需在停放的汽车之间添加所有可用空间,就足以容纳您的车辆。但是目前不可能,因为它不是连续的区域。

当您获得此内存时,这差不多发生了什么。可能有很多可用的总内存,但是没有连续的部分可以满足所需的分配。实际上,例如,如果您尝试将 StringBuilder的MaxCapacity属性扩展到该属性之外,则会发生此异常。

System.InvalidCastException

此异常也具有不言自明的名称。当代码由于未定义强制类型转换而无法从一种类型转换为另一种类型时,将引发该错误。以下代码将引发此类型的异常:

1
2
object o = "10";
int x = (int)o;

这是您通常不会捕获的异常。相反,您将以不会发生的方式编写代码。例如,以下代码在尝试强制类型转换之前进行类型检查:

1
2
3
4
5
6
7
8
public override bool Equals (object obj )
{
if (!obj is Foo)
return false;

Foo other = (Foo)obj;
return this.bar == other.bar;
}

上面的代码可以使用简化的as操作符,在这里我没有进一步具体说明,作为一个练习留给读者。无论如何,这就是问题:您实际上应该尝试做的是避免问题,而不是首先进行转换。利用泛型来防止陷入需要强制转换的情况。

System.InvalidOperationException

像之前的许多其他异常一样,这种异常通常是您不会发现的。相反,您应该做的是编写不会发生的代码。考虑以下示例:

1
2
var numbers = new List<int> { 1, 3, 5 };
var firstGreaterThanFive = numbers.Where(x => x > 5).First();

当序列不产生任何结果时,将抛出First LINQ扩展方法。如果您知道该序列有时可能不会产生结果-没关系-您应该改用FirstOrDefault。在空序列上调用此方法时,将返回序列类型的默认值,而不是抛出异常。

System.ObjectDisposedException

我们将在这篇文章中介绍的最后一个C#异常也属于“不应该处理,请修复代码”类别。换句话说,这是开发人员错误。当您尝试使用已处理的IDisposable进行操作时,将引发此异常。

此当通常发生在开发者调用Dispose,Close或其它类似方法和后来试图访问该对象的一个成员时。

如何成功的进行异常处理?

错误处理是软件开发教学中经常被忽略的话题,出现异常有时非常不幸。如果没有可靠的错误处理策略,您的应用程序将永远是劣等的。

通过本文,我们希望通过定义异常的概念并对C#异常的主要类型进行快速概述,以帮助解决该问题。但本文并没有涵盖异常处理的全部。恰好相反,我认为这是一个机会,可以开始引导您对该主题的学习,并且永不停止学习和练习。

祝您能想出一种对您的应用程序有效的策略!

如何使用ABP进行软件开发(2)三层架构与领域驱动设计的对比

Posted on 2020-07-19 | Edited on 2021-04-25 | In 技术

简述

上一篇简述了ABP框架中的一些基础理论,包括ABP前后端项目的分层结构,以及后端项目中涉及到的知识点,例如DTO,应用服务层,整洁架构,领域对象(如实体,聚合,值对象)等。

笔者也曾经提到,ABP依赖于领域驱动设计这门方法论,由于其门槛较高,给使用者带来了不少理解上的难度。尤其是三层架构对.NET开发者影响太深,有时很难对领域驱动设计产生直观的理解。

在本文中,打算从传统的简单三层架构谈起,介绍一个实际场景下的三层业务逻辑实现,然后再与领域驱动设计中的对应实现形成对比,以便让开发者形成直观具体的印象。

回顾三层架构

对于.NET开发者来说,三层架构相比都不陌生,这种架构,将代码层次划分为用户界面层,业务逻辑层、数据访问层三个逻辑层次,实现了代码的关注度分离,且因其易于理解,已经成为众多.NET开发者的”条件反射”。

三层架构简介

三层架构就是为了符合“高内聚,低耦合”思想,把各个功能模块划分为表示层(UI)、业务逻辑层(BLL)和数据访问层(DAL)三层架构,各层之间采用接口相互访问,并通过对象模型的实体类(Model)作为数据传递的载体,不同的对象模型的实体类一般对应于数据库的不同表,实体类的属性与数据库表的字段名一致。

三层架构区分层次的目的是为了 “高内聚,低耦合”。开发人员分工更明确,将精力更专注于应用系统核心业务逻辑的分析、设计和开发,加快项目的进度,提高了开发效率,有利于项目的更新和维护工作。

三层架构的分层逻辑

UI层:用户界面层,实现与UI交互有关的逻辑。用于输入用户数据,输出和呈现数据。在基于WebAPI的现代Web框架中,往往会使用MVC架构,将界面的数据行为,拆分成“模型-视图-控制器”,实现了针对对UI层上关注度的进一步分离,

业务逻辑层: 业务逻辑层是用户界面层和数据访问层之间的转换层,负责完成对数据的业务组装,界面数据处理,将数据层的对象输出(转换)给用户界面层。

数据访问层:实现数据的存储(持久化)操作,包括集成存储过程,集成SQL语句,或集成现代ORM组件的形式,实现实现数据的存储。

图片

三层架构的应用

遇到项目,先从实体关系建模开始,使用PowerDesign或其他数据库设计软件分析业务与业务之间的关系,是一对多,还是一对一,还是多对多,绘制实体关系图。

图片

在进行软件开发时,根据数据需求,定制想要的数据接口,从而实现以数据为核心的业务功能开发。

于是,在业务层次上,这种三层架构,进一步可以表示为如下分层结构:

图片

在三层架构中,实体是业务的核心,所有的业务代码,都是围绕实体展开,而左侧三个功能层,其主要目的都是为了实现对实体的“增删改查”操作。

以下代码简述了一个订单对象提交的全过程。(模型和代码仅供参考,不能直接运行)

图片

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
/// <summary>
/// UI控制器
/// </summary>
public class OrderController
{
private OrderBll OrderBll = new OrderBll();
/// <summary>
/// 新增订单
/// </summary>
/// <param name="userId"></param>
/// <param name="productId"></param>
/// <param name="count"></param>
public void AddOrder(int userId, int productId, int count)
{
    OrderBll.AddOrder(userId, productId, count);
}
}

/// <summary>
/// 业务逻辑层
/// </summary>
public class OrderBll
{
private UserInfoDal userInfoDal = new UserInfoDal();
private ProductInfoDal productInfoDal = new ProductInfoDal();
private OrderDal orderDal = new OrderDal();
/// <summary>
/// 新增订单
/// </summary>
/// <param name="userId"></param>
/// <param name="productId"></param>
/// <param name="count"></param>
public void CreateOrder(int userId, int productId, int count)
{
    UserInfo userInfo = userInfoDal.Get(userId);
    ProductInfo productInfo = productInfoDal.Get(productId);
    //新订单
    Order order = new Order();
    order.Address = userInfo.Address;
    order.UserId = userId;
    order.TotalPrice = productInfo.Price * count;
    order.ProductId = productId;
    orderDal.Insert(order);
}
}
/// <summary>
/// 数据访问层
/// </summary>
public class OrderDal
{
    /// <summary>
    /// 插入数据
    /// </summary>
    /// <param name="order"></param>
    public void Insert(Order order)
    {
    }
}

这种基于实体驱动建模的三层架构,变成了以数据为核心的“表模块模式”。
参见《企业应用架构模式》第87页中关于表模块的介绍:

表模块以一个类对应数据库中的一个表来组织领域逻辑,而且使用单一的类实体来包含将对数据进行的各种操作程序。
通常,表模块会与面向表的后端数据结构一起使用。以列表形式排列的数据通常是某个SQL调用的结果,它们被至于一个记录集中,用于某一个SQL表。表模块提供了一个明确的基于方法的接口对数据进行操作。
要进行一些实际的操作,一般需要多个表模块的行为。
表模块中的“表”一词,暗示你数据库中的每一个表对应一个表模块。虽然大多数情况下都是如此,但也并非绝对。对于通用的视图或其他查询,建立一个表模块也是有用的。事实上,表模块的结构并非真的取决于数据库表的结构,更多的是由应用程序能识别的虚拟表所标识,例如视图或查询。

在《Microsoft.NET企业级架构设计》一书中,作者认为“多数.NET开发者在成长的过程中都受到了表模块模式的影响”。而相比之下,多数Java开发者则“深陷事务脚本的泥足”。

三层架构的优缺点

优点:

软件分层架构的目的是为了分离关注点,三层架构也同样如此,简简单单的三层代码+ER图,就能设计出一个良好结构的软件系统。

这种模式,建立了以数据库表为核心的开发模式,使得开发者能够很便捷的对业务进行分析,进而驱动软件功能的快速开发。

在应对简单业务变迁过程中,由于能够快速完成代码的堆积,也使得开发者只需关注数据库表的拼凑,就能快速的完成代码开发,为开发项目带来了不少便利。

除了简单业务普遍采用三层,事实上许多复杂项目也会同样采用,大概是由于三层架构的思想已经深入人心,许多资深开发者都形成的只要有表就能完成项目的开发的思维定势。

缺点:

还是使用上述示例代码,我们假设需求发生了变化,要求减少订单的数量或增加订单,我们会怎么做?也许,我们很容易就写出了下面的代码:

(当然,实际项目中,如果订单已经提交,很少会直接对订单数量进行修改的,往往会重新发起新订单,但为了演示方便,我们先设定有这么一个奇怪的需求吧。)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
/// <summary>
/// 减少订单数量
/// </summary>
/// <param name="orderId"></param>
/// <param name="minusCount"></param>
public void MinusOrder(int orderId, int minusCount)
{
    Order order = orderDal.Get(orderId);
    order.Count -= minusCount;
    order.TotalPrice -= order.Price * minusCount;
    orderDal.Update(order);
}
/// <summary>
/// 增加订单数量
/// </summary>
/// <param name="orderId"></param>
/// <param name="minusCount"></param>
public void AddOrder(int orderId, int addCount)
{
    Order order = orderDal.Get(orderId);
    order.Count += addCount;
    order.TotalPrice += order.Price * addCount;
    orderDal.Update(order);
}

这个代码写起来非常快,因为只是新增了两小段代码逻辑,而从减少订单,到新增订单,只是加法和减法的区别,自然而然就更快了。
但是,速度快,一定是优点么?如果需求继续持续不断的累积呢。

例如,我们要修改订单收货人,收货地址,修改订单价格,是不是我们这种代码逻辑会越来越多,而且不同的业务逻辑互相搅合,使得后期的维护变得越来越困难?

所以,笔者认为,三层架构的缺点,就是前期开发速度太快,由于缺乏设计思想和设计模式的参与,太容易导致异味、垃圾代码、重复代码等问题产生。

所有这些问题,最终都被归类于“技术债”的范畴。

详见维基百科。
技术债:指开发人员为了加速软件开发,在应该采用最佳方案时进行了妥协,改用了短期内能加速软件开发的方案,从而在未来给自己带来的额外开发负担。
这种技术上的选择,就像一笔债务一样,虽然眼前看起来可以得到好处,但必须在未来偿还。
软件工程师必须付出额外的时间和精力持续修复之前的妥协所造成的问题及副作用,或是进行重构,把架构改善为最佳实现方式。

回顾领域驱动设计

领域驱动设计简介

领域驱动设计思想来源于埃里克埃文斯在2002年前后出版的技术书籍《领域驱动设计·软件系统复杂性核心应对之道》,在这本书中,作者介绍了领域驱动设计相关的核心模式,例如:统一语言,模型驱动设计,领域实体,聚合,值对象,仓储,限界上下文等模式。

随着微服务的不断兴起,领域驱动设计也越来越受到互联网人的广泛追捧,在许多不同的行业应用实践过程中,已经逐渐扮演了非常基础的作用。无论是微服务架构下的服务粒度拆分,或者甚至是中台应用,以及传统的单体应用,都可以利用领域驱动设计思想下提供的模式,为应用程序的开发插上想象的翅膀。

领域驱动设计的分层逻辑

在上一篇博客中,我们也介绍了领域驱动设计思想分层逻辑结构,共划分为如下四个层次:

图片

  • 用户界面层(或者表示层):负责向用户显示信息和解释用户指令。这里的用户,既可以是使用用户界面的人,也可以是另外一个计算机系统。
  • 应用层:定义软件要完成的任务,并且只会表达领域概念的对象来解决问题。这一层实际上负责的是系统与应用层进行交互的必要渠道。
  • 领域层:负责表达业务概念、业务状态信息以及业务规则。尽管技术细节由基础设施层实现,但业务情况状态的反映则需要有领域层进行控制。领域层是业务软件的核心。
  • 基础设施层:为上面各层提供通用的技术能力:为应用层传递消息,为领域层提供持久化机制,为用户界面层绘制屏幕组件等等。基础设施层还能够通过架构框架来支持4个层次间的交互模式。

    领域驱动设计的应用步骤

1)形成统一语言

统一语言是围绕产品展开的一系列流程,方案,术语和名词解释及匹配的注释。在领域驱动设计为每个应用设计成体系的【统一语言】是核心要点。

统一语言的形成是团队成员协同参与,围绕不同的需求,达成一致性理解的过程。

形成统一语言有时需要领域专家的参与,但有时可能难以达到这个条件,用需求代言人也同样能够满足这个条件。

2)使用UML建模和画图

  1. 建模的必要性

在我们工作过程中模型无处不在,不管是在纸上绘制的简单模型,或者使用专业软件绘制的各种模型,都是模型。领域驱动设计本身,依然依赖于模型驱动设计。

学会建模对于广大开发者来说,都是一项基本技能,当然也是众多最弱技能中的一种,因为广泛依赖于实体关系建模的思维模式,使得开发者已经很难形成有效的模型设计思想,代码也越来越趋于【过程化】。

有时开发者甚至连实体关系建模这个步骤都会省略,直接使用Code First或甚至数据库开始建表,这样看起来速度非常快,但是太容易翻车了。

在团队协作项目中,没有良好的模型,仅凭高级开发者或有经验开发者的“”一面之词”进行设计,几乎很难完成一个复杂项目。

而uml统一建模语言也是这样的良好工具。

  1. 使用哪些模型

笔者曾经有幸请教国内.NET技术圈拥有多年DDD实践经验的阿里技术专家,汤雪华老师,他指出:

采用实体关系建模很容易看出对象与对象的关系,但仅此而已。数据并非对象,数据也无法看出行为,如果要依托实体关系建模来构建系统,往往需要开发者发挥自己的主观抽象思维,根据客户提供的资料或可用的原型,自行思考代码的逻辑实现。
但显然,具备优秀逻辑思维能力和设计思想的开发者凤毛麟角,仅凭ER图,代码写出来往往很糟糕。

他认为,采用领域驱动设计,产品架构图,系统架构图,领域模型图,类图,关键业务场景的交互时序图,这些是必不可少的。

  • 产品架构图:列出产品功能,表现出产品模块间的相关性。

图片

图来自http://www.woshipm.com/pmd/1065960.html

  • 系统架构图:从技术层面列出系统模块组成关系。

图片

原图来自互联网

  • 领域模型关系图:反映出各领域模型间的相关性,限界上下文,聚合,和聚合根。

图片

来自https://102.alibaba.com/detail?id=174

  1. 如何建模?

如果说代码语言是为了与其他开发者进行沟通交流,那我们建立的各种软件设计模型将极大的方便不同领域的人员进行交流。建模也可以称之为语言的一部分。利用uml建立类图,是一种可以比较易于接受的方式。我们可以采用以下手段来建立领域模型。

1)建立一个与实现绑定的模型。初版的模型也许很简陋,但是它可以成为一个基础,然后在后期逐渐完善。

2)建立一种基于模型的通用语言或表达形式和机制。通过通用语言让参与项目的所有人理解模型。

3)开发一个蕴含丰富知识的模型。模型不是单纯的数据结构,它更是各类知识的聚合体。

4)提炼模型,模型应该能在项目过程中动态改变,发现新的概念就加进来,过时的概念就适时移除,避免臃肿。

5)头脑风暴和实验。模型在于实践和应用,它需要项目参与者共同的努力,而头脑风暴是发挥集体智慧的良好方式。对模型进行实验或者进行场景的模拟,有利于让模型更符合需求。

当然,对于领域专家而言,不同类型的模型也许无法理解,例如类图可能过于复杂,可以使用画图的形式,通过解释性的图形,甚至纸面上的图,更能直观的表现出领域的逻辑层次。

图片

这张来自TW分享的一张图,就是一个基于.NET MVC的产品设计UML设计图。

建模也并非这篇博客所能讲清楚的,包括笔者自己,也只是偶尔设计过用例图,时序图和类图,可能需要在后期系统的学习一下。

3)代码实现

回到最开始的那个三层架构下的代码示例,如果采用领域驱动设计,大概如下图所示:

图片

回到开始那个示例代码,如果采用DDD的代码实现,大概是这样的:

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
/// <summary>
/// 应用服务层
/// </summary>
public class OrderAppService
{
    private OrderRepository _orderRepository;
    private UserInfoRepository _userInfoRepository;
    private ProductInfoRepository _productInfoRepository;
    public OrderAppService(OrderRepository orderRepository, UserInfoRepository userInfoRepository, ProductInfoRepository productInfoRepository)
    {
        _orderRepository = orderRepository;
        _userInfoRepository = userInfoRepository;
        _productInfoRepository = productInfoRepository; 
    }
  /// <summary>
/// 新增订单
        /// </summary>
        /// <param name="userId"></param>
        /// <param name="productId"></param>
        /// <param name="count"></param>
        public void CreateOrder(int userId, int productId, int count)
        {
              UserInfo userInfo = _userInfoRepository.Get(userId);
            ProductInfo productInfo = _productInfoRepository.Get(productId);
            if (userInfo != null && productInfo != null)
            {
                //新订单
                Order order = Order.CreateOrder(productId, userInfo.Address, userId, productInfo.Price, count);
                _orderRepository.Insert(order);
            }
        }
        /// <summary>
        /// 减少订单数量
        /// </summary>
        /// <param name="orderId"></param>
        /// <param name="minusCount"></param>
        public void MinusOrder(int orderId, int minusCount)
        {
            Order order = _orderRepository.Get(orderId);
            order.Minus(minusCount);
            _orderRepository.Update(order);
        }
        /// <summary>
        /// 增加订单数量
        /// </summary>
        /// <param name="orderId"></param>
        /// <param name="minusCount"></param>
        public void AddOrder(int orderId, int addCount)
        {
            Order order = _orderRepository.Get(orderId);
            order.Add(addCount);
            _orderRepository.Update(order);
        }
}
    /// <summary>
    ///订单对象 
    /// </summary>
    public class Order
    {
        /// <summary>
        /// 主键
        /// </summary>
        public int Id { get; protected set; }
        /// <summary>
        /// 地址
        /// </summary>
        public string Address { get; protected set; }
        /// <summary>
        /// 用户id
        /// </summary>
        public int UserId { get; protected set; }
        /// <summary>
        /// 产品id
        /// </summary>
        public int ProductId { get; protected set; }
        /// <summary>
        /// 数量
        /// </summary>
        public int Count { get; protected set; }

        /// <summary>
        /// 单价
        /// </summary>
        public double Price { get; protected set; }
        /// <summary>
        /// 总价
        /// </summary>
        public double TotalPrice { get; protected set; }
        /// <summary>
        /// 创建订单
        /// </summary>
        /// <param name="productId"></param>
        /// <param name="address"></param>
        /// <param name="userId"></param>
        /// <param name="price"></param>
        /// <param name="count"></param>
        /// <returns></returns>
        public static Order CreateOrder(int productId, string address, int userId, double price, int count)
        {
            return new Order()
            {
                Address = address,
                UserId = userId,
                TotalPrice = price * count,
                ProductId = productId,
            };
        }
        /// <summary>
        /// 新增
        /// </summary>
        /// <param name="count"></param>
        public void Add(int count)
        {
        }
        /// <summary>
        /// 减少
        /// </summary>
        /// <param name="count"></param>
        public void Minus(int count)
        {
        }

    }

这段代码,最主要的变化是如下几点:

  1. 引入领域模型,在三层架构的示例代码中,我们建立了如下模型:

图片

这个模型是当我们Entity Framework脚本时生成的实体模型,在业内通常称其为“贫血模型”。对人类来说,红细胞负责把氧气输送到组织细胞,然后新陈代谢,产生ATP,产生动力。

而“贫血模型”这个术语恰如其份的表现出这类模型虽然还能有效的工作,但是需要由其他对象来驱动其完成动作的含义。

领域模型与贫血模型相比,更关注对象的行为,而关注行为的目的是创建带有公共接口并与在现实世界观察到的实体相似的对象,使得依照统一语言的名字和规则进行建模变得更加容易。

  1. 将原来的Order对象抽象化建模为一个DDD实体。DDD实体是一个包含数据(属性)和行为(方法)的POCO对象。

在《Microsoft .NET企业级应用架构实战》书第9.2.2中指出了领域实体的特点:

定义明确的身份标识。
通过公共和非公共方法表示行为。
通过只读属性暴露状态。
限制基元类型的使用,使用值对象代替。
工厂方法优于多个构造函数。
3. 私有set或protected set:

在示例代码中将Order中的所有属性设置为

1
public double TotalPrice { get; protected set; }

这样的目的是为了避免对该属性的随意更改,使得开发者在对属性进行操作过程中,多了一个环节,即需要谨慎思考这样的代码修改,从行为角度来分析是否符合业务需要。
在设计时实体时,开放set可能会带来严重的副作用,例如影响实体的状态。

在张逸老师的《领域驱动设计实战,战术篇第15课》中,作者指出:

对象之间若要默契配合,形成良好的协作关系,就需要通过行为进行协作,而不是让参与协作的对象成为数据的提供者。
《ThoughtWorks 软件开发沉思录》中的“对象健身操”提出了优秀软件设计的九条规则,其中最后一条提出:不使用任何 Getter/Setter/Property。
作者 Jeff Bay 认为:“如果可以从对象之外随便询问实例变量的值,那么行为与数据就不可能被封装到一处。在严格的封装边界背后,真正的动机是迫使程序员在完成编码之后,一定有为这段代码的行为找到一个适合的位置,确保它在对象模型中的唯一性。”

当然,在实际开发过程中,有时并不一定把get方法也设置为protected,毕竟有时候还需要有所妥协。

  1. 将创建方法从业务逻辑层,移动到了领域对象Order中的静态工厂方法。
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    /// <summary>
    /// 创建订单
    /// </summary>
    /// <param name="productId"></param>
    /// <param name="address"></param>
    /// <param name="userId"></param>
    /// <param name="price"></param>
    /// <param name="count"></param>
    /// <returns></returns>
    public static Order CreateOrder(int productId, string address, int userId, double price, int count)
    {       
        return new Order()
        {
            Address = address,
            UserId = userId,
            TotalPrice = price * count,
            ProductId = productId,
        };
    }

创建过程应该是一个非常严谨的过程,而原来在业务逻辑层中初始化对象的方法,随意性比较高,很容易就出现开发者在创建过程中将无关属性赋值的现象。
但如果把创建过程改成使用构造方法,又可能会造成可读性问题,而使用工厂方法,并创建一个受保护的构造方法则不会造成这个担忧。

  1. 将订单新增内容和减少内容从业务逻辑层移动到了领域对象上,并封装为方法。采用迪米卡法则,只暴露最小的参数,每次只对最该赋值的属性进行操作,也容易约束开发者的操作。
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    /// <summary>
    /// 新增
    /// </summary>
    /// <param name="count"></param>
    public void Add(int count)
    {
    }

    /// <summary>
    /// 减少
    /// </summary>
    /// <param name="count"></param>
    public void Minus(int count)
    {

    }

大概修改过程是最容易造成领域知识丢失的地方,而通过封装为方法,使得这个过程得以以受控的形式进行,有助于让其他开发者通过暴露的方法。
但这样做要确保所使用的命名规范符合统一语言,否则会重蹈贫血模型的覆辙。当然,在领域设计中,经常会纠结于哪些行为应该放在领域对象中,可以参考这样的规则:

  • 如果方法只处理实体的成员,它可能属于这个实体。
  • 如果方法访问相同聚合的其他实体或值对象,它可能属于聚合根。
  • 如何方法里的代码需要查询或更新持久层,或者需要用到实体(或聚合)边界以外的引用,它属于领域服务方法。

    对比分析

二者的对比

笔者整理了一个简单的图表来表现二者的对比关系。显然,三层架构并非毫无优势,领域驱动设计也并非银弹。

三层架构 领域驱动设计
业务识别方法 结合瀑布模型,通过需求分析,形成数据字典,指导数据库设计。 团队协作形成统一语言,并从统一语言中提取术语,指导类、流程,变量,行为定义等。
业务参与者 具备IT知识的开发人员,业务人员只能提供需求,往往不能参与设计过程。 由需求提供者或客户、开发者、测试、产品经理等组成的跨职能团队全力参与。
建模方法 实体关系建模为主,有时可以用UML 以UML方法为主,画图为辅
业务代码分层 业务代码理论上应该在业务逻辑层,但有时游离在控制器、业务逻辑层或数据访问层,甚至受依赖的其他业务逻辑中 业务代码在领域层,有时在领域对象上,有时在领域服务中。
修改代码的难易程度 随时随地想改就改 需要遵循一定的设计原则或步骤、流程
可维护性 项目简单时,易于维护;复杂时,难于维护。 掌握方法时,维护难度比较平滑。
数据持久化 在数据访问层中完成,有时可以适当复用;也有开发者将数据访问层提取出仓储的模板方法进行复用。 一般在仓储层中实现,且仓储一般是基础设施,意味着除特定场景外,基础设施不会依赖于领域而二外定制行为。
多业务逻辑的整合 一般在业务逻辑层中实现 一般在应用服务层实现。
可测试性 比较难以加入测试代码 易于加入测试代码;也可以根据UML使用TDD来进行开发。

该如何取舍?

下图这种流传已久,同样来自马丁弗勒老爷子《企业架构应用模式》。

表现了随着软件复杂度的逐渐提升,数据驱动设计和领域驱动设计模式两种不同类型的设计模式的开发效率(时间)对比曲线。

图片

  • 数据驱动设计建立了一个比较平滑的发展轨迹,但是随着拐点的到来,将变得越来越为难以维护,最终造出一个难以维护的“意大利面”。

图片

  • 领域驱动设计,前期的起点确实比数据驱动设计要高很多,而且甚至在刚刚使用一段时间后,由于业务复杂度的提升,会迎来一个拐点。这个拐点有点像“邓宁·克鲁格效应”中遇到的“绝望之谷”,让开发者和管理层感觉有点力不从心,不少企业最终又拆掉了他们的领域驱动设计搭建的软件;
  • 使用领域驱动设计,随着复杂度的逐渐推移,软件开发人员的信心越来越足,代码自然也能够不断演进,平滑发展,。

图片

  • 从长期来看,领域驱动建模将给复杂系统带来更加高效的维护效能。

    总结

本文介绍了三层架构和领域驱动设计两种不同的设计思想中如何实现业务逻辑代码的过程,并对针对代码的维护性问题进行了分析。由于时间仓促,部分观点、设计图、代码可能还不够成熟,还请大家批评指正。

下一篇将介绍ABP框架开发中的具体实践步骤。

如何在ASP.NET Core中使用SignalR构建与Angular通信的实时通信应用程序

Posted on 2020-06-25 | Edited on 2021-04-25 | In 技术

图片

假设我们要创建一个监视Web应用程序,该应用程序为用户提供了一个能够显示一系列信息的仪表板,这些信息会随着时间的推移而更新。

第一种方法是在定义的时间间隔(轮询)定期调用API 以更新仪表板上的数据。

无论如何,还是有一个问题:如果没有更新的数据,我们会因请求而不必要地增加网络流量。一种替代方法是长轮询技术:如果服务器没有可用数据,则它可以使请求保持活动状态,直到发生某种情况或达到预设的超时时间为止,而不是发送空响应。如果存在新数据,则完整的响应将到达客户端。完全不同的方法是反转角色:当有新数据可用(推送)时,后端与客户端联系。

请记住,HTML 5具有标准化的WebSocket,这是一个永久的双向连接,可以在兼容的浏览器中使用Javascript接口进行配置。不幸的是,必须在客户端和服务器端都对WebSocket提供完全支持,以使其可用。然后,我们需要提供替代系统(fallback),无论如何,该替代系统都允许我们的应用程序运行。

微软于2013年发布了一个名为SignalR for ASP.NET的开源库,该库已于 2018年为ASP.NET Core进行了重写。SignalR从与通信机制有关的所有细节中进行抽象,并从可用的信息中选择最佳的一种。结果是有可能编写代码,就像我们一直处于push-mode一样。使用SignalR,服务器可以在其所有连接的客户端或特定客户端上调用JavaScript方法。

我们使用web-api模板创建一个ASP.NET Core项目,删除已生成的示例控制器。使用NuGet,我们将Microsoft.AspNet.SignalR添加到项目中,以创建Hub。集线器是能够调用客户端代码,发送包含所请求方法的名称和参数的消息的高级管道。作为参数发送的对象将使用适当的协议反序列化。客户端在页面代码中搜索与名称相对应的方法,如果找到该名称,则将其调用并传递反序列化的数据作为参数。

1
2
3
4
5
6
using Microsoft.AspNetCore.SignalR;
 
namespace SignalR.Hubs
{
    public class NotificationHub : Hub { }
}

您可能知道,在ASP.NET Core中,可以配置HTTP请求的管理管道,以添加一些中间件,该中间件可拦截请求,添加已配置的功能并使其进入下一个中间件。必须预先配置SignalR中间件,在Startup 类的ConfigureServices方法中添加扩展方法services.AddSignalR()。现在,我们可以使用Startup类的Configure方法中的扩展方法app.UseSignalR()将中间件添加到管道中。在此操作期间,我们可以传递配置参数,包括集线器的路由:

1
2
3
4
app.UseSignalR(route =>
{
    route.MapHub<notificationhub>("/notificationHub");
})

一个有趣的场景允许我们查看ASP.NET Core中的另一个有趣功能,即在后台工作进程上下文中托管 SignalR Hub 。
假设我们要实现以下用例:

  • 运行业务逻辑
  • 等一下
  • 决定是停止还是重复该过程。

在ASP.NET Core中,我们可以使用框架提供的IHostedService接口在.NET Core应用程序中在后台实现进程的执行。方法要实现是StartAsync()和*StopAsync() *。非常简单:StartAsync调用到主机启动,而StopAsync调用到主机关闭。

然后,我们将一个类DashboardHostedService添加到项目中,该类实现IHostedService。我们在Startup类的ConfigureServices方法中添加接口注册:

1
services.AddHostedService<dashboardhostedservice>();

在类构造函数DashboardHostedService中,我们注入IHubContext 访问添加到我们应用程序的集线器。
在方法StartAsync中,我们设置了一个计时器,它将每两秒钟运行一次方法DoWork()中包含的代码。此方法发送带有四个随意生成的字符串的消息。

但是它向谁传播呢?在我们的示例中,我们正在将消息发送到所有连接的客户端。但是,SignalR提供了向单个用户或用户组发送消息的机会。在本文中,您将找到涉及ASP.NET Core中的身份验证和授权功能的详细信息。有趣的是,用户可以同时在台式机和移动设备上连接。每个设备都有一个单独的SignalR连接,但是它们都将与同一用户关联。

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 Microsoft.AspNetCore.SignalR;
using Microsoft.Extensions.Hosting;
using SignalR.Hubs;
using System;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
 
namespace SignalR
{
    public class DashboardHostedService: IHostedService
    {
        private Timer _timer;
        private readonly IHubContext<notificationhub> _hubContext;
 
        public DashboardHostedService(IHubContext<notificationhub> hubContext)
        {
            _hubContext = hubContext;
        }
 
        public Task StartAsync(CancellationToken cancellationToken)
        {
            _timer = new Timer(DoWork, null, TimeSpan.Zero,
            TimeSpan.FromSeconds(2));
 
            return Task.CompletedTask;
        }
 
        private void DoWork(object state)
        {
            _hubContext.Clients.All.SendAsync("SendMessage",
                new {
                    val1 = getRandomString(),
                    val2 = getRandomString(),
                    val3 = getRandomString(),
                    val4 = getRandomString()
                });
        }
 
        public Task StopAsync(CancellationToken cancellationToken)
        {
            _timer?.Change(Timeout.Infinite, 0);
 
            return Task.CompletedTask;
        }
    }
}

让我们看看如何管理客户端部分。例如,我们使用Angular CLI的ng new SignalR命令创建Angular应用程序。然后我们安装SignalR的包节点(npm i @ aspnet / signalr)。然后添加一个服务,该服务使我们可以连接到先前创建的集线器并接收消息。
在这里,第一种可能的方法是,基于服务getMessage()中Observable 的服务,通过使用私有声明的Subject 来返回(Message是与从Object返回的对象相对应的Typescript接口。后端):

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
@Injectable({
 providedIn: 'root'
})
export class SignalRService {
 private message$: Subject<message>;
 private connection: signalR.HubConnection;
 
 constructor() {
   this.message$ = new Subject<message>();
   this.connection = new signalR.HubConnectionBuilder()
   .withUrl(environment.hubUrl)
   .build();
   this.connect();
 }
 private connect() {
   this.connection.start().catch(err => console.log(err));
   this.connection.on('SendMessage', (message) => {
     this.message$.next(message);
   });
 }
 public getMessage(): Observable<message> {
   return this.message$.asObservable();
 }
 public disconnect() {
   this.connection.stop();
 }
}

在constructor()内部,我们创建一个SignalR.HubConnection类型对象,该对象将用于连接到服务器。我们通过使用文件environment.ts将其传递到其中心URL:

1
2
3
this.connection = new signalR.HubConnectionBuilder()
   .withUrl(environment.hubUrl)
   .build();

构造函数还负责调用connect()方法,该方法进行实际连接,并在控制台中记录可能的错误。

1
2
3
4
this.connection.start().catch(err => console.log(err));
this.connection.on('SendMessage', (message) => {
  this.message$.next(message);
});

想要显示来自后端的消息的组件(将其注入到构造函数中的服务),应该订阅getMessage()方法并管理到达的消息。以AppComponent为例,例如:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
@Component({
 selector: 'app-root',
 templateUrl: './app.component.html',
 styleUrls: ['./app.component.css']
})
export class AppComponent implements OnDestroy {
 private signalRSubscription: Subscription;
 
 public content: Message;
 
 constructor(private signalrService: SignalRService) {
   this.signalRSubscription = this.signalrService.getMessage().subscribe(
     (message) => {
       this.content = message;
   });
 }
 ngOnDestroy(): void {
   this.signalrService.disconnect();
   this.signalRSubscription.unsubscribe();
 }
}

使用主题允许我们同时管理更多组件,而无论从中心返回的消息(用于订阅还是用于取消订阅)都可以,但是我们必须注意对主题的粗心使用。让我们考虑以下getMessage()版本:

1
2
3
public getMessage(): Observable<message> {
   return this.message$;
}

现在,该组件也可以使用以下简单代码发送一条消息:

1
2
3
const produceMessage = this.signalrService.getMessage() as Subject<any>;
 produceMessage.next( {val1: 'a'});
</any>

如果方法getMessage()返回Subject asObservable,则此代码将引发异常!
我们可以在单个组件的情况下使用的第二种方法(更简单)对管理来自后端的消息感兴趣:

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
@Injectable({
 providedIn: 'root'
})
export class SignalrService {
 connection: signalR.HubConnection;
 
 constructor() {
   this.connection = new signalR.HubConnectionBuilder()
   .withUrl(environment.hubAddress)
   .build();
   this.connect();
 }
 
 public connect() {
   if (this.connection.state === signalR.HubConnectionState.Disconnected) {
     this.connection.start().catch(err => console.log(err));
   }
 }
 
 public getMessage(next) {
     this.connection.on('SendMessage', (message) => {
       next(message);
     });
 }
 
 public disconnect() {
   this.connection.stop();
 }
}

我们可以简单地将函数回调传递给方法getMessage,该函数将来自后端的消息作为参数。在这种情况下,AppComponent可以成为:

1
2
3
4
5
6
7
8
9
10
11
public content: IMessage;
constructor(private signalrService: SignalrService) {
   this.signalrService.getMessage(
     (message: IMessage) => {
       this.content = message;
     }
   );
}
ngOnDestroy(): void {
   this.signalrService.disconnect();
}

最后几行代码分别位于app.component.html和app.component.css中,以赋予一些时尚,并且该应用程序已完成。

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
<div style="text-align:center">
  <h1>
    DASHBOARD
  </h1>
</div>
<div class="card-container">
  <div class="card">
    <div class="container">
      <h4><b>Valore 1</b></h4>
      <p>{{content.val1}}</p>
    </div>
  </div>
  <div class="card">
    <div class="container">
      <h4><b>Valore 2</b></h4>
      <p>{{content.val2}}</p>
    </div>
  </div>
  <div class="card">
    <div class="container">
      <h4><b>Valore 3</b></h4>
      <p>{{content.val3}}</p>
    </div>
  </div>
  <div class="card">
    <div class="container">
      <h4><b>Valore 4</b></h4>
      <p>{{content.val4}}</p>
    </div>
  </div>
</div>
 
.card-container {
  display: flex;
  flex-wrap: wrap;
}
 
.card {
  box-shadow: 0 4px 8px 0 rgba(0,0,0,0.2);
  transition: 0.3s;
  width: 40%;
  flex-grow: 1;
  margin: 10px;
}
 
.card:hover {
  box-shadow: 0 8px 16px 0 rgba(0,0,0,0.2);
}
 
.container {
  padding: 2px 16px;
}

我们首先启动后端,然后启动前端并检查最终结果:
图片

看起来不错!您可以在这里找到代码:https : //github.com/AARNOLD87/SignalRWithAngular

下次见!

如何使用ABP进行软件开发(1)基础概览

Posted on 2020-06-24 | Edited on 2021-04-25 | In 技术

ABP框架简述

1)简介

在.NET众多的技术框架中,ABP框架(本系列中指aspnetboilerplate项目)以其独特的魅力吸引了一群优秀开发者广泛的使用。

在该框架的赋能之下,开发者可根据需求通过官方网站【https://aspnetboilerplate.com/Templates】选择下载例如Vue/AngluarJS/MVC等不同类型的模板项目,轻松加入ABP开发者的队伍中,尽享基于ABP开发带来的乐趣。

图片

ABP开发框架也提供了丰富的文档,能够为开发者带来许多便捷。目前ABP的文档网站为:

官方文档:https://aspnetboilerplate.com/Pages/Documents

文档库不可谓不全,加上国内众多的ABP开发者参与的活跃的技术圈子,使得学习成本只是在第一个项目中比较高,后期将会越来越平滑。

2)现状

当然,目前ABP的框架开发者和社区已经把更多的精力投入到了ABP.VNEXT开发框架,这个新框架以其DDD+微服务+模块化的理念获得了大量拥趸,使ABP框架的开发优先级已经开始逐渐降低。

但这是因为ABP框架的功能已经成熟稳定,且ABP是一种增量式的架构设计,开发者在熟练掌握这种框架后,可以根据自己的需要进行方便的扩展,使其成为小项目架构选型中一种不错的备选方案。

当然,也存在一些弊端。例如由于ABP被称为.NET众多开发框架中面向领域驱动设计的最佳实践,而囿于领域驱动设计本身不低的门槛,使得学习的过程变得看起来非常陡峭;

除此之外,ABP也广泛使用了目前Asp.NET/Asp.NET Core框架的大量比较新的特性,对于不少无法由于各种原因无法享受.NET技术飞速发展红利的传统开发者来说,无形中也提高了技术门槛。

3)综述

在这个系列中,本文计划分成三篇来介绍ABP框架,第一篇介绍ABP的基础概览,介绍基础知识,第二篇介绍ABP的模式实践,第三篇,试图介绍如何从更传统的三层甚至是单层+SQL的单层架构,如何迁移到ABP框架。

(毕竟。。.NET遗留应用实在是太多了,拯救或不拯救?)

代码结构结构

基本文件夹简述

当我们通过ABP模板项目的官方网站下载一个项目后,我们所获得的代码包的结构如下图所示,其中:

图片)图片

  • vue为使用iview框架构建的管理系统基本模板,该脚手架使用了yarn作为包管理器,并集成了vuex/axios等常用框架,并提供了用户,租户,权限三个基本功能的示例代码,开发者只需发挥聪明才智就能快速的通过该框架入手前端项目。
  • (当然,该项目广泛使用了typescript+面向对象的设计,似乎前端开发者。。普遍不擅长面向对象开发?)
  • aspnet-core则是一个完整的asp.netcore项目的快速开发脚手架。该脚手架集成了docker打包于一体,并包含基本的单元测试示例,使用了identity作为权限控制单元,使用swagger作为接口文档管理工具,集成了efcore、jwt等常用组件,对于开发者来说,基本上算是开箱即用了。

    前端vue项目

打开vue文件夹之后,该项目的基本目录如下图所示。(src文件夹)

图片

lib文件夹

定义了与abp+vue脚手架项目的基础组件和常见类库,封装了一系列基本方法。例如权限控制,数据请求,菜单操作,SignalR等基础组件的用法。

router文件夹

定义了vue项目的路由规则,其中index.ts文件是项目的入口,router.ts文件定义了vue文件的路由规则。

store文件夹

由于本项目使用了vuex框架,所以我们可以来看看对于store文件夹的介绍。

在vuex框架中:

每一个 Vuex 应用的核心就是 store(仓库)。“store”基本上就是一个容器,它包含着你的应用中大部分的状态 (state)。
Vuex 和单纯的全局对象有以下两点不同:
Vuex 的状态存储是响应式的。当 Vue 组件从 store 中读取状态的时候,若 store 中的状态发生变化,那么相应的组件也会相应地得到高效更新。
你不能直接改变 store 中的状态。改变 store 中的状态的唯一途径就是显式地提交 (commit) mutation。这样使得我们可以方便地跟踪每一个状态的变化,从而让我们能够实现一些工具帮助我们更好地了解我们的应用。

即vuex框架中,将原来的请求链路,抽象化为状态的变化,通过维护状态,使得数据的管理更加便捷,也易于扩展。

views文件夹

定义了登录、首页、用户、角色、租户的基本页面,并提供了新增、查看、编辑、删除的代码示例。

综上,该项目是一个结构清晰,逻辑缜密的前端框架,可以作为常见管理系统的脚手架。

后端项目

图片

简介

后端项目是一个遵循了领域驱动设计的分层,同时又符合Robert Martin在《代码整洁之道》提出的【整洁架构】。

图片

领域驱动设计简介

在领域驱动设计的分层设计中,共有四个功能分层,分别是:

表示层(Presentation Layer):为用户提供接口,使用应用层实现用户交互。

应用层(Application Layer):介于用户层和领域层之间,协调用户对象,完成对应的任务。

领域层(Domain Layer):包含业务对象和规则,是应用程序的心脏。

基础设施层(Infrastructure Layer):提供高层级的通用技术功能,主要使用第三方库完成。

在后文中,基于abp对领域驱动设计的功能分层将进行多次、详细叙述,本小节不再赘述。

整洁架构简介

整洁架构是由Bob大叔提出的一种架构模型,来源于《整洁架构》这本书,顾名思义,其目的并不是为了介绍这一种优秀的架构本身,而是介绍如何设计一种整洁的架构,使得代码结构易于维护。

图片

(整洁架构就是这样一个洋葱,所以也有人称它为“洋葱”架构)

  1. 依赖规则(Dependency Rule)

用一组同心圆来表示软件的不同领域。一般来说,越深入代表你的软件层次越高。外圆是战术是实现机制(mechanisms),内圆的是核心原则(policy)。

Policy means the application logic.

Mechanism means the domain primitives.

使此体系架构能够工作的关键是依赖规则。这条规则规定软件模块只能向内依赖,而里面的部分对外面的模块一无所知,也就是内部不依赖外部,而外部依赖内部。同样,在外面圈中使用的数据格式不应被内圈中使用,特别是如果这些数据格式是由外面一圈的框架生成的。我们不希望任何外圆的东西会影响内圈层

  1. 实体 (Entities)

实体封装的是整个企业范围内的业务核心原则(policy),一个实体能是一个带有方法的对象,或者是一系列数据结构和函数,只要这个实体能够被不同的应用程序使用即可。

如果你没有编写企业软件,只是编写简单的应用程序,这些实体就是应用的业务对象,它们封装着最普通的高级别业务规则,你不能希望这些实体对象被一个页面的分页导航功能改变,也不能被安全机制改变,操作实现层面的任何改变不能影响实体层,只有业务需求改变了才可以改变实体

  1. 用例 (Use case)

在这个层的软件包含只和应用相关的业务规则,它封装和实现系统的所有用例,这些用例会混合各种来自实体的各种数据流程,并且指导这些实体使用企业规则来完成用例的功能目标。

我们并不期望改变这层会影响实体层. 我们也不期望这层被更外部如数据库 UI或普通框架影响,而这也正是我们分离出这一层来的原因所在。

然而,应用层面的操作改变将会影响到这个用例层,如果需求中用例发生改变,这个层的代码就会随之发生改变。所以可以看到,这一层是和应用本身紧密相关的

  1. 接口适配器 (Interface Adapters)

这一层的软件基本都是一些适配器,主要用于将用例和实体中的数据转换为外部系统如数据库或Web使用的数据,在这个层次,可以包含一些GUI的MVC架构,表现视图 控制器都属于这个层,模型Model是从控制器传递到用例或从用例传递到视图的数据结构。

通常在这个层数据被转换,从用例和实体使用的数据格式转换到持久层框架使用的数据,主要是为了存储到数据库中,这个圈层的代码是一点和数据库没有任何关系,如果数据库是一个SQL数据库, 这个层限制使用SQL语句以及任何和数据库打交道的事情。

  1. 框架和驱动器

最外面一圈通常是由一些框架和工具组成,如数据库Database, Web框架等. 通常你不必在这个层不必写太多代码,而是写些胶水性质的代码与内层进行粘结通讯。

这个层是细节所在,Web技术是细节,数据库是细节,我们将这些实现细节放在外面以免它们对我们的业务规则造成影响伤害

ABP的分层实现

在ABP项目中,层次划分如下。

1. 应用层(Application项目)

在领域驱动设计的分层式架构中,应用层作为应用系统的北向网关,对外提供业务外观的功能。在Abp模板项目中,Application项目也是编写主要用例代码的位置,开发者们在此定义与界面有关的数据行为,实现面向接口的开发实践。

图片

应用服务层包含应用服务,数据传输单元,工作单元等对象。

  • Application Service

为面向用户界面层实现业务逻辑代码。例如需要为某些界面对象组装模型,通常会定义ApplicationService,并通过DTO对象,实现与界面表现层的数据交换。

  • Data Transfer Object (DTO)

最常见的数据结构为DTO(数据传输对象),这是来源于马丁弗勒在《企业架构应用模式》中提到的名词,其主要作用为:

是一种设计模式之间传输数据的软件应用系统。 数据传输目标往往是数据访问对象从数据库中检索数据。

在ABP的设计中,有两种不同类型的DTO,分别是用于新增、修改、删除的Input DTO,和用于查询的Output DTO。

  • Unit of Work:

工作单元。工作单元与事务类似,封装了一系列原子级的数据库操作。

2. 核心层(Core项目)

核心层包含领域实体、值对象、聚合根,以及领域上下文实现。

  • Entity(实体):

实体有别于传统意义上大家所理解的与数据库字段一一匹配的实体模型,在领域驱动设计中,虽然实体同样可能持久化到数据库,但实体包含属性和行为两种不同的抽象。

例如,如果有一个实体为User,其中有一个属性为Phone,数据为086-132xxxxxxxx,我们有时需要判断该手机号码的国际代号,可能会添加一个新的判定 GetNationCode(),可以通过从Phone字段中取出086来实现,这就是一种通俗意义上的行为。

  • Value Object(值对象):

值对象无需持久化到数据库,往往是从其他实体或聚合中“剥离”出来的与某些聚合具备逻辑相关性或语义相关性的对象,有时值对象甚至只有个别属性。

例如,上述实体,包含Phone字段,我们可以将整个Phone“剥离”为一个Telephone对象,该对象可包含PhoneNumber和NationCode字段。

1
2
3
4
5
6
7
8
9
public class User
{
public Telephone Phone{public get;private set;}
}
public class Telephone
{
public string PhoneNumber {get;set;}
public string NationCode {get;set;}
}
  • Aggregate & Aggregate Root(聚合,聚合根):

聚合是业务的最小工作单元,有时,一个实体就是一个小聚合,而为聚合对外提供访问机制的对象,就是聚合根。

在领域驱动设计中,识别聚合也是一件非常重要的工作,有一组系统的方法论可以为我们提供参考。

当然,事实上识别领域对象,包括且不限定于识别聚合、值对象、实体识别该对象的行为或(方法)本身是一件需要经验完成的工作,有时需要UML建模方法的广泛参与。

有时,我们会习惯于通过属性赋值完成梭代码的过程,从而造成领域行为流失在业务逻辑层的问题,那么或许可以采取这样的方法:

1、对象的创建,使用构造函数赋值,或工厂方法创建。

2、将所有对于属性的访问级别都设置为

1
public string Phone{public get;private set;}

然后再通过一个绑定手机号码的方法,来给这个对象设置手机号码。

1
2
3
public string BindPhone(string phone)
{
}

将所有一切涉及到对Phone的操作,都只能通过规定的方法来赋值,这样可以实现我们开发过程中,无意识的通过属性赋值,可能导致的“领域行为”丢失的现象发生。
这种方式可以使得对对象某些属性的操作,只能通过唯一的入口完成,符合单一职责原则的合理运用,如果要扩展方法,可以使用开闭原则来解决。

但是,采用这种方式,得尽量避免出现:SetPhone(string phone) 这样的方法出现,毕竟这样的方法,其实和直接的属性赋值,没有任何区别。

  • Repository(仓储)

仓储封装了一系列对象数据库操作的方法,完成对象从数据库到对象的转换过程。在领域驱动设计中,一个仓储往往会负责一个聚合对象从数据库到创建的全过程。

  • Domain Service(领域服务)

领域服务就是“实干家”,那些不适合在领域对象中出现,又不属于对象数据库操作的方法,又与领域对象息息相关的方法,都可以放到领域服务中实现。

  • Specification(规格定义)

规范模式是一种特殊的软件设计模式,通过使用布尔逻辑将业务规则链接在一起,可以重新组合业务规则。

实际上,它主要用于为实体或其他业务对象定义可重用的过滤器。

3. 其他基础设施(EntityFrameworkCore,Web.Core,Web.Host项目)

EntityFrameworkCore负责定义数据库上下文和对EFCore操作的一系列规则、例如种子数据的初始化等。

图片

Web.Core:定义了应用程序的外观和接口。虽然从表面上看,Web.Core定义了作为Web访问入口的控制器方法和登录验证的逻辑,看起来像是用户表现层的东西,但是仔细想想,这些东西,何尝不是一种基础设施?

Web.Host:定义WEB应用程序的入口。

总结

本文简述了ABP框架的前后端项目的分层结构,通过了解这些结构,将有助于我们在后续的实战中更快入手,为应用开发插上翅膀。

在Asp.NET Core中如何优雅的管理用户机密数据

Posted on 2020-06-09 | Edited on 2021-04-25 | In 技术

在Asp.NET Core中如何优雅的管理用户机密数据

背景

回顾

在软件开发过程中,使用配置文件来管理某些对应用程序运行中需要使用的参数是常见的作法。在早期VB/VB.NET时代,经常使用.ini文件来进行配置管理;而在.NET FX开发中,我们则倾向于使用web.config文件,通过配置appsetting的配置节来处理;而在.NET Core开发中,我们有了新的基于json格式的appsetting.json文件。

无论采用哪种方式,其实配置管理从来都是一件看起来简单,但影响非常深远的基础性工作。尤其是配置的安全性,贯穿应用程序的始终,如果没能做好安全性问题,极有可能会给系统带来不可控的风向。

源代码比配置文件安全么?

有人以为把配置存放在源代码中,可能比存放在明文的配置文件中似乎更安全,其实是“皇帝的新装”。

在前不久,笔者的一位朋友就跟我说了一段故事:他说一位同事在离职后,直接将曾经写过的一段代码上传到github的公共仓库,而这段代码中包含了某些涉及到原企业的机密数据,还好被github的安全机制提前发现而及时终止了该行为,否则后果不堪设想。

于是,笔者顺手查了一下由于有意或无意泄露企业机密,造成企业损失的案例,发现还真不少。例如大疆前员工通过 Github 泄露公司源代码,被罚 20 万、获刑半年 这起案件,也是一个典型的案例。

该员工离职后,将包含关键配置信息的源代码上传到github的公共仓库,被黑客利用,使得大量用户私人数据被黑客获取,该前员工最终被刑拘。 大疆前员工通过Github泄露公司源代码,被罚20万、获刑半年

图片来源: http://www.digitalmunition.com/WhyIWalkedFrom3k.pdf

大部分IT公司都会在入职前进行背景调查,而一旦有案底,可能就已经与许多IT公司无缘;即便是成为创业者,也可能面临无法跟很多正规企业合作的问题。

小结

所以,安全性问题不容小觑,哪怕时间再忙,也不要急匆匆的就将数据库连接字符串或其他包含敏感信息的内容轻易的记录在源代码或配置文件中。在这个点上,一旦出现问题,往往都是非常严重的问题。

如何实现

在.NET FX时代,我们可以使用对web.config文件的关键配置节进行加密的方式,来保护我们的敏感信息,在.NET Core中,自然也有这些东西,接下来我将简述在开发环境和生产环境下不同的配置加密手段,希望能够给读者带来启迪。

开发环境

在开发环境下,我们可以使用visual studio 工具提供的用户机密管理器,只需0行代码,即可轻松完成关键配置节的处理。

机密管理器概述

根据微软官方文档 的描述:

ASP.NET Core 机密管理器工具提供了开发过程中在源代码外部保存机密的另一种方法 。 若要使用机密管理器工具,请在项目文件中安装包 Microsoft.Extensions.Configuration.SecretManager 。 如果该依赖项存在并且已还原,则可以使用 dotnet user-secrets 命令来通过命令行设置机密的值。 这些机密将存储在用户配置文件目录中的 JSON 文件中(详细信息随操作系统而异),与源代码无关。

机密管理器工具设置的机密是由使用机密的项目的 UserSecretsId 属性组织的。 因此,必须确保在项目文件中设置 UserSecretsId 属性,如下面的代码片段所示。 默认值是 Visual Studio 分配的 GUID,但实际字符串并不重要,只要它在计算机中是唯一的。

1
2
3
4
> <PropertyGroup>
> <UserSecretsId>UniqueIdentifyingString</UserSecretsId>
> </PropertyGroup>
>

Secret Manager工具允许开发人员在开发ASP.NET Core应用程序期间存储和检索敏感数据。敏感数据存储在与应用程序源代码不同的位置。由于Secret Manager将秘密与源代码分开存储,因此敏感数据不会提交到源代码存储库。但机密管理器不会对存储的敏感数据进行加密,因此不应将其视为可信存储。敏感数据作为键值对存储在JSON文件中。最好不要在开发和测试环境中使用生产机密。查看引文。

存放位置

在windows平台下,机密数据的存放位置为:

1
%APPDATA%\Microsoft\UserSecrets\\secrets.json

而在Linux/MacOs平台下,机密数据的存放位置为:

1
~/.microsoft/usersecrets/<user_secrets_id>/secrets.json

在前面的文件路径中, ``将替换UserSecretsId为 .csproj文件中指定的值。

在Windows环境下使用机密管理器

在windows下,如果使用Visual Studio2019作为主力开发环境,只需在项目右键单击,选择菜单【管理用户机密】,即可添加用户机密数据。

在管理用户机密数据中,添加的配置信息和传统的配置信息没有任何区别。

{
“ConnectionStrings”: {
“Default”: “Server=xxx;Database=xxx;User ID=xxx;Password=xxx;”
}
}

我们同样也可以使用IConfiguration的方式、IOptions的方式,进行配置的访问。

在非Windows/非Visual Studio环境下使用机密管理器

完成安装dotnet-cli后,在控制台输入

1
dotnet user-secrets init

前面的命令将在UserSecretsId .csproj 文件的PropertyGroup中添加 .csproj一个元素。 UserSecretsId是对项目是唯一的Guid值。

1
2
3
4
<PropertyGroup>  
<TargetFramework>netcoreapp3.1</TargetFramework>
<UserSecretsId>79a3edd0-2092-40a2-a04d-dcb46d5ca9ed</UserSecretsId>
</PropertyGroup>

设置机密

1
dotnet user-secrets set "Movies:ServiceApiKey" "12345"

列出机密

1
dotnet user-secrets list

删除机密

1
dotnet user-secrets remove "Movies:ConnectionString"

清除所有机密

1
dotnet user-secrets clear

生产环境

机密管理器为开发者在开发环境下提供了一种保留机密数据的方法,但在开发环境下是不建议使用的,如果想在生产环境下,对机密数据进行保存该怎么办?

按照微软官方文档的说法,推荐使用Azure Key Vault 来保护机密数据,但。。我不是贵云的用户(当然,买不起贵云不是贵云太贵,而是我个人的问题[手动狗头])。

其次,与Azure Key Valut类似的套件,例如其他云,差不多都有,所以都可以为我们所用。

但。。如果您如果跟我一样,不想通过第三方依赖的形式来解决这个问题,那不如就用最简单的办法,例如AES加密。

使用AES加密配置节

该方法与平时使用AES对字符串进行加密和解密的方法并无区别,此处从略。

使用数据保护Api(DataProtect Api实现)

在平时开发过程中,能够动手撸AES加密是一种非常好的习惯,而微软官方提供的数据保护API则将这个过程进一步简化,只需调Api即可完成相应的数据加密操作。

关于数据保护api, Savorboard 大佬曾经写过3篇博客讨论这个技术问题,大家可以参考下面的文章来获取信息。

ASP.NET Core 数据保护(Data Protection 集群场景)【上】

ASP.NET Core 数据保护(Data Protection 集群场景)【中】

ASP.NET Core 数据保护(Data Protection 集群场景)【下】

(接下来我要贴代码了,如果没兴趣,请出门左拐,代码不能完整运行,查看代码)

首先,注入配置项

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public static IServiceCollection AddProtectedConfiguration(this IServiceCollection services, string directory)
{
services
.AddDataProtection()
.PersistKeysToFileSystem(new DirectoryInfo(directory))
.UseCustomCryptographicAlgorithms(new ManagedAuthenticatedEncryptorConfiguration
{
EncryptionAlgorithmType = typeof(Aes),
EncryptionAlgorithmKeySize = 256,
ValidationAlgorithmType = typeof(HMACSHA256)
});
;

return services;
}

其次,实现对配置节的加/解密。(使用AES算法的数据保护机制)

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

public class ProtectedConfigurationSection : IConfigurationSection
{
private readonly IDataProtectionProvider _dataProtectionProvider;
private readonly IConfigurationSection _section;
private readonly Lazy<IDataProtector> _protector;

public ProtectedConfigurationSection(
IDataProtectionProvider dataProtectionProvider,
IConfigurationSection section)
{
_dataProtectionProvider = dataProtectionProvider;
_section = section;

_protector = new Lazy<IDataProtector>(() => dataProtectionProvider.CreateProtector(section.Path));
}

public IConfigurationSection GetSection(string key)
{
return new ProtectedConfigurationSection(_dataProtectionProvider, _section.GetSection(key));
}

public IEnumerable<IConfigurationSection> GetChildren()
{
return _section.GetChildren()
.Select(x => new ProtectedConfigurationSection(_dataProtectionProvider, x));
}

public IChangeToken GetReloadToken()
{
return _section.GetReloadToken();
}

public string this[string key]
{
get => GetProtectedValue(_section[key]);
set => _section[key] = _protector.Value.Protect(value);
}

public string Key => _section.Key;
public string Path => _section.Path;

public string Value
{
get => GetProtectedValue(_section.Value);
set => _section.Value = _protector.Value.Protect(value);
}

private string GetProtectedValue(string value)
{
if (value == null)
return null;

return _protector.Value.Unprotect(value);
}
}

再次,在使用前,先将待加密的字符串转换成BASE64纯文本,然后再使用数据保护API对数据进行处理,得到处理后的字符串。

1
2
3
4
5
6
7
8
9
10
private readonly IDataProtectionProvider _dataProtectorTokenProvider;
public TokenAuthController( IDataProtectionProvider dataProtectorTokenProvider)
{
}
[Route("encrypt"), HttpGet, HttpPost]
public string Encrypt(string section, string value)
{
var protector = _dataProtectorTokenProvider.CreateProtector(section);
return protector.Protect(value);
}

再替换配置文件中的对应内容。

1
2
3
4
5
{
"ConnectionStrings": {
"Default": "此处是加密后的字符串"
}
}

然后我们就可以按照平时获取IOptions的方式来获取了。

问题

公众号【DotNET骚操作】号主【周杰】同学提出以下观点:

1、在生产环境下,使用AES加密,其实依然是一种不够安全的行为,充其量也就能忽悠下产品经理,毕竟几条简单的语句,就能把机密数据dump出来。

也许在这种情况下,我们应该优先考虑accessKeyId/accessSecret,尽量通过设置多级子账号,通过授权Api的机制来管理机密数据,而不是直接暴露类似于数据库连接字符串这样的关键配置信息。另外,应该定期更换数据库的密码,尽量将类似的问题可能造成的风险降到最低。数据保护api也提供的类似的机制,使得开发者能够轻松的管理机密数据的时效性问题。

2、配置文件放到CI/CD中,发布的时候在CI/CD中进行组装,然后运维只是负责管理CI/CD的账户信息,而最高机密数据,则由其他人负责配置。

嗯,我完全同意他的第二种做法,另外考虑到由于运维同样有可能会有意无意泄露机密数据,所以如果再给运维配备一本《刑法》,并让他日常补习【侵犯商业秘密罪】相关条款,这个流程就更加闭环了。

结语

本文简述了在.NET Core中,如何在开发环境下使用用户机密管理器、在生产环境下使用AES+IDataProvider的方式来保护我们的用户敏感数据。由于时间仓促,如有考虑不周之处,还请各位大佬批评指正。

123…9

溪源

81 posts
4 categories
4 tags
博客园 CSDN
© 2021 溪源
Powered by Hexo v3.9.0
|
Theme – NexT.Pisces v7.1.2
#
ICP备案号:湘ICP备19001531号-2