xiyuan技术圈

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


  • Home

  • 技术

  • 随笔

  • 读书

  • 管理

  • 归档

C#编程中最常见的10个错误

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

原文来自:https://www.toptal.com/c-sharp/top-10-mistakes-that-c-sharp-programmers-make

帕特里克·赖德(PATRICK RYDER)在Microsoft工作期间帮助创建了VB 1.0和更高版本的.NET平台。自2000年以来,他专注于全栈项目。

C#是针对Microsoft 公共语言运行库(CLR)的几种语言之一。面向CLR的语言受益于多种功能,例如跨语言集成和异常处理,增强的安全性,简化的组件交互模型以及调试和性能分析服务。在当今的CLR语言中,C#被广泛用于针对Windows台式机,移动或服务器环境的复杂,专业的开发项目中。(译者注,目前已支持各类跨平台的操作系统环境)

C#是一种面向对象的强类型语言。在编译和运行时,C#中严格的类型检查会导致尽早报告大多数典型的C#编程错误,并准确定位其位置。这可以在C Sharp编程中节省大量时间,相比之下,在更自由地执行类型安全的语言中,跟踪令人困惑的错误的原因可能会在违规操作发生很久之后才发生。但是,许多C#编码人员无意间(或不小心)放弃了这种检测的好处,这导致了本C#教程中讨论的一些问题。

关于本C Sharp编程教程

本教程描述了C#程序员犯下的10种最常见的C#编程错误或应避免的问题,并为他们提供了帮助。

尽管本文中讨论的大多数错误都是C#特定的,但有些错误也与其他以CLR为目标或使用框架类库(FCL)的语言有关。

常见的C#编程错误#1:使用值类型与引用相等,或反过来

C ++和许多其他语言的程序员习惯于控制他们分配给变量的值是简单的值还是对现有对象的引用。但是,在C Sharp编程中,该决定由编写对象的程序员决定,而不是由实例化该对象并将其分配给变量的程序员做出。对于那些试图学习C#编程的人来说,这是一个常见的“陷阱”。

如果您不知道所使用的对象是值类型还是引用类型,则可能会遇到一些意外。例如:

1
2
3
4
5
6
7
8
9
10
11
Point point1 = new Point(20, 30);
Point point2 = point1;
point2.X = 50;
Console.WriteLine(point1.X); // 20 (does this surprise you?)
Console.WriteLine(point2.X); // 50

Pen pen1 = new Pen(Color.Black);
Pen pen2 = pen1;
pen2.Color = Color.Blue;
Console.WriteLine(pen1.Color); // Blue (or does this surprise you?)
Console.WriteLine(pen2.Color); // Blue

正如你所看到的,都Point和Pen对象创建方式不尽相同,但值point1保持不变,当一个新的X坐标值被分配到point2,而价值pen1 是当一个新的颜色被分配到修改pen2。因此,我们可以推断出,point1并且point2每个Point对象都包含自己的对象副本,而pen1和pen2都包含对同一Pen对象的引用。但是,如果不进行此实验,我们怎么知道呢?
答案是查看对象类型的定义(您可以在Visual Studio中通过将光标置于对象类型的名称上并按F12轻松地完成此操作):

1
2
public struct Point { ... }     // defines a “value” type
public class Pen { ... } // defines a “reference” type

如上所示,在C#编程中,struct关键字用于定义值类型,而class关键字用于定义引用类型。对于那些具有C ++背景的人,由于C ++和C#关键字之间的许多相似之处而陷入一种错误的安全感,这种行为可能会让人感到意外,您可能会从C#教程中寻求帮助。
如果您要依赖值和引用类型之间不同的某些行为(例如,将对象作为方法参数传递并让该方法更改对象状态的能力),请确保您正在处理正确的对象类型,以避免C#编程问题。

常见的C#编程错误#2:误解了未初始化变量的默认值

在C#中,值类型不能为null。根据定义,值类型具有值,甚至值类型的未初始化变量也必须具有值。这称为该类型的默认值。当检查变量是否未初始化时,这会导致以下结果,通常是意外的结果:

1
2
3
4
5
6
7
8
class Program {
static Point point1;
static Pen pen1;
static void Main(string[] args) {
Console.WriteLine(pen1 == null); // True
Console.WriteLine(point1 == null); // False (huh?)
}
}

为什么point1不为空?答案是Point是值类型,它的默认值为Point(0,0),而不是null。未能意识到这一点是在C#中非常容易(也是常见)的错误。
许多(但不是全部)值类型都有一个IsEmpty属性,您可以检查该属性是否等于其默认值:

1
Console.WriteLine(point1.IsEmpty);        // True

当您检查变量是否已初始化时,请确保您知道该类型的未初始化变量在默认情况下将具有什么值,并且不要依赖于它为null。

常见的C#编程错误#3:使用不正确或未指定的字符串比较方法

比较C#中的字符串有很多不同的方法。

尽管许多程序员使用==运算符进行字符串比较,但这实际上是最不希望采用的方法之一,主要是因为它没有在代码中明确指定需要哪种类型的比较。

相反,在C#编程中测试字符串相等性的首选方法是使用以下Equals方法:

public bool Equals(string value);

public bool Equals(string value, StringComparison comparisonType);

第一个方法签名(即不带comparisonType参数)实际上与使用==运算符相同,但是具有显式应用于字符串的好处。它执行字符串的序数比较,基本上是逐字节比较。在很多情况下,这正是您想要的比较类型,尤其是在比较以编程方式设置值的字符串(例如文件名,环境变量,属性等)时。在这些情况下,只要序数比较确实是正确的类型这种情况下的比较,使用Equals方法不带 comparisonType参数的唯一缺点是,阅读代码的人可能不知道您要进行哪种类型的比较。

但是,使用Equals每次比较字符串包含comparisonType的方法签名,不仅可以使代码更清晰,还可以使您明确考虑需要进行哪种类型的比较。这是一件值得做的事情,因为即使英语在序数比较和对文化敏感的比较之间不能提供很多差异,其他语言也可以提供很多好处,而忽略其他语言的可能性正在为您提供巨大的潜力错误的道路。例如:

1
2
3
4
5
6
7
8
9
10
11
12
string s = "strasse";

// outputs False:
Console.WriteLine(s == "straße");
Console.WriteLine(s.Equals("straße"));
Console.WriteLine(s.Equals("straße", StringComparison.Ordinal));
Console.WriteLine(s.Equals("Straße", StringComparison.CurrentCulture));
Console.WriteLine(s.Equals("straße", StringComparison.OrdinalIgnoreCase));

// outputs True:
Console.WriteLine(s.Equals("straße", StringComparison.CurrentCulture));
Console.WriteLine(s.Equals("Straße", StringComparison.CurrentCultureIgnoreCase));

最安全的做法是始终为该Equals方法提供comparisonType参数。以下是一些基本准则:

  • 在比较用户输入的字符串或要显示给用户的字符串时,请使用区分区域性的比较(CurrentCulture或CurrentCultureIgnoreCase)。
  • 比较程序字符串时,请使用序数比较(Ordinal或OrdinalIgnoreCase)。
  • InvariantCulture和InvariantCultureIgnoreCase一般不被除了在非常有限的情况下使用,因为顺序比较是更有效的。如果需要进行文化意识比较,则通常应针对当前文化或其他特定文化进行比较。

除了Equals方法之外,字符串还提供了 Compare方法,该方法为您提供有关字符串相对顺序的信息,而不仅仅是进行相等性测试。此方法是优选的<,<=,>和>=运算符,对于上述的为讨论避免C#的问题同样的原因。

常见的C#编程错误#4:使用迭代(而不是声明性)语句来操作集合

在C#3.0中,向语言添加语言集成查询(LINQ)永远改变了查询和操作集合的方式。从那时起,如果您使用迭代语句来操作集合,那么您本来应该使用LINQ。

一些C#程序员甚至不知道LINQ的存在,但是幸运的是,这个数目正在变得越来越小。但是,许多人仍然认为,由于LINQ关键字和SQL语句之间的相似性,它的唯一用途是在查询数据库的代码中。

尽管数据库查询是LINQ语句的一种非常普遍的用法,但它们实际上是在任何可枚举的集合(即,实现IEnumerable接口的任何对象)上工作的。因此,例如,如果您有一个Accounts数组,而不是为每个each编写一个C#List:

1
2
3
4
5
6
7
8
9
10
      decimal total = 0;
foreach (Account account in myAccounts) {
if (account.Status == "active") {
total += account.Balance;
}
}
你可以这样写:
decimal total = (from account in myAccounts
where account.Status == "active"
select account.Balance).Sum();

尽管这是一个非常简单的示例,说明如何避免这种常见的C#编程问题,但在某些情况下,单个LINQ语句可以轻松替换代码中的迭代循环(或嵌套循环)中的数十个语句。更少的通用代码意味着更少的引入错误的机会。但是请记住,在性能方面可能会有所取舍。在对性能有严格要求的情况下,尤其是在迭代代码能够对LINQ无法进行的集合进行假设的情况下,请确保在这两种方法之间进行性能比较。

常见的C#编程错误#5:无法考虑LINQ语句中的基础对象

LINQ非常适合抽象处理集合的任务,无论它们是内存中对象,数据库表还是XML文档。在理想环境中,您不需要知道底层对象是什么。但是这里的错误是假设我们生活在一个完美的世界中。实际上,如果相同的LINQ语句恰好采用不同的格式,则当它们对完全相同的数据执行时,它们可以返回不同的结果。

例如,考虑以下语句:

1
2
3
decimal total = (from account in myAccounts
where account.Status == "active"
select account.Balance).Sum();

如果对象的其中一个account.Status等于“活动”(请注意大写字母A)会怎样?好吧,如果myAccounts是一个DbSet对象(使用默认的不区分大小写的默认配置设置),则where表达式仍会匹配该元素。但是,如果myAccounts位于内存阵列中,则它将不匹配,因此将产生总计不同的结果。
等一下 在前面讨论字符串比较时,我们看到==运算符对字符串进行了序数比较。那么,为什么在这种情况下==操作员执行不区分大小写的比较?

答案是,当LINQ语句中的基础对象是对SQL表数据的引用时(如本示例中的Entity Framework DbSet对象一样),该语句将转换为T-SQL语句。然后,操作员将遵循T-SQL编程规则,而不是C#编程规则,因此,上述情况下的比较最终不区分大小写。

通常,即使LINQ是查询对象集合的有用且一致的方式,实际上,您仍然需要知道您的语句是否将转换为C#以外的其他内容,以确保代码的行为能够在运行时达到预期。

常见的C#编程错误#6:扩展方法使您感到困惑或冒充

如前所述,LINQ语句可在实现IEnumerable的任何对象上工作。例如,以下简单功能将在任何帐户集合上累加余额:

1
2
3
public decimal SumAccounts(IEnumerable<Account> myAccounts) {
return myAccounts.Sum(a => a.Balance);
}

在上面的代码中,myAccounts参数的类型声明为 IEnumerable。由于myAccounts引用Sum方法(C#使用熟悉的“点符号”来引用类或接口上的方法),因此我们希望看到Sum()在IEnumerable接口定义上调用的方法。但是,定义 IEnumerable未引用任何Sum方法,而只是这样:

1
2
3
public interface IEnumerable<out T> : IEnumerable {
IEnumerator<T> GetEnumerator();
}

那么该Sum()方法在哪里定义?C#是强类型的,因此,如果对该Sum方法的引用无效,则C#编译器肯定会将其标记为错误。因此,我们知道它必须存在,但是在哪里?此外,LINQ为查询或汇总这些集合提供的所有其他方法的定义在哪里?
答案是这Sum()不是IEnumerable接口上定义的方法 。相反,它是在System.Linq.Enumerable类上定义的静态方法(称为“扩展方法”):

1
2
3
4
5
6
7
8
9
10
namespace System.Linq {
public static class Enumerable {
...
// the reference here to “this IEnumerable<TSource> source” is
// the magic sauce that provides access to the extension method Sum
public static decimal Sum<TSource>(this IEnumerable<TSource> source,
Func<TSource, decimal> selector);
...
}
}

那么,什么使扩展方法与任何其他静态方法不同,又使我们能够在其他类中访问它呢?
扩展方法的显着特征是this其第一个参数上的 修饰符。这是“魔术”,可以将其标识为编译器的扩展方法。它修改的参数的类型(在本例中为IEnumerable)表示将要实现此方法的类或接口。

(另一方面,IEnumerable接口名称和Enumerable定义扩展方法的类的名称 之间的相似性并没有什么神奇的。这种相似性只是一个任意的样式选择。)

有了这种理解,我们还可以看到sumAccounts上面介绍的功能可以改为如下实现:

public decimal SumAccounts(IEnumerable<Account> myAccounts) {

    return Enumerable.Sum(myAccounts, a => a.Balance);

}

我们本可以以这种方式实现它的事实反而引起了一个问题,为什么根本没有扩展方法? 扩展方法本质上是C#编程语言的一种便利,它使您可以将方法“添加”到现有类型,而无需创建新的派生类型,重新编译或修改原始类型。

通过using [namespace];在文件顶部包含一条语句,可将扩展方法纳入范围。您需要知道哪个C#名称空间包含要查找的扩展方法,但是一旦知道要查找的内容,就很容易确定。

当C#编译器在对象的实例上遇到方法调用,但未找到在引用的对象类上定义的方法时,它将查看范围内的所有扩展方法,以尝试查找与所需方法匹配的扩展方法。签名和类。如果找到一个,它将实例引用作为该扩展方法的第一个参数传递,然后其余参数(如果有)将作为后续参数传递给扩展方法。(如果C#编译器在范围内找不到任何相应的扩展方法,它将抛出错误。)

扩展方法是C#编译器中“语法糖”的一个示例,它使我们能够编写(通常)更清晰,更可维护的代码。更清楚的是,如果您知道它们的用法。否则,可能会有些混乱,尤其是在开始时。

尽管使用扩展方法当然具有优势,但它们可能会引起问题,并且对于那些不了解它们或不正确理解它们的开发人员,C#编程帮助会大声疾呼。当在线查看代码示例或任何其他预先编写的代码时,尤其如此。当此类代码产生编译器错误时(因为它调用的类显然没有定义方法),人们倾向于认为该代码适用于该库的不同版本,或完全适用于不同的库。可能会花费大量时间搜索不存在的新版本或幻影“缺少库”。

当对象上存在具有相同名称的方法时,即使熟悉扩展方法的开发人员仍然偶尔会被捕获,但是其方法签名与扩展方法的方法签名之间存在细微的差异。寻找错别字或错误可能会浪费很多时间。

在C#库中使用扩展方法变得越来越普遍。除LINQ之外,Unity Application Block和Web API框架是Microsoft经常使用的两个现代库的示例,它们也使用扩展方法,并且还有许多其他方法。框架越现代,就越有可能包含扩展方法。

当然,您也可以编写自己的扩展方法。请意识到,尽管扩展方法看起来像常规实例方法一样被调用,但这实际上只是一种幻想。特别是,您的扩展方法不能引用它们正在扩展的类的私有成员或受保护成员,因此不能完全替代更传统的类继承。

常见的C#编程错误#7:为当前任务使用错误的集合类型

C#提供了大量的各种对象集合,具有以下仅为部分清单:

Array,ArrayList,BitArray,BitVector32,Dictionary<K,V>,HashTable,HybridDictionary,List,NameValueCollection,OrderedDictionary,Queue, Queue,SortedList,Stack, Stack,StringCollection,StringDictionary。

尽管在某些情况下,太多的选择和不足的选择一样糟糕,但对于集合对象却并非如此。可用的选项数量肯定可以使您受益。预先花一些时间进行研究,然后为您的目的选择最佳的收集类型。这可能会导致更好的性能和更少的错误空间。

如果有一种收集类型专门针对您拥有的元素类型(例如字符串或位),则倾向于首先使用该元素。当针对特定类型的元素时,实现通常会更高效。

为了利用C#的类型安全性,通常应首选使用通用接口而不是非通用接口。泛型接口的元素是您在声明对象时指定的类型,而非泛型接口的元素则是object类型。使用非泛型接口时,C#编译器无法对您的代码进行类型检查。同样,在处理原始值类型的集合时,使用非泛型集合将导致这些类型的重复 装箱/拆箱,与适当类型的泛型集合相比,可能会对性能产生重大的负面影响。

另一个常见的C#问题是编写您自己的集合对象。但这并不是说它永远不合适,但是通过提供.NET提供的广泛选择,您可以通过使用或扩展已经存在的扩展而不是重新发明轮子来节省大量时间。特别是,用于C#和CLI的C5通用集合库“开箱即用”提供了各种各样的附加集合,例如持久树数据结构,基于堆的优先级队列,哈希索引数组列表,链接列表等等。

常见的C#编程错误#8:忽略释放资源

CLR环境使用垃圾回收器,因此您无需显式释放为任何对象创建的内存。实际上,您不能。没有C ++ delete运算符或free()这样的函数。但这并不意味着您在使用完所有对象后就可以忘记所有对象。许多类型的对象封装了其他类型的系统资源(例如,磁盘文件,数据库连接,网络套接字等)。保持这些资源开放状态会迅速耗尽系统资源的总数,从而降低性能并最终导致程序错误。

尽管可以在任何C#类上定义析构函数方法,但析构函数(在C#中也称为终结器)存在的问题是,您不确定是否会调用它们。它们在将来的不确定时间内被垃圾收集器调用(在单独的线程上,这可能会导致其他问题)。尝试通过强制使用垃圾回收来克服这些限制 GC.Collect()不是C#最佳实践,因为这将在线程收集所有符合收集条件的对象时在未知时间内阻塞线程。

这并不是说终结器没有很好的用途,但是以确定性方式释放资源并不是其中之一。相反,当您在文件,网络或数据库连接上进行操作时,您希望在完成使用后立即显式释放基础资源。

在几乎所有环境中,资源泄漏都是一个问题。但是,C#提供了一种健壮且易于使用的机制,如果使用该机制,则使泄漏的情况更加罕见。.NET框架定义了IDisposable仅由Dispose()方法组成的接口 。任何实现的对象都IDisposable希望在对象的使用者完成对它的操作后才调用该方法。这导致显式,确定性的资源释放。

如果要在单个代码块的上下文中创建和处理对象,则忘记调用基本上是不可原谅的 Dispose(),因为C#提供了一条using语句, Dispose()无论代码块如何退出(无论它是例外,return陈述式,或是干脆关闭区块)。是的,这与using前面提到的语句相同,该语句用于在文件顶部包含C#名称空间。它有第二个完全不相关的目的,许多C#开发人员都不知道。即,确保Dispose()在退出代码块时对对象进行调用:

1
2
3
using (FileStream myFile = File.OpenRead("foo.txt")) {
myFile.Read(buffer, 0, 100);
}

通过using 在上面的示例中创建一个块,您可以确定 myFile.Dispose()在处理完文件后立即调用该块,无论是否Read()引发异常。

常见的C#编程错误#9:回避异常

C#将其类型安全性强制实施到运行时。这使您能够比在C ++等语言中更快地查明C#中的许多类型的错误,在C#中错误的类型转换可能导致将任意值分配给对象的字段。但是,程序员再次可以浪费这一强大功能,从而导致C#问题。之所以陷入这种陷阱,是因为C#提供了两种不同的处理方式,一种可以引发异常,而另一种则不能。有些人会回避异常路由,认为不必编写try / catch块可以节省一些代码。

例如,以下两种方法可以在C#中执行显式类型转换:

1
2
3
4
5
6
7
8
// METHOD 1:
// Throws an exception if account can't be cast to SavingsAccount
SavingsAccount savingsAccount = (SavingsAccount)account;

// METHOD 2:
// Does NOT throw an exception if account can't be cast to
// SavingsAccount; will just set savingsAccount to null instead
SavingsAccount savingsAccount = account as SavingsAccount;

使用方法2可能发生的最明显的错误是无法检查返回值。这可能会导致最终的NullReferenceException,该异常可能会在更晚的时间浮出水面,从而更加难以找到问题的根源。相反,方法1会立即抛出一个 InvalidCastException问题,使问题的根源更加明显。
而且,即使您记得在方法2中检查过返回值,如果发现它为空,您将怎么办?您编写的方法是否适合报告错误?如果强制转换失败,您还可以尝试其他方法吗?如果不是,那么抛出异常是正确的事,因此您最好让它尽可能地靠近问题的根源。

这是其他两个常见方法对的两个示例,其中一个抛出异常而另一个不抛出异常:

1
2
3
4
5
int.Parse();     // throws exception if argument can’t be parsed
int.TryParse(); // returns a bool to denote whether parse succeeded

IEnumerable.First(); // throws exception if sequence is empty
IEnumerable.FirstOrDefault(); // returns null/default value if sequence is empty

一些C#开发人员是如此“异常不利”,以至于他们自动认为不抛出异常的方法是更好的。尽管在某些特定情况下这可能是正确的,但作为概括,它根本不正确。
作为一个特定的示例,如果您有替代的合法(例如,默认)操作要发生,那么将产生异常,那么非异常方法可能是一个合法的选择。在这种情况下,写这样的东西确实更好:

1
2
3
4
5
if (int.TryParse(myString, out myInt)) {
// use myInt
} else {
// use default value
}

代替:

1
2
3
4
5
6
try {
myInt = int.Parse(myString);
// use myInt
} catch (FormatException) {
// use default value
}

但是,认为TryParse必然是“更好”的方法是不正确的。有时候是这种情况,有时候不是。这就是为什么有两种方法可以做到这一点。在您所处的环境中使用正确的方法,请记住,作为开发人员,异常肯定可以成为您的朋友。

常见的C#编程错误#10:允许编译器警告累积

尽管此问题绝对不是C#特有的,但由于放弃了C#编译器提供的严格类型检查的优点,因此在C#编程中尤为突出。

产生警告是有原因的。尽管所有C#编译器错误都表明您的代码有缺陷,但许多警告也是如此。两者的区别在于,在出现警告的情况下,编译器在发出代码所表示的指令时没有问题。即使这样,它也会发现您的代码有些混乱,并且您的代码有可能无法准确反映您的意图。

就本C#编程教程而言,一个常见的简单示例是,当您修改算法以消除对正在使用的变量的使用时,却忘记了删除变量声明。该程序将完美运行,但编译器将标记无用的变量声明。程序运行完美的事实导致程序员忽略了修复警告原因的方法。此外,编码人员还利用了Visual Studio功能,该功能使他们可以轻松地将警告隐藏在“错误列表”窗口中,从而使他们只能专注于错误。很快就出现了数十种警告,所有这些警告都被幸福地忽略了(或更糟的是隐藏了)。

但是,如果您迟早忽略这种类型的警告,则类似这样的内容很可能会在您的代码中找到:

1
2
3
4
5
6
7
8
9
10
11
class Account {

int myId;
int Id; // compiler warned you about this, but you didn’t listen!

// Constructor
Account(int id) {
this.myId = Id; // OOPS!
}

}

而且,以Intellisense允许我们编写代码的速度,此错误并不像看起来那样不可能。
现在,您的程序中出现了严重错误(尽管出于已经说明的原因,编译器仅将其标记为警告),并且根据程序的复杂程度,您可能会浪费大量时间来跟踪该程序。如果您首先注意了此警告,则只需五秒钟即可解决此问题。

记住,如果您正在侦听,C Sharp编译器会为您提供有关代码健壮性的许多有用信息。不要忽略警告。通常,它们只需要花费几秒钟的时间进行修复,而在发生新问题时修复它们可以节省您的时间。训练自己,使Visual Studio“错误列表”窗口显示“ 0错误,0警告”,以便所有警告使您感到不舒服,无法立即解决它们。

当然,每个规则都有例外。因此,有时您的代码对编译器来说似乎有些混乱,即使这正是您的预期。在极少数情况下,请#pragma warning disable [warning id]仅在周围使用触发警告的代码,并仅使用其触发的警告ID。这将取消该警告,并且仅禁止该警告,因此您仍然可以保持警惕以防出现新的警告。

结论

C#是一种功能强大且灵活的语言,具有许多可以极大地提高生产率的机制和范例。但是,就像使用任何软件工具或语言一样,对其功能的有限了解或欣赏有时可能更多的是障碍而不是收益,可能会导致生产环境代码的问题频发。为此,我们需要更多的了解C#语言中那些常见的错误,并不断的持续优化,确保每一行代码都处于可控的状态。

在你的日常开发过程中,你是否也曾经遇到过这些常见错误?赶紧跟你身边的伙伴一起分享吧~

15个基本的C#面试问题

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

1、给定一个int数组,编写方法以统计所有偶数的值。

有很多方法可以做到这一点,但是最直接的两种方法是:

1
2
3
static long TotalAllEvenNumbers(int[] intArray) {
return intArray.Where(i => i % 2 == 0).Sum(i => (long)i);
}

还有就是

1
2
3
static long TotalAllEvenNumbers(int[] intArray) {
return (from i in intArray where i % 2 == 0 select (long)i).Sum();
}

当然,你还需要注意以下关键:

  1. 你是否利用 C#语言特性 一行就解决问题。(即,不是使用包含循环,条件语句和累加器的更长篇幅的解决方案)

  2. 你是否考虑过溢出的可能性。例如,诸如

    return intArray.Where(i => i % 2 == 0).Sum()(与函数的返回类型无关)

这可能一个很”明显”的单行,但这样溢出的可能性很高。虽然上面的答案中使用的转换为long的方法并没有消除这种可能性,但是它使得发生溢出异常的可能性非常小。但请注意,如果你写答案的时候询问数组的预期大小及其成员的大小,则显然你在做这道题目的时候在考虑此溢出问题,这很棒。

2、下面的代码的输出是什么?解释你的答案。

1
2
3
4
5
6
7
8
9
class Program {
static String location;
static DateTime time;

static void Main() {
Console.WriteLine(location == null ? "location is null" : location);
Console.WriteLine(time == null ? "time is null" : time.ToString());
}
}

输出将是:

1
2
location is null
1/1/0001 12:00:00 AM

下面的简短程序的输出是什么?解释你的答案。简短程序的输出是什么?解释你的答案。
尽管两个变量都未初始化,但是String是引用类型 、DateTime 是值类型。作为值类型,单位化DateTime变量设置为默认值 公元1年晚上12点,而不是 null

3、下面语句中 time 和null 的比较是有效还是无效的?

1
2
3
4
5
6
static DateTime time;
/* ... */
if (time == null)
{
/* do something */
}

有人可能会认为,由于变量永远不可能为null (它被自动初始化为1月1日的值),所以编译器在比较某个变量时就会报错。具体来说,操作符将其操作数强制转换为不同的允许类型,以便在两边都得到一个通用类型,然后可以对其进行比较。这就是为什么像这样的东西会给你期望的结果(而不是失败或意外的行为,因为操作数是不同的类型):

1
2
3
double x = 5.0;
int y = 5;
Console.WriteLine(x == y); // outputs true

然而,这有时会导致意外的行为,例如DateTime变量和null的比较。在这种情况下,DateTime变量和null文字都可以转换为可空的。因此,比较这两个值是合法的,即使结果总是假的。

4、给定circle以下类的实例:

1
2
3
4
5
6
7
public sealed class Circle {
private double radius;

public double Calculate(Func<double, double> op) {
return op(radius);
}
}

简编写代码以计算圆的周长,而无需修改Circle类本身。
首选的答案如下:

1
circle.Calculate(r => 2 * Math.PI * r);

由于我们不能访问对象的私有半径字段,所以我们通过内联传递计算函数,让对象本身计算周长。

许多c#程序员回避(或不理解)函数值参数。虽然在这种情况下,这个例子有点做作,但其目的是看看申请人是否了解如何制定一个调用来计算哪个与方法的定义相匹配。

另外,一个有效的(虽然不那么优雅的)解决方案是从对象中检索半径值本身,然后执行计算结果:

1
2
var radius = circle.Calculate(r => r);
var circumference = 2 * Math.PI * radius;

无论哪种方式。我们在这里主要寻找的是面试者是否熟悉并理解如何调用Calculate方法。

5、下面程序的输出是什么?解释你的答案。

1
2
3
4
5
6
7
8
9
10
11
12
13
class Program {
private static string result;

static void Main() {
SaySomething();
Console.WriteLine(result);
}

static async Task<string> SaySomething() {
await Task.Delay(5);
result = "Hello world!";
return “Something”;
}

下面
此外,如果我们替换wait task,答案会改变吗? 比如 thread . sleep (5) ? 为什么?的简短

程序的输出是什么?解释你的答案。序的输出是什么?解释你的答案。

回答:

问题第一部分(即带有的代码版本await Task.Delay(5);)的答案是该程序将仅输出一个空行(而不是 “ Hello world!”)。这是因为调用result时仍将未初始化Console.WriteLine。

大多数程序和面向对象的程序员都希望函数return在返回调用函数之前从头到尾执行,或者从语句执行。C#async函数不是这种情况。它们只执行到第一个await语句,然后返回到调用方。由await(在此例中为Task.Delay)调用的函数是异步执行的,并且该await语句之后的行直到Task.Delay完成(在5毫秒内)之前都不会发出信号。但是,在这段时间内,控制权已经返回给调用者,该调用者Console.WriteLine对尚未初始化的字符串执行该语句。

调用await Task.Delay(5) 可让当前线程继续其正在执行的操作,如果已完成(等待任何等待),则将其返回到线程池。这是异步/等待机制的主要好处。它允许CLR使用线程池中的更少线程来服务更多请求。

异步编程已经变得越来越普遍,因为执行许多活动的网络服务请求或数据库请求的设备越来越普遍。C#具有一些出色的编程结构,可以极大地简化异步方法的编程任务,并且意识到它们的程序员将产生更好的程序。

关于问题的第二部分,如果将await Task.Delay(5);其替换为Thread.Sleep(5),则程序将输出Hello world!。一种没有至少一个语句的async方法,其操作就像同步方法一样。也就是说,它将从头到尾执行,或者直到遇到一条语句为止。调用只是阻塞了当前正在运行的线程,因此调用仅将方法的执行时间增加了5毫秒。awaitreturnThread.Sleep()Thread.Sleep(5)SaySomething()

6、下面的程序输出是什么?解释你的答案。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
delegate void Printer();

static void Main()
{
List<Printer> printers = new List<Printer>();
int i=0;
for(; i < 10; i++)
{
printers.Add(delegate { Console.WriteLine(i); });
}

foreach (var printer in printers)
{
printer();
}
}

这个程序将把数字10输出十次。

原因如下: 委托被添加到 for循环中l了,而 “引用” (或者“指针”)被存储到i中,而不是值本身。因此,在我们退出循环之后,变量i被设置为10,所以到调用每个委托时,传递给它们的值都是10。

7、是否可以将混合数据类型(例如int,string,float,char)全部存储在一个数组中?

是! 之所以可以这样做,是因为数组的类型object不仅可以存储任何数据类型,还可以存储类的对象,如下所示:

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

namespace ConsoleApplication8
{
class Program
{
class Customer
{
public int ID { get; set; }
public string Name { get; set; }
public override string ToString()
{
return this.Name;
}
}
static void Main(string[] args)
{
object[] array = new object[3];
array[0] = 101;
array[1] = "C#";
Customer c = new Customer();
c.ID = 55;
c.Name = "Manish";
array[2] = c;
foreach (object obj in array)
{
Console.WriteLine(obj);
}
Console.ReadLine();
}
}
}

8、比较C#中的结构和类。他们有什么共同点?它们有何不同?

C#中的类和结构确实有一些共同点,即:

他们都是

是复合数据类型

可以包含方法和事件

可以支持接口

但是有许多差异。比较一下:

类:

支持继承

是引用(指针)类型

引用可以为空

每个新实例都有内存开销

结构:

不支持继承

是值类型

按值传递(如整数)

不能有空引用(除非使用了Nullable)

每个新实例没有内存开销(除非“装箱”)

9、这里有一个包含一个或多个$符号的字串,例如:

1
"foo bar foo $ bar $ foo bar $ "

问题:如何$从给定的字符串中删除第二和第三次出现的?
答案:

使用如下正则表达式:

1
2
3
4
string s = "like for example $  you don't have $  network $  access";       
Regex rgx = new Regex("\\$\\s+");
s = Regex.Replace(s, @"(\$\s+.*?)\$\s+", "$1$$");
Console.WriteLine("string is: {0}",s);

说明:

  • ($\s+.*?)-第1组,捕获一个文字$,一个或多个空格字符,然后捕获除换行符以外的任意数量的字符,并尽可能少地捕获到下一个最接近的匹配项
  • $\s+—单个$符号和一个或多个空格字符
  • $1引用组1的值,它只是将其插入被替换的字符串中,$$代表替换模式中的$符号。

    10、下面的程序输出是什么?

    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
    public class TestStatic
    {
    public static int TestValue;

    public TestStatic()
    {
    if (TestValue == 0)
    {
    TestValue = 5;
    }
    }
    static TestStatic()
    {
    if (TestValue == 0)
    {
    TestValue = 10;
    }
    }

    public void Print()
    {
    if (TestValue == 5)
    {
    TestValue = 6;
    }
    Console.WriteLine("TestValue : " + TestValue);

    }
    }

    public void Main(string[] args)
    {
    TestStatic t = new TestStatic();
    t.Print();
    }

TestValue : 10

在创建该类的任何实例之前,将调用该类的静态构造函数。此处调用的静态构造函数TestValue首先将变量初始化。

11、有没有一种方法可以修改ClassA、以便您可以在调用Main方法时使用参数调用构造函数,而无需创建任何其他新实例ClassA?

1
2
3
4
5
6
class ClassA
{
public ClassA() { }

public ClassA(int pValue) { }
}

启动类

1
2
3
4
5
6
7
class Program
{
static void Main(string[] args)
{
ClassA refA = new ClassA();
}
}

回答:

所述this关键字被用于调用其他构造,初始化该类对象。下面是实现:

1
2
3
4
5
6
7
8
class ClassA
{
public ClassA() : this(10)
{ }

public ClassA(int pValue)
{ }
}

12、以下代码输出什么?

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

namespace main1
{
class Program
{
static void Main(string[] args)
{
try
{
Console.WriteLine("Hello");
}
catch (ArgumentNullException)
{
Console.WriteLine("A");
}
catch (Exception)
{
Console.WriteLine("B");
}
finally
{
Console.WriteLine("C");
}
Console.ReadKey();
}
}
}

答案:

1
2
Hello
C

13、描述依赖注入。

依赖注入是一种使紧密链接的类分离的方式,从而减少了类之间的直接依赖。有多种方法可以实现依赖项注入:

  1. 构造函数依赖
  2. 属性依赖
  3. 方法依赖

    14、编写一个C#程序,该程序接受以千米为单位的距离,将其转换为米,然后显示结果。

    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; 

    class abc

    {
    public static Void Main()

    {

    int ndistance, nresult;

    Console.WriteLine("Enter the distance in kilometers");

    ndistance = convert.ToInt32(Console.ReadLine());

    nresult = ndistance * 1000;

    Console.WriteLine("Distance in meters: " + nresult);

    Console.ReadLine();

    }

    }

15、描述装箱和拆箱。并写一个例子。

装箱是将值类型隐式转换为该类型object或该值类型实现的任何接口类型。将值类型装箱会创建一个包含该值的对象实例,并将其存储在堆中。

例:

1
2
3
4
5
int x = 101;
object o = x; // boxing value of x into object o

o = 999;
x = (int)o; // unboxing value of o into integer x

最后:

面试不仅要基础扎实,更重要的是能解决棘手的技术问题,所以以上这些内容仅供参考。并非每个值得招聘的优秀候选人都能够回答所有问题,也不能确定能够全部回答,就能保证他是一个优秀候选人。归根结底,招聘仍然是一门艺术,一门科学以及许多工作。

如果你有招聘的要求,也欢迎和我们公众号联系,我们有12万的粉丝,相信能在其中找到适合您公司的 .net 候选人。

恭喜你!全部看完,看来您高手只有一步之遥,赶紧转发朋友圈吧!让其他.net 新手也来瞻仰瞻仰。

.NET Core的HttpClient连接池管理

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

本文来源于史蒂夫·戈登(Steve Gordon)是Microsoft MVP,Pluralsight的作者,布莱顿(英国西南部城市)的高级开发人员和社区负责人。他的个人博客为:www.stevejgordon.co.uk。

导读:

.NET Core(从2.1开始)中的HttpClient执行连接池和这些连接的生命周期管理。这支持使用单个HttpClient实例,通过单例减少了套接字耗尽的机会,同时确保连接定期重新连接以反映DNS更改。

回顾HttpClient的历史

HttpClient最初是作为NuGet包开始的,该包可以选择包含在.NET Framework 4.0项目中。在.NET Framework 4.5中,它作为BCL(基本类库)的一部分在框中提供。它建立在预先存在的HttpWebRequest实现之上。在.NET Framework中,ServicePoint API可用于控制和管理HTTP连接,包括通过为端点配置ConnectionLeaseTimeout来设置连接寿命。

图片

.NET Core 1.0最初于2016年6月发布。与.NET Framework中可用的版本相比,此第一个版本的API接口要小得多,主要用于构建ASP.NET Core Web应用程序。由于.NET Core 1.0是HttpClient,因此提供了API。但是,不包括用于HttpWebRequest和ServicePoint的API。.NET Core 1.0中的HttpClient直接建立在使用非托管代码的OS平台API之上,Windows API使用WinHTTP,Linux和Mac使用LibCurl。

图片

到2016年8月,很快就注意到,重新使用HttpClient实例以防止套接字耗尽的建议有一个相当麻烦的副作用。Oren Novotny(译者注:.NET基金会执行董事,.NET团队的项目经理)揭开了一个长期存在的GitHub问题,题为“ Singleton HttpClient doesn’t respect DNS changes ”(单例HttpClient不遵守DNS 更改)。在此问题中,人们认识到重新使用单个HttpClient实例将导致连接无限期保持打开状态,因此,DNS更改可能会导致请求失败或与过时的终结点通信。

在.NET Core 2.0中,添加了HttpWebRequest以支持.NET Standard 2.0。它位于HttpClient实现的顶层,这与.NET Framework 4.5+中的工作原理相反。还添加了ServicePoint,尽管它的许多API接口要么要么会抛出未实现的异常,要么根本就没有实现。

图片

自.NET CORE 2.1以来的变化

这种有问题的行为导致团队不同团队进行了两项工作。ASP.NET团队开始研究Microsoft.Extensions.Http包,该包的主要功能是IHttpClientFactory。这个针对HttpClient实例自用的工厂还包括基础HttpMessageHandler链的生命周期管理。如果您想了解有关此功能的更多信息,可以查看我的系列博客文章,我将在此介绍。

IHttpClientFactory功能是作为ASP.NET Core 2.1的一部分发布的,对于许多人来说,这是一个很好的折衷方案,它解决了连接重用以及生命周期管理的问题。

在同一时间范围内,.NET团队正在研究自己的解决方案。该团队也在.NET Core 2.1中发布,在HttpClient的处理程序链的核心引入了一个新的SocketsHttpHandler。该处理程序直接建立在Socket API之上,并在托管代码中实现HTTP。这项工作的一部分包括连接池系统以及为这些连接设置最大生存期的能力。此功能将是本文其余部分的重点。

图片

但是在开始之前,我想指出,虽然默认情况下从.NET Core 2.1启用了SocketsHttpHandler,但实现仅限于HTTP / 1.1通信。那些需要HTTP / 2的用户必须禁用该功能并使用较旧的处理程序链,该处理程序链像以前一样依赖非托管代码,并且不包括连接池。

幸运的是,.NET Core 3.0中已消除了此限制,并且现在提供了HTTP/2支持。这应该使用基于适合所有对象的SocketsHttpHandler链的HttpClient。

什么是连接池?

SocketsHttpHandler为每个唯一端点建立连接池,您的应用程序通过HttpClient向该唯一端点发出出站HTTP请求。在对端点的第一个请求上,当不存在现有连接时,将建立一个新的HTTP连接并将其用于该请求。该请求完成后,连接将保持打开状态并返回到池中。

对同一端点的后续请求将尝试从池中找到可用的连接。如果没有可用的连接,并且尚未达到该端点的连接限制,则将建立新的连接。达到连接限制后,请求将保留在队列中,直到连接可以自由发送它们为止。

我一直在研究此实现的内部代码,并可能在以后的博客文章中对池的行为进行更深入的分析。

如何控制连接池

有三个主要设置可用于控制连接池的行为。

PooledConnectionLifetime,定义连接在池中保持活动状态的时间。此生存期到期后,将不再为将来的请求而合并或发出连接。

PooledConnectionIdleTimeout,定义闲置连接在未使用时在池中保留的时间。一旦此生存期到期,空闲连接将被清除并从池中删除。

MaxConnectionsPerServer,定义每个端点将建立的最大出站连接数。每个端点的连接分别池化。例如,如果最大连接数为2,则您的应用程序将请求发送到两个www.github.com和www.google.com,总共可能最多有4个打开的连接。

默认情况下,从.NET Core 2.1开始,更高级别的HttpClientHandler将SocketsHttpHandler用作内部处理程序。没有任何自定义配置,将应用连接池的默认设置。

该PooledConnectionLifetime默认是无限的,因此,虽然经常使用的请求,连接可能会无限期地保持打开状态。该PooledConnectionIdleTimeout默认为2分钟,如果在连接池中长时间未使用将被清理。MaxConnectionsPerServer默认为int.MaxValue,因此连接基本上不受限制。

如果希望控制这些值中的任何一个,则可以手动创建SocketsHttpHandler实例,并根据需要进行配置。

1
2
3
4
5
6
7
8
9
var socketsHandler = new SocketsHttpHandler
{
PooledConnectionLifetime = TimeSpan.FromMinutes(10),
PooledConnectionIdleTimeout = TimeSpan.FromMinutes(5),
MaxConnectionsPerServer = 10
};


var client = new HttpClient(socketsHandler);

在前面的示例中,对SocketsHttpHandler进行了配置,以使连接将最多在10分钟后停止重新发出并关闭。如果闲置5分钟,则连接将在池的清理过程中被更早地删除。我们还将最大连接数(每个端点)限制为十个。如果我们需要并行发出更多出站请求,则某些请求可能会排队等待,直到10个池中的连接可用为止。
要应用处理程序,它将被传递到HttpClient的构造函数中。

测试连接寿命

以这个示例程序为例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
using System;
using System.Net.Http;
using System.Threading.Tasks;


namespace HttpConnectionPoolingSamples
{
class Program
{
static async Task Main(string[] args)
{
var ips = await Dns.GetHostAddressesAsync("www.google.com");


foreach (var ipAddress in ips)
{
Console.WriteLine(ipAddress.MapToIPv4().ToString());
}

var socketsHandler = new SocketsHttpHandler
{
PooledConnectionLifetime = TimeSpan.FromMinutes(10),
PooledConnectionIdleTimeout = TimeSpan.FromMinutes(5),
MaxConnectionsPerServer = 10
};


var client = new HttpClient(socketsHandler);

for (var i = 0; i < 5; i++)
{
_ = await client.GetAsync("https://www.google.com");
await Task.Delay(TimeSpan.FromSeconds(2));
}


Console.WriteLine("Press a key to exit...");
Console.ReadKey();
}
}
}

使用我们刚刚讨论的设置,此代码依次向同一端点发出5个请求。在每个请求之间,它会暂停两秒钟。该代码还输出从DNS检索到的Google服务器的IPv4地址。我们可以使用此IP地址来查看通过PowerShell中发出的netstat命令对其打开的连接:

1
netstat -ano | findstr 216.58.211

在我的例子中,此命令的输出为:

1
TCP   192.168.1.139:53040   216.58.211.164:443   ESTABLISHED   20372

我们可以看到,在这种情况下,到远程端点的连接只有1个。在每个请求之后,该连接将返回到池中,因此在发出下一个请求时可以重新使用。
如果我们更改连接的生存期,以使它们在1秒后过期,那么我们可以测试这对行为的影响:

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
using System;
using System.Net;
using System.Net.Http;
using System.Threading.Tasks;


namespace HttpConnectionPoolingSamples
{
class Program
{
static async Task Main(string[] args)
{
var ips = await Dns.GetHostAddressesAsync("www.google.com");


foreach (var ipAddress in ips)
{
Console.WriteLine(ipAddress.MapToIPv4().ToString());
}


var socketsHandler = new SocketsHttpHandler
{
PooledConnectionLifetime = TimeSpan.FromSeconds(1),
PooledConnectionIdleTimeout = TimeSpan.FromSeconds(1),
MaxConnectionsPerServer = 10
};


var client = new HttpClient(socketsHandler);

for (var i = 0; i < 5; i++)
{
_ = await client.GetAsync("https://www.google.com");
await Task.Delay(TimeSpan.FromSeconds(2));
}


Console.WriteLine("Press a key to exit...");
Console.ReadKey();
}
}
}
1
2
3
4
5
TCP   192.168.1.139:53115   216.58.211.164:443   TIME_WAIT     0
TCP 192.168.1.139:53116 216.58.211.164:443 TIME_WAIT 0
TCP 192.168.1.139:53118 216.58.211.164:443 TIME_WAIT 0
TCP 192.168.1.139:53120 216.58.211.164:443 TIME_WAIT 0
TCP 192.168.1.139:53121 216.58.211.164:443 ESTABLISHED 25948

在这种情况下,我们可以看到使用了五个连接。其中的前四个在1秒后从池中删除,因此无法在下一个请求中重复使用。结果,每个请求都打开了一个新连接。现在,原始连接处于TIME_WAIT状态,并且操作系统无法将其重新用于新的出站连接。最终连接显示为ESTABLISHED,因为我在它过期之前就抓住了它。

测试最大连接数

对于下一个测试用例,我们将使用以下程序:

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
using System;
using System.Diagnostics;
using System.Linq;
using System.Net;
using System.Net.Http;
using System.Threading.Tasks;


namespace HttpConnectionPoolingSamples
{
class Program
{
static async Task Main(string[] args)
{
var ips = await Dns.GetHostAddressesAsync("www.google.com");


foreach (var ipAddress in ips)
{
Console.WriteLine(ipAddress.MapToIPv4().ToString());
}


var socketsHandler = new SocketsHttpHandler
{
PooledConnectionLifetime = TimeSpan.FromSeconds(60),
PooledConnectionIdleTimeout = TimeSpan.FromMinutes(20),
MaxConnectionsPerServer = 2
};


var client = new HttpClient(socketsHandler);


var sw = Stopwatch.StartNew();


var tasks = Enumerable.Range(0, 200).Select(i => client.GetAsync("https://www.google.com"));


await Task.WhenAll(tasks);


sw.Stop();


Console.WriteLine($"{sw.ElapsedMilliseconds}ms taken for 200 requests");


Console.WriteLine("Press a key to exit...");
Console.ReadKey();
}
}
}

该代码将MaxConnectionsPerServer限制为2。然后启动200个任务,每个任务都向同一端点发出HTTP请求。这些任务将同时运行。所有请求竞争所花费的时间将写入控制台。
在我的机器上运行此命令后,输出为:

1
8013ms taken for 200 requests

如果使用netstat查看连接,则根据定义的限制,我们可以看到两个已建立的连接。
已建立

1
2
TCP   192.168.1.139:52780   216.58.204.36:443   ESTABLISHED   16076
TCP 192.168.1.139:52780 216.58.204.36:443 ESTABLISHED 16076

如果我们调整此代码以允许MaxConnectionsPerServer = 10,则可以重新运行该应用程序。这次所花费的时间减少了大约4倍。

1
2123ms taken for 200 requests

当我们查看连接时,我们可以看到确实建立了十个连接。

1
2
3
4
5
6
7
8
9
10
TCP   192.168.1.139:52798   216.58.204.36:443   ESTABLISHED   30856
TCP 192.168.1.139:52799 216.58.204.36:443 ESTABLISHED 30856
TCP 192.168.1.139:52800 216.58.204.36:443 ESTABLISHED 30856
TCP 192.168.1.139:52801 216.58.204.36:443 ESTABLISHED 30856
TCP 192.168.1.139:52802 216.58.204.36:443 ESTABLISHED 30856
TCP 192.168.1.139:52803 216.58.204.36:443 ESTABLISHED 30856
TCP 192.168.1.139:52804 216.58.204.36:443 ESTABLISHED 30856
TCP 192.168.1.139:52805 216.58.204.36:443 ESTABLISHED 30856
TCP 192.168.1.139:52806 216.58.204.36:443 ESTABLISHED 30856
TCP 192.168.1.139:52807 216.58.204.36:443 ESTABLISHED 30856

结果,提高了吞吐量。我们允许更多的出站连接,因此可以更快地处理请求队列,并通过额外的连接并行发出更多请求。

我还需要IHttpClientFactory吗?

这是一个非常合乎逻辑的问题,可能是该帖子的结果。IHttpClientFactory的功能之一是HttpMessageHandler链的生命周期管理,因此也是基础连接的生命周期管理。有了HttpClient和SocketsHttpHandler可以达到相同效果的知识,我们是否需要使用IHttpClientFactory?

我的观点是,IHttpClientFactory除了帮助管理连接生存期外还有其他好处,并且在发出出站HTTP请求时仍然可以增加价值。它提供了一种很好的模式,可以使用命名或类型化的客户端方法为HttpClient实例定义逻辑配置。后来有类型的客户是我个人的最爱。

这些逻辑客户端的流畅配置方法还使定制的DelegatingHandlers与客户端的使用非常简单明了。这包括ASP.NET团队对该方法的扩展,以便与Polly集成,以便轻松地对出站请求应用弹性和瞬时故障处理。

即使没有生命周期管理,我也希望在将来的一段时间内将工厂用于我的应用程序。根据我在网上看到的讨论,很有可能在将来的版本中,寿命管理功能将从IHttpClientFactory中弃用和/或删除,因为它解决的问题不再适用。

摘要

在本文中,我们看到自从.NET Core 2.1发布以来,使用默认的SocketsHttpHandler实现时,将维护连接池。使用池的设置,我们可以控制连接的生存期并限制每个端点可能创建的出站连接的数量。

我们还讨论了IHttpClientFactory不仅具有连接生存期管理的优点和功能,因此仍然是一个有价值的工具。

如何快速融入团队并成为团队核心(九)

Posted on 2020-03-11 | Edited on 2021-04-25 | In 随笔

不知从何时起,这个系列的文章已经偏离标题越来越远了~

大概,如果要融入团队,其实或许只需做好跟周围同事的关系,几乎已经能够很好的融入了,但是如果要成为团队精英呢?我们难免得多做一些额外的努力,例如,了解价值观、试图融入企业文化,试图提高自己的工作效率,合理的支配自己的时间,尤其还有一点,就是做好目标管理。

一、画饼吃不下

某种意义上讲,决定我们能否成为高效开发者的核心关键是我们对于目标的管理能力,某种意义上讲,一个企业是否能够在群狼环伺的商界混得如鱼得水,中高层管理者是否能够理解和传达老板的意图和目标,基层能否理解老板的目标,并高效的执行,毋庸置疑都是至关重要的环节。

当然,大部分情况我们不用直接理解老板的目标,他们只需按照领导的指示,在自己的职责范围内把事情干好,就足够优秀了。但是事实上有时候我们会更加重视流程本身,往往忽略了对于目标的理解。甚至有时候公司老板喜欢站在更高的层面,画一个很大的饼,结果到具体执行时,给执行工作的人带来的巨大困扰。

“饼太大,吃不下”。一旦老板习惯于画饼,或许他自己也会陷入一个奇怪的状态,他会想,这件事情究竟是干呢,还是不干呢,嗯,我先吹出去,说不定有人会干,而且还能干出什么东西来着。但是作为中高层管理者显然更尴尬,我干了会不会犯错,犯错了会不会打板子,那我还是不干吧?一个长期画饼的老板,底下一定有一群听惯了“狼来了”谎话的中高层,他们已经习惯于对老板夸的海口无动于衷,最终导致了中高层管理者和老板之间的隔阂。

二、信息的层层递减

当然,大部分情况下,中高层管理者都经历离时间和战火的历练,总是能够从老板的一言一行中理解领导的需求,但是即便如此,也容易产生返工的现象。

事实上,我们在工作过程中总是会遇到返工,这种情况发生的根源也同样来源于我们对于目标的误读,甚至曲解了领导对于工作的原意。返工不仅仅会浪费我们的时间,也同样会极大的打击我们对于工作的信心,甚至会让我们对于职场发展的前途产生迷茫。

造成目标识别错误的原因往往是沟通因素,如信息传递漏斗所描述的:

图片

在这个环节中,信息是层层递减的,我们能表达出我们想表达的内容的百分之八十,听众能听到我们想表达信息的百分之六十,而观众所能理解的,只有我们想表达信息的百分之四十。

除此之外,假设我们的任务来源于其他人的二手信息,那么这种层层递减的边际效应或许也将更加明显。尤其是有时候领导喜欢说话只说一半,并指望下属能够从他的几句话中揣摩出他的全部心意,那这种效果或许自然而然就会更差了。

三、目标识别和执行

所以我们显然不能把期望完全寄托在理解对方的沟通上,而是需要辅以其他手段使得我们能够更好的将领导的需求落实下来。

1、记录:我们需要记录从领导下达的指令的内容,确保在信息传输过程中尽可能的减少误误解。

2、适当的过滤和提取:从信息中,要提取与目标相关的内容,去掉与目标无关的噪声干扰,以便减轻对于目标识别的复杂程度。

3、重聚:从信息中按照任务的相关性,建立必要的链接,并使得目标得以形成体系。

4、反馈:尽早与领导反馈,以便确保自己的目标符合对方的意图,并根据领导的指示对目标进行修正,例如对时间节点进行细化,或者领导是不是已经有了下一步工作安排等等。

我们就识别到了可以执行的具体目标,这样在进行任务执行时,也便于我们对于目标的解读。

基于识别到的目标,我们可以辅以一些管理学的手段,例如运用计划、执行、检查、纠偏的PDCA工具,让我们在执行过程中不断的进行修正,以便使我们能够在任务的每个执行过程中都能建立起反馈的机制,同时有能让领导能够及时的参与其中,使得任务更加完整的符合领导的预期。

图片

四、结语

随着互联网企业开始实施基于OKR的管理方法,对大家来说或多或少造成了一些影响,不过显然,建立一个更加完整的目标管理机制,对我们确实会带来一些好处,最起码的少返工就已经是一个不错的进步了。

图片

如何快速融入团队并成为团队核心(八)

Posted on 2020-03-11 | Edited on 2021-04-25 | In 随笔

一

感慨时间流逝,韶华易老似乎是我们与生俱来的焦虑,仿佛每当过年的时候总会感觉,呀,我这一年似乎什么都没干呀。

你看,连孔子都说:逝者如斯夫,不舍昼夜。所以他老人家是不是也很焦虑。。

然而我们的时间都去哪里了?

图片

尤其是作为互联网开发者的我们,其实更加明显,有时候上午9点到公司,然后随便写几行代码,就到中午了,然后再下午又调几个昨天写的bug,于是,这一天就似乎这么结束了。

我也曾经列过一个清单,汇总了当天的工作分配情况,其中,修复bug占比和沟通所占的比重最多,大概表明作为开发者的我代码质量确实是个问题。

图片

不过撇开代码质量这个因素不说,其实也反应了主观上在进行时间管控方面确实存在一些问题,这些问题也许平时看起来不明显,但是一旦我们开始进行记录,就会觉得比较明显。

例如,层出不穷的产品需求沟通、缺陷跟进、日常的会议和其他工作安排,这些看起来其实是非常常见的具体的事件,原本属于开发过程中正常的沟通,但是如果缺乏有效的节制,依然会造成效率的降低。毕竟我们的大脑进行任务的切换本身也是需要时间的,有时候如果正在进行某些需要消耗脑力的工作时,一旦被打断了,或许需要花更长的时间才能弥补过来。

二

当然有时候我们可能会习惯于白天被人打断,而把工作拖到晚上再花时间来弥补,或许这也是程序员们不得不辛苦的选择996这种不太合理的作息制度的一个客观原因。

坦率而言,时间对每个人来说都是公平的,每天24个小时分配得其实已经够合理了,但是同样的时间,不同的人却能做出不同的成绩;例如,我最近在阅读《程序员的三门课》,就深深的感觉同样是在阿里巴巴集团工作的张洪亮,从2015年加入阿里巴巴,到目前短短五年时间,我们都可以看到他的博客,几年时间写了几百篇博客,显然这些博客都来源于他日常工作中的实际场景,这也说明他不仅仅能够把代码写得很完美,也同样能够把自己的业余时间安排得非常饱满、同样的选择职场奋斗,为何他能够把时间安排得如此妥当呢。

这确实是一个值得大家深思的问题。

我最近开始有意识的管理自己的时间,例如我发现我的iphone中显示了一组数据:

1、每天抬起手机61次。

2、使用微信一小时。

3、屏幕使用时长1个半小时。

我观察了我一周中,最少的屏幕使用时间差不多也是一小时多一点,多的一天大概是将近两小时,这实际上是在工作时间记录的时间,当然,撇开每天在地铁上上下班(约一个半小时)的日常的手机使用情况,依然每天有将近一个小时的时间看起来似乎是不合理的支配了。

我来粗略估算一下自己的时间粒度,大概是这样的:

1、休息时间,每天8个小时。这个时间包含晚上休息和中午休息的时间。

2、工作通勤路上的时间:1个半小时。

3、每天的用餐时间:30分钟。

4、每天的如厕时间,20分钟。

5、工作时间:8-9小时。

6、每天的阅读学习和其他时间:约两小时。

没错,以上数据之和不等于24小时,说明还有一些时间损耗看起来不明显,但累积起来也同样惊人。当然实际上工作时间看起来有8-9小时,实际上真的能实现满打满算的工作这么长的时间么,这大概是个自欺欺人的问题,很惭愧的说大概在这个工作时段,有那么一段时间,或者说不短于一个半小时,或许是在各种聊天工具间来回切换。

图片

(以上数据过于真实,请大家尽管批评吧。)

所以,我曾经看过,越是成功的人士,越善于合理的利用时间,例如,像王健林同志,据说已经把时间安排在15分钟这个粒度。而目前的我,大概离一个小时这个粒度都还有一点困难,所以离成功还差十万八千里也不是没有理由的。

三

作为一位技术从业者,是否能够合理的支配自己的时间确实是非常重要的事情,从某种意义上来说,能够每天花10个小时,或者8个小时全付身心的投入到工作中,其实已经足够完成许多任务了。

个人认为,我们大概可以做以下这些工作来更好的实现自己对时间的支配:

1、对自己每天的实际时间支配情况进行总结,分析时间的主要分布情况,并以清单的形式列入,以便自己有目的性的进行改善。

2、难免需要一些断舍离,比如关闭某些app的推送通知,将非工作用的聊天工具,从windows桌面右下角的图标中隐藏,避免对工作造成的干扰,然后每天花一段特定的时间来处理非工作场景下的消息。

3、每天早上简单的概述自己今天需要完成的工作,并预估工作所需要的时间,适当的给自己一些相对较长的时间,在这个时间段内,尽量避免外界因素的干扰。每天早上和下午工作开始一小时后的是能够利用的长块时间,而且效率也比较高,尽量将重要性工作安排在这样的时间段进行,尽量避免在这样的时间段开会,以免降低工作效率。(例如,微软有一个go dark的机制,允许在某些特殊的情况下与外界因素隔离开,踏踏实实的沉浸到写代码中)。

4、合理的利用等待期。在同时需要处理多个任务时,有的任务往往由于各种条件或资源尚不具备,这会需要花不少时间来等待,如果我们将置于等待期的任务提前规划、也是一种不错的方法。

在《卓有成效的管理者》这本书中,将会管理自己的时间作为一个优秀管理者开始其管理工作的第一步,只有开始学会梳理自己的时间和管理自己的时间,才能成为合适的管理者,这一点我觉得非常有道理。

一步一步的构建整洁、可维护的RESTful APIs

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

译者荐语:利用周末的时间,本人拜读了长沙.NET技术社区翻译的技术文章《微软RESTFul API指南》,打算按照步骤写一个完整的教程,后来无意中看到了这篇文章,与我要写的主题有不少相似之处,特意翻译下来。前方高能。

图片一步一步的构建整洁、可维护的RESTful APIs

查看原文

总览

RESTful不是一个新名词。它是一种架构风格,这种架构风格使用Web服务从客户端应用程序接收数据和向客户端应用程序发送数据。其目标是集中不同客户端应用程序将使用的数据。

选择正确的工具来编写RESTful服务至关重要,因为我们需要关注可伸缩性,维护,文档以及所有其他相关方面。在ASP.NET Core为我们提供了一个功能强大、易于使用的API,使用这些API将很好的实现这个目标。

在本文中,我将向您展示如何使用ASP.NET Core框架为“几乎”现实世界的场景编写结构良好的RESTful API。我将详细介绍常见的模式和策略以简化开发过程。

我还将向您展示如何集成通用框架和库,例如Entity Framework Core和AutoMapper,以提供必要的功能。

先决条件

我希望您了解面向对象的编程概念。

即使我将介绍C#编程语言的许多细节,我还是建议您具有该主题的基本知识。

我还假设您知道什么是REST,HTTP协议如何工作,什么是API端点以及什么是JSON。这是关于此主题的出色的入门教程。最后,您需要了解关系数据库的工作原理。

要与我一起编码,您将必须安装.NET Core 2.2以及Postman(我将用来测试API的工具)。我建议您使用诸如Visual Studio Code之类的代码编辑器来开发API。选择您喜欢的代码编辑器。如果选择Visual Studio Code作为您的代码编辑器,建议您安装C#扩展以更好地突出显示代码。

您可以在本文末尾找到该API的Github的链接,以检查最终结果。

范围

让我们为一家超市编写一个虚构的Web API。假设我们必须实现以下范围:

  • 创建一个RESTful服务,该服务允许客户端应用程序管理超市的产品目录。它需要公开端点以创建,读取,编辑和删除产品类别,例如乳制品和化妆品,还需要管理这些类别的产品。
  • 对于类别,我们需要存储其名称。对于产品,我们需要存储其名称,度量单位(例如,按重量测量的产品为KG),包装中的数量(例如,如果一包饼干是10,则为10)及其各自的类别。

为了简化示例,我将不处理库存产品,产品运输,安全性和任何其他功能。这个范围足以向您展示ASP.NET Core的工作方式。

要开发此服务,我们基本上需要两个API 端点(译者注:指控制器):一个用于管理类别,一个用于管理产品。在JSON通讯方面,我们可以认为响应如下:

1
2
3
4
5
6
7
8
9
API endpoint: /api/categories
JSON Response (for GET requests):
{
[
{ "id": 1, "name": "Fruits and Vegetables" },
{ "id": 2, "name": "Breads" },
… // Other categories
]
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
API endpoint: /api/products
JSON Response (for GET requests):
{
[
{
"id": 1,
"name": "Sugar",
"quantityInPackage": 1,
"unitOfMeasurement": "KG"
"category": {
"id": 3,
"name": "Sugar"
}
},
… // Other products
]
}

让我们开始编写应用程序。

第1步-创建API

首先,我们必须为Web服务创建文件夹结构,然后我们必须使用.NET CLI工具来构建基本的Web API。打开终端或命令提示符(取决于您使用的操作系统),并依次键入以下命令:

1
2
3
4
5
mkdir src/Supermarket.API

cd src/Supermarket.API

dotnet new webapi

前两个命令只是为API创建一个新目录,然后将当前位置更改为新文件夹。最后一个遵循Web API模板生成一个新项目,这是我们正在开发的应用程序。您可以阅读有关这些命令和其他项目模板的更多信息,并可以通过检查此链接来生成其他项目模板。
现在,新目录将具有以下结构:

图片

项目结构

结构概述

ASP.NET Core应用程序由在类中配置的一组中间件(应用程序流水线中的小块应用程序,用于处理请求和响应)组成Startup。如果您以前已经使用过Express.js之类的框架,那么这个概念对您来说并不是什么新鲜事物。

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
public class Startup
{
public Startup(IConfiguration configuration)
{
Configuration = configuration;
}


public IConfiguration Configuration { get; }


// This method gets called by the runtime. Use this method to add services to the container.
public void ConfigureServices(IServiceCollection services)
{
services.AddMvc().SetCompatibilityVersion(CompatibilityVersion.Version_2_2);
}


// This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
public void Configure(IApplicationBuilder app, IHostingEnvironment env)
{
if (env.IsDevelopment())
{
app.UseDeveloperExceptionPage();
}
else
{
// The default HSTS value is 30 days. You may want to change this for production scenarios, see https://aka.ms/aspnetcore-hsts.
app.UseHsts();
}


app.UseHttpsRedirection();
app.UseMvc();
}
}

当应用程序启动时,将调用类中的Main** **方法Program。它使用启动配置创建默认的Web主机,通过HTTP通过特定端口(默认情况下,HTTP为5000,HTTPS为5001)公开应用程序。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
namespace Supermarket.API
{
public class Program
{
public static void Main(string[] args)
{
CreateWebHostBuilder(args).Build().Run();
}


public static IWebHostBuilder CreateWebHostBuilder(string[] args) =>
WebHost.CreateDefaultBuilder(args)
.UseStartup<Startup>();
}
}

看一下文件夹中的ValuesController类Controllers。它公开了API通过路由接收请求时将调用的方法/api/values。

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
[Route("api/[controller]")]
[ApiController]
public class ValuesController : ControllerBase
{
// GET api/values
[HttpGet]
public ActionResult<IEnumerable<string>> Get()
{
return new string[] { "value1", "value2" };
}


// GET api/values/5
[HttpGet("{id}")]
public ActionResult<string> Get(int id)
{
return "value";
}


// POST api/values
[HttpPost]
public void Post([FromBody] string value)
{
}


// PUT api/values/5
[HttpPut("{id}")]
public void Put(int id, [FromBody] string value)
{
}


// DELETE api/values/5
[HttpDelete("{id}")]
public void Delete(int id)
{
}
}

如果您不了解此代码的某些部分,请不要担心。在开发必要的API端点时,我将详细介绍每一个。现在,只需删除此类,因为我们不会使用它。

第2步-创建领域模型

我将应用一些设计概念,以使应用程序简单易维护。

编写可以由您自己理解和维护的代码并不难,但是您必须牢记您将成为团队的一部分。如果您不注意如何编写代码,那么结果将是一个庞然大物,这将使您和您的团队成员头痛不已。听起来很极端吧?但是相信我,这就是事实。

图片

衡量好代码的标准是WTF的频率。原图来自smitty42,发表于filckr。该图遵循CC-BY-2.0。

在Supermarket.API目录中,创建一个名为的新文件夹Domain。在新的领域文件夹中,创建另一个名为的文件夹Models。我们必须添加到此文件夹的第一个模型是Category。最初,它将是一个简单的Plain Old CLR Object(POCO)类。这意味着该类将仅具有描述其基本信息的属性。

1
2
3
4
5
6
7
8
9
10
11
12
using System.Collections.Generic;


namespace Supermarket.API.Domain.Models
{
public class Category
{
public int Id { get; set; }
public string Name { get; set; }
public IList<Product> Products { get; set; } = new List<Product>();
}
}

该类具有一个Id** 属性(用于标识类别)和一个Name属性。以及一个Products 属性。最后一个属性将由Entity Framework Core使用**,大多数ASP.NET Core应用程序使用ORM将数据持久化到数据库中,以映射类别和产品之间的关系。由于类别具有许多相关产品,因此在面向对象的编程方面也具有合理的思维能力。
我们还必须创建产品模型。在同一文件夹中,添加一个新Product类。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
namespace Supermarket.API.Domain.Models
{
public class Product
{
public int Id { get; set; }
public string Name { get; set; }
public short QuantityInPackage { get; set; }
public EUnitOfMeasurement UnitOfMeasurement { get; set; }


public int CategoryId { get; set; }
public Category Category { get; set; }
}
}

该产品还具有ID和名称的属性。属性QuantityInPackage,它告诉我们一包中有多少个产品单位(请记住应用范围的饼干示例)和一个UnitOfMeasurement** 属性,这是表示一个枚举类型,它表示可能的度量单位的枚举。最后两个属性,CategoryId **和Category将由ORM用于映射的产品和类别之间的关系。它表明一种产品只有一个类别。

让我们定义领域模型的最后一部分,EUnitOfMeasurement** **枚举。

按照惯例,枚举不需要在名称前以“ E”开头,但是在某些库和框架中,您会发现此前缀是将枚举与接口和类区分开的一种方式。

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
using System.ComponentModel;


namespace Supermarket.API.Domain.Models
{
public enum EUnitOfMeasurement : byte
{
[Description("UN")]
Unity = 1,


[Description("MG")]
Milligram = 2,


[Description("G")]
Gram = 3,


[Description("KG")]
Kilogram = 4,


[Description("L")]
Liter = 5
}
}

该代码非常简单。在这里,我们仅定义了几种度量单位的可能性,但是,在实际的超市系统中,您可能具有许多其他度量单位,并且可能还有一个单独的模型。
注意,【Description】特性应用于所有枚举可能性。特性是一种在C#语言的类,接口,属性和其他组件上定义元数据的方法。在这种情况下,我们将使用它来简化产品API端点的响应,但是您现在不必关心它。我们待会再回到这里。

我们的基本模型已准备就绪,可以使用。现在,我们可以开始编写将管理所有类别的API端点。

第3步-类别API

在Controllers文件夹中,添加一个名为的新类CategoriesController。

按照惯例,该文件夹中所有后缀为“ Controller”的类都将成为我们应用程序的控制器。这意味着他们将处理请求和响应。您必须从命名空间【Microsoft.AspNetCore.Mvc】继承Controller。

命名空间由一组相关的类,接口,枚举和结构组成。您可以将其视为类似于Java语言模块或Java 程序包的东西。

新的控制器应通过路由/api/categories做出响应。我们通过Route** **在类名称上方添加属性,指定占位符来实现此目的,该占位符表示路由应按照惯例使用不带控制器后缀的类名称。

1
2
3
4
5
6
7
8
9
10
using Microsoft.AspNetCore.Mvc;


namespace Supermarket.API.Controllers
{
[Route("/api/[controller]")]
public class CategoriesController : Controller
{
}
}

让我们开始处理GET请求。首先,当有人/api/categories通过GET动词请求数据时,API需要返回所有类别。为此,我们可以创建类别服务。
从概念上讲,服务基本上是定义用于处理某些业务逻辑的方法的类或接口。创建用于处理业务逻辑的服务是许多不同编程语言的一种常见做法,例如身份验证和授权,付款,复杂的数据流,缓存和需要其他服务或模型之间进行某些交互的任务。

使用服务,我们可以将请求和响应处理与完成任务所需的真实逻辑隔离开来。

该服务,我们要创建将首先定义一个单独的行为,或方法:一个list方法。我们希望该方法返回数据库中所有现有的类别。

为简单起见,在这篇博客中,我们将不处理数据分页或过滤,(译者注:基于RESTFul规范,提供了一套完整的分页和过滤的规则)。将来,我将写一篇文章,展示如何轻松处理这些功能。

为了定义C#(以及其他面向对象的语言,例如Java)中某事物的预期行为,我们定义一个interface。一个接口告诉某些事情应该如何工作,但是没有实现行为的真实逻辑。逻辑在实现接口的类中实现。如果您不清楚此概念,请不要担心。一段时间后您将了解它。

在Domain文件夹中,创建一个名为的新目录Services。在此添加一个名为ICategoryService的接口。按照惯例,所有接口都应以C#中的大写字母“ I”开头。定义接口代码,如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
using System.Collections.Generic;
using System.Threading.Tasks;
using Supermarket.API.Domain.Models;


namespace Supermarket.API.Domain.Services
{
public interface ICategoryService
{
Task<IEnumerable<Category>> ListAsync();
}
}

该ListAsync方法的实现必须异步返回类别的可枚举对象。
Task封装返回的类表示异步。由于必须等待数据库完成操作才能返回数据,因此我们需要考虑执行此过程可能需要一段时间,因此我们需要使用异步方法。另请注意“Async”后缀。这是一个约定,告诉我们的方法应异步执行。

我们有很多约定,对吗?我个人喜欢它,因为它使应用程序易于阅读,即使你在一家使用.NET技术的公司是新人。

图片

“-好的,我们定义了此接口,但是它什么也没做。有什么用?”

如果您来自Javascript或其他非强类型语言,则此概念可能看起来很奇怪。

接口使我们能够从实际实现中抽象出所需的行为。使用称为依赖注入的机制,我们可以实现这些接口并将它们与其他组件隔离。

基本上,当您使用依赖项注入时,您可以使用接口定义一些行为。然后,创建一个实现该接口的类。最后,将引用从接口绑定到您创建的类。

”-听起来确实令人困惑。我们不能简单地创建一个为我们做这些事情的类吗?”

让我们继续实现我们的API,您将了解为什么使用这种方法。

更改CategoriesController代码,如下所示:

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
using System.Collections.Generic;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Mvc;
using Supermarket.API.Domain.Models;
using Supermarket.API.Domain.Services;


namespace Supermarket.API.Controllers
{
[Route("/api/[controller]")]
public class CategoriesController : Controller
{
private readonly ICategoryService _categoryService;

public CategoriesController(ICategoryService categoryService)
{
_categoryService = categoryService;
}


[HttpGet]
public async Task<IEnumerable<Category>> GetAllAsync()
{
var categories = await _categoryService.ListAsync();
return categories;
}
}
}

我已经为控制器定义了一个构造函数(当创建一个类的新实例时会调用一个构造函数),并且它接收的实例ICategoryService。这意味着实例可以是任何实现服务接口的实例。我将此实例存储在一个私有的只读字段中_categoryService。我们将使用此字段访问类别服务实现的方法。
顺便说一下,下划线前缀是表示字段的另一个通用约定。特别地,.NET的官方命名约定指南不建议使用此约定,但是这是一种非常普遍的做法,可以避免使用“ this”关键字来区分类字段和局部变量。我个人认为阅读起来要干净得多,并且许多框架和库都使用此约定。

在构造函数下,我定义了用于处理请求的方法/api/categories。该HttpGet** **属性告诉ASP.NET Core管道使用该属性来处理GET请求(可以省略此属性,但是最好编写它以便于阅读)。

该方法使用我们的CategoryService实例列出所有类别,然后将类别返回给客户端。框架管道将数据序列化为JSON对象。IEnumerable类型告诉框架,我们想要返回一个类别的枚举,而Task类型(使用async关键字修饰)告诉管道,这个方法应该异步执行。最后,当我们定义一个异步方法时,我们必须使用await关键字来处理需要一些时间的任务。

好的,我们定义了API的初始结构。现在,有必要真正实现类别服务。

步骤4-实现类别服务

在API的根文件夹(即Supermarket.API文件夹)中,创建一个名为的新文件夹Services。在这里,我们将放置所有服务实现。在新文件夹中,添加一个名为CategoryService的新类。更改代码,如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
using System.Collections.Generic;
using System.Threading.Tasks;
using Supermarket.API.Domain.Models;
using Supermarket.API.Domain.Services;


namespace Supermarket.API.Services
{
public class CategoryService : ICategoryService
{
public async Task<IEnumerable<Category>> ListAsync()
{
}
}
}

以上只是接口实现的基本代码,我们暂时仍不处理任何逻辑。让我们考虑一下列表方法应该如何实现。
我们需要访问数据库并返回所有类别,然后我们需要将此数据返回给客户端。

服务类不是应该处理数据访问的类。我们将使用一种称为“仓储模式”的设计模式,定义仓储类,用于管理数据库中的数据。

在使用仓储模式时,我们定义了repository 类,该类基本上封装了处理数据访问的所有逻辑。这些仓储类使方法可以列出,创建,编辑和删除给定模型的对象,与操作集合的方式相同。在内部,这些方法与数据库对话以执行CRUD操作,从而将数据库访问与应用程序的其余部分隔离开。

我们的服务需要调用类别仓储,以获取列表对象。

从概念上讲,服务可以与一个或多个仓储或其他服务“对话”以执行操作。

创建用于处理数据访问逻辑的新定义似乎是多余的,但是您将在一段时间内看到将这种逻辑与服务类隔离是非常有利的。

让我们创建一个仓储,该仓储负责与数据库通信,作为持久化保存类别的一种方式。

步骤5-类别仓储和持久层

在该Domain文件夹内,创建一个名为的新目录Repositories。然后,添加一个名为的新接口ICategoryRespository。定义接口如下:

1
2
3
4
5
6
7
8
9
10
using System.Collections.Generic;
using System.Threading.Tasks;
using Supermarket.API.Domain.Models;
namespace Supermarket.API.Domain.Repositories
{
public interface ICategoryRepository
{
Task<IEnumerable<Category>> ListAsync();
}
}

初始代码基本上与服务接口的代码相同。
定义了接口之后,我们可以返回服务类并使用的实例ICategoryRepository返回数据来完成实现list方法。

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
using System.Collections.Generic;
using System.Threading.Tasks;
using Supermarket.API.Domain.Models;
using Supermarket.API.Domain.Repositories;
using Supermarket.API.Domain.Services;


namespace Supermarket.API.Services
{
public class CategoryService : ICategoryService
{
private readonly ICategoryRepository _categoryRepository;


public CategoryService(ICategoryRepository categoryRepository)
{
this._categoryRepository = categoryRepository;
}


public async Task<IEnumerable<Category>> ListAsync()
{
return await _categoryRepository.ListAsync();
}
}
}

现在,我们必须实现类别仓储的真实逻辑。在这样做之前,我们必须考虑如何访问数据库。
顺便说一句,我们仍然没有数据库!

我们将使用Entity Framework Core(为简单起见,我将其称为EF Core)作为我们的数据库ORM。该框架是ASP.NET Core的默认ORM,并公开了一个友好的API,该API使我们能够将应用程序的类映射到数据库表。

EF Core还允许我们先设计应用程序,然后根据我们在代码中定义的内容生成数据库。此技术称为Code First。我们将使用Code First方法来生成数据库(实际上,在此示例中,我将使用内存数据库,但是您可以轻松地将其更改为像SQL Server或MySQL服务器这样的实例数据库)。

在API的根文件夹中,创建一个名为的新目录Persistence。此目录将包含我们访问数据库所需的所有内容,例如仓储实现。

在新文件夹中,创建一个名为的新目录Contexts,然后添加一个名为的新类AppDbContext。此类必须继承DbContext,EF Core通过DBContext用来将您的模型映射到数据库表的类。通过以下方式更改代码:

1
2
3
4
5
6
7
8
9
10
11
12
using Microsoft.EntityFrameworkCore;


namespace Supermarket.API.Domain.Persistence.Contexts
{
public class AppDbContext : DbContext
{
public AppDbContext(DbContextOptions<AppDbContext> options) : base(options)
{
}
}
}

我们添加到此类的构造函数负责通过依赖注入将数据库配置传递给基类。稍后您将看到其工作原理。
现在,我们必须创建两个DbSet属性。这些属性是将模型映射到数据库表的集合(唯一对象的集合)。

另外,我们必须将模型的属性映射到相应的列,指定哪些属性是主键,哪些是外键,列类型等。我们可以使用称为Fluent API的功能来覆盖OnModelCreating方法,以指定数据库映射。更改AppDbContext类,如下所示:

该代码是如此直观。

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
using Microsoft.EntityFrameworkCore;
using Supermarket.API.Domain.Models;


namespace Supermarket.API.Persistence.Contexts
{
public class AppDbContext : DbContext
{
public DbSet<Category> Categories { get; set; }
public DbSet<Product> Products { get; set; }


public AppDbContext(DbContextOptions<AppDbContext> options) : base(options) { }


protected override void OnModelCreating(ModelBuilder builder)
{
base.OnModelCreating(builder);

builder.Entity<Category>().ToTable("Categories");
builder.Entity<Category>().HasKey(p => p.Id);
builder.Entity<Category>().Property(p => p.Id).IsRequired().ValueGeneratedOnAdd();
builder.Entity<Category>().Property(p => p.Name).IsRequired().HasMaxLength(30);
builder.Entity<Category>().HasMany(p => p.Products).WithOne(p => p.Category).HasForeignKey(p => p.CategoryId);


builder.Entity<Category>().HasData
(
new Category { Id = 100, Name = "Fruits and Vegetables" }, // Id set manually due to in-memory provider
new Category { Id = 101, Name = "Dairy" }
);


builder.Entity<Product>().ToTable("Products");
builder.Entity<Product>().HasKey(p => p.Id);
builder.Entity<Product>().Property(p => p.Id).IsRequired().ValueGeneratedOnAdd();
builder.Entity<Product>().Property(p => p.Name).IsRequired().HasMaxLength(50);
builder.Entity<Product>().Property(p => p.QuantityInPackage).IsRequired();
builder.Entity<Product>().Property(p => p.UnitOfMeasurement).IsRequired();
}
}
}

我们指定我们的模型应映射到哪些表。此外,我们设置了主键,使用该方法HasKey,该表的列,使用Property方法,和一些限制,例如IsRequired,HasMaxLength,和ValueGeneratedOnAdd,这些都是使用FluentApi的方式基于Lamada 表达式语法实现的(链式语法)。
看一下下面的代码:

1
2
3
4
builder.Entity<Category>()
.HasMany(p => p.Products)
.WithOne(p => p.Category)
.HasForeignKey(p => p.CategoryId);

在这里,我们指定表之间的关系。我们说一个类别有很多产品,我们设置了将映射此关系的属性(Products,来自Category类,和Category,来自Product类)。我们还设置了外键(CategoryId)。
如果您想学习如何使用EF Core配置一对一和多对多关系,以及如何完整的使用它,请看一下本教程。

还有一种用于通过HasData方法配置种子数据的方法:

1
2
3
4
5
builder.Entity<Category>().HasData
(
new Category { Id = 100, Name = "Fruits and Vegetables" },
new Category { Id = 101, Name = "Dairy" }
);

默认情况下,在这里我们仅添加两个示例类别。这对我们完成后进行API的测试来说是非常有必要的。

注意:我们在Id这里手动设置属性,因为内存提供程序的工作机制需要。我将标识符设置为大数字,以避免自动生成的标识符和种子数据之间发生冲突。

真正的关系数据库提供程序中不存在此限制,因此,例如,如果要使用SQL Server等数据库,则不必指定这些标识符。如果您想了解此行为,请检查此Github问题。

在实现数据库上下文类之后,我们可以实现类别仓储。添加一个名为新的文件夹Repositories里面Persistence的文件夹,然后添加一个名为新类BaseRepository。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
using Supermarket.API.Persistence.Contexts;


namespace Supermarket.API.Persistence.Repositories
{
public abstract class BaseRepository
{
protected readonly AppDbContext _context;


public BaseRepository(AppDbContext context)
{
_context = context;
}
}
}

此类只是我们所有仓储都将继承的抽象类。抽象类是没有直接实例的类。您必须创建直接类来创建实例。
在BaseRepository接受我们的实例,AppDbContext通过依赖注入暴露了一个受保护的属性称为(只能是由子类访问一个属性)_context,即可以访问我们需要处理数据库操作的所有方法。

在相同文件夹中添加一个新类CategoryRepository。现在,我们将真正实现仓储逻辑:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
using System.Collections.Generic;
using System.Threading.Tasks;
using Microsoft.EntityFrameworkCore;
using Supermarket.API.Domain.Models;
using Supermarket.API.Domain.Repositories;
using Supermarket.API.Persistence.Contexts;


namespace Supermarket.API.Persistence.Repositories
{
public class CategoryRepository : BaseRepository, ICategoryRepository
{
public CategoryRepository(AppDbContext context) : base(context)
{
}


public async Task<IEnumerable<Category>> ListAsync()
{
return await _context.Categories.ToListAsync();
}
}
}

仓储继承BaseRepository和实现ICategoryRepository。
注意实现list方法是很简单的。我们使用Categories数据库集访问类别表,然后调用扩展方法ToListAsync,该方法负责将查询结果转换为类别的集合。

EF Core 将我们的方法调用转换为SQL查询,这是最有效的方法。这种方式仅当您调用将数据转换为集合的方法或使用方法获取特定数据时才执行查询。

现在,我们有了类别控制器,服务和仓储库的代码实现。

我们将关注点分离开来,创建了只执行应做的事情的类。

测试应用程序之前的最后一步是使用ASP.NET Core依赖项注入机制将我们的接口绑定到相应的类。

第6步-配置依赖注入

现在是时候让您最终了解此概念的工作原理了。

图片

在应用程序的根文件夹中,打开Startup类。此类负责在应用程序启动时配置各种配置。

该ConfigureServices和Configure方法通过框架管道在运行时调用来配置应用程序应该如何工作,必须使用哪些组件。

打开ConfigureServices方法。在这里,我们只有一行配置应用程序以使用MVC管道,这基本上意味着该应用程序将使用控制器类来处理请求和响应(在这段代码背后发生了很多事情,但目前您仅需要知道这些)。

我们可以使用ConfigureServices访问services参数的方法来配置我们的依赖项绑定。清理类代码,删除所有注释并按如下所示更改代码:

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
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Mvc;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Supermarket.API.Domain.Repositories;
using Supermarket.API.Domain.Services;
using Supermarket.API.Persistence.Contexts;
using Supermarket.API.Persistence.Repositories;
using Supermarket.API.Services;


namespace Supermarket.API
{
public class Startup
{
public IConfiguration Configuration { get; }


public Startup(IConfiguration configuration)
{
Configuration = configuration;
}


public void ConfigureServices(IServiceCollection services)
{
services.AddMvc().SetCompatibilityVersion(CompatibilityVersion.Version_2_2);


services.AddDbContext<AppDbContext>(options => {
options.UseInMemoryDatabase("supermarket-api-in-memory");
});


services.AddScoped<ICategoryRepository, CategoryRepository>();
services.AddScoped<ICategoryService, CategoryService>();
}


public void Configure(IApplicationBuilder app, IHostingEnvironment env)
{
if (env.IsDevelopment())
{
app.UseDeveloperExceptionPage();
}
else
{
// The default HSTS value is 30 days. You may want to change this for production scenarios, see https://aka.ms/aspnetcore-hsts.
app.UseHsts();
}


app.UseHttpsRedirection();
app.UseMvc();
}
}
}

看一下这段代码:

1
2
3
4
5
services.AddDbContext<AppDbContext>(options => {

options.UseInMemoryDatabase("supermarket-api-in-memory");

});

在这里,我们配置数据库上下文。我们告诉ASP.NET Core将其AppDbContext与内存数据库实现一起使用,该实现由作为参数传递给我们方法的字符串标识。通常,在编写集成测试时才会使用内存数据库,但是为了简单起见,我在这里使用了内存数据库。这样,我们无需连接到真实的数据库即可测试应用程序。
这些代码行在内部配置我们的数据库上下文,以便使用确定作用域的生存周期进行依赖注入。

scoped生存周期告诉ASP.NET Core管道,每当它需要解析接收AppDbContext作为构造函数参数的实例的类时,都应使用该类的相同实例。如果内存中没有实例,则管道将创建一个新实例,并在给定请求期间在需要它的所有类中重用它。这样,您无需在需要使用时手动创建类实例。

如果你想了解其他有关生命周期的知识,可以阅读官方文档。

依赖注入技术为我们提供了许多优势,例如:

  • 代码可重用性;
  • 更高的生产力,因为当我们不得不更改实现时,我们无需费心去更改您使用该功能的一百个地方;
  • 您可以轻松地测试应用程序,因为我们可以使用mock(类的伪实现)隔离必须测试的内容,而我们必须将接口作为构造函数参数进行传递。
  • 当一个类需要通过构造函数接收更多的依赖关系时,您不必手动更改正在创建实例的所有位置(太赞了!)。

配置数据库上下文之后,我们还将我们的服务和仓储绑定到相应的类。

1
2
3
services.AddScoped<ICategoryRepository, CategoryRepository>();

services.AddScoped<ICategoryService, CategoryService>();

在这里,我们还使用了scoped生存周期,因为这些类在内部必须使用数据库上下文类。在这种情况下,指定相同的范围是有意义的。
现在我们配置了依赖绑定,我们必须在Program类上进行一些小的更改,以便数据库正确地初始化种子数据。此步骤仅在使用内存数据库提供程序时才需要执行(请参阅此Github问题以了解原因)。

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
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.AspNetCore;
using Microsoft.AspNetCore.Hosting;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using Supermarket.API.Persistence.Contexts;


namespace Supermarket.API
{
public class Program
{
public static void Main(string[] args)
{
var host = BuildWebHost(args);


using(var scope = host.Services.CreateScope())
using(var context = scope.ServiceProvider.GetService<AppDbContext>())
{
context.Database.EnsureCreated();
}


host.Run();
}


public static IWebHost BuildWebHost(string[] args) =>
WebHost.CreateDefaultBuilder(args)
.UseStartup<Startup>()
.Build();
}
}

由于我们使用的是内存提供程序,因此有必要更改Main方法 添加“ context.Database.EnsureCreated();”代码以确保在应用程序启动时将“创建”数据库。没有此更改,将不会创建我们想要的初始化种子数据。
实现了所有基本功能后,就该测试我们的API端点了。

第7步-测试类别

在API根文件夹中打开终端或命令提示符,然后键入以下命令:

1
dotnet run

上面的命令启动应用程序。控制台将显示类似于以下内容的输出:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
info: Microsoft.EntityFrameworkCore.Infrastructure[10403]

Entity Framework Core 2.2.0-rtm-35687 initialized ‘AppDbContext’ using provider ‘Microsoft.EntityFrameworkCore.InMemory’ with options: StoreName=supermarket-api-in-memory

info: Microsoft.EntityFrameworkCore.Update[30100]

Saved 2 entities to in-memory store.

info: Microsoft.AspNetCore.DataProtection.KeyManagement.XmlKeyManager[0]

User profile is available. Using ‘C:\Users\evgomes\AppData\Local\ASP.NET\DataProtection-Keys’ as key repository and Windows DPAPI to encrypt keys at rest.

Hosting environment: Development

Content root path: C:\Users\evgomes\Desktop\Tutorials\src\Supermarket.API

Now listening on: https://localhost:5001

Now listening on: http://localhost:5000

Application started. Press Ctrl+C to shut down.

您可以看到调用了EF Core来初始化数据库。最后几行显示应用程序在哪个端口上运行。
打开浏览器,然后导航到 http://localhost:5000/api/categories (或控制台输出上显示的URL)。如果您发现由于HTTPS导致的安全错误,则只需为应用程序添加一个例外。

浏览器将显示以下JSON数据作为输出:

1
2
3
4
5
6
7
8
9
10
11
12
[
{
"id": 100,
"name": "Fruits and Vegetables",
"products": []
},
{
"id": 101,
"name": "Dairy",
"products": []
}
]

在这里,我们看到配置数据库上下文时添加到数据库的数据。此输出确认我们的代码正在运行。
您使用很少的代码行创建了GET API端点,并且由于当前API项目的架构模式,您的代码结构确实很容易更改。

现在,该向您展示在由于业务需要而不得不对其进行更改时,更改此代码有多么容易。

步骤8-创建类别资源

如果您还记得API端点的规范,您会注意到我们的实际JSON响应还有一个额外的属性:products数组。看一下所需响应的示例:

1
2
3
4
5
6
7
{
[
{ "id": 1, "name": "Fruits and Vegetables" },
{ "id": 2, "name": "Breads" },
… // Other categories
]
}

产品数组出现在我们当前的JSON响应中,因为我们的Category模型具有Products,EF Core需要的属性,以正确映射给定类别的产品。
我们不希望在响应中使用此属性,但是不能更改模型类以排除此属性。当我们尝试管理类别数据时,这将导致EF Core引发错误,并且也将破坏我们的领域模型设计,因为没有产品的产品类别没有意义。

要返回仅包含超级市场类别的标识符和名称的JSON数据,我们必须创建一个资源类。

资源类是一种包含将客户端应用程序和API端点之间进行交换的类型,通常以JSON数据的形式出现,以表示一些特定信息的类。

来自API端点的所有响应都必须返回资源。

将真实模型表示形式作为响应返回是一种不好的做法,因为它可能包含客户端应用程序不需要或没有其权限的信息(例如,用户模型可以返回用户密码的信息) ,这将是一个很大的安全问题)。

我们需要一种资源来仅代表我们的类别,而没有产品。

现在您知道什么是资源,让我们实现它。首先,在命令行中按Ctrl + C停止正在运行的应用程序。在应用程序的根文件夹中,创建一个名为Resources的新文件夹。在其中添加一个名为的新类CategoryResource。

1
2
3
4
5
6
7
8
namespace Supermarket.API.Resources
{
public class CategoryResource
{
public int Id { get; set; }
public string Name { get; set; }
}
}

我们必须将类别服务提供的类别模型集合映射到类别资源集合。
我们将使用一个名为AutoMapper的库来处理对象之间的映射。AutoMapper是.NET世界中非常流行的库,并且在许多商业和开源项目中使用。

在命令行中输入以下命令,以将AutoMapper添加到我们的应用程序中:

1
2
3
dotnet add package AutoMapper

dotnet add package AutoMapper.Extensions.Microsoft.DependencyInjection

要使用AutoMapper,我们必须做两件事:

  • 注册它以进行依赖注入;
  • 创建一个类,该类将告诉AutoMapper如何处理类映射。

首先,打开Startup课程。在该ConfigureServices方法的最后一行之后,添加以下代码:

1
services.AddAutoMapper();

此行处理AutoMapper的所有必需配置,例如注册它以进行依赖项注入以及在启动过程中扫描应用程序以配置映射配置文件。
现在,在根目录中,添加一个名为的新文件夹Mapping,然后添加一个名为的类ModelToResourceProfile。通过以下方式更改代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
using AutoMapper;
using Supermarket.API.Domain.Models;
using Supermarket.API.Resources;


namespace Supermarket.API.Mapping
{
public class ModelToResourceProfile : Profile
{
public ModelToResourceProfile()
{
CreateMap<Category, CategoryResource>();
}
}
}

该类继承Profile了AutoMapper用于检查我们的映射如何工作的类类型。在构造函数上,我们在Category模型类和CategoryResource类之间创建一个映射。由于类的属性具有相同的名称和类型,因此我们不必为其使用任何特殊的配置。
最后一步包括更改类别控制器以使用AutoMapper处理我们的对象映射。

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
using System.Collections.Generic;
using System.Threading.Tasks;
using AutoMapper;
using Microsoft.AspNetCore.Mvc;
using Supermarket.API.Domain.Models;
using Supermarket.API.Domain.Services;
using Supermarket.API.Resources;


namespace Supermarket.API.Controllers
{
[Route("/api/[controller]")]
public class CategoriesController : Controller
{
private readonly ICategoryService _categoryService;
private readonly IMapper _mapper;


public CategoriesController(ICategoryService categoryService, IMapper mapper)
{
_categoryService = categoryService;
_mapper = mapper;
}


[HttpGet]
public async Task<IEnumerable<CategoryResource>> GetAllAsync()
{
var categories = await _categoryService.ListAsync();
var resources = _mapper.Map<IEnumerable<Category>, IEnumerable<CategoryResource>>(categories);

return resources;
}
}
}

我更改了构造函数以接收IMapper实现的实例。您可以使用这些接口方法来使用AutoMapper映射方法。
我还更改了GetAllAsync使用Map方法将类别枚举映射到资源枚举的方法。此方法接收我们要映射的类或集合的实例,并通过通用类型定义定义必须映射到什么类型的类或集合。

注意,我们只需将新的依赖项(IMapper)注入构造函数,就可以轻松地更改实现,而不必修改服务类或仓储。

依赖注入使您的应用程序可维护且易于更改,因为您不必中断所有代码实现即可添加或删除功能。

您可能意识到,不仅控制器类,而且所有接收依赖项的类(包括依赖项本身)都会根据绑定配置自动解析为接收正确的类。

依赖注入如此的Amazing,不是吗?

图片

现在,使用dotnet run命令再次启动API,然后转到http://localhost:5000/api/categories以查看新的JSON响应。

图片

这是您应该看到的响应数据

我们已经有了GET端点。现在,让我们为POST(创建)类别创建一个新端点。

第9步-创建新类别

在处理资源创建时,我们必须关心很多事情,例如:

  • 数据验证和数据完整性;
  • 授权创建资源;
  • 错误处理;
  • 正在记录。

在本教程中,我不会显示如何处理身份验证和授权,但是您可以阅读JSON Web令牌身份验证教程,了解如何轻松实现这些功能。

另外,有一个非常流行的框架称为ASP.NET Identity,该框架提供了有关安全性和用户注册的内置解决方案,您可以在应用程序中使用它们。它包括与EF Core配合使用的提供程序,例如IdentityDbContext可以使用的内置程序。您可以在此处了解更多信息。

让我们编写一个HTTP POST端点,该端点将涵盖其他场景(日志记录除外,它可以根据不同的范围和工具进行更改)。

在创建新端点之前,我们需要一个新资源。此资源会将客户端应用程序发送到此端点的数据(在本例中为类别名称)映射到我们应用程序的类。

由于我们正在创建一个新类别,因此我们还没有ID,这意味着我们需要一种资源来表示仅包含其名称的类别。

在Resources文件夹中,添加一个新类SaveCategoryResource:

1
2
3
4
5
6
7
8
9
10
11
12
using System.ComponentModel.DataAnnotations;


namespace Supermarket.API.Resources
{
public class SaveCategoryResource
{
[Required]
[MaxLength(30)]
public string Name { get; set; }
}
}

注意Name属性上的Required和MaxLength特性。这些属性称为数据注释。ASP.NET Core管道使用此元数据来验证请求和响应。顾名思义,类别名称是必填项,最大长度为30个字符。
现在,让我们定义新API端点的形状。将以下代码添加到类别控制器:

1
2
3
4
[HttpPost]
public async Task<IActionResult> PostAsync([FromBody] SaveCategoryResource resource)
{
}

我们使用HttpPost特性告诉框架这是一个HTTP POST端点。
注意此方法的响应类型Task。控制器类中存在的方法称为动作,它们具有此签名,因为在应用程序执行动作之后,我们可以返回一个以上的可能结果。

在这种情况下,如果类别名称无效或出现问题,我们必须返回400代码(错误请求)响应,该响应通常包含一条错误消息,客户端应用程序可以使用该错误消息来解决该问题,或者我们可以如果一切正常,则对数据进行200次响应(成功)。

可以将多种类型的操作类型用作响应,但是通常,我们可以使用此接口,并且ASP.NET Core将为此使用默认类。

该FromBody属性告诉ASP.NET Core将请求正文数据解析为我们的新资源类。这意味着当包含类别名称的JSON发送到我们的应用程序时,框架将自动将其解析为我们的新类。

现在,让我们实现路由逻辑。我们必须遵循一些步骤才能成功创建新类别:

  • 首先,我们必须验证传入的请求。如果请求无效,我们必须返回包含错误消息的错误请求响应;
  • 然后,如果请求有效,则必须使用AutoMapper将新资源映射到类别模型类。
  • 现在,我们需要调用我们的服务,告诉它保存我们的新类别。如果执行保存逻辑没有问题,它将返回一个包含我们新类别数据的响应。如果没有,它应该给我们一个指示,表明该过程失败了,并可能出现错误消息。
  • 最后,如果有错误,我们将返回错误的请求。如果没有,我们将新的类别模型映射到类别资源,并向客户端返回包含新类别数据的成功响应。

这似乎很复杂,但是使用为API构建的服务架构来实现此逻辑确实很容易。

让我们开始验证传入的请求。

步骤10-使用模型状态验证请求主体

ASP.NET Core控制器具有名为ModelState的属性。在执行我们的操作之前,该属性在请求执行期间填充。它是ModelStateDictionary的实例,该类包含诸如请求是否有效以及潜在的验证错误消息之类的信息。

如下更改端点代码:

1
2
3
4
5
6
[HttpPost]
public async Task<IActionResult> PostAsync([FromBody] SaveCategoryResource resource)
{
if (!ModelState.IsValid)
return BadRequest(ModelState.GetErrorMessages());
}

这段代码检查模型状态(在这种情况下为请求正文中发送的数据)是否无效,并检查我们的数据注释。如果不是,则API返回错误的请求(状态代码400),以及我们的注释元数据提供的默认错误消息。
该ModelState.GetErrorMessages()方法尚未实现。这是一种扩展方法(一种扩展现有类或接口功能的方法),我将实现该方法将验证错误转换为简单的字符串以返回给客户端。

Extensions在我们的API的根目录中添加一个新文件夹,然后添加一个新类ModelStateExtensions。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
using System.Collections.Generic;
using System.Linq;
using Microsoft.AspNetCore.Mvc.ModelBinding;


namespace Supermarket.API.Extensions
{
public static class ModelStateExtensions
{
public static List<string> GetErrorMessages(this ModelStateDictionary dictionary)
{
return dictionary.SelectMany(m => m.Value.Errors)
.Select(m => m.ErrorMessage)
.ToList();
}
}
}

所有扩展方法以及声明它们的类都应该是静态的。** **这意味着它们不处理特定的实例数据,并且在应用程序启动时仅被加载一次。
this参数声明前面的关键字告诉C#编译器将其视为扩展方法。结果是我们可以像此类的常规方法一样调用它,因为我们在要使用扩展的地方包含的特定的using代码。

该扩展使用LINQ查询,这是.NET的非常有用的功能,它使我们能够使用链式语法来查询和转换数据。此处的表达式将验证错误方法转换为包含错误消息的字符串列表。

Supermarket.API.Extensions在进行下一步之前,将名称空间导入Categories控制器。

1
using Supermarket.API.Extensions;

让我们通过将新资源映射到类别模型类来继续实现端点逻辑。

步骤11-映射新资源

我们已经定义了映射配置文件,可以将模型转换为资源。现在,我们需要一个与之相反的新配置项。

ResourceToModelProfile在Mapping文件夹中添加一个新类:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
using AutoMapper;
using Supermarket.API.Domain.Models;
using Supermarket.API.Resources;


namespace Supermarket.API.Mapping
{
public class ResourceToModelProfile : Profile
{
public ResourceToModelProfile()
{
CreateMap<SaveCategoryResource, Category>();
}
}
}

这里没有新内容。由于依赖注入的魔力,AutoMapper将在应用程序启动时自动注册此配置文件,而我们无需更改任何其他位置即可使用它。
现在,我们可以将新资源映射到相应的模型类:

1
2
3
4
5
6
7
8
9
[HttpPost]
public async Task<IActionResult> PostAsync([FromBody] SaveCategoryResource resource)
{
if (!ModelState.IsValid)
return BadRequest(ModelState.GetErrorMessages());


var category = _mapper.Map<SaveCategoryResource, Category>(resource);
}

第12步-应用请求-响应模式来处理保存逻辑

现在我们必须实现最有趣的逻辑:保存一个新类别。我们希望我们的服务能够做到。

由于连接到数据库时出现问题,或者由于任何内部业务规则使我们的数据无效,因此保存逻辑可能会失败。

如果出现问题,我们不能简单地抛出一个错误,因为它可能会停止API,并且客户端应用程序也不知道如何处理该问题。另外,我们可能会有某种日志记录机制来记录错误。

保存方法的约定(即方法的签名和响应类型)需要指示我们是否正确执行了该过程。如果处理正常,我们将接收类别数据。如果没有,我们至少必须收到一条错误消息,告诉您该过程失败的原因。

我们可以通过应用request-response模式来实现此功能。这种企业设计模式将我们的请求和响应参数封装到类中,以封装我们的服务将用于处理某些任务并将信息返回给正在使用该服务的类的信息。

这种模式为我们提供了一些优势,例如:

  • 如果我们需要更改服务以接收更多参数,则不必破坏其签名;
  • 我们可以为我们的请求和/或响应定义标准合同;
  • 我们可以在不停止应用程序流程的情况下处理业务逻辑和潜在的失败,并且我们不需要使用大量的try-catch块。

让我们为处理数据更改的服务方法创建一个标准响应类型。对于这种类型的每个请求,我们都想知道该请求是否被正确执行。如果失败,我们要向客户端返回错误消息。

在Domain文件夹的内部Services,添加一个名为的新目录Communication。在此处添加一个名为的新类BaseResponse。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
namespace Supermarket.API.Domain.Services.Communication
{
public abstract class BaseResponse
{
public bool Success { get; protected set; }
public string Message { get; protected set; }


public BaseResponse(bool success, string message)
{
Success = success;
Message = message;
}
}
}

那是我们的响应类型将继承的抽象类。
抽象定义了一个Success属性和一个Message属性,该属性将告知请求是否已成功完成,如果失败,该属性将显示错误消息。

请注意,这些属性是必需的,只有继承的类才能设置此数据,因为子类必须通过构造函数传递此信息。

提示:为所有内容定义基类不是一个好习惯,因为基类会耦合您的代码并阻止您轻松对其进行修改。优先使用组合而不是继承。

在此API的范围内,使用基类并不是真正的问题,因为我们的服务不会增长太多。如果您意识到服务或应用程序会经常增长和更改,请避免使用基类。

现在,在同一文件夹中,添加一个名为的新类SaveCategoryResponse。

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
using Supermarket.API.Domain.Models;


namespace Supermarket.API.Domain.Services.Communication
{
public class SaveCategoryResponse : BaseResponse
{
public Category Category { get; private set; }


private SaveCategoryResponse(bool success, string message, Category category) : base(success, message)
{
Category = category;
}


/// <summary>
/// Creates a success response.
/// </summary>
/// <param name="category">Saved category.</param>
/// <returns>Response.</returns>
public SaveCategoryResponse(Category category) : this(true, string.Empty, category)
{ }


/// <summary>
/// Creates am error response.
/// </summary>
/// <param name="message">Error message.</param>
/// <returns>Response.</returns>
public SaveCategoryResponse(string message) : this(false, message, null)
{ }
}
}

响应类型还设置了一个Category属性,如果请求成功完成,该属性将包含我们的类别数据。
请注意,我为此类定义了三种不同的构造函数:

  • 一个私有的,它将把成功和消息参数传递给基类,并设置Category属性。
  • 仅接收类别作为参数的构造函数。这将创建一个成功的响应,调用私有构造函数来设置各自的属性;
  • 第三个构造函数仅指定消息。这将用于创建故障响应。

因为C#支持多个构造函数,所以我们仅通过使用不同的构造函数就简化了响应的创建过程,而无需定义其他方法来处理此问题。

现在,我们可以更改服务界面以添加新的保存方法合同。

更改ICategoryService接口,如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
using System.Collections.Generic;
using System.Threading.Tasks;
using Supermarket.API.Domain.Models;
using Supermarket.API.Domain.Services.Communication;


namespace Supermarket.API.Domain.Services
{
public interface ICategoryService
{
Task<IEnumerable<Category>> ListAsync();
Task<SaveCategoryResponse> SaveAsync(Category category);
}
}

我们只需将类别传递给此方法,它将处理保存模型数据,编排仓储和其他必要服务所需的所有逻辑。
请注意,由于我们不需要任何其他参数来执行此任务,因此我不在此处创建特定的请求类。计算机编程中有一个名为KISS的概念 —Keep It Simple,Stupid的简称。基本上,它说您应该使您的应用程序尽可能简单。

设计应用程序时请记住这一点:仅应用解决问题所需的内容。不要过度设计您的应用程序。

现在我们可以完成端点逻辑:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
[HttpPost]
public async Task<IActionResult> PostAsync([FromBody] SaveCategoryResource resource)
{
if (!ModelState.IsValid)
return BadRequest(ModelState.GetErrorMessages());


var category = _mapper.Map<SaveCategoryResource, Category>(resource);
var result = await _categoryService.SaveAsync(category);


if (!result.Success)
return BadRequest(result.Message);


var categoryResource = _mapper.Map<Category, CategoryResource>(result.Category);
return Ok(categoryResource);
}

在验证请求数据并将资源映射到我们的模型之后,我们将其传递给我们的服务以保留数据。
如果失败,则API返回错误的请求。如果没有,API会将新类别(现在包括诸如new的数据Id)映射到我们先前创建的类别CategoryResource,并将其发送给客户端。

现在,让我们为服务实现真正的逻辑。

第13步—数据库逻辑和工作单元模式

由于我们要将数据持久化到数据库中,因此我们需要在仓储中使用一种新方法。

向ICategoryRepository接口添加AddAsync新方法:

1
2
3
4
5
public interface ICategoryRepository
{
Task<IEnumerable<Category>> ListAsync();
Task AddAsync(Category category);
}

现在,让我们在真正的仓储类中实现此方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public class CategoryRepository : BaseRepository, ICategoryRepository
{
public CategoryRepository(AppDbContext context) : base(context)
{ }


public async Task<IEnumerable<Category>> ListAsync()
{
return await _context.Categories.ToListAsync();
}


public async Task AddAsync(Category category)
{
await _context.Categories.AddAsync(category);
}
}

在这里,我们只是在集合中添加一个新类别。
当我们向中添加类时DBSet<>,EF Core将开始跟踪模型发生的所有更改,并在当前状态下使用此数据生成将插入,更新或删除模型的查询。

当前的实现只是将模型添加到我们的集合中,但是我们的数据仍然不会保存。

在上下文类中提供了SaveChanges的方法,我们必须调用该方法才能真正将查询执行到数据库中。我之所以没有在这里调用它,是因为仓储不应该持久化数据,它只是一种内存集合对象。

即使在经验丰富的.NET开发人员之间,该主题也引起很大争议,但是让我向您解释为什么您不应该在仓储类中调用SaveChanges方法。

我们可以从概念上将仓储像.NET框架中存在的任何其他集合一样。在.NET(和许多其他编程语言,例如Javascript和Java)中处理集合时,通常可以:

  • 向其中添加新项(例如,当您将数据推送到列表,数组和字典时);
  • 查找或过滤项目;
  • 从集合中删除一个项目;
  • 替换给定的项目,或更新它。

想一想现实世界中的清单。想象一下,您正在编写一份购物清单以在超市购买东西(巧合,不是吗?)。

在列表中,写下您需要购买的所有水果。您可以将水果添加到此列表中,如果放弃购买就删除水果,也可以替换水果的名称。但是您无法将水果保存到列表中。用简单的英语说这样的话是没有意义的。

提示:在使用面向对象的编程语言设计类和接口时,请尝试使用自然语言来检查您所做的工作是否正确。

例如,说人实现了person的接口是有道理的,但是说一个人实现了一个帐户却没有道理。

如果您要“保存”水果清单(在这种情况下,要购买所有水果),请付款,然后超市会处理库存数据以检查他们是否必须从供应商处购买更多水果。

编程时可以应用相同的逻辑。仓储不应保存,更新或删除数据。相反,他们应该将其委托给其他类来处理此逻辑。

将数据直接保存到仓储中时,还有另一个问题:您不能使用transaction。

想象一下,我们的应用程序具有一种日志记录机制,该机制存储一些用户名,并且每次对API数据进行更改时都会执行操作。

现在想象一下,由于某种原因,您调用了一个更新用户名的服务(这是不常见的情况,但让我们考虑一下)。

您同意要更改虚拟用户表中的用户名,首先必须更新所有日志以正确告诉谁执行了该操作,对吗?

现在想象我们已经为用户和不同仓储中的日志实现了update方法,它们都调用了SaveChanges。如果这些方法之一在更新过程中失败,会发生什么?最终会导致数据不一致。

只有在一切完成之后,我们才应该将更改保存到数据库中。为此,我们必须使用transaction,这基本上是大多数数据库实现的功能,只有在完成复杂的操作后才能保存数据。

“-好的,所以如果我们不能在这里保存东西,我们应该在哪里做?”

处理此问题的常见模式是工作单元模式。此模式包含一个类,该类将我们的AppDbContext实例作为依赖项接收,并公开用于开始,完成或中止事务的方法。

在这里,我们将使用工作单元的简单实现来解决我们的问题。

Repositories在Domain层的仓储文件夹Repositories内添加一个新接口IUnitOfWork:

1
2
3
4
5
6
7
8
9
10
using System.Threading.Tasks;


namespace Supermarket.API.Domain.Repositories
{
public interface IUnitOfWork
{
Task CompleteAsync();
}
}

如您所见,它仅公开一种将异步完成数据管理操作的方法。
现在让我们添加实际的实现。

在Persistence层RepositoriesRepositories文件夹中的添加一个名为的UnitOfWork的新类:

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.Threading.Tasks;
using Supermarket.API.Domain.Repositories;
using Supermarket.API.Persistence.Contexts;


namespace Supermarket.API.Persistence.Repositories
{
public class UnitOfWork : IUnitOfWork
{
private readonly AppDbContext _context;


public UnitOfWork(AppDbContext context)
{
_context = context;
}


public async Task CompleteAsync()
{
await _context.SaveChangesAsync();
}
}
}

这是一个简单,干净的实现,仅在使用仓储修改完所有更改后,才将所有更改保存到数据库中。
如果研究工作单元模式的实现,则会发现实现回滚操作的更复杂的模式。

由于EF Core已经在后台实现了仓储模式和工作单元,因此我们不必在意回滚方法。

“ - 什么?那么为什么我们必须创建所有这些接口和类?”

将持久性逻辑与业务规则分开在代码可重用性和维护方面具有许多优势。如果直接使用EF Core,我们最终将拥有更复杂的类,这些类将很难更改。

想象一下,将来您决定将ORM框架更改为其他框架,例如Dapper,或者由于性能而必须实施纯SQL查询。如果将查询逻辑与服务耦合在一起,将很难更改该逻辑,因为您必须在许多类中进行此操作。

使用仓储模式,您可以简单地实现一个新的仓储类并使用依赖注入将其绑定。

因此,基本上,如果您直接在服务中使用EF Core,并且必须进行一些更改,那么您将获得:

就像我说的那样,EF Core在后台实现了工作单元和仓储模式。我们可以将DbSet<>属性视为仓储。而且,SaveChanges仅在所有数据库操作成功的情况下才保留数据。

现在,您知道什么是工作单元以及为什么将其与仓储一起使用,让我们实现真实服务的逻辑。

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
public class CategoryService : ICategoryService
{
private readonly ICategoryRepository _categoryRepository;
private readonly IUnitOfWork _unitOfWork;


public CategoryService(ICategoryRepository categoryRepository, IUnitOfWork unitOfWork)
{
_categoryRepository = categoryRepository;
_unitOfWork = unitOfWork;
}


public async Task<IEnumerable<Category>> ListAsync()
{
return await _categoryRepository.ListAsync();
}


public async Task<SaveCategoryResponse> SaveAsync(Category category)
{
try
{
await _categoryRepository.AddAsync(category);
await _unitOfWork.CompleteAsync();

return new SaveCategoryResponse(category);
}
catch (Exception ex)
{
// Do some logging stuff
return new SaveCategoryResponse($"An error occurred when saving the category: {ex.Message}");
}
}
}

多亏了我们的解耦架构,我们可以简单地将实例UnitOfWork作为此类的依赖传递。
我们的业务逻辑非常简单。

首先,我们尝试将新类别添加到数据库中,然后API尝试保存新类别,将所有内容包装在try-catch块中。

如果失败,则API会调用一些虚构的日志记录服务,并返回指示失败的响应。

如果该过程顺利完成,则应用程序将返回成功响应,并发送我们的类别数据。简单吧?

提示:在现实世界的应用程序中,您不应将所有内容包装在通用的try-catch块中,而应分别处理所有可能的错误。

简单地添加一个try-catch块并不能解决大多数可能的失败情况。请确保正确实现错误处理。

测试我们的API之前的最后一步是将工作单元接口绑定到其各自的类。

将此新行添加到类的ConfigureServices方法中Startup:

1
services.AddScoped<IUnitOfWork, UnitOfWork>();

现在让我们测试一下!
第14步-使用Postman测试我们的POST端点

重新启动我们的应用程序dotnet run。

我们无法使用浏览器测试POST端点。让我们使用Postman测试我们的端点。这是测试RESTful API的非常有用的工具。

打开Postman,然后关闭介绍性消息。您会看到这样的屏幕:

图片

屏幕显示测试端点的选项

GET默认情况下,将所选内容更改为选择框POST。

在Enter request URL字段中输入API地址。

我们必须提供请求正文数据以发送到我们的API。单击Body菜单项,然后将其下方显示的选项更改为raw。

Postman将在右侧显示一个Text选项,将其更改为JSON (application/json)并粘贴以下JSON数据:

1
2
3
{
"name": ""
}

图片发送请求前的屏幕

如您所见,我们将向我们的新端点发送一个空的名称字符串。

点击Send按钮。您将收到如下输出:

图片

如您所见,我们的验证逻辑有效!

您还记得我们为端点创建的验证逻辑吗?此输出是它起作用的证明!

还要注意右侧显示的400状态代码。该BadRequest结果自动将此状态码的响应。

现在,让我们将JSON数据更改为有效数据,以查看新的响应:

图片

最后,我们期望得到的结果

API正确创建了我们的新资源。

到目前为止,我们的API可以列出和创建类别。您学到了很多有关C#语言,ASP.NET Core框架以及构造API的通用设计方法的知识。

让我们继续我们的类别API,创建用于更新类别的端点。

从现在开始,由于我向您解释了大多数概念,因此我将加快解释速度,并专注于新主题,以免浪费您的时间。 Let’s go!

第15步-更新类别

要更新类别,我们需要一个HTTP PUT端点。

我们必须编写的逻辑与POST逻辑非常相似:

  • 首先,我们必须使用来验证传入的请求ModelState。
  • 如果请求有效,则API应使用AutoMapper将传入资源映射到模型类。
  • 然后,我们需要调用我们的服务,告诉它更新类别,提供相应的类别Id和更新的数据;
  • 如果Id数据库中没有给定的类别,我们将返回错误的请求。我们可以使用NotFound结果来代替,但是对于这个范围而言,这并不重要,因为我们向客户端应用程序提供了错误消息。
  • 如果正确执行了保存逻辑,则服务必须返回包含更新的类别数据的响应。如果不是,它应该给我们指示该过程失败,并显示一条消息指示原因;
  • 最后,如果有错误,则API返回错误的请求。如果不是,它将更新的类别模型映射到类别资源,并将成功响应返回给客户端应用程序。

让我们将新PutAsync方法添加到控制器类中:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
[HttpPut("{id}")]
public async Task<IActionResult> PutAsync(int id, [FromBody] SaveCategoryResource resource)
{
if (!ModelState.IsValid)
return BadRequest(ModelState.GetErrorMessages());


var category = _mapper.Map<SaveCategoryResource, Category>(resource);
var result = await _categoryService.UpdateAsync(id, category);


if (!result.Success)
return BadRequest(result.Message);


var categoryResource = _mapper.Map<Category, CategoryResource>(result.Category);
return Ok(categoryResource);
}

如果将其与POST逻辑进行比较,您会注意到这里只有一个区别:HttPut属性指定给定路由应接收的参数。
我们将调用此端点,将类别指定Id 为最后一个URL片段,例如/api/categories/1。ASP.NET Core管道将此片段解析为相同名称的参数。

现在我们必须UpdateAsync在ICategoryService接口中定义方法签名:

1
2
3
4
5
6
public interface ICategoryService
{
Task<IEnumerable<Category>> ListAsync();
Task<SaveCategoryResponse> SaveAsync(Category category);
Task<SaveCategoryResponse> UpdateAsync(int id, Category category);
}

现在让我们转向真正的逻辑。

第16步-更新逻辑

首先,要更新类别,我们需要从数据库中返回当前数据(如果存在)。我们还需要将其更新到我们的中DBSet<>。

让我们在ICategoryService界面中添加两个新的方法约定:

1
2
3
4
5
6
7
public interface ICategoryRepository
{
Task<IEnumerable<Category>> ListAsync();
Task AddAsync(Category category);
Task<Category> FindByIdAsync(int id);
void Update(Category category);
}

我们已经定义了FindByIdAsync方法,该方法将从数据库中异步返回一个类别,以及该Update方法。请注意,该Update方法不是异步的,因为EF Core API不需要异步方法来更新模型。
现在,让我们在CategoryRepository类中实现真正的逻辑:

1
2
3
4
5
6
7
8
9
10
public async Task<Category> FindByIdAsync(int id)
{
return await _context.Categories.FindAsync(id);
}


public void Update(Category category)
{
_context.Categories.Update(category);
}

最后,我们可以对服务逻辑进行编码:

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 async Task<SaveCategoryResponse> UpdateAsync(int id, Category category)
{
var existingCategory = await _categoryRepository.FindByIdAsync(id);


if (existingCategory == null)
return new SaveCategoryResponse("Category not found.");


existingCategory.Name = category.Name;


try
{
_categoryRepository.Update(existingCategory);
await _unitOfWork.CompleteAsync();


return new SaveCategoryResponse(existingCategory);
}
catch (Exception ex)
{
// Do some logging stuff
return new SaveCategoryResponse($"An error occurred when updating the category: {ex.Message}");
}
}

API尝试从数据库中获取类别。如果结果为null,我们将返回一个响应,告知该类别不存在。如果类别存在,我们需要设置其新名称。
然后,API会尝试保存更改,例如创建新类别时。如果该过程完成,则该服务将返回成功响应。如果不是,则执行日志记录逻辑,并且端点接收包含错误消息的响应。

现在让我们对其进行测试。首先,让我们添加一个新类别Id以使用有效类别。我们可以使用播种到数据库中的类别的标识符,但是我想通过这种方式向您展示我们的API将更新正确的资源。

再次运行该应用程序,然后使用Postman将新类别发布到数据库中:

图片

添加新类别以供日后更新

使用一个可用的数据Id,将POST 选项更改PUT为选择框,然后在URL的末尾添加ID值。将name属性更改为其他名称,然后发送请求以检查结果:

图片

类别数据已成功更新

您可以将GET请求发送到API端点,以确保您正确编辑了类别名称:

图片

那是现在GET请求的结果

我们必须对类别执行的最后一项操作是排除类别。让我们创建一个HTTP Delete端点。

第17步-删除类别

删除类别的逻辑确实很容易实现,因为我们所需的大多数方法都是先前构建的。

这些是我们工作路线的必要步骤:

  • API需要调用我们的服务,告诉它删除我们的类别,并提供相应的Id;
  • 如果数据库中没有具有给定ID的类别,则该服务应返回一条消息指出该类别;
  • 如果执行删除逻辑没有问题,则服务应返回包含我们已删除类别数据的响应。如果没有,它应该给我们一个指示,表明该过程失败了,并可能出现错误消息。
  • 最后,如果有错误,则API返回错误的请求。如果不是,则API会将更新的类别映射到资源,并向客户端返回成功响应。

让我们开始添加新的端点逻辑:

1
2
3
4
5
6
7
8
9
10
11
12
13
[HttpDelete("{id}")]
public async Task<IActionResult> DeleteAsync(int id)
{
var result = await _categoryService.DeleteAsync(id);


if (!result.Success)
return BadRequest(result.Message);


var categoryResource = _mapper.Map<Category, CategoryResource>(result.Category);
return Ok(categoryResource);
}

该HttpDelete属性还定义了一个id 模板。
在将DeleteAsync签名添加到我们的ICategoryService接口之前,我们需要做一些小的重构。

新的服务方法必须返回包含类别数据的响应,就像对PostAsyncand UpdateAsync方法所做的一样。我们可以SaveCategoryResponse为此目的重用,但在这种情况下我们不会保存数据。

为了避免创建具有相同形状的新类来满足此要求,我们可以将我们重命名SaveCategoryResponse为CategoryResponse。

如果您使用的是Visual Studio Code,则可以打开SaveCategoryResponse类,将鼠标光标放在类名上方,然后使用选项Change All Occurrences* *来重命名该类:

图片

确保也重命名文件名。

让我们将DeleteAsync方法签名添加到ICategoryService 接口中:

1
2
3
4
5
6
7
public interface ICategoryService
{
Task<IEnumerable<Category>> ListAsync();
Task<CategoryResponse> SaveAsync(Category category);
Task<CategoryResponse> UpdateAsync(int id, Category category);
Task<CategoryResponse> DeleteAsync(int id);
}

在实施删除逻辑之前,我们需要在仓储中使用一种新方法。
将Remove方法签名添加到ICategoryRepository接口:

1
void Remove(Category category);

现在,在仓储类上添加真正的实现:

1
2
3
4
public void Remove(Category category)
{
_context.Categories.Remove(category);
}

EF Core要求将模型的实例传递给Remove方法,以正确了解我们要删除的模型,而不是简单地传递Id。
最后,让我们在CategoryService类上实现逻辑:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
public async Task<CategoryResponse> DeleteAsync(int id)
{
var existingCategory = await _categoryRepository.FindByIdAsync(id);


if (existingCategory == null)
return new CategoryResponse("Category not found.");


try
{
_categoryRepository.Remove(existingCategory);
await _unitOfWork.CompleteAsync();


return new CategoryResponse(existingCategory);
}
catch (Exception ex)
{
// Do some logging stuff
return new CategoryResponse($"An error occurred when deleting the category: {ex.Message}");
}
}

这里没有新内容。该服务尝试通过ID查找类别,然后调用我们的仓储以删除类别。最后,工作单元完成将实际操作执行到数据库中的事务。
“-嘿,但是每个类别的产品呢?为避免出现错误,您是否不需要先创建仓储并删除产品?”

答案是否定的。借助EF Core跟踪机制,当我们从数据库中加载模型时,框架便知道了该模型具有哪些关系。如果我们删除它,EF Core知道它应该首先递归删除所有相关模型。

在将类映射到数据库表时,我们可以禁用此功能,但这在本教程的范围之外。如果您想了解此功能,请看这里。

现在是时候测试我们的新端点了。再次运行该应用程序,并使用Postman发送DELETE请求,如下所示:

图片

如您所见,API毫无问题地删除了现有类别

我们可以通过发送GET请求来检查我们的API是否正常工作:

图片我们已经完成了类别API。现在是时候转向产品API。

步骤18-产品API

到目前为止,您已经学习了如何实现所有基本的HTTP动词来使用ASP.NET Core处理CRUD操作。让我们进入实现产品API的下一个层次。

我将不再详细介绍所有HTTP动词,因为这将是详尽无遗的。在本教程的最后一部分,我将仅介绍GET请求,以向您展示在从数据库查询数据时如何包括相关实体,以及如何使用Description我们为EUnitOfMeasurement 枚举值定义的属性。

将新控制器ProductsController添加到名为Controllers的文件夹中。

在这里编写任何代码之前,我们必须创建产品资源。

让我刷新您的记忆,再次显示我们的资源应如何:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
{
[
{
"id": 1,
"name": "Sugar",
"quantityInPackage": 1,
"unitOfMeasurement": "KG"
"category": {
"id": 3,
"name": "Sugar"
}
},
… // Other products
]
}

我们想要一个包含数据库中所有产品的JSON数组。
JSON数据与产品模型有两点不同:

  • 测量单位以较短的方式显示,仅显示其缩写。
  • 我们输出类别数据而不包括CategoryId属性。

为了表示度量单位,我们可以使用简单的字符串属性代替枚举类型(顺便说一下,我们没有JSON数据的默认枚举类型,因此我们必须将其转换为其他类型)。

现在,我们现在要塑造新资源,让我们创建它。ProductResource在Resources文件夹中添加一个新类:

1
2
3
4
5
6
7
8
9
10
11
namespace Supermarket.API.Resources
{
public class ProductResource
{
public int Id { get; set; }
public string Name { get; set; }
public int QuantityInPackage { get; set; }
public string UnitOfMeasurement { get; set; }
public CategoryResource Category {get;set;}
}
}

现在,我们必须配置模型类和新资源类之间的映射。
映射配置将与用于其他映射的配置几乎相同,但是在这里,我们必须处理将EUnitOfMeasurement枚举转换为字符串的操作。

您还记得StringValue应用于枚举类型的属性吗?现在,我将向您展示如何使用.NET框架的强大功能:反射 API提取此信息。

反射 API是一组强大的资源工具集,可让我们提取和操作元数据。许多框架和库(包括ASP.NET Core本身)都利用这些资源来处理许多后台工作。

现在让我们看看它在实践中是如何工作的。将新类添加到Extensions名为的文件夹中EnumExtensions。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
using System.ComponentModel;
using System.Reflection;


namespace Supermarket.API.Extensions
{
public static class EnumExtensions
{
public static string ToDescriptionString<TEnum>(this TEnum @enum)
{
FieldInfo info = @enum.GetType().GetField(@enum.ToString());
var attributes = (DescriptionAttribute[])info.GetCustomAttributes(typeof(DescriptionAttribute), false);


return attributes?[0].Description ?? @enum.ToString();
}
}
}

第一次看代码可能会让人感到恐惧,但这并不复杂。让我们分解代码定义以了解其工作原理。
首先,我们定义了一种通用方法(一种方法,该方法可以接收不止一种类型的参数,在这种情况下,该方法由TEnum声明表示),该方法接收给定的枚举作为参数。

由于enum是C#中的保留关键字,因此我们在参数名称前面添加了@,以使其成为有效名称。

该方法的第一步是使用该方法获取参数的类型信息(类,接口,枚举或结构定义)GetType。

然后,该方法使用来获取特定的枚举值(例如Kilogram)GetField(@enum.ToString())。

下一行找到Description应用于枚举值的所有属性,并将其数据存储到数组中(在某些情况下,我们可以为同一属性指定多个属性)。

最后一行使用较短的语法来检查我们是否至少有一个枚举类型的描述属性。如果有,我们将返回Description此属性提供的值。如果不是,我们使用默认的强制类型转换将枚举作为字符串返回。

?.操作者(零条件运算)检查该值是否null访问其属性之前。

??运算符(空合并运算符)告诉应用程序在左边的返回值,如果它不为空,或者在正确的,否则价值。

现在我们有了扩展方法来提取描述,让我们配置模型和资源之间的映射。多亏了AutoMapper,我们只需要多一行就可以做到这一点。

打开ModelToResourceProfile类并通过以下方式更改代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
using AutoMapper;
using Supermarket.API.Domain.Models;
using Supermarket.API.Extensions;
using Supermarket.API.Resources;


namespace Supermarket.API.Mapping
{
public class ModelToResourceProfile : Profile
{
public ModelToResourceProfile()
{
CreateMap<Category, CategoryResource>();


CreateMap<Product, ProductResource>()
.ForMember(src => src.UnitOfMeasurement,
opt => opt.MapFrom(src => src.UnitOfMeasurement.ToDescriptionString()));
}
}
}

此语法告诉AutoMapper使用新的扩展方法将我们的EUnitOfMeasurement值转换为包含其描述的字符串。简单吧?您可以阅读官方文档以了解完整语法。
注意,我们尚未为category属性定义任何映射配置。因为我们之前为类别配置了映射,并且由于产品模型具有相同类型和名称的category属性,所以AutoMapper隐式知道应该使用各自的配置来映射它。

现在,我们添加端点代码。更改ProductsController代码:

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
using System.Collections.Generic;
using System.Threading.Tasks;
using AutoMapper;
using Microsoft.AspNetCore.Mvc;
using Supermarket.API.Domain.Models;
using Supermarket.API.Domain.Services;
using Supermarket.API.Resources;


namespace Supermarket.API.Controllers
{
[Route("/api/[controller]")]
public class ProductsController : Controller
{
private readonly IProductService _productService;
private readonly IMapper _mapper;


public ProductsController(IProductService productService, IMapper mapper)
{
_productService = productService;
_mapper = mapper;
}


[HttpGet]
public async Task<IEnumerable<ProductResource>> ListAsync()
{
var products = await _productService.ListAsync();
var resources = _mapper.Map<IEnumerable<Product>, IEnumerable<ProductResource>>(products);
return resources;
}
}
}

基本上,为类别控制器定义的结构相同。
让我们进入服务部分。将一个新IProductService接口添加到Domain层中的Services文件夹中:

1
2
3
4
5
6
7
8
9
10
11
12
using System.Collections.Generic;
using System.Threading.Tasks;
using Supermarket.API.Domain.Models;


namespace Supermarket.API.Domain.Services
{
public interface IProductService
{
Task<IEnumerable<Product>> ListAsync();
}
}

您应该已经意识到,在真正实现新服务之前,我们需要一个仓储。
IProductRepository在相应的文件夹中添加一个名为的新接口:

1
2
3
4
5
6
7
8
9
10
11
12
using System.Collections.Generic;
using System.Threading.Tasks;
using Supermarket.API.Domain.Models;


namespace Supermarket.API.Domain.Repositories
{
public interface IProductRepository
{
Task<IEnumerable<Product>> ListAsync();
}
}

现在,我们实现仓储。除了必须在查询数据时返回每个产品的相应类别数据外,我们几乎必须像对类别仓储一样实现。
默认情况下,EF Core在查询数据时不包括与模型相关的实体,因为它可能非常慢(想象一个具有十个相关实体的模型,所有相关实体都有自己的关系)。

要包括类别数据,我们只需要多一行:

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.Collections.Generic;
using System.Threading.Tasks;
using Microsoft.EntityFrameworkCore;
using Supermarket.API.Domain.Models;
using Supermarket.API.Domain.Repositories;
using Supermarket.API.Persistence.Contexts;


namespace Supermarket.API.Persistence.Repositories
{
public class ProductRepository : BaseRepository, IProductRepository
{
public ProductRepository(AppDbContext context) : base(context)
{
}


public async Task<IEnumerable<Product>> ListAsync()
{
return await _context.Products.Include(p => p.Category)
.ToListAsync();
}
}
}

请注意对的调用Include(p => p.Category)。我们可以链接此语法,以在查询数据时包含尽可能多的实体。执行选择时,EF Core会将其转换为联接。
现在,我们可以ProductService像处理类别一样实现类:

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.Collections.Generic;
using System.Threading.Tasks;
using Supermarket.API.Domain.Models;
using Supermarket.API.Domain.Repositories;
using Supermarket.API.Domain.Services;


namespace Supermarket.API.Services
{
public class ProductService : IProductService
{
private readonly IProductRepository _productRepository;

public ProductService(IProductRepository productRepository)
{
_productRepository = productRepository;
}


public async Task<IEnumerable<Product>> ListAsync()
{
return await _productRepository.ListAsync();
}
}
}

让我们绑定更改Startup类的新依赖项:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
public void ConfigureServices(IServiceCollection services)
{
services.AddMvc().SetCompatibilityVersion(CompatibilityVersion.Version_2_2);


services.AddDbContext<AppDbContext>(options =>
{
options.UseInMemoryDatabase("supermarket-api-in-memory");
});


services.AddScoped<ICategoryRepository, CategoryRepository>();
services.AddScoped<IProductRepository, ProductRepository>();
services.AddScoped<IUnitOfWork, UnitOfWork>();


services.AddScoped<ICategoryService, CategoryService>();
services.AddScoped<IProductService, ProductService>();


services.AddAutoMapper();
}

最后,在测试API之前,让我们AppDbContext在初始化应用程序时更改类以包括一些产品,以便我们看到结果:

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
protected override void OnModelCreating(ModelBuilder builder)
{
base.OnModelCreating(builder);

builder.Entity<Category>().ToTable("Categories");
builder.Entity<Category>().HasKey(p => p.Id);
builder.Entity<Category>().Property(p => p.Id).IsRequired().ValueGeneratedOnAdd().HasValueGenerator<InMemoryIntegerValueGenerator<int>>();
builder.Entity<Category>().Property(p => p.Name).IsRequired().HasMaxLength(30);
builder.Entity<Category>().HasMany(p => p.Products).WithOne(p => p.Category).HasForeignKey(p => p.CategoryId);


builder.Entity<Category>().HasData
(
new Category { Id = 100, Name = "Fruits and Vegetables" }, // Id set manually due to in-memory provider
new Category { Id = 101, Name = "Dairy" }
);


builder.Entity<Product>().ToTable("Products");
builder.Entity<Product>().HasKey(p => p.Id);
builder.Entity<Product>().Property(p => p.Id).IsRequired().ValueGeneratedOnAdd();
builder.Entity<Product>().Property(p => p.Name).IsRequired().HasMaxLength(50);
builder.Entity<Product>().Property(p => p.QuantityInPackage).IsRequired();
builder.Entity<Product>().Property(p => p.UnitOfMeasurement).IsRequired();


builder.Entity<Product>().HasData
(
new Product
{
Id = 100,
Name = "Apple",
QuantityInPackage = 1,
UnitOfMeasurement = EUnitOfMeasurement.Unity,
CategoryId = 100
},
new Product
{
Id = 101,
Name = "Milk",
QuantityInPackage = 2,
UnitOfMeasurement = EUnitOfMeasurement.Liter,
CategoryId = 101,
}
);
}

我添加了两个虚构产品,将它们与初始化应用程序时我们播种的类别相关联。
该测试了!再次运行API并发送GET请求以/api/products使用Postman:

图片

就是这样!恭喜你!

现在,您将了解如何使用解耦的代码架构使用ASP.NET Core构建RESTful API。您了解了.NET Core框架的许多知识,如何使用C#,EF Core和AutoMapper的基础知识以及在设计应用程序时要使用的许多有用的模式。

您可以检查API的完整实现,包括产品的其他HTTP动词,并检查Github仓储:

evgomes / supermarket-api

使用ASP.NET Core 2.2构建的简单RESTful API,展示了如何使用分离的,可维护的……创建RESTful服务。github.com

结论

ASP.NET Core是创建Web应用程序时使用的出色框架。它带有许多有用的API,可用于构建干净,可维护的应用程序。创建专业应用程序时,可以将其视为一种选择。

本文并未涵盖专业API的所有方面,但您已学习了所有基础知识。您还学到了许多有用的模式,可以解决我们每天面临的模式。

希望您喜欢这篇文章,希望对您有所帮助。期待你的反馈,以便我能进一步提高。

进一步学习的可用参考资料

.NET Core教程-Microsoft文档

ASP.NET Core文档-Microsoft文档

WPF学习路线概述

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

引言

在桌面开发领域,虽然在某些领域,基于electron的跨平台方案能够为我们带来某些便利,但是由于WPF技术能够更好的运用Direct3D带来的性能提升、以及海量Windows操作系统和硬件资源的支持,所以他依然有着得天独厚的优势。

当然,选用一门技术,依然看公司的基因土壤和综合因素或者老板的心血来潮,例如QT也同样是一门非常不错的跨平台图形界面解决方案。

目前我们公司在桌面开发领域广泛应用了WPF技术,主要是使用其作为大屏数据可视化相关的UI呈现,包括一些数据展示效果、动画效果等。由于之前我对WPF仅有三周经验,因此在开发和设计相关功能时,一些简单功能还能勉强完成,稍微复杂一点的就有点费时过长了,因此这篇文章主要梳理自己的学习笔记,以便总结学习成果。

如何学习WPF技术?

在Quote上有人提出了这样一个相同的问题,查看问题,开发者Srikanth Pagadala如是回答:

1、以了解基础控件作为学习的起步过程:这些控件包括TextBox,Button,TextBlock及其他的,理解这些控件对外提供的属性,以及如何使用。
2、了解和使用布局空间:例如Grid、StackPanel、DockerPanel和其他控件,在这一点上,你需要花费大量的时间。同时你需要学会创建复杂的UI设计。
3、了解循环类型的空间,例如ItemControl控件。
4、了解关于模板的概念。包括如何定义包含CheckBox的Combox,同时这个控件还包含了一张图片的按钮,以及如何在ItemsControl中使用不同的模板。
5、理解数据绑定的运行机制。尝试创建一个MVVM或类似类型的应用程序。
6、创建一个典型的控件,探索DependencyProperties(依赖属性)和AttachedProperties(附加属性)。
7、创建一个样式资源,理解如何给控件设计样式。

除此之外,还有其他开发者给出了补充回答:

1、学习控件的数据绑定过程,在DataGrid上实现数据绑定。
2、学习和实现INotifyPropertyChanged类。查看如何实现
3、学习Observable Collection。该类型的集合广泛使用于数据集合绑定方面,同时也提供了数据改变通知的机制。
4、使网格上的列可编辑。用文本控件(用户项目模板)替换列。为每个捕获文本更改事件的列创建一个属性。在文本控件上使用绑定类型。尝试捕获您在后端在网格上所做的更改。
5、成功将数据控件中的文本控件与后端属性绑定后,请在同一页面上创建网格的副本。尝试同步这两个网格。例如,您在第一个网格中所做的每个更改都必须在第二个网格中自动更新。

网站“https://www.wpf-tutorial.com/”是一个专门用于学习WPF的网站,通过这个网站,可以快速的入门WPF。

由于WPF技术已经比较熟悉,所以书籍也比较多,网友推荐来自刘铁猛老师的《深入浅出WPF》这本书,而我通过Kindle则看到了一本比较有意思的书《葵花宝典-WPF自学手册》,这本书写得比较生动,通过故事的形式讲了WPF的许多技术原理,无形中让我对WPF的概念有了许多新的认识。当然,这本书已经有点年头了。

WPF的常用控件

| 控件类型 | 控件名称 | 控件说明 | 链接地址 |
|:—-:|:—-:|:—-:|:—-:|:—-:|:—-:|:—-:|:—-:|
| 组件 | Window | 窗口 | 查看示例 |
| | Page | 页面 | 查看示例 |
| | NavigationWindow | 导航窗口 | 查看示例 |
| | Frame | | 查看示例 |
| 常规控件 | Button | 按钮控件,提供Content作为内容 | 查看示例 |
| | TextBox | 文本框控件,用以输入文本 | 查看示例 |
| | TextBlock | 文本块,用以显示文本 | 查看示例 |
| | Label | 标签,用以显示文本 | 查看示例 |
| | ProgressBar | 进度条 | 查看示例 |
| | ToggleButton | 一种可以设置开关三态的按钮 | 查看示例 |
| | Image | 图像控件,通过Source设置资源路径 | 查看示例 |
| | CheckBox | 勾选框,可以设置是否勾选的三种状态 | 查看示例 |
| | RichTextBox | 富文本框,可以多种格式显示和输入文本 | 查看示例 |
| | TreeView | 树视图,以树状图的形式显示绑定内容,可以显示是否勾选三态。 | 查看示例 |
| | WebBrowser | 浏览器,基于IE内核的浏览器控件 | 查看示例 |
| | Calendar | 日历控件 | 查看示例 |
| | ComboBox | 下拉列表 | 查看示例 |
| | ContentControl | 内容控件 | 查看示例 |
| | Expander | 扩展器,可以显示和折叠面板内的元素 | 查看示例 |
| | GroupBox | 分组框 | 查看示例 |
| | StatusBar | 状态栏,用于在页面下方显示状态信息。 | 查看示例 |
| | DateTimePicker | 时间控件,可以设置时间状态。 | 查看示例 |
| | DocumentViewer | 文档查看器 | 查看示例 |
| | RadioButton | 单选按钮 | 查看示例 |
| | ScollViewer | 滚动视图 | 查看示例 |
| | ScollBar | 滚动条 | 查看示例 |
| | Separator | 分隔器 | 查看示例 |
| | ToolBar | 工具条 | 查看示例 |
| | Slider | | 查看示例 |
| | Menu | 菜单 | 查看示例 |
| | MediaElement | 多媒体控件 | 查看示例 |
| | PasswordBox | 密码输入框 | 查看示例 |
| | TabControl | 选项卡 | 查看示例 |
| | ToolBarTray | 工具条 | 查看示例 |
| | WindowsFormsHost | 用以承载WinForm | 查看示例 |
| | Border | 边框 | 查看示例 |
| 数据控件 | ListView | 列表视图 | 查看示例 |
| | DataGrid | 数据表 | 查看示例 |
| | ListBox | 列表框 | 查看示例 |
| 布局 | WrapPanel | 可变面板 | 查看示例 |
| | StackPanel | 固定面板 | 查看示例 |
| | DockerPanel | 停靠面板 | 查看示例 |
| | Grid | 表格布局 | 查看示例 |
| | UniformGrid | 统一分布表格布局 | 查看示例 |
| 查看示例 | Canvas | 画布 | 查看示例 |
| 图形 | Point | 点 | 查看示例 |
| | Line | 线 | 查看示例 |
| | Path | 路径 | 查看示例 |
| | Polygon | 多边形 | 查看示例 |
| | Polyline | 多段线 | 查看示例 |
| | Rectangle | 矩形 | 查看示例 |
| | Shape | 画笔 | 查看示例 |
| | Rectangle | 矩形 | 查看示例 |
| | Ellipse | 椭圆 | 查看示例 |

WPF的XAML语法

概述

在WPF技术中引入的XAML语法算是该技术的一大特色,也是被学习者视同为学习路径陡峭的“罪魁祸首”。原因是在前端技术飞速发展的今天,HTML的语法体系由于更早的被开发者接受,所以也自然而然更容易成为开发者的首选。

而XAML是一种脱胎于XML,并吸收了HTML的精华的语法体系,是一种界面描述语言,XML语法本身相对而言较为臃肿的体系,看似成为了他的历史负担,但是其实倒也没那么复杂,通过几个简单的示例,其实就足够掌握这门新的语法体系了。例如,使用这样的语法,完全可以平滑过渡到这样的语法体系。(部分标签其实只是大小写不同)。当然,在XAML中熟练编写样式,确实需要花一点点时间。

在WPF中,通过XAML定义面向用户交互层的界面,然后编译成baml运行,后端则使用C#或VB.NET这样的CLR语法来实现逻辑交互。

XAML的语法定义

XAML的根元素定义

根元素定义是定义XAML的命名空间。

1
2
3
<Page
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">

XAML的属性语法

通过xaml定义按钮,并设置文本为 helloworld 。这种写法在官方文档中称为“属性语法”,即直接在XAML中对属性进行设置。

1
<Button Background="Blue" Foreground="Red" Content="hello world"/>

XAML的属性元素语法

通过xaml定义按钮,并设置其背景为蓝色画笔,字体颜色为红色画笔,内容 为helloworld。这种写法在官方文档中称为“属性元素语法”。

1
2
3
4
5
6
7
8
9
10
11
<Button>
<Button.Background>
<SolidColorBrush Color="Blue"/>
</Button.Background>
<Button.Foreground>
<SolidColorBrush Color="Red"/>
</Button.Foreground>
<Button.Content>
hello world
</Button.Content>
</Button>

XAML的集合语法

定义按钮的颜色为红色和蓝色渐变色,内容为helloworld。这种称为“集合语法”。

1
2
3
4
5
6
7
<LinearGradientBrush>
<LinearGradientBrush.GradientStops>
<!-- no explicit new GradientStopCollection, parser knows how to find or create -->
<GradientStop Offset="0.0" Color="Red" />
<GradientStop Offset="1.0" Color="Blue" />
</LinearGradientBrush.GradientStops>
</LinearGradientBrush>

XAML的样式定义

通过属性语法来定义按钮的外观

样式定义使用 标签,然后在中间对样式的内容进行定义。

例如,以下表示通过XAML语法对 ToggleButton 按钮定义了一个命名为 ToggleLikeButtonStyle 的样式。

1
2
3
4
5
6
7
8
9
10
11
12
 <Style TargetType="ToggleButton" x:Key="ToggleLikeButtonStyle">
            <Setter Property="Margin"   Value="4" />

            <Setter Property="FontWeight" Value="Black"/>

            <Setter Property="Foreground"  Value="Black" />

            <Setter Property="BorderThickness" Value="0"/>

            <Setter Property="IsThreeState" Value="False"></Setter>

</Style>

WPF中的模板Template

WPF中的控件可以通过模板 Template 的形式来定义其内容,使得开发者能够通过 XAML 灵活的对控件的外观进行扩展。例如,如下定义了一个 Template,这个控件模板将会对控件(Button)定义填充制定颜色。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
 <Setter Property="Template">  
                <Setter.Value>
                    <ControlTemplate>
                        <Border BorderThickness="0" CornerRadius="3">
                            <Border.Background>
                                <LinearGradientBrush EndPoint="0,1" StartPoint="0,0">
                                    <GradientStop Color="#4696F2" Offset="0.5"/>
                                </LinearGradientBrush>
                            </Border.Background>
                            <ContentPresenter Content="{TemplateBinding ContentControl.Content}" HorizontalAlignment="Center" VerticalAlignment="Center" />
                        </Border>
                    </ControlTemplate>
                </Setter.Value>
            </Setter>

XAML中的触发器Triggers

传统的WinForm开发者习惯于通过事件的机制对按钮的外观进行定义,而在WPF中,则可以通过属性的形式对外观进行设置,这使得开发者更能够写出高质量的代码。

例如,如下代码通过定义触发器,设置控件(控件为 ToggleButton),当控件的勾选状态属性为“IsChecked” 时,其边框填充色为#4696F2颜色。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<ControlTemplate.Triggers>  
                            <Trigger Property="IsChecked" Value="True">
  <Setter Property="Border.Background" TargetName="PART_Background"
<Setter.Value>
<LinearGradientBrush EndPoint="0,1" StartPoint="0,0">
<GradientStop Color="#4696F2" Offset="0.5"/>
</LinearGradientBrush>
</Setter.Value>
</Setter>
<Setter Property="Width" TargetName="PART_Background" Value="60"></Setter>
<Setter Property="Content" TargetName="contextPresenter" Value="已点赞"></Setter>
<Setter Property="Visibility" TargetName="contextPresenter" Value="Visible"></Setter>
<Setter Property="Visibility" TargetName="contextImage" Value="Hidden"/>
</Trigger>
</ControlTemplate.Triggers>

部分完整代码

在上述事例中,共定义了两个按钮的样式,分别是:

  • FlatButtonStyle,这是个圆角按钮。图片

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    <Style TargetType="Button" x:Key="FlatButtonStyle">
                <Setter Property="Margin"   Value="4" />
                <Setter Property="FontWeight" Value="Black"/>
                <Setter Property="Foreground"  Value="Black" />
                <Setter Property="BorderThickness" Value="0"/>
                <Setter Property="Template">
                    <Setter.Value>
                        <ControlTemplate>
                            <Border BorderThickness="0" CornerRadius="3">
                                <Border.Background>
                                    <LinearGradientBrush EndPoint="0,1" StartPoint="0,0">
                                        <GradientStop Color="#4696F2" Offset="0.5"/>
                                    </LinearGradientBrush>
                                </Border.Background>
                                <ContentPresenter Content="{TemplateBinding ContentControl.Content}" HorizontalAlignment="Center" VerticalAlignment="Center" />
                            </Border>
                        </ControlTemplate>
                    </Setter.Value>
                </Setter>
            </Style>
  • ToggleLikeButtonStyle,这是一个点赞按钮。图片。

    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
    <Style TargetType="ToggleButton" x:Key="ToggleLikeButtonStyle">
                <Setter Property="Margin"   Value="4" />
                <Setter Property="FontWeight" Value="Black"/>
                <Setter Property="Foreground"  Value="Black" />
                <Setter Property="BorderThickness" Value="0"/>
                <Setter Property="IsThreeState" Value="False"></Setter>
                <Setter Property="Template">
                    <Setter.Value>
                        <ControlTemplate TargetType="{x:Type ToggleButton}">
                            <Border BorderThickness="0" CornerRadius="3" Name="PART_Background">
                                <Border.Background>
                                    <LinearGradientBrush EndPoint="0,1" StartPoint="0,0">
                                        <GradientStop Color="#525252" Offset="0.5"/>
                                    </LinearGradientBrush>
                                </Border.Background>
                                <Grid>
                                    <ContentPresenter x:Name="contextPresenter" Content="{TemplateBinding ContentControl.Content}" HorizontalAlignment="Center" VerticalAlignment="Center" />
                                    <Image x:Name="contextImage" Width="24" Height="24" Source="assests/thumbs-up-outline.png" HorizontalAlignment="Center" VerticalAlignment="Center"/>
                                </Grid>
                            </Border>
                            <ControlTemplate.Triggers>
                                <Trigger Property="IsChecked" Value="True">
                                    <Setter Property="Border.Background" TargetName="PART_Background">
                                        <Setter.Value>
                                            <LinearGradientBrush EndPoint="0,1" StartPoint="0,0">
                                                <GradientStop Color="#4696F2" Offset="0.5"/>
                                            </LinearGradientBrush>
                                        </Setter.Value>
                                    </Setter>
                                    <Setter Property="Width" TargetName="PART_Background" Value="60"></Setter>
                                    <Setter Property="Content"  TargetName="contextPresenter" Value="已点赞"></Setter>
                                    <Setter Property="Visibility"  TargetName="contextPresenter" Value="Visible"></Setter>
                                    <Setter Property="Visibility" TargetName="contextImage" Value="Hidden"/>
                                </Trigger>
                                <Trigger Property="IsChecked" Value="False">
                                    <Setter Property="Border.Background" TargetName="PART_Background">
                                        <Setter.Value>
                                            <LinearGradientBrush EndPoint="0,1" StartPoint="0,0">
                                                <GradientStop Color="#525252" Offset="0.5"/>
                                            </LinearGradientBrush>
                                        </Setter.Value>
                                    </Setter>
                                    <Setter Property="Width" TargetName="PART_Background" Value="40"></Setter>
                                    <Setter Property="Visibility"  TargetName="contextPresenter" Value="Hidden"></Setter>
                                    <Setter Property="Visibility" TargetName="contextImage" Value="Visible"/>
                                </Trigger>
                            </ControlTemplate.Triggers>
                        </ControlTemplate>
                    </Setter.Value>
                </Setter>
    </Style>

XAML的标记扩展

通过了解WPF的常用控件,我们可以知道自己需要使用的控件有哪些属性,并能使用 XAML 语法对相应的属性进行设置,这种设置方法有别于通过C#代码的形式进行定义的方法,在 XAML中的属性称为 “标记”。标记使用 “{}” 花括号,编译器通过该花括号将语法和XAML语法进行区分。

例如:

1
 HeaderTemplate="{DynamicResource StretchedHeaderTemplate}"

标记值的转换与TypeConverters

在进行标记值转换时,有时候需要使用TypeConverters实现类型转换。例如,在上述示例代码中,可以看到使用了字符串“#525252”来定义颜色,在内部就是实现了从字符串到 Color 类的转换过程。限于篇幅有限,此处就暂时略过。

XAML中内置特殊标记扩展

  • x:Type:特定类型

    1
    <object property="{x:Type prefix:typeNameValue}" .../>
  • x:Static:使用静态值。

    1
    <object property="{x:Static prefix:typeName.staticMemberName}" .../>
  • x:Null:使用空对象定义为属性值。

    1
    <object property="{x:Null}" .../>
  • x:Array:使用数组对象。

    1
    2
    3
    <x:Array Type="typeName">    
    arrayContents
    </x:Array>

常见的标记扩展

  1. StaticResource:通过替换已定义资源的值来为属性提供内容,该资源标记在XAML加载时自动执行。静态资源无法通过在XAML语法体系中对其引用关系进行前向引用,意味着无法通过多层级关系定义可复用的样式资源,如果需要这样做,则需要使用DynamicResource。

    1
    <object property="{StaticResource key}" .../>
  2. DynamicResource:在运行时为资源提供内容。

    1
    <object property="{DynamicResource key}" .../>
  3. Binding:在运行时为使用数据上下文为数据提供内容。

    1
    2
    3
    4
    5
    6
    7
    8
    <object property="{Binding}" .../>    
    -or-
    <object property="{Binding bindProp1=value1[, bindPropN=valueN]*}" ...
    />
    -or-
    <object property="{Binding path}" .../>
    -or
    <object property="{Binding path[, bindPropN=valueN]*}" .../>
  4. RelativeSource:提供了可在运行时对象树中导航几个可能的关系的 Binding 的源信息。

    1
    <Binding RelativeSource="{RelativeSource modeEnumValue}" .../>
  5. TemplateBinding:使控件模板能够使用模板化属性的值,这些属性来自于将使用该模板的类的对象模型定义属性。

    1
    <object property="{TemplateBinding sourceProperty}" .../>
  6. ColorConvertedBitmap:提供一种方法,用于指定没有嵌入的配置文件的位图源。 颜色上下文/配置文件由 URI 指定,与映像源 URI 相同。

    1
    <object property="{ColorConvertedBitmap imageSource sourceIIC destinationIIC}" .../>
  7. ComponentResourceKey和TemplateResourceKey:

    1
    <object x:Key="{ComponentResourceKey {x:Type targetTypeName}, targetID}" .../>

XAML资源复用

在开发过程中,我们可以直接在按钮上进行按钮模板的定义,例如下面的代码。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
<Button Width="40" Height="40" Style="{DynamicResource CubeImageButtonStyle}" Click="Button_Click" Content="点赞">
<Button.Background>
<ImageBrush ImageSource="/assests/favicon.png" Stretch="Fill"/>
</Button.Background>
</Button>
  <Setter Property="Template"
<Setter.Value>
<ControlTemplate>
<Border BorderThickness="0" CornerRadius="3">
<Border.Background>
<LinearGradientBrush EndPoint="0,1" StartPoint="0,0">
<GradientStop Color="#4696F2" Offset="0.5"/>
</LinearGradientBrush>
</Border.Background>
<ContentPresenter Content="{TemplateBinding ContentControl.Content}" HorizontalAlignment="Center" VerticalAlignment="Center" />
</Border>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
</Window.Resources>
<Grid>
<Button Grid.Column="0" Grid.Row="1" Style="{StaticResource FlatButtonStyle}" HorizontalAlignment="Center" VerticalAlignment="Top" Width="48" Height="16" FontSize="10" Background="#4696F2" Content="获取"></Button>
</Grid>

这样的代码在界面比较简单时,还无所谓,但是随着控件的样式越来越复杂,可能会成为一团乱麻,这对于追求优雅代码的我们来说,可能是难以忍受的,所以往往会使用资源引用来完成。

StaticResource

例如,我们可以在当前页面代码中定义对应的样式,这种样式可以使用 StaticResource 的形式引入。但是这样的引用形式,没有对象图的访问权限,意味着无法访问资源依赖的其他资源。

1
2
3
4
5
6
7
 <Window.Resources>  
<Style TargetType="Button" x:Key="FlatButtonStyle"> 
            <Setter Property="Margin"   Value="4" />
            <Setter Property="FontWeight" Value="Black"/>
            <Setter Property="Foreground"  Value="Black" />    
            <Setter Property="BorderThickness" Value="0"/>
</Window.Resources>

DynamicResource

将上述代码中的{StaticResource FlatButtonStyle} 改成{StaticResource FlatButtonStyle}则会在运行时加载样式,并可以访问相应的对象图。

当然,这样的更改意义不大,如果该FlatButtonStyle引用了其他样式或元素,会发生作用。

1
2
3
<Grid
<Button Grid.Column="0" Grid.Row="1" Style="{StaticResource FlatButtonStyle}" HorizontalAlignment="Center" VerticalAlignment="Top" Width="48" Height="16" FontSize="10" Background="#4696F2" Content="获取"></Button>
</Grid>

注意事项

1、由于XAML语法脱胎于XML语法,而XML语法中本身对某些输入字符,如“<>”存在限制,所以在XAML中也会出现这类问题,并会被Visual Studio检测出错误而无法编译,需要使用UTF-8编码进行转换。

用户控件和自定义控件

用户控件

而用户控件,使用于控件组合的场景。

自定义控件

在笔者进行开发时,总是思考究竟是使用用户控件,还是自定义控件,后来在阅读《葵花宝典-WPF自学手册》这本书中,终于得以大彻大悟。

作者指出:“不要被控件的外观所欺骗,要考虑其内在本质”。即思考控件的基本特征,首先想到该控件的行为与原有控件的行为是否相似,如果能够找到,则修改原有控件,而不是定义一个控件。尤其是在XAML语法中,能够通过Content 模型和模板、附加属性的运用,使得自定义控件的用途得到了进一步缩减,只有当实在万不得已时,在定义自定义控件。

作者给出了使用自定义控件的分析思路:

图片

例如,在示例代码*ToggleLikeButtonStyle *中,我实现了一个点赞和取消点赞的状态,则使用了ToggleButton来完成,就没必要使用 Button 扩展出一个是否点赞的状态了。

而如果我们需要实现的功能有这么复杂,那大概使用传统的控件就无法实现,就得使用自定义控件了。(点击查看示例代码)

图片

作者定义了自定义控件 ButtonEx,并实现了依赖属性 ButtonType,见【依赖属性】,并定义了不同类型的样式特征。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
 <Trigger Property="ButtonType" Value="Icon">
                <Setter Property="Cursor" Value="Hand"/>
                <Setter Property="Template">
                    <Setter.Value>
                        <ControlTemplate TargetType="{x:Type controls:ButtonEx}">
                            <Border Width="{TemplateBinding Width}" Height="{TemplateBinding Height}">
                                <Image x:Name="Img" VerticalAlignment="Center" HorizontalAlignment="Center" Source="{TemplateBinding Icon}" Stretch="None"/>
                            </Border>
                            <ControlTemplate.Triggers>
                                <Trigger Property="IsMouseOver" Value="True">
                                    <Setter Property="Opacity" Value="0.8"/>
                                </Trigger>
                                <Trigger Property="IsPressed" Value="True">
                                    <Setter Property="Opacity" Value="0.9"/>
                                </Trigger>
                            </ControlTemplate.Triggers>
                        </ControlTemplate>
                    </Setter.Value>
                </Setter>
            </Trigger>

使用时,只需这样设置,即可实现不同类型的按钮外观。

1
 <controls:ButtonEx Icon="/Images/search.png"  Margin="10" ButtonType="Icon"/>

属性和事件

依赖属性

依赖属性是为既有WPF控件对象定义自定义属性,以便支持其扩展,例如在上述自定义控件的示例中,就定义了依赖属性 ButtonType,实现了不同类型的按钮外观。

1
2
3
4
5
6
7
8
public ButtonType ButtonType  
        {
            get { return (ButtonType)GetValue(ButtonTypeProperty); }
            set { SetValue(ButtonTypeProperty, value); }
        }


        public static readonly DependencyProperty ButtonTypeProperty =

DependencyProperty.Register(“ButtonType”, typeof(ButtonType), typeof(ButtonEx), new PropertyMetadata(ButtonType.Normal));

附加属性
按照官方的说法就是“附加属性旨在用作可在任何对象上设置的一类全局属性”,例如,DockPanel面板中的子对象,继承了来自于容器对象的附加属性,使得其能够在父对象中实现停靠的功能。

1
2
3
<DockPanel>
<CheckBox DockPanel.Dock="Top">Hello</CheckBox>
</DockPanel>

路由事件

基本定义

假设我们定义了几个这样的控件。

1
2
3
4
5
6
7
<Border Height="50" Width="300" BorderBrush="Gray" BorderThickness="1">
<StackPanel Background="LightGray" Orientation="Horizontal" Button.Click="CommonClickHandler">
<Button Name="YesButton" Width="Auto" >Yes</Button>
<Button Name="NoButton" Width="Auto" >No</Button>
<Button Name="CancelButton" Width="Auto" >Cancel</Button>
</StackPanel>
</Border>

实现了这样的界面图片

路由事件就是针对这组元素树中多个元素调用处理程序的事件。当我们点击了按钮Button时,将会触发 Button=>StackPanel=>Border的事件路由,而不是像WinForm应用一样,只能触发最上层的Button的按钮点击事件。

路由策略

  • 冒泡事件(官方称为浮升,这个翻译有点。。):调用事件源上的事件处理程序。 路由事件随后会路由到后续的父级元素,直到到达元素树的根。 大多数路由事件都使用浮升路由策略。 浮升路由事件通常用于报告来自不同控件或其他 UI 元素的输入或状态变化。

  • 直接: 只有源元素本身才有机会调用处理程序以进行响应。通过使用 EventSetter 和 EventTrigger使用来设置处理程序。例如,可以使用RoutedEventArgs的Handled,设置为 true 将事件标记为已处理,将 “停止” 路由用于隧道路由或冒泡路由。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    void MakeButton2()
    {
    Button b2 = new Button();
    b2.Click += new RoutedEventHandler(Onb2Click2);
    }
    void Onb2Click2(object sender, RoutedEventArgs e)
    {
    //logic to handle the Click event
    }
  • 隧道:最初将调用元素树的根处的事件处理程序。 随后,路由事件将朝着路由事件的源节点元素(即引发路由事件的元素)方向,沿路由线路传播到后续的子元素。

  • WPF中约定,隧道路由事件的名称以单词“Preview”开头。 输入事件通常成对出现,一个是浮升事件,另一个是隧道事件。例如,如下图所示,假设按钮2为触发事件的源。

图片

1、处理Border根元素的隧道事件PreviewMouseDown

2、处理StackPanel面板的隧道事件PreviewMouseDown.

3、处理Button按钮的隧道事件的PreMouseDown。

4、处理Button按钮的MouseDown事件。

5、处理StackPanel的MouseDown事件。

6、处理Border的MouseDown事件。

总结

WPF是一个非常庞大的技术体系,以上学习路径仅供开发者进行简单的入门,由于篇幅有限,对于标记扩展还需要进一步理解透彻,以及格式转换、图形绘制、数据绑定、MVVM等内容未能一一描述。

如果果想要对WPF进一步了解,最好通过系统的学习相关知识,除了前面提到的网站和几本书,最好的入门网站依然是微软官方文档。

如何快速融入团队并成为团队核心(七)

Posted on 2020-02-17 | Edited on 2021-04-25 | In 随笔

一、引言

今天继续讨论一下如何提高自己的工作效率,讨论完个人精力分配,接着讨论开会的问题。开会确实是影响个人效率的关键问题,我曾经分析过,一家中小型互联网公司的中层管理者,几乎每周有八分之一到六分之一的时间是在开会中度过。

中国人是如此的热衷于开会,大事开大会,小事开小会,似乎没什么是开会不能解决的问题。如果说“有人的地方就有江湖”,那么大概还有一个类似的说法可以说成“有人的地方就有会议”。

开会大概本身是为了提高效率,却最终变成了一种低效的行为,这背后究竟发生了什么呢。

二、神仙会

我还记得曾经在一家公司工作时,那家公司老板特别喜欢做下面这件事:

每次选在上午10点多左右,以讨论需求和产品远期发展规划为由,拉上一群相关或不相关的人员,然后大家一起在会议室中,对着空气发表对未来的规划,老板开始抽他的芙蓉王,听大家互相扯淡。
闲时老板还允许你自己泡一杯咖啡或一杯茶,总之就是空谈;然后就这么闲聊到下午两点,然后老板一拍大腿,呀,不小小心又开了这么久的会了,走,一起吃饭去。
然后大家聚个餐,到下午四点。然后这个会算是正式结束了么?并没有,晚上11点,老板再给几个相关人员打电话,一起聚集到他位于湘江边两百平大宅附近的一个茶馆中,继续讨论白天的话题,一壶红茶不加七八次水,几乎也难得消停,就这么开到凌晨三四点,然后散会。(第二天早上得准点9点上班。)

我的同事称其为“神仙会”。“一壶茶一包烟,快乐似神仙”。

图片

当然,等闲的人大概是没有资格参加这些会的,能够有幸参会并能把全场通关的往往都是中层或高层管理者,毕竟开会其实也是企业文化的一部分,如果你连这点开会都不能接受,又如何能接受公司的价值观和企业文化呢。。

当然,每次叫我开会,我都会选择听前15分钟,因为,有时候如果一个事情连15分钟都不能说明白,说明这个会的价值已经不太存在了,要么是双方根本不在同一个频道上,要么就是有一方根本没用心的准备与会议相关的话题。当然,有许多相对来说年长的管理者信奉会议就是企业管理的核心,总是想尽一切办法拉更多的人来开会,而且动不动就头脑风暴,这样的效率到底是高还是不高呢,大家其实内心都很清楚。

二、为何开会?

必须承认,有时选择开会是团队内部进行澄清事实、发现问题的非常关键的方法,有时候确实也如同第四篇说的,甚至可以称为是不同团队间进行团队建设的一种形式,不过显然在互联网时代的今天,层出不穷的各种会议往往让从业人员深受其害。

例如基于敏捷项目管理的产品研发流程中,也规定了“站立会议”,“计划会”,“评审会”,“反思会”,其中站立会议说的是不超过十五分钟,要求每个人用三个问题将自己做的事情讲清楚“我做完了什么”,“遇到了什么”,“计划今天做什么问题”,但是实际操作过程中,也难免会陷入文山会海的谜团中。

图片

例如,某某某作为后端工程师,同时参与了3个产品的研发,如果每个产品需要开一个站立会议,那么意味着保守得准备一小时的开会时间。问题是,如何确保一个会能够在十五分钟开完,其次,究竟有几个人能够用短短几句话讲清楚自己的需求?

于是有的会,原本应该是任务会的,结果开成了头脑风暴会,一群人针对某个议题讨论半天,看起来最后也形成了结论,但是这样的结论却是以消耗了四五个人两三个小时的工时为代价,如果不是通过这样开会的形式,难道就无法形成一个可用的结果么。

有事没事就头脑风暴,似乎也成为互联网开发者们经常吐槽的话题。仿佛如果一群人如果不通过开头脑风暴的形式,似乎就无法收集到足够多的创意一般。当然,大概是因为平时大家手头上的工作本身也做不完,但是作为领导呢,总担心大家会闲着,所以想着法子让大家过得更加充实。通过开会的形式,让大家先把手头上的工作先暂时放一下,我们先切换到一个新的副本=》会议室。

图片

然后针对性一个或许与当前工作无关的话题讨论一下,以便把大家脑海中的创造性思维彻底的掏空(毕竟互联网公司已经不需要创意了,要的是如何快速的借鉴(copy to our company))。

当然,开会也使得我们能够放空一下~但是这样的放空最终牺牲了下班后的时间,从这一点上看开会大概约等于996,如果哪一天开了四次会,那大概这一天的白天全部荒废了。然后就花费了大量精力做的许多事情,都是在用自己的苦劳证明自己在为公司抛头颅洒热血般的拼命而已。

三、结语

所以在团队中如果要提高自己的工作效率,就得想办法思考如何提高团队会议效率的方法,也许这些方法无法在短期内实现自己劳动生产力的提高,但是会逐渐的形成我们自己的微习惯,并进而逐渐改变我们做事的方式。

图片

这些方法或许可以包括以下这些策略:

1、灵活的运用罗伯特议事规则:这是一个来源于大洋彼岸的开会规则,甚至于联合国或美国国会开会都是用这套规则,美国国会参议院和众议院都是大几百号议员开会,每天讨论的都是世界性大事,我想我们的开会复杂度应该不会比这个高了吧。在这套规则中,包括大概动议、复议、陈述议题、辩论、表决等许多个步骤,制定了一系列规则,例如未经动议不可讨论,主持人不能偏袒动议、强调发言完整等。(当然,大概许多人都会认为自己公司还没有复杂到需要动用罗伯特议事规则来开会的情况,但是谁知道呢。开会的时候大部分情况下的纠结,还不是不同观点持有者从自己的专业角度出发,认为某些问题值得做,不值得做,该怎么做,结果与其他人意见相左而引发的矛盾纠纷呢。。)

2、不要盲目的开头脑风暴会,尽可能的先用头脑风暴调查。先针对需要讨论的产品需求提出调查,让相关方针对问题进行思考,形成一个大概的方向后,再通过会议的形式进行交流讨论,从而使得问题的思考时间尽可能的前置,避免了参会者在会议的前十五分钟还不清楚这个会是干什么的尴尬情况。

3、能不开会就别开会、要开也得站着开。

4、每次开会先选择一个主持人,这个主持人配备一个秒表,超时立马让有关人员结束他的表述。如果一个人连五分钟都无法把自己的问题讲清楚。。那这个人还是很厉害的,他有做老板的潜质。

真爱生命,远离低效的会议,我们将逐渐积累属于自己的小成功。

如何看待程序媛们的职场焦虑和未来职业规划?

Posted on 2020-02-16 | Edited on 2021-04-25 | In 随笔

一 引子

昨天,有一位通过我们长沙.NET技术社区公众号【DotNET技术圈】加我的好友的姑娘问了一个关于职业规划的问题:

“在你的身边有没有认识的女程序员,可以告诉一下我们她们的职业规划么?”

“是你想知道么?”

“是的。我最近挺迷茫的。”

“我感觉你似乎有点焦虑。你参加工作多久啦?”

“是的。我参加工作一年了,目前主要是从事.NET技术开发。不知道自己该干嘛,也不知道自己未来能干嘛。”

。。。

二 职场焦虑

2.1 焦虑感从何而来?

不知大家是否观察到,在博客园也好,在其他技术群也好,总是有人有意无意的透露出一些焦虑的气氛,而且这种焦虑似乎是一种传染病,有时一个人感染了,还会连带着使得其他人也受到影响。

职场焦虑无处不在,如同空鼻症患者的每一次呼吸一般,无时无刻不在影响着我们的生活。有时候早上起床,都会感觉到莫名其妙的压力席卷而来,让人一天都很难打起精神来,自然而然也会对我们的工作造成不少干扰。 虽然,有时候适度的焦虑,能够让我们维持比较积极的工作态度,使得我们能够以正确的方式面临来自工作中的压力。但是当代开发者们给自己太会给自己找压力了,有时候一些很正常的文字也会让我们产生错误的心理暗示,以为是对方在向我们输出焦虑。

当然,我们也得承认,许多自媒体作者确实比较喜欢通过输出焦虑的形式来获得眼球,还有一些主要面向线上教育的公众号广告,也会有意无意的释放一些让人感觉焦虑的文字,使得许多读者倍感压力山大。于是这些有意无意制造出来的焦虑感,不仅不能给我们带来任何积极的作用,反而会影响工作效率的降低,影响我们的人际交往,进而影响我们的生活质量。

2.2 焦虑感竟无处不在?

过度焦虑已经成为普遍性的问题,尤其是30岁左右的这种习惯意义上的资深开发者或相对而言更加年长的开发者们。他们在30岁-40岁这个年龄,看似积累了不少经验理应能够在职场承担更多的工作压力,但是由于市场的影响,使得他们的工作本身并没有想象中那么稳定。与日俱增的裁员潮,随着年龄增长逐渐降低的性价比,来自家庭的压力本身,给30岁以上的开发者带来的压力或许许多人都有深刻的体会。

如果说中年开发者们的焦虑其实与来自家庭的压力有关,那为何现在这种焦虑感已经蔓延到了更广的人群,甚至包括刚刚参加工作的开发者呢?许多刚刚参加工作的开发者,他们本来应该处于对待知识充满无穷渴望的大好年龄,但是由于来自普遍人群的焦虑情绪影响,使得他们对于自己的职业发展前途充满迷茫,不知自己今天的努力是否有价值,不知自己是否能够如同身边那些“其他人”一样,能够快速的在某些城市定居,获得生活上的安定。

2.3 该如何排挤焦虑感?

对于职场工作中的我们来说,一旦我们沉浸在工作中,总是会易于排挤焦虑,而一旦安静下来,反而会陷入深深的焦虑之中。所以最近由于新冠肺炎蔓延给大家带来的延长假期,我想可能会给大家带来的,或许不仅仅是充足的睡眠,或许还有远胜于以往的焦虑情绪。

如果你总是莫名其妙的感到焦躁不安,而这种感觉又是与职场发展有一定的关系,那大概你也是职场焦虑症的受害者。我建议你得想办法从这个困境中走出来,例如学会积极的与人沟通、学会排挤工作中压力,采用适度的学习方法使自己的生活过得充实等等,没必要使自己过度焦虑,好好的找一个能够使自己专注的事情安定下来吧。

三 姑娘们的职业规划

3.1 客观存在的职场偏见

回到开始的那个问题,关于程序媛的职业规划问题。这确实是一个很尴尬的问题。如果说程序员们的职场下坡路,大概是从30以后开始,或者35岁以后开始,那么程序媛们则是从结婚生子开始。

不管程序媛曾经是如何优秀,一旦开始成为母亲,再回到职场上,竞争力将急剧降低,哪怕是有开明的人事或管理者试图留住像你这样的优秀开发者,也总会有其他人劝他三思而后行。

这样的现象在互联网行业中层出不穷,不仅仅软件工程师岗位如此,其他岗位也同样如此,例如产品经理,市场推广,设计岗位等等。大概是因为在互联网时代,许多岗位已经不再是靠技能和创造力来创造价值,而是需要依靠更长的待机时间,更强大的心理承受力,更积极的工作态度来应对,而成为母亲的开发者们,往往会将更多的精力分给自己的家庭。

当然,我们得承认,大部分情况下,程序媛们无法在职场进一步发展,难道程序媛就无法适应高强度的工作?性格不适合?技术不行?

其实往往都不是,仅仅只是由于一种习惯性的职场偏见,或者曾经有过某些程序媛前辈们未能平衡职场和家庭的关系,让公司受到了一点点损失,因此在这方面吃过苦的中高层管理者们,即便想招人,有时候也不敢轻易聘用程序媛。

当然这样的偏见其实已经在逐渐改善了,如果有一位程序媛能够正确的将家庭和事业的水端平,其实还是能够找到合适的工作的。而一旦行业开始正确的看待程序媛,那么真正影响我们职业规划的,其实就是程序媛们是否正确的看待自己的职业规划的问题。

3.2 职场规划的核心

首先是自己的兴趣,你得明确自己的内心是否真正热爱软件编程,并把他当做一份事业?软件编程这份工作确实会给我们带来比其他行业相对较高的收入水平,但是这样的收入水平实际上依然取决于你的学习态度,如果始终保持着积极的学习态度,往往会有更多的机会选择,而如果抱着拿一份工资,上一份班的心态,这样的收入实际上并不长久。

不要总想着转行,许多人总是不能正确的看待自己的职场,大概是因为他们总觉得自己在这份工作上干不长久。这样的心理暗示,也直接影响了他对于工作的长期规划。许多像我这样上了年纪的开发者都有深深的感悟,那些能够在这个行业一直干下来的人,他们不一定天赋异禀或者学习能力超群,恰好相反,他们能够坐得住冷板凳,不管工资高或低,他们都能始终如一的坚持这份工作。而还有许多潜力超群的人,他们今天想干这个,明天想干那个,到头来反而一事无成。

第三,努力使自己成为最专业、不可替代的人。不管在什么岗位,只要使自己更加专业,一定不会错。你是否以专业的工程师思维应对未知技术问题带来的挑战?你是否能够以高度职业化的精神投入到工作中?你是否能够以专业的态度应对客户提出的刁钻需求。从这个点上看,其实程序员和程序媛本身没有太大的区别。

真正影响程序员们未来发展的,年龄固然是一个关键性因素,但是是否足够专业、专注、不可替代这样的因素大概是核心因素。

四 反思:如果不想干了,怎么办?

当然,退一万步讲,如果你真的想转行怎么办?好吧,我们始终得相信一点,你当前所从事的岗位并不能证明你只适合干这个,而你能干什么,实际上依然取决于你是否在刻意培养对应岗位适合的能力。

例如,你觉得自己能够成为产品经理,那么可以在写代码之余,刻意提高产品经理相关的技能,例如对于产品的感知力,对于需求的判断能力,如果到时候有一个成为产品经理的机会摆在你的面前,你也一定能够顺利的将相应的工作干好。

例如你对文字特别有感知,那么或许你有成为文案的天赋。

当前你选择的职业本身似乎对你的未来发展来说,其实只是人生中的一个小桥段而已,哪怕今天你从事软件开发,也无法说明一生都只能成为软件开发者。事实上单份工作对于人生的影响并没有那么重要,毕竟中国这么大,只要你具有某方面的能力,总是能找到合适的工作岗位,最重要的还是能够沉下心来认真工作。

无论是未来干什么,你眼下的选择依然非常重要。如果你没办法预料自己未来的发展,那不如把眼下这份工作干到极致吧,谁知道上帝会不会给你开窗又开门呢?

本文纯属个人见解,不代表本号或长沙.NET技术社区意见。部分观点受知乎文章的影响。如有疏漏错误,还请批评指正。

如何看待程序媛的未来职业规划

Posted on 2020-02-16 | Edited on 2021-04-25 | In 随笔
1
我今年三十岁,我很迷茫,不知道未来该选择什么发展方向。

这是我无意中在社区微信群中看到的一位年轻的开发者说的话,之前他也经常会在技术群中抛出一些有深度有内容的问题,并能积极乐观的进行问题思考和探寻技术解决方案,有时候可能会错误的以为他这样一位热爱学习的开发者,在公司也一定是技术的引领者,对自己的职场规划有着清楚的认识。
所以他抛出这样的问题着实有点意外,不过细想之后,或许也属实正常是。对于30岁左右的开发者来说,从20出头的毛头小伙,逐渐成长为家庭和公司的顶梁柱,如果一直从事技术领域,大概很容易就到了瓶颈期,如下图,一个人的学习能力增长曲线,大概三年或五年左右,增长速度就会逐渐放缓。
图片
(手画的)
到了这个阶段会非常痛苦,如果身处一家优秀的公司还好,有一套完整的体系保证你持续成长,否则就得陷入了吃老本的时期,或许将一直这么平庸的下去,直到被迫退出行业。
而那些不甘如此的优秀者,都渴望去改变现状,他们们往往承受着无比巨大的压力,尤其是互联网技术飞速发展、各种新技术新方法席卷而来,究竟该如何选择技术的发展方向和个人的职场规划,这个问题自然而然就出现在他们面前。
以下是互联网开发者的“人才三角”结构,由专业技能、行业知识和软实力组成。每一个圆环均代表一个知识边界。
图片

  1. 专业技能

专业技能是开发者赖以生存的宝贵技能,而其中开发语言是最重要的一个方面。近年来飞速发展的互联网业,似乎对.NET开发者带来的冲击相当巨大,在许多城市,甚至出现了Java与.NET的招聘信息比例10比1的惊人情况,而在国内互联网公司,似乎除了同程艺龙、蔚来汽车等公司,已经找不出太多叫得上名头的大企业坚持使用.NET技术栈。
但是即便如此,这并没有说明.NET技术已经逐渐落伍,尤其在技术领域,看起来火热的市场,从来也不是一门语言所能主宰。真正优秀的开发者从来不会被语言束缚,事实上虽然掌握一门核心语言非常重要,其次在语言之外的其他专业技能,诸如设计模式和设计原则、对于算法等的理解程度,都是开发者非常重要的能力,对他的价值评估产生了非常深远的影响。
在众多专业技能中,比较争议的大概是数据库、各类组件和其他专业技能,以及DevOps和微服务。首先是这些新概念,铺天盖地而来,让人难以抗拒学习的欲望,甚至不学习,也经常会被人说成知识体系闭塞。但是学习了之后,如果得不到实践,不见得能够带来短期或长期受益。除此之外能够恰到好处的掌握好数据库和各类第三方组件(诸如Redis、Consul)等,虽然能够让开发者获得不少机会,但是却并非每位开发者都有时间或精力了解这么多东西。毕竟每天只有24小时,花时间学习了A组件,往往就得忽略其他组件。而且如果只是浅尝辄止的了解某些组件,倒不如仔细的钻研众多同类组件中的一种。
微服务容易让人陷入知识的海洋无可自拔,在宝玉老师的专栏《软件工程之美》也提到微服务架构并非软件系统的天然演进,要靠组织力量来完成。而在微服务书籍《微服务设计模式》中也提出,微服务实际上应该是一种组织架构形式的体现,而不仅仅只是一种软件架构。为了微服务而微服务,不见得会带来了团队沟通成本的提高,也不见得有利于软件系统的高效迭代。同样,为了DevOps而上容器,这样也不是一种值得推崇的策略。
我把DDD和TDD纳入到专业技能中非常重要的一环。因为DDD是一种非常重要的软件问题分析方法,从宏观来看有点像团队问题分析方法,如果时间有限,大概不太有必要独自研究,毕竟要将其实践下去,还是要更多的依赖团队,不如拉几个同事一起探究。如果没办法将DDD完美的实践下去,也不必苛求,运用好领域的分层思想,可以让软件系统尽可能可控的管理各领域的知识,实现业务知识的沉淀。
TDD看似能够让软件尽可能的减少后期的维护成本,但是要将其实践下去,取决于大环境和个人是否持续不断的刻意练习,而且需要一个漫长的周期。能够刻意练习显然是极好的,相信他一定能给你带来非常巨大的好处。
2. 行业知识

所处行业、和行业知识是决定开发者的关键,而不是专业技能本身。例如红得发紫的电商行业,阿里巴巴或拼多多等电商平台,最能给开发者带来丰厚收益的行业,也是竞争最为激烈的行业。而例如社区团购这种新兴商业业态在长沙,也同样为长沙技术圈的从业者带来的新的机遇。
但是似乎许多行业的开发者们对本身行业知识的兴趣并不大。许多开发者往往执着于对专业技能的钻研,而忽略了对行业知识的了解。而事实上行业知识的认知深度,恰好更能体现产品的水平和公司的专业程度。如果每个人都矢志于为公司打造最专业的产品,这显然将为公司、为个人带来无穷收获。
3. 软实力

软实力是让你我与众不同的核心关键。他体现在外在,是一个人的谈吐和为人处世的态度。他通过领导力、管理能力、表达能力等许多种形式让你的优点得以更好的表现在世人面前。
软实力,是程序员成功的关键。例如你是否能够与团队成员融洽的沟通,这能够促进你与团队成员间的相互认同、你是否能够将你打造的产品更好的展示在领导或客户面前,这有利于你和公司产品价值的彰显、你是否能用文字或PPT对你的工作进行总结,这又能体现你的组织表达能力。而你在公司与其他同事的良好沟通、与客户的良好沟通,既是在积累人脉,更是在投资未来。
我独辟蹊径的把运气列入软实力的范畴,是因为我觉得运气是很重要的一环,不过如果没有足够的软实力积累,即便有不错的机遇,也不见得有能力支撑住。
4. 总结

在我参加工作的早期,总是片面的认为技术能力才是决定人与人区别的最大关键。但是事实上恰好相反。我们身边那些混得最好的,他们往往并不是技术最好的。他们往往更善于钻研一个行业,并从中发掘出价值。他们往往更注重软实力的培养,并通过这些软实力,获得许多常人不能拥有的机会。
优秀的开发者也善于抓住职场中的每一次挑战。他们将每段职场当做修行和学习的机会,无时无刻不忘提升自己,哪怕在小公司,也同样能创造不平凡的成绩。
短暂的迷茫不可怕,怕的是看到了迷茫还不试图改变,例如知道能力有不足,却依然忽略了对能力的积累。能力的积累从来不仅仅只是停留在纸面上的简单文字,再多的迷茫,不如从眼下开始刻意练习和主动培养。例如作为.NET开发者,或许我们都需要更加深层次的了解C#/.NET技术的核心知识,一本好书《C# 7.0 核心技术指南》将为你解惑无穷。
无论当下我们做的什么技术,最重要的关键,就是认清楚自己的缺点,然后有意识的去改善。不必苛求昨天的自己比今天的自己优秀,努力使明天的自己比今天更优秀,坚持一个方向,总能让你获得更多机会。

1…456…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