原文:
zh.annas-archive.org/md5/b71782931b1ec965a99239b2cd142e50译者:飞龙
第五章:第五章:.NET 6 中的依赖注入
企业应用程序可能面临的一个大问题是将不同元素连接起来并管理它们的生命周期的复杂性。为了解决这个问题,我们使用 控制反转(IoC)原则,该原则建议移除对象之间的依赖关系。通过委派控制流,IoC 使程序可扩展并增加了模块化。事件、回调委托、观察者模式和 依赖注入(DI)是实现 IoC 的几种方法。
在本章中,我们将学习以下内容:
-
什么是 DI?
-
ASP.NET Core 6 中的 DI
-
管理应用程序服务
-
使用第三方容器
到本章结束时,你将很好地了解 DI 以及它在 .NET 6 应用程序中的利用方式,ASP.NET Core 6 中提供的范围类型,以及如何在项目中利用它们。
技术要求
本章使用的代码可以在以下位置找到:github.com/PacktPublishing/Enterprise-Application-Development-with-C-10-and-.NET-6-Second-Edition/tree/main/Chapter05。
什么是 DI?
DI 是一种技术,其中对象接收它所依赖的对象。DI 模式实现了作为 单一职责原则、开闭原则、里氏替换原则、接口隔离原则和依赖倒置原则(SOLID)设计原则一部分的 DI 原则,如 第一章,设计和架构企业应用程序 中所述。使用 DI,代码将更易于维护、阅读、测试和扩展。
DI 是帮助实现更易于维护代码的最知名方法之一。DI 涉及三个实体,如下面的图所示:
图 5.1 – DI 关系
IOrderRepository 负责处理 Order 实体。.NET IoC 容器(IOrderRepository (OrderController(客户端))。
IoC 容器(也称为 DI 容器)是一个用于实现自动 DI 的框架。在 图 5.1 中,这被称为 注入器。它负责创建或引用依赖关系并将其注入到 客户端。
现在我们已经了解了什么是 DI,让我们来了解 DI 的类型。
DI 类型
服务可以以多种方式注入到依赖项中。根据服务注入客户端对象的方式,DI 被分为三种类型,如下所述:
-
IWeatherProvider依赖项通过构造函数参数注入:public class WeatherService { private readonly IweatherProvider weatherProvider; public WeatherService(IWeatherProvider weatherProvider) => this.weatherProvider = weatherProvider; public WeatherForecast GetForecast(string location) => this.weatherProvider. GetForecastOfLocation (location); }
在前面的示例中,WeatherService 依赖于 IWeatherProvider,它通过构造函数参数注入。
注意
对于 WeatherProvider 服务的实现,请参考 GitHub 上的示例代码,该代码可在以下链接找到:github.com/PacktPublishing/Enterprise-Application-Development-with-C-10-and-.NET-6-Second-Edition/blob/main/Chapter05/DITypes/Service/WeatherProvider.cs。
在初始化 WeatherService2 时,IWeatherProvider 依赖项未设置。它是在对象初始化后通过 WeatherProvider 属性设置的:
public class WeatherService2
{
private IWeatherProvider _weatherProvider;
public IWeatherProvider WeatherProvider
{
get => _weatherProvider == null ?
throw new
InvalidOperationException(
"WeatherService is not
initialized")
: _weatherProvider;
set => _weatherProvider = value;
}
public WeatherForecast GetForecast(string
location) =>
this.WeatherProvider.
GetForecastOfLocation(location);
}
IWeatherProvider依赖项作为所需的方法参数进行注入。
在以下代码片段中,IWeatherProvider 服务通过 GetForecast 方法注入到 WeatherService 中:
public class WeatherService
{
public WeatherForecast GetForecast(
string location, IWeatherProvider
weatherProvider)
{
if(weatherProvider == null)
{
throw new ArgumentNullException(
nameof(weatherProvider));
}
return weatherProvider.
GetForecastOfLocation (location);
}
}
以下是一些建议,可以帮助选择依赖注入的类型:
-
当类有依赖项且没有这个依赖项功能无法工作时,请使用构造函数注入。
-
当依赖项在类的多个函数中使用时,请使用构造函数注入。
-
当依赖项在类实例化后可以更改时,请使用属性注入。
-
当依赖项的实现随着每次调用而改变时,请使用方法注入。
在大多数情况下,构造函数注入将被用于干净且解耦的代码,但根据需要,我们还将利用方法和属性注入技术。
我们现在已经学习了依赖注入的概念。让我们深入了解 .NET 6 提供的依赖注入实现。
ASP.NET Core 6 中的依赖注入
.NET 6 内置了 IoC 容器框架,这简化了依赖注入。这包括 Microsoft.Extensions.DependencyInjection NuGet 包和 ASP.NET Core 6 框架本身严重依赖于它。为了支持依赖注入,容器需要对对象/服务支持三个基本操作,如下所述:
-
注册:容器应提供注册依赖项的机制。这将有助于将正确的类型映射到类,以便它可以创建正确的依赖项实例。
-
解析:容器应通过创建依赖对象并将其注入到依赖实例中来解析依赖项。IoC 容器通过传递所有必需的依赖项来管理已注册对象的形成。
-
释放:容器负责管理通过它创建的依赖项的生命周期。
在 .NET 6 中,以下术语被使用:
-
服务:指由容器管理的依赖项
-
IConfiguration、ILoggerFactory、IWebHostEnvironment等 -
我们在上一节中使用的
IWeatherProvider服务
为了使应用程序启动,ASP.NET Core 6 框架注入了一些依赖项,这些依赖项被称为 WebApplicationBuilder 注入所需的框架服务,如 IConfiguration 和 IWebHostEnvironment。当您尝试打印已注册的服务时,如下代码片段所示(参考 github.com/PacktPublishing/Enterprise-Application-Development-with-C-10-and-.NET-6-Second-Edition/blob/main/Chapter05/DITypes/Program.cs#L16 中的代码),我们可以列出已注册的框架服务:
foreach(var i in builder.Services.AsEnumerable())
{
Console.WriteLine($"{i.Lifetime} - {i.ServiceType.ToString()}");
}
在 ASP.NET Core 6.0 中,IWebHostEnvironment 框架服务可通过 builder.Environment 属性获取。同样,配置可通过 builder.Configuration 属性获取。
ASP.NET Core 6 运行时实例化所有必需的框架服务并将它们注册到 IoC 容器中。从 ASP.NET Core 6 开始,它们可通过 Program.cs 中的 WebApplication 和 WebApplicationBuilder 的属性访问。这些框架服务可以通过我们在上一节中讨论的任何 DI 类型注入到控制器和其他服务中。
应用程序服务是由开发人员注入到容器中的服务。这些服务将使用 WebApplicationBuilder 的 Services 属性进行注册。以下代码片段展示了如何将 IWeatherProvider 应用程序服务注册到容器中:
builder.Services.AddScoped<IWeatherProvider, WeatherProvider>();
在下一节中,我们将了解这些服务的生命周期以及它们是如何被管理的。
注意
请参阅第十章,创建 ASP.NET Core 6 Web API,了解 Program.cs 文件中的代码。
理解服务生命周期
当您使用指定的生命周期注册服务时,容器将根据指定的生命周期自动销毁对象。在 Microsoft DI 容器中,可以使用以下三种类型的生命周期:
-
AddTransient扩展方法用于注册此生命周期,如下代码片段所示:public static IServiceCollection AddTransient(this IServiceCollection services, Type serviceType);注意
临时生命周期通常用于无状态、轻量级的服务。
-
ServiceProvider在应用程序关闭时被销毁。使用AddSingleton扩展方法注册此生命周期,如下代码片段所示:public static IServiceCollection AddSingleton(this IServiceCollection services, Type serviceType); -
DbContext使用作用域生命周期进行注册。使用AddScoped扩展方法注册到作用域生命周期范围,如下代码片段所示:public static IServiceCollection AddScoped(this IServiceCollection services, Type serviceType);
在应用程序开发中,需要明智地选择生命周期类型。一个服务不应依赖于生命周期比其短的服务;例如,注册为单例的服务不应依赖于注册为瞬时的服务。以下表格显示了哪些生命周期可以安全地依赖于哪些其他生命周期范围:
<https://github.com/OpenDocCN/freelearn-csharp-zh/raw/master/docs/entp-appdev-csp10-dn6/img/Figure_5.1 – DI relationship>
表 5.1 – 生命周期依赖
作为开发者,你不需要担心范围验证。内置的范围验证在 ASP.NET Core 6 中完成,当环境设置为 InvalidOperationException 时抛出。这可以通过在注册 ServiceProvider 时为所有环境配置启用 ValidateScopes 选项来显式打开。在此代码片段中,当创建主机构建器时,ValidateScopes 设置为 true 以打开范围验证:
builder.Host.UseDefaultServiceProvider(opt => { opt.ValidateScopes = true; });
让我们创建一个 ASP.NET Core 6 Web 应用程序来了解服务生命周期。我们将创建不同的服务并将它们注册为单例、作用域和瞬态生命周期范围,并观察它们的行为。按照以下步骤进行:
-
创建一个新的 ASP.NET Core Web 应用程序(
DISampleWeb)。 -
创建一个名为
Services的新项目文件夹,并添加三个类:ScopedService、SingletonService和TransientService。添加以下代码(所有这些服务都将相同,其中没有任何实际代码;我们只是根据它们的名称将它们注册为不同的生命周期范围):(所有这些服务都将相同,其中没有任何实际代码;我们只是根据它们的名称将它们注册为不同的生命周期范围)public interface IScopedService { } public class ScopedService : IScopedService { } -
SingletonService.cs:此类将使用单例生命周期范围进行注册,如下代码片段所示:public interface ISingletonService { } public class SingletonService : ISingletonService { } -
TransientService.cs:此类将使用瞬态生命周期范围进行注册,如下代码片段所示:public interface ITransientService { } public class TransientService : ITransientService{ } -
现在,在
Program.cs中使用IServiceCollection注册这些服务,如下所示://Register as Scoped builder.Services.AddScoped<IScopedService,ScopedService>(); //Register as Singleton builder.Services.AddSingleton<ISingletonService,SingletonService>(); //Register as Transient builder.Services.AddTransient<ITransientService,TransientService>();
服务描述符集合 IServiceCollection 通过 WebApplicationBuilder 的 Services 属性公开。
-
现在,在
Models文件夹下添加HomeViewModel模型类,该类将用于显示从先前注册的服务检索到的数据。以下代码片段说明了如何进行此操作:public class HomeViewModel { public int Singleton { get; set; } public int Scoped { get; set; } public int Scoped2 { get; internal set; } public int Transient { get; set; } public int Transient2 { get; internal set; } }
由于我们已使用 ASP.NET Core 6 IoC 容器注册了 ScopedService、SingletonService 和 TransientService,我们将通过构造函数注入获取这些服务。
- 现在,我们将添加代码以在
HomeController和Views中获取这些服务,以在主页上显示从这些对象检索到的数据。修改主页控制器以获取两个ScopedService和TransientService的实例,并使用服务对象的哈希码设置ViewModel。解决方案结构如下截图所示:
<https://github.com/OpenDocCN/freelearn-csharp-zh/raw/master/docs/entp-appdev-csp10-dn6/img/Figure_5.2 – Solution structure>
图 5.2 – 解决方案结构
注意
GetHashCode 方法返回对象的哈希码。这将根据实例而变化。
-
修改
HomeController的构造函数以接受已注册的服务,并定义私有字段以引用服务实例,如下所示:private readonly ILogger<HomeController> _logger; private readonly IScopedService scopedService; private readonly IScopedService scopedService2; private readonly ISingletonService singletonService; private readonly ITransientService transientService; private readonly ITransientService transientService2; public HomeController(ILogger<HomeController> logger, IScopedService scopedService, IScopedService scopedService2, ISingletonService singletonService, ITransientService transientService, ITransientService transientService2) { this._logger = logger; this.scopedService = scopedService; this.scopedService2 = scopedService2; this.singletonService = singletonService; this.transientService = transientService; this.transientService2 = transientService2; } -
现在,修改
HomeController下的Index方法,设置HomeViewModel,如下所示:public IActionResult Index() { var viewModel = new HomeViewModel { Scoped = scopedService.GetHashCode(), Scoped2 = scopedService2.GetHashCode(), Singleton = singletonService.GetHashCode(), Transient = transientService.GetHashCode(), Transient2 = transientService2.GetHashCode(), }; return View(viewModel); } -
接下来,修改
~/Views/Home文件夹下的Index.cshtml,以在页面上显示HomeViewModel,如下所示:@model HomeViewModel @{ ViewData["Title"] = "Home Page"; } <h2 class="text-success">Singleton.</h2> <p> <strong>ID:</strong> <code>@Model.Singleton </code> </p> <h2 class="text-success">Scoped instance 1</h2> <p> <strong>ID:</strong> <code>@Model.Scoped</code> </p> <h2 class="text-success">Scoped instance 2</h2> <p> <strong>ID:</strong> <code>@Model.Scoped2</code> </p> <h2 class="text-success">Transient instance 1</h2> <p> <strong>ID:</strong> <code>@Model.Transient</code> </p> <h2 class="text-success">Transient instance 2</h2> <p> <strong>ID:</strong> <code>@Model.Transient2</code> </p> -
现在,运行应用程序。你会看到如下输出:
图 5.3 – 第一次运行的示例输出
如果我们观察输出,ScopedService是相同的。这是因为对于每个请求作用域,只为IScopedService创建一个对象。请注意,当你运行代码时,ID 可能不同,因为它们是在运行时生成的。
临时服务的 ID 对于两个服务都是不同的。正如我们所学的,这是因为每次对 IoC 容器的请求都会创建一个新的实例。
- 现在,再次刷新页面。你会看到类似如下输出:
图 5.4 – 第二次运行的示例输出
如果我们比较图 5.3 和图 5.4 中的输出,我们会注意到SingletonService的 ID 没有改变——这是因为应用程序的生命周期中,对于单例对象只创建一个对象。到目前为止,我们已经看到了如何根据注册来管理服务生命周期。了解何时销毁对象也同样重要。在下一节中,我们将学习关于服务销毁的内容。
服务销毁
如我们在本章前面所学,对象的销毁是 IoC 容器框架的责任。容器会调用实现IDisposable接口的服务的Dispose方法。由容器创建的服务不应被开发者显式销毁。同样,开发者负责销毁他们创建的实例。
考虑以下代码片段,其中SingletonService实例以单例作用域注册:
var _disposableSingletonService= new DisposableSingletonService();
// Registering an instance of a class with singleton lifetime
builder.Services.AddSingleton<IDisposableSingletonService>(_disposableSingletonService);
注意
对于DisposableSingletonService的简单实现,请参考以下链接中的代码:github.com/PacktPublishing/Enterprise-Application-Development-with-C-10-and-.NET-6-Second-Edition/blob/main/Chapter05/DISampleWeb/Services/DisposableSingletonService.cs。
在前面的代码片段中,我们创建了一个 DisposableSingletonService 的对象,并将其注册到 IoC 容器中。服务实例不是由容器创建的。在这种情况下,IoC 容器不会释放该对象;开发者有责任释放它。我们可以在 IHostApplicationLifetime 的 ApplicationStopping 事件触发时释放对象,该事件通过 WebApplication 的 Lifetime 属性公开,如下面的代码片段所示:
app.Lifetime.ApplicationStopping.Register(() => {
_disposableSingletonService.Dispose();
});
在前面的代码片段中,IHostApplicationLifetime 由运行时注入到 WebApplication 中。此接口允许消费者接收 ApplicationStarted、ApplicationStopped 和 ApplicationStopping 应用程序生命周期事件的通知。为了释放单例对象,我们将通过注册到 ApplicationStopping 生命周期事件来调用 Dispose() 方法。
.NET 6 中 DI 的新增功能是对作用域的 IAsyncDisposable 支持。在 IServiceProvider 中添加了一个新的 CreateAsyncScope 扩展方法来支持异步服务作用域的创建,并添加了一个 AsyncServiceScope 包装器,它实现了 IAsyncDisposable。以下代码展示了如何异步地释放作用域:
// Refer AsyncDisposableScope sample code for the implementation
await using (var scope = provider.CreateAsyncScope())
{
var foo = scope.ServiceProvider.GetRequiredService<IWeatherProviderAsync>();
}
从现在开始,如果你在手动创建作用域的地方使用依赖 CreateAsyncScope。
注意
请参考以下 Microsoft 文档以了解更多关于 DI 指南的信息:
docs.microsoft.com/en-us/dotnet/core/extensions/dependency-injection-guidelines
到目前为止,我们已经探讨了服务生命周期以及它们在 .NET 6 中的释放方式。在下一节中,我们将学习如何管理应用程序服务。
管理应用程序服务
在 ASP.NET Core 6 中,当请求被 MvcMiddleware 接收到时,会使用路由来选择控制器和操作方法。IControllerActivator 会创建控制器的实例,并从依赖注入容器中加载构造函数参数。
在 理解服务生命周期 部分,我们看到了应用程序服务是如何注册的以及它们的生命周期是如何管理的。在示例中,服务是通过构造函数注入的,这被称为构造函数注入。在本节中,我们将了解如何实现方法注入,并探讨在 ASP.NET Core 6 的 IoC 容器中应用程序服务可以如何注册和访问。
通过方法注入访问已注册的服务
在前面的章节中,我们看到了依赖服务是如何注入到控制器构造函数中,并且引用被存储在一个局部字段中,用于调用依赖的方法/应用程序编程接口(API)。
有时,我们不想在控制器的所有操作中都有依赖服务可用。在这种情况下,可以通过方法注入来注入服务。这通过创建一个带有[FromServices]属性的参数来完成,如下面的示例所示:
public IActionResult Index([FromServices] ISingletonService singletonService2)
{
}
ASP.NET Core 6 中引入的最小 API 允许我们在路由处理程序中请求 DI 服务,而无需显式使用[FromServices]属性,如下所示:
app.MapGet("/", (ISingletonService service) => service.DoAction());
你可能想知道运行时如何区分注入的服务和其他参数。为了实现这一点,.NET6 引入了一个新的IServiceProviderIsService接口,它有助于识别给定的服务类型已注册在 DI 容器中,而不需要创建其实例。
在下一节中,我们将看到同一服务类型的多个实例的注册以及如何访问它们。
注册多个实例
对于给定的接口,我们可以使用 IoC 容器注册多个实现。
注意
如果为同一服务类型注册了多个实现,则最后一个注册将优先于所有之前的注册。
考虑以下服务注册,其中IWeatherForecastService服务注册了两个实现——WeatherForecastService和WeatherForecastServiceV2:
services.AddScoped<IWeatherForecastService, WeatherForecastService>();
services.AddScoped<IWeatherForecastService, WeatherForecastServiceV2>();
现在,当从控制器请求IWeatherForecastService的实例时,将提供WeatherForecastServiceV2的实例,如下面的代码片段所示:
private readonly IWeatherForecastService weatherForecastService;
public WeatherForecastController(ILogger<WeatherForecastController> logger, IWeatherForecastService weatherForecastService)
{
_logger = logger;
this.weatherForecastService = weatherForecastService;
}
在前面的示例中,可能看起来WeatherForecastV2的注册覆盖了WeatherForecastService的先前注册。然而,ASP.NET Core 6 的 IoC 容器将包含所有IWeatherForecastService的注册。要获取所有注册,请按如下方式获取服务作为IEnumerable:
private readonly IEnumerable<IWeatherForecastService> weatherForecastServices;
public WeatherForecastController(
ILogger<WeatherForecastController> logger, IEnumerable<IWeatherForecastService> weatherForecastServices)
{
_logger = logger;
this.weatherForecastServices = weatherForecastServices;
}
这在执行WebApplicationBuilder的Services属性等场景中可能很有用。因此,在未来,当添加或删除现有规则时,更改将仅限于向Services属性注入新的服务。
使用 TryAdd
在本节中,我们将了解如何避免意外覆盖已注册的服务。
TryAdd扩展方法仅在不存在相同服务的注册时注册服务。TryAdd扩展方法对所有生命周期范围(TryAddScoped、TryAddSingleton和TryAddTransient)都可用。
如以下代码片段所示,使用服务注册时,当请求IWeatherForecastService时,IoC 容器提供WeatherForecastService,而不是WeatherForecastServiceV2:
services.AddScoped<IWeatherForecastService, WeatherForecastService>();
services.TryAddScoped<IWeatherForecastService, WeatherForecastServiceV2>();
为了克服可能因重复注册而产生的副作用,始终建议使用TryAdd扩展方法来注册服务。
现在,让我们看看如何替换已注册的服务。
替换现有注册
ASP.NET Core 6 IoC 容器提供了一种替换现有注册的方法。在下面的示例中,IWeatherForecastService 首先使用 WeatherForecastService 进行注册。然后它被替换为 WeatherForecastServiceV2:
builder.Services.TryAddScoped<IWeatherForecastService, WeatherForecastService>();
builder.Services.Replace(ServiceDescriptor.Scoped<IWeatherForecastService, WeatherForecastServiceV2>());
与 WeatherForecastServiceV2 的 Replace 实例一样,一个实现被提供给 WeatherForecastController 构造函数。在下面的代码片段中,与 注册多个实例 部分不同,我们将在 weatherForecastService 构造函数变量中只看到一个对象:
public WeatherForecastController(ILogger<WeatherForecastController> logger, IEnumerable<IWeatherForecastService> weatherForecastService)
{
_logger = logger;
this.weatherForecastService = weatherForecastService;
}
到目前为止,在本节中,我们已经学习了如何使用 IoC 容器注册和替换服务。有时我们可能需要删除当前的注册。考虑这样一个场景,你希望利用库中的服务和注册,但你没有访问其源代码的权限。如果你重新实现了该库的一些接口并将它们重新注册到容器中,你可能会看到一些意外的行为。在下一节中,我们将了解如何删除已注册的服务。
删除现有注册
要删除现有注册,ASP.Net Core 6 IoC 容器提供了 Remove 扩展方法。你可以使用 RemoveAll 方法删除与某个服务相关的所有注册,如下面的代码片段所示:
services.RemoveAll<IWeatherForecastService>();
在下面的代码片段中,Remove 方法从容器中移除了 WeatherForecastService 实现的注册:
//Removes the first registration of IWeatherForecastService
Builder.Services.Remove(ServiceDescriptor.Scoped<IWeatherForecastService, WeatherForecastService>());
到目前为止,我们已经看到了如何处理复杂的服务,但当涉及到泛型开放类型时,注册每个构造的泛型类型将会变得困难。在下一节中,我们将学习如何处理泛型开放类型服务。
注意
要了解更多关于泛型类型的信息,你可以参考以下网站:docs.microsoft.com/en-us/dotnet/csharp/language-reference/language-specification/types。
注册泛型
本节将介绍如何使用依赖注入处理泛型类型服务。
对于泛型类型,为每种正在使用的实现类型注册服务是没有意义的。ASP.NET Core 6 IoC 容器提供了一种简化泛型类型注册的方法。框架本身已经提供的一个此类类型是 ILogger,如下面的代码片段所示:
Builder.Services.TryAdd(ServiceDescriptor.Singleton(typeof(ILogger<>), typeof(Logger<>)));
注意
为了方便参考,你可以访问以下链接:github.com/dotnet/runtime/blob/main/src/libraries/Microsoft.Extensions.Logging/src/LoggingBuilderExtensions.cs。
泛型的一个用例是与数据访问层一起使用的泛型仓储模式。
在我们拥有的所有注册信息中,ConfigureServices 方法可能会变得很大,以至于不再可读。下一节将帮助你了解如何解决这个问题。
代码可读性的扩展方法
ASP.NET Core 6 框架遵循的使代码看起来更易读的模式是创建一个具有逻辑分组的服务注册扩展方法。以下代码尝试使用扩展方法对通知相关服务进行分组和注册。一般做法是使用 Microsoft.Extensions.DependencyInjection 命名空间来定义服务注册扩展方法。这将使开发者只需使用 Microsoft.Extensions.DependencyInjection 命名空间就能使用所有与依赖注入相关的功能。
在下面的代码片段中,使用 AddNotificationServices 注册了与通知相关的服务:
namespace Microsoft.Extensions.DependencyInjection
{
public static class NotificationServicesServiceCollectionExtension
{
public static IServiceCollection AddNotificationServices(this IServiceCollection services)
{
services.TryAddScoped<INotificationService, EmailNotificationService>();
services.TryAddScoped<INotificationService, SMSNotificationService>();
return services;
}
}
现在扩展方法已经创建,我们可以使用 AddNotificationServices 方法在 ConfigureServices 下注册通知服务。这将使 ConfigureServices 更易于阅读。代码如下所示:
builder.Services.AddNotificationServices();
我们已经看到了如何将服务注入到控制器和其他类中。在下一节中,我们将学习如何将服务注入到视图中。
Razor Pages 中的依赖注入
MVC 中视图的目的是显示数据。大多数情况下,在视图中显示的数据是从控制器传递过来的。考虑到 关注点分离(SoC)原则,建议从控制器传递所有必要的数据,但可能存在我们想要从诸如本地化和遥测服务之类的页面查看特定服务的情况。使用由 Razor 视图支持的依赖注入,我们可以将这些服务注入到视图中。
要了解如何将服务注入到视图中,让我们修改在前几章中创建的 DISampleWeb 应用程序。我们将修改 DISampleWeb 应用程序,以便在设置飞行标志的情况下在主页上显示额外内容。将 isFlightOn 配置添加到 appsettings.json 中,如下面的代码片段所示:
{
"AllowedHosts": "*",
"isFlightOn": "true"
}
现在,修改 Home 下的索引视图以显示 Flight 下的内容,如下面的代码片段所示:
@using Microsoft.Extensions.Configuration
@inject Iconfiguration Configuration
@{
string isFlightOn = Configuration["isFlightOn"];
if (string.Equals(isFlightOn, "true", StringComparison.OrdinalIgnoreCase))
{
<h1>
<strong>Flight content</strong>
</h1>
}
}
在这里,提供读取配置文件功能的 IConfiguration 服务通过 @inject 关键字注入到 Razor 视图中。注入的配置服务用于获取配置并基于设置显示额外内容。我们可以使用 @inject 关键字将任何已注册到 IServiceCollection 的服务注入到 Razor 视图中。
到目前为止,我们已经看到了如何利用 .NET 6 内置的 IoC 容器。在下一节中,我们将学习如何利用第三方容器。
使用第三方容器
虽然内置容器对于大多数场景来说已经足够,但 .NET 6 提供了一种与第三方容器集成的途径,如果需要的话可以加以利用。
让我们更详细地看看框架是如何连接服务的。当 Startup 类在 Program.cs 中与 HostBuilder 注册时,.NET 框架使用反射来识别并调用 Configure 和 ConfigureServices 方法。
下面是从 ASP.NET Core 6 的StartupLoader类的LoadMethods方法中摘录的一段代码(请参阅github.com/dotnet/aspnetcore/blob/main/src/Hosting/Hosting/src/Internal/StartupLoader.cs中的代码):
public static StartupMethods LoadMethods(IServiceProvider hostingServiceProvider, [DynamicallyAccessedMembers(StartupLinkerOptions.Accessibility)] Type startupType, string environmentName, object? instance = null)
{
var configureMethod = FindConfigureDelegate(startupType, environmentName);
var servicesMethod = FindConfigureServicesDelegate(startupType, environmentName);
var configureContainerMethod = FindConfigureContainerDelegate(startupType, environmentName);
-----------------------
}
从前面的代码片段中,我们可以看到前两个方法,FindConfigureDelegate和FindConfigureServicesDelegate,是为了找到Configure和ConfigureServices方法。
最后一行是用于ConfigureContainer的。我们可以在Startup类中定义一个ConfigureContainer方法来配置服务到第三方容器中。
下面是可用于 ASP.NET Core 6 的一些流行的 DI 框架:
-
Unity: Unity 最初由微软构建,目前是开源的。这是.NET 中最古老的 DI 容器之一。文档可在以下链接找到:
unitycontainer.org/. -
Autofac: 这是最受欢迎的 DI 容器之一。它提供了全面的文档,可在以下链接找到:
autofaccn.readthedocs.io/en/latest/index.html. -
Simple Injector: 这是列表中较晚出现的一个。文档可在以下链接找到:
simpleinjector.readthedocs.io/en/latest/index.html. -
Castle Windsor: 这是.NET 中可用的最古老的 DI 框架之一。可在以下链接查看其文档:
www.castleproject.org/projects/windsor/.
虽然这些框架之间存在一些差异,但通常功能是相同的。大多数情况下,开发者的体验决定了框架的选择。
在下一节中,我们将看看如何利用 Autofac 第三方 IoC 容器。
Autofac IoC 容器
Autofac 是开发者社区中最受欢迎的 IoC 容器之一。与其他任何 IoC 容器一样,它管理类之间的依赖关系,以便随着应用程序复杂性和规模的增加,应用程序仍然易于更改。让我们学习如何使用 Autofac 注册我们在本章前面使用过的相同WeatherProvider服务。按照以下步骤进行:
-
使用 ASP.NET Core Web API 模板创建一个新的项目,并将其命名为
AutofacSample。 -
将
Autofac.Extensions.DependencyInjectionNuGet 包引用添加到AutofacSample项目中,如图所示:
Figure 5.5 – 添加 Autofac.Extensions.DependencyInjection NuGet 包
-
我们需要将
AutofacServiceProviderFactory与ConfigureHostBuilder注册,以便运行时使用 Autofac IoC 容器。在Program.cs中,注册 Autofac SP 工厂,如下面的代码片段所示:builder.Host.UseServiceProviderFactory(new AutofacServiceProviderFactory()); -
现在,让我们将我们在DI 类型部分使用的
IWeatherProvider服务注册到 Autofac 容器中。在Program.cs中,通过WebApplicationBuilder的ConfigureHostBuilder属性的ConfigureContainer方法将IWeatherProvider与WeatherProvider实现注册,如下所示:builder.Host.ConfigureContainer<ContainerBuilder>(builder => { builder.RegisterType<WeatherProvider>() .As<IWeatherProvider>(); }); -
与默认.NET IoC 容器类似,我们将
IWeatherForecast服务注入到WeatherForecastController控制器中,如下面的代码片段所示:public class WeatherForecastController : ControllerBase { private readonly ILogger<WeatherForecastController> _logger; private readonly IWeatherProvider weatherProvider; public WeatherForecastController( ILogger<WeatherForecastController> logger, IWeatherProvider weatherProvider) { _logger = logger; this.weatherProvider = weatherProvider; } [HttpGet] public IEnumerable<WeatherForecast> Get() { return weatherProvider.GetForecast(); } }
现在,当你运行项目并导航到https://localhost:7184/WeatherForecast 统一资源标识符 (URI)时,你将在浏览器中看到以下输出:
图 5.6 – 容器的最终输出
在上一个示例中,我们看到了使用第三方 Autofac IoC 容器代替.NET 6 提供的默认容器。
摘要
本章向您介绍了 DI 的概念,这有助于编写松散耦合、更易于测试和更易于阅读的代码。本章涵盖了 DI 的类型以及它们如何在 ASP.NET Core 6 中得到支持。我们还看到了如何使用不同类型的注册来管理对象的生命周期。本章还向您介绍了一些流行的第三方 IoC 容器,以进一步探索。我们将使用本章学到的概念来构建我们的电子商务应用程序。在第十五章的测试中,我们还将看到 DI 如何帮助提高可测试性。
如第一章中建议的,在关注点分离/单一责任架构部分,我们总是尝试通过接口注册服务。这将有助于在任何时候更改具体实现,而不会更改客户端实现。
在下一章中,我们将学习如何配置.NET 6 并了解不同的配置,同时学习如何构建自定义配置。
问题
- 以下哪个不是框架服务?
a. IConfiguration
b. IApplicationBuilder
c. IWeatherService
d. IWebHostEnvironment
答案:c
- 真或假:DI 是实现 IoC 的一种机制。
a. 真的
b. 假的
答案:a
- 真或假:注入的服务可以依赖于生命周期比其短的服务。
a. 真的
b. 假的
答案:b
- 以下哪个不是 ASP.NET Core 6 IoC 容器的有效生命周期范围?
a. 作用域
b. 单例
c. 持久
d. 动态
答案:d
第六章:第六章:.NET 6 中的配置
.NET 6 中的配置包括默认设置以及应用程序的运行时设置;配置是一个非常强大的功能。我们可以更新设置,如功能标志以启用或禁用功能、依赖服务端点、数据库连接字符串、日志级别等,并在不重新编译的情况下控制应用程序的运行时行为。
在本章中,我们将涵盖以下主题:
-
理解配置
-
利用内置配置提供程序
-
构建自定义配置提供程序
到本章结束时,你将很好地掌握配置概念、配置提供程序以及如何在项目中利用它们,以及能够识别适合你应用程序的配置和配置源。
技术要求
你需要对 .NET 和 Azure 有基本的了解。本章的代码可以在以下位置找到:https://github.com/PacktPublishing/Enterprise-Application-Development-with-C-10-and-.NET-6-Second-Edition/tree/main/Chapter06。
理解配置
配置通常存储为 Program.cs(),你将获得 .NET 6 提供的默认配置。此外,你可以配置不同的内置和自定义配置源,并在需要时使用不同的配置提供程序读取它们:
图 6.1 – 应用和配置
上述图示显示了应用程序、配置提供程序和配置文件之间的高级关系。应用程序使用配置提供程序从配置源读取配置;配置可以是环境特定的。Env A 可能是你的开发环境,而 Env B 可能是你的生产环境。在运行时,应用程序将根据其运行时的上下文和环境读取正确的配置。
在下一节中,我们将了解默认配置的工作原理以及如何从 appsettings.json 文件中添加和读取配置。
默认配置
要了解默认配置的工作原理,让我们创建一个新的 .NET 6 Web API,将项目名称设置为 TestConfiguration,并打开 Program.cs 文件。以下是从 Program.cs 文件中的代码片段:
var builder = WebApplication.CreateBuilder(args);
// Add services to the container.
builder.Services.AddControllers();
builder.Services.AddEndpointsApiExplorer();
builder.Services.AddSwaggerGen();
var app = builder.Build();
// Configure the HTTP request pipeline.
if (app.Environment.IsDevelopment())
{
app.UseSwagger();
app.UseSwaggerUI();
}
app.UseHttpsRedirection();
app.UseAuthorization();
app.MapControllers();
app.Run();
从前面的代码中,我们看到 WebApplication.CreateBuilder 负责为应用程序提供默认配置。
配置的加载顺序如下:
-
MemoryConfigurationProvider:此提供程序从内存集合中加载配置,作为配置键值对。 -
ChainedConfigurationProvider: 这添加主机配置并将其设置为第一个来源。有关主机配置的更多详细信息,您可以参考此链接:docs.microsoft.com/en-us/aspnet/core/fundamentals/configuration/?view=aspnetcore-6.0。 -
JsonConfigurationProvider: 这将从appsettings.json文件中加载配置。 -
JsonConfigurationProvider: 这将从appsettings.Environment.json文件中加载配置;在appsettings.Environment.json中的Environment可以设置为开发、测试或生产。 -
EnvironmentVariablesConfigurationProvider: 这将加载环境变量配置。
如本节开头所述,配置在来源中指定为键值对。后来添加的配置提供程序(按顺序)覆盖了之前的键值对设置。例如,如果您在 MemoryConfigurationProvider 和 JsonConfigurationProvider 中都有 DbConnectionString 键,则 JsonConfigurationProvider 中的 DbConnectionString 键的值将覆盖 MemoryConfigurationProvider 的键值对设置。
当您调试 Program.cs 代码时,您可以看到由 CreateDefaultBuilder 提供的默认配置被注入到配置中,如下所示:
图 6.2 – 默认配置来源
在下一节中,我们将看看如何添加我们应用程序所需的配置。
添加配置
如前一小节所示,有多种配置来源可用。在现实世界的项目中,appsettings.json 文件是最广泛使用的,用于添加应用程序所需的配置,除非它是秘密信息,不能以纯文本形式存储。
让我们看看在需要配置的几个常见场景:
-
如果我们需要一个
ApplicationInsights仪表化密钥来添加应用程序遥测,这可以是配置的一部分 -
如果我们有需要调用的依赖服务,这些服务可以是配置的一部分
这些配置可能因环境而异(开发环境和生产环境中的值不同)。
您可以将以下配置添加到 appsettings.json 文件中,以便在发生更改时直接更新它,并在无需重新编译和部署的情况下开始使用它:
"ApplicationInsights": {
"InstrumentationKey": "<Your instrumentation key>"
}
"ApiConfigs": {
"Service 1": {
"Name": "<Your dependent service name 1>",
"BaseUri": "<Service base uri>",
"HttpTimeOutInSeconds": "<Time out value in
seconds>",
"ApiURLs": [
{
"EndpointName": "<End point 1>"
},
{
"EndpointName": "<End point 2>"
}
]
},
"Service 2": {
"Name": "<Your dependent service name 2>",
"BaseUri": "<Service base uri>",
"HttpTimeOutInSeconds": "<Time out value in
seconds>",
"ApiURLs": [
{
"EndpointName": "<End point 1>"
},
{
"EndpointName": "<End point 2>"
}
]
}
}
从前面的代码中,我们看到我们为 ApplicationInsights 仪表化密钥添加了一个键值对,其中键是 InstrumentationKey 字符串,值是应用程序需要用于在 ApplicationInsights 中仪表化遥测的实际仪表化密钥。在 ApiConfigs 部分,我们以分层顺序添加了多个键值对,包括调用我们的依赖服务所需的配置。
在下一节中,我们将看到如何读取我们已添加的配置。
读取配置
我们已经看到了如何将配置添加到 appsettings.json。在本节中,我们将看到如何使用不同的选项在我们的项目中读取它们。
在 Program.cs 中由 WebApplication.CreateBuilder 提供的 builder.Configuration 对象实现了 Microsoft.Extensions.Configuration.IConfiguration 类型,您有以下选项可用于读取 IConfiguration:
// Summary:
// Gets or sets a configuration value.
// Parameters:
// key:
// The configuration key.
// Returns:
// The configuration value.
string this[string key] { get; set; }
// Summary:
//Gets the immediate descendant configuration sub-
//sections.
// Returns:
// The configuration sub-sections.
IEnumerable<IConfigurationSection> GetChildren();
// Summary:
// Gets a configuration sub-section with the
// specified key.
// Parameters:
// key:
// The key of the configuration section.
// Returns:
//The Microsoft.Extensions.Configuration
//.IConfigurationSection.
// Remarks:
// This method will never return null. If
// no matching sub-section is found with
// the specified key, an empty
//Microsoft.Extensions.Configuration.IConfiguration
// Section will be returned.
IConfigurationSection GetSection(string key);
让我们看看如何利用 Iconfiguration 中的这些选项来读取我们在上一节中添加的配置,添加配置。
要从 appsettings.json 中读取 ApplicationInsights 仪表化密钥,我们可以在 Program.cs 中使用以下代码使用 string this[string key] { get; set; } 选项:
builder.Configuration["ApplicationInsights:InstrumentationKey"];
要读取 ApiConfigs,我们可以使用以下代码。我们可以使用分隔符在配置键中读取层次化配置:
builder.Configuration["ApiConfigs:Service 1:Name"];
注意
使用分隔符这种方式读取容易出错且难以维护。首选的方法是使用 ASP.NET Core 提供的 options 模式。而不是逐个读取每个键/设置值,选项模式使用类,这也会为您提供对相关设置的强类型访问。
当配置设置通过场景隔离到强类型类中时,应用程序遵循两个重要的设计原则:
-
接口隔离原则(ISP)或封装原则
-
关注点分离
使用 ISP 或封装,您通过一个定义良好的接口或契约读取配置,并且只依赖于您需要的配置设置。此外,如果有一个非常大的配置文件,这将有助于关注点分离,因为应用程序的不同部分不会依赖于相同的配置,从而允许它们解耦。让我们看看我们如何利用代码中的选项模式。
您可以创建以下 ApiConfig 和 ApiUrl 类并将它们添加到您的项目中:
public class ApiConfig
{
public string Name { get; set; }
public string BaseUri { get; set; }
public int HttpTimeOutInSeconds { get; set; }
public List<ApiUrl> ApiUrls { get; set; }
}
public class ApiUrl
{
public string EndpointName { get; set; }
}
在 Program.cs 中添加以下代码以使用 GetSection 方法读取配置,然后调用 Bind 以将配置绑定到我们已有的强类型类:
List<ApiConfig> apiConfigs = new List<ApiConfig>();
builder.Configuration.GetSection("ApiConfigs").Bind(apiConfigs);
GetSection 将会读取 appsettings.json 中指定的特定部分。Bind 将尝试通过匹配属性名到配置键来将给定的对象实例绑定到配置值。GetSection(string sectionName) 如果请求的部分不存在,将返回 null。在实际的程序中,请确保您添加了空值检查。
在本节中,我们看到了如何通过使用配置 API 添加和读取 appsettings.json 中的数据。我还提到我们应该使用 appsettings.json 用于纯文本,而不是用于机密。在下一节中,我们将探讨内置配置提供程序以及如何使用 Azure Key Vault 配置提供程序添加和读取机密。
利用内置配置提供程序
除了 appsettings.json 之外,还有多个配置源可用,.NET 6 提供了多个内置配置提供程序来读取它们。以下是为 .NET 6 可用的内置提供程序:
-
Azure Key Vault 配置提供程序从 Azure Key Vault 中读取配置。
-
文件配置提供程序从 INI、JSON 和 XML 文件中读取配置。
-
命令行配置提供程序从命令行参数中读取配置。
-
环境变量配置提供程序从环境变量中读取配置。
-
内存配置提供程序从内存集合中读取配置。
-
Azure 应用配置提供程序从 Azure 应用配置中读取配置。
-
按文件密钥配置提供程序从目录的文件中读取配置。
让我们看看如何利用 Azure Key Vault 配置提供程序和文件配置提供程序,因为与其它提供程序相比,它们更为重要且更广泛地被使用。
注意
您可以使用以下链接了解我们在此处未详细介绍的其它配置提供程序:https://docs.microsoft.com/en-us/dotnet/core/extensions/configuration-providers。
Azure Key Vault 配置提供程序
Azure Key Vault 是一种基于云的服务,它提供了一个集中的配置源,用于安全地存储密码、证书、API 密钥和其他机密。这有助于确保我们的应用程序安全并符合安全漏洞的合规性。让我们看看如何创建一个密钥库,向其中添加一个机密,并使用 Azure Key Vault 配置提供程序从应用程序中访问它。
创建密钥库并添加机密
在本节中,我们将使用 Azure Cloud Shell 创建一个密钥库并添加一个机密。Azure Cloud Shell 是基于浏览器的,可以用来管理 Azure 资源。以下是需要您采取的步骤列表:
- 使用
portal.azure.com登录到 Azure 门户。在门户页面上选择云 Shell 图标:
图 6.3 – Azure 云 Shell
- 您将获得选择 Bash 或 PowerShell 的选项。选择 PowerShell。您可以在任何时候更改外壳:
图 6.4 – Azure 云 Shell 选项 – PowerShell 和 Bash
-
使用以下命令创建一个资源组:
az group create --name "{RESOURCE GROUP NAME}" --location {LOCATION}
我为这个演示实际运行的命令如下:
az group create --name "ConfigurationDemoVaultRG" --location "East US"
{RESOURCE GROUP NAME} 代表新资源组的资源组名称,而{LOCATION}代表 Azure 区域(数据中心)。
-
使用以下命令在资源组中创建密钥保管库:
az keyvault create --name {KEY VAULT NAME} --resource-group "{RESOURCE GROUP NAME}" --location {LOCATION}
这里是我在此演示中实际运行的命令:
az keyvault create --name "TestKeyVaultForConfig" --resource-group "ConfigurationDemoVaultRG" --location "East US"
{KEY VAULT NAME} 是新密钥保管库的唯一名称。
{RESOURCE GROUP NAME} 是在上一步骤中创建的新资源组的资源组名称。
{LOCATION} 是 Azure 区域(数据中心)。
-
使用以下命令在密钥保管库中以名称-值对的形式创建密钥:
az keyvault secret set --vault-name {KEY VAULT NAME} --name "SecretName" --value "SecretValue"
这里是我在此演示中实际运行的命令:
az keyvault secret set --vault-name "TestKeyVaultForConfig" --name "TestKey" --value "TestValue"
{KEY VAULT NAME} 与您在上一步骤中创建的密钥保管库名称相同。
SecretName 是您的密钥名称。
SecretValue 是您的密钥值。
我们现在已成功创建了一个名为TestKeyVaultForConfig的密钥保管库,并使用 Azure Cloud Shell 添加了一个密钥为TestKey、值为TestValue的密钥:
图 6.5 – Azure 密钥保管库密钥
您也可以使用 Azure 命令行界面(CLI)创建和管理 Azure 资源。您可以在以下位置了解更多关于 Azure CLI 的信息:https://docs.microsoft.com/en-us/cli/azure/?view=azure-cli-latest。
在下一节中,我们将看到如何让我们的应用程序访问密钥保管库。
授予应用程序访问密钥保管库的权限
在本节中,让我们看看我们的TestConfiguration Web API 如何通过以下步骤访问密钥保管库:
-
在Azure Active Directory(AAD)中注册
TestConfiguration应用程序并创建一个身份。使用 https://portal.azure.com 登录 Azure 门户。 -
导航到Azure Active Directory | 应用程序注册。点击新建注册:
图 6.6 – AAD 新应用程序注册
- 填写默认值并点击注册,如图下所示截图,并记下应用程序(客户端)ID值。稍后访问密钥保管库时需要使用此值:
图 6.7 – AAD 注册完成
- 点击证书和密钥(1) | 新建客户端密钥(2),输入描述(3)值,然后点击添加(4),如图下所示截图。记下在新客户端密钥下显示的AppClientSecret值,这是应用程序在请求令牌时用来证明其身份的:
图 6.8 – 为其身份创建 AAD 新应用程序密钥
- 使用访问策略授予应用程序访问密钥保管库的权限。搜索您刚刚创建的密钥保管库并选择它:
图 6.9 – 密钥保管库搜索
- 在密钥保管库属性中,选择 设置 下的 访问策略,然后点击 添加访问策略:
![Figure 6.10 – Key Vault 访问策略
![img/Figure_6.10_B18507.jpg]
Figure 6.10 – Key Vault 访问策略
- 在 选择主体 字段中,搜索您的应用程序并选择您的应用程序访问 Key Vault 所需的权限,然后点击 添加:
![Figure 6.11 – 添加访问策略
![img/Figure_6.11_B18507.jpg]
Figure 6.11 – 添加访问策略
- 添加策略后,您必须保存它。这将完成授予您的应用程序访问 Key Vault 的过程。
我们现在已授予我们的应用程序访问 Key Vault 的权限。在下一节中,我们将看到如何使用 Azure Key Vault 配置提供程序从我们的应用程序访问 Key Vault。
利用 Azure Key Vault 配置提供程序
在本节中,我们将对我们的应用程序进行配置和代码更改,以利用 Azure Key Vault 配置提供程序并从 Key Vault 获取密钥,如下所示:
![Figure 6.12 – 开发期间访问 Key Vault
![img/Figure_6.12_B18507.jpg]
Figure 6.12 – 开发期间访问 Key Vault
以下是要更改的列表:
- 将密钥保管库名称、从 Figure 6.7 记下的
AppClientId值和从 AAD 的 Figure 6.8 记下的AppClientSecret值添加到您的TestConfigurationWeb API 的appsettings.json文件中:
![Figure 6.13 – appsettings.json 中的 Key Vault 部分
![img/Figure_6.13_B18507.jpg]
Figure 6.13 – appsettings.json 中的 Key Vault 部分
-
安装以下 NuGet 包:
-
Microsoft.Azure.KeyVault -
Microsoft.Extensions.Configuration.AzureKeyVault -
Microsoft.Azure.Services.AppAuthentication
-
-
将
Program.cs更新为利用 Azure Key Vault 配置提供程序来使用您的密钥保管库。以下代码将 Azure Key Vault 添加为另一个配置源,并使用 Azure Key Vault 配置提供程序获取所有配置:using TestConfiguration; var builder = WebApplication.CreateBuilder(args); // Add services to the container. builder.Services.AddControllers(); //Removed code for brevity builder.Configuration.AddAzureKeyVault($"https://{builder.Configuration["KeyVault:Name"]}.vault.azure.net/", builder.Configuration["KeyVault:AppClientId"], builder.Configuration["KeyVault:AppClientSecret"]); var app = builder.Build(); //Removed code for brevity -
将
WeatherForecastController.cs更新为从 Key Vault 读取密钥,如下所示:[ApiController] [Route("[controller]")] public class WeatherForecastController : ControllerBase { private readonly ILogger<WeatherForecastController> _logger; private readonly IConfiguration _configuration; public WeatherForecastController(ILogger<Weather ForecastController> logger, IConfiguration configuration) { _logger = logger; _configuration = configuration; } [HttpGet] public IEnumerable<string> Get() { return new string[] { "TestKey", _configuration["TestKey"] }; } }
按照此处共享的代码示例包含所有引用。您可以运行应用程序并查看结果:
![Figure 6.14 – Key Vault 结果
![img/Figure_6.14_B18507.jpg]
Figure 6.14 – Key Vault 结果
应用程序将能够使用 Azure Key Vault 配置提供程序访问密钥保管库并获取密钥。这非常简单,因为所有繁重的工作都由 .NET 6 完成,我们只需要安装 NuGet 包并添加几行代码。然而,你现在可能正在思考 AppClientId 和 AppClientSecret 是如何添加到 appsettings.json 配置文件中,以及这种方式并不十分安全。你完全正确。
我们可以通过两种方式解决这个问题:
-
一种选择是将这些值加密并存储在
appsettings.json中;然后可以在代码中读取和解密。参考
对于开发中应用程序机密的安全存储,请参阅 https://docs.microsoft.com/en-us/aspnet/core/security/app-secrets?view=aspnetcore-6.0&tabs=windows。
-
另一个选项是使用托管身份访问 Azure 资源,这允许应用程序使用 AAD 认证对 Azure Key Vault 进行身份验证,而无需凭证(应用程序 ID 和密码/客户端密钥)。
您的应用程序可以使用其身份通过支持 AAD 认证的服务进行身份验证,例如 Azure Key Vault,这将帮助我们从代码中移除凭证:
图 6.15 – 应用程序部署后生产环境中访问密钥保管库
注意
这是我们将应用于已部署到生产环境的应用程序的最佳实践。在代码中管理凭证是一个常见的挑战,保持凭证的安全和保密是一个重要的安全要求。Azure 资源在 Azure Active Directory (AAD) 中的托管身份有助于解决这个挑战。托管身份为 Azure 服务提供在 AAD 中自动管理的身份。您可以使用此身份对支持 AAD 认证的任何服务进行身份验证,包括密钥保管库,而无需在您的代码中包含任何凭证。
您可以在此处了解更多关于托管身份的信息:https://docs.microsoft.com/en-us/azure/active-directory/managed-identities-azure-resources/overview。
在本节中,我们学习了如何创建密钥保管库,如何将机密添加到密钥保管库,如何将我们的TestConfiguration Web API 注册到 Azure Active Directory (AAD),如何创建机密或身份,如何让TestConfiguration Web API 获取对密钥保管库的访问权限,以及如何使用 Azure Key Vault 配置提供程序从我们的代码中访问密钥保管库。您还可以通过使用 Visual Studio 连接服务将密钥保管库添加到您的 Web 应用程序中,具体操作请参阅 https://docs.microsoft.com/en-us/azure/key-vault/general/vs-key-vault-add-connected-service:
图 6.16 – Azure Key Vault 作为连接服务
在下一节中,我们将了解如何利用文件配置提供程序。
文件配置提供程序
文件配置提供程序帮助我们从文件系统中加载配置。JSON 配置提供程序和 XML 配置提供程序分别从文件配置提供程序类继承,用于从 JSON 文件和 XML 文件中读取键值对配置。让我们看看如何将它们作为CreateHostBuilder的一部分添加到配置源中。
JSON 配置提供程序
可以使用以下代码在Program.cs中配置 JSON 配置提供程序:
//Removed code for brevity
builder.Configuration.AddJsonFile("AdditionalConfig.json",
optional: true,
reloadOnChange: true);
//Removed code for brevity
在这种情况下,JSON 配置提供者将加载 AdditionalConfig.json 文件,AddJsonFile 方法的三个参数为我们提供了指定文件名、文件是否可选以及文件在文件被修改时是否必须重新加载的选项。
以下是一个 AdditionalConfig.json 示例文件:
{ "TestKeyFromAdditionalConfigJSON":"TestValueFromAdditional ConfigJSON"}
然后,我们将更新 WeatherForecastController.cs 以从加载自 AdditionalConfig.json 配置文件的配置中读取键值对,如下所示:
//Removed code for brevity
[HttpGet]
public Ienumerable<string> Get()
{
return new string[] {
" TestKeyFromAdditionalConfigJSON",
_configuration["TestKeyFromAdditionalConfigJSON"] };
}
//Removed code for brevity
您可以运行应用程序并查看结果。应用程序将能够访问 AdditionalConfig.json 文件并读取配置。在下一节中,我们将探讨 XML 配置提供者。
XML 配置提供者
我们将在项目中添加一个名为 AdditionalXMLConfig.xml 的新文件和所需的配置。然后,可以使用以下代码在 Program.cs 中配置 XML 配置提供者,以从我们添加的文件中读取:
//Removed code for brevity
builder.Configuration.AddXmlFile("AdditionalXMLConfig.xml",
optional: true,
reloadOnChange: true);
//Removed code for brevity
在这种情况下,XML 配置提供者将加载 AdditionalXMLConfig.xml 文件,三个参数为我们提供了指定 XML 文件、文件是否可选以及文件在发生任何更改时是否必须重新加载的选项。
以下是一个 AdditionalXMLConfig.xml 示例文件:
<?xml version="1.0" encoding="utf-8" ?>
<configuration>
<TestKeyFromAdditionalXMLConfig>TestValueFrom
AdditionalXMLConfig</TestKeyFromAdditionalXMLConfig>
</configuration>
接下来,我们将更新 WeatherForecastController.cs 以从 AdditionalXMLConfig.xml 加载的配置中读取键值对,如下所示:
[HttpGet]
public Ienumerable<string> Get()
{
return new string[] {
"TestKeyFromAdditionalXMLConfig",
_configuration["TestKeyFromAdditionalXMLConfig"] };
}
您可以运行应用程序并查看结果。应用程序将能够访问 AdditionalXMLConfig.xml 并读取配置。在 .NET 6 中,JSON 配置文件和 JSON 配置提供者可用,因此您不需要 XML 配置文件和 XML 配置提供者。话虽如此,我们刚才讨论的内容是针对那些喜欢 XML 文件和开闭标签的人来说的,例如。
在下一节中,我们将探讨为什么需要自定义配置提供者以及如何构建一个。
构建自定义配置提供者
在上一节中,我们讨论了 .NET 6 中的内置或现有配置提供者。存在一些场景,许多系统在数据库中维护应用程序配置设置。这些设置可以由管理员通过门户管理,或者由支持工程师通过运行数据库脚本来创建/更新/删除应用程序配置设置。
.NET 6 并没有内置的提供者来从数据库中读取配置。让我们看看如何通过以下步骤构建一个自定义配置提供者来从数据库中读取:
-
实现配置源:为了创建配置提供者的实例
-
实现配置提供者:为了从适当的源加载配置
-
实现配置扩展:为了将配置源添加到配置构建器中
让我们从配置源开始。
配置源
配置源的责任是创建配置提供者的一个实例并将其返回到源。它需要继承自IConfigurationSource接口,这要求我们实现ConfigurationProvider Build(IConfigurationBuilder builder)方法。
在Build方法实现中,我们需要创建自定义配置提供者的一个实例并返回它。还应该有构建构建器所需的参数。在这种情况下,因为我们正在构建自定义 SQL 配置提供者,所以重要的参数是连接字符串和 SQL 查询。以下代码片段展示了SqlConfigurationSource类的一个示例实现:
public class SqlConfigurationSource : IConfigurationSource
{
public string ConnectionString { get; set; }
public string Query { get; set; }
public SqlConfigurationSource(string
connectionString, string query)
{
ConnectionString = connectionString;
Query = query;
}
public IConfigurationProvider
Build(IConfigurationBuilder builder)
{
return new SqlConfigurationProvider(this);
}
}
如您所见,实现这个方法非常简单且容易。您获取构建提供者所需的参数,然后创建提供者的新实例,然后返回这些参数。让我们看看如何在下一节中构建一个 SQL 配置提供者。
配置提供者
配置提供者的责任是从适当的位置加载所需的配置并返回相同的配置。它需要继承自IConfigurationProvider接口,这要求我们实现Load()方法。配置提供者类可以继承自ConfigurationProvider基类,因为它已经实现了IConfigurationProvider接口中的所有方法。这将帮助我们节省时间,因为我们不需要实现未使用的方法,而只需实现Load方法即可。
在Load方法实现中,我们需要有从源获取配置数据的逻辑。在这种情况下,我们将执行一个查询从 SQL 存储中获取数据。以下代码片段展示了SqlConfigurationProvider类的一个示例实现:
public class SqlConfigurationProvider : ConfigurationProvider
{
public SqlConfigurationSource Source { get; }
public SqlConfigurationProvider
(SqlConfigurationSource source)
{
Source = source;
}
public override void Load()
{
try
{
// create a connection object
SqlConnection sqlConnection = new
SqlConnection(Source.ConnectionString);
// Create a command object
SqlCommand sqlCommand = new
SqlCommand(Source.Query, sqlConnection);
sqlConnection.Open();
// Call ExecuteReader to return a
// DataReader
SqlDataReader salDataReader =
sqlCommand.ExecuteReader();
while (salDataReader.Read())
{
Data.Add(salDataReader.GetString(0),
salDataReader.GetString(1));
}
salDataReader.Close();
sqlCommand.Dispose();
sqlConnection.Close();
}
}
}
让我们看看如何在下一节中构建配置扩展。
配置扩展
与其他提供者一样,我们可以使用扩展方法将配置源添加到配置构建器中。
注意
扩展方法是静态方法,您可以在不修改或重新编译原始类的情况下向现有类中添加方法。
以下代码片段展示了在配置构建器中SqlConfigurationExtensions类的一个示例实现:
public static class SqlConfigurationExtensions
{
public static IConfigurationBuilder
AddSql(this IConfigurationBuilder
configuration, string connectionString,
string query)
{
configuration.Add(new
SqlConfigurationSource(connectionString,
query));
return configuration;
}
}
扩展方法将减少我们应用程序启动时的代码量。
我们可以在Program.cs中添加引导代码,就像我们为其他配置提供者添加的那样,如下所示:
builder.Configuration.AddSql("Connectionstring","Query");
以下截图显示了数据库中的一些示例配置设置。您可以在config.AddSql()中传递适当的连接字符串和 SQL 查询,并从数据库加载以下配置。SQL 查询可能是一个简单的select语句,用于读取所有键值对,就像以下截图所示:
图 6.17 – 数据库配置设置
按照以下方式更新 WeatherForecastController.cs 以从 SQL 配置提供程序加载的配置中读取键值对:
[HttpGet]
public IEnumerable<string> Get()
{
return new string[] { "TestSqlKey",
_configuration["TestSqlKey"] };
}
}
你可以运行应用程序并查看结果。应用程序将能够访问 SQL 配置并读取配置。
这只是一个自定义配置提供程序的例子。你可能能够想到不同的场景,在这些场景中你会构建其他不同的自定义配置提供程序,例如从 CSV 文件中读取或从 JSON 或 XML 文件中读取加密值并解密它们。
摘要
在本章中,我们看到了 .NET 6 中配置的工作方式,如何向应用程序提供默认配置,如何以分层顺序添加键值对配置,如何读取配置,如何利用 Azure Key Vault 配置提供程序和文件配置提供程序,以及如何构建自定义配置提供程序以从 SQL 数据库中读取。你现在拥有了根据具体需求在项目中实现不同配置所需的知识。
在下一章中,我们将学习关于日志以及它在 .NET 6 中的工作方式。
问题
在阅读本章后,你应该能够回答以下问题:
- 在 .NET 6 中,负责提供应用程序默认配置的是什么?
a. CreateDefaultBuilder
b. ChainedConfigurationProvider
c. JsonConfigurationProvider
d. 所有上述选项
答案:a
- 以下哪个说法是不正确的?
a. Azure Key Vault 配置提供程序从 Azure Key Vault 读取配置。
b. 文件配置提供程序从 INI、JSON 和 XML 文件中读取配置。
c. 命令行配置提供程序从数据库中读取配置。
d. 内存配置提供程序从内存集合中读取配置。
答案:c
- 用于在运行时访问配置并通过依赖注入注入的接口是什么?
a. IConfig
b. IConfiguration
c. IConfigurationSource
d. IConfigurationProvider
答案:b
- 在生产中存储密钥推荐使用哪个提供程序/源?
a. 从 appsettings.json 中的 JSON
b. 从 XML 文件中的 FileConfiguration
c. 从 AzureKeyVault 的 AzureKeyVaultProvider
d. 从命令行来的命令行配置提供程序
答案:c
进一步阅读
-
https://docs.microsoft.com/en-us/aspnet/core/blazor/fundamentals/configuration?view=aspnetcore-6.0
-
https://docs.microsoft.com/en-us/aspnet/core/fundamentals/configuration/options?view=aspnetcore-6.0
第七章:第七章:.NET 6 中的日志记录
日志记录可以帮助你在运行时记录应用程序的不同数据的行为,你可以控制你想记录什么以及你想在哪里记录它。一旦你的功能开发完成,你可以在开发 PC 上彻底进行单元测试,然后在测试环境中进行彻底的集成测试,然后部署到生产环境中,最后对许多用户开放。与开发机器相比,你的应用程序运行的上下文(如服务器、数据和负载)在测试环境和生产环境中是不同的,你可能会在测试和生产环境的最初几天遇到意外问题。
在这里,日志记录在记录端到端流程中不同组件执行其功能并相互交互时的运行时发生的事情中起着非常重要的作用。有了日志信息,我们可以调试生产问题并构建非常有用的见解。我们将学习日志最佳实践和可用的不同日志提供程序,例如 Azure App Service 日志记录和 Application Insights 日志记录,并将构建一个可重复使用的日志库,该库可用于不同的项目。
在本章中,我们将涵盖以下主题:
-
良好日志记录的特点
-
理解可用的日志提供程序
-
与 Azure App Service 一起工作
-
Application Insights 中的实时遥测
-
创建.NET 6 日志类库
到本章结束时,你将对日志记录有一个很好的了解,以及一些可以在部署时应用的 Azure App Service 和 Application Insights 的平台级概念。
技术要求
需要具备对 Microsoft .NET 和 Azure 的基本了解。
本章的代码可以在以下位置找到:github.com/PacktPublishing/Enterprise-Application-Development-with-C-10-and-.NET-6-Second-Edition/tree/main/Chapter07.
良好日志记录的特点
日志记录已实现,但日志中的信息对构建见解或调试生产问题没有用。你见过多少次这个问题?
这就是最佳实践发挥作用的地方,在你的应用程序中实现良好的日志记录。良好日志记录的一些特点如下:
-
它不应影响实际的应用程序性能。
-
它应该是准确和完整的。
-
它应该用于数据分析和学习应用程序的使用情况,例如并发用户、峰值负载时间和最/最少使用的功能。
-
它应该帮助我们重现报告的问题,以进行根本原因分析,并最小化无法重现的实例。
-
它应该是分布式的,并且易于每个人(开发人员、产品所有者和支持人员)访问。
-
它不应包含受保护或敏感信息,个人身份信息(PII),或重复或不必要的日志。
除了这些,它还应捕获以下关键信息的一些内容:
-
关联 ID:用于在日志存储中搜索的问题的唯一标识符。
-
日志级别:信息、警告和错误等。
-
时间戳:日志条目的时间(始终使用一个同意的标准格式,例如 UTC 或服务器时间,不要混合使用)。
-
消息:要记录的消息。这可能是一个信息或自定义错误消息,实际的异常消息,或自定义和实际错误消息的组合。
-
机器/服务器/实例名称:负载均衡器中可能有多个服务器。这将帮助我们找到日志发生的服务器。
-
程序集:日志发生的位置的程序集名称。
你想记录什么?这就是日志级别指导介入的地方:
表 7.1
日志级别是可配置的,并基于指定的级别;它将从指定的级别启用到所有更高级别。例如,如果您在配置中指定日志级别为信息,则所有来自信息、警告、错误和致命的日志消息都将被记录,而调试和跟踪消息将不会记录,如下表所示。如果没有指定日志级别,则默认为信息级别:
表 7.2
你想在何处记录?这就是日志提供程序介入的地方。让我们在下一节中看看它们。
理解可用的日志提供程序
.NET 6 支持多个内置日志提供程序以及几个第三方日志提供程序。这些提供程序公开的 API 帮助将日志输出写入不同的来源,例如提供程序支持的文件或事件日志。您的代码还可以启用多个提供程序,这在您从一个提供程序迁移到另一个提供程序时是一个非常常见的场景,您可以保留旧的,监控新的,一旦您满意,您就可以退役旧的提供程序。让我们详细讨论这两种类型的提供程序。
内置日志提供程序
所有内置日志提供程序都支持在 Microsoft.Extensions.Logging 命名空间中。让我们看看其中的一些:
https://github.com/OpenDocCN/freelearn-csharp-zh/raw/master/docs/entp-appdev-csp10-dn6/img/Table_7.3(a.jpg)https://github.com/OpenDocCN/freelearn-csharp-zh/raw/master/docs/entp-appdev-csp10-dn6/img/Table_7.3(b.jpg)
表 7.3
第三方日志提供程序
虽然 .NET 6 提供了几个强大的内置日志提供程序,但它也支持第三方日志提供程序。让我们来看看它们:
表 7.4
在简要了解了多个内置和第三方提供程序之后,让我们在下一节中深入探讨 Azure App Service 和 Application Insights。
使用 Azure App Service
在 基础设施即服务(IaaS)托管模型中,您对机器上安装的操作系统和软件拥有完全控制权。这与我们许多人习惯的本地部署非常相似。您可以通过远程桌面访问服务器,查看 IIS 日志、Windows 事件查看器或文件。当您迁移到 平台即服务(PaaS)托管模型时,Azure 会完全负责管理实例。这有助于节省大量时间,因为您的工程师不必花费时间管理服务器,以保持操作系统、基础设施和安全更新的最新状态。
在本节中,我们将了解如何在将应用程序部署到 Azure App Service 计划(Microsoft 的重要 PaaS 提供之一)时进行广泛的日志记录和监控。
在 Azure App Service 中启用应用程序日志记录
要启用应用程序日志记录,您需要执行以下步骤:
- 使用
dotnet add <.csproj> package <Nuget package> -v <Version number>命令在您的现有 .NET 6 项目中添加AzureAppServices包,如下所示:
![Figure 7.1 – Installing a package from the CLI
![img/Figure_7.1_B18507.jpg]
图 7.1 – 从 CLI 安装包
您可以从 docs.microsoft.com/en-us/dotnet/core/tools/dotnet 获取有关 .NET CLI 命令的更多详细信息。
您也可以右键单击 Microsoft.Extensions.Logging.AzureAppServices 包,并按以下截图所示进行安装:
![Figure 7.2 – 从 IDE 安装包
![img/Figure_7.2_B18507.jpg]
图 7.2 – 从 IDE 安装包
-
在您的 .NET 6 应用程序的
Program.cs文件中,在CreateHostBuilder方法中添加以下突出显示的代码://Removed code for brevity builder.Logging.AddAzureWebAppDiagnostics(); //Removed code for brevity
CreateHostBuilder 为我们正在开发的程序执行默认配置。让我们在这里添加一个日志配置,这也会动态注入 _logger 对象(对象创建使用 依赖注入(DI)进行,如第五章,.NET 6 中的依赖注入所述)。
-
添加日志记录:将以下突出显示的日志代码添加到任何控制器中的方法中,通常位于核心逻辑处,以测试日志记录是否正常工作:
[ApiController] [Route("[controller]")] public class WeatherForecastController : ControllerBase { private static readonly string[] Summaries = new[] { "Freezing", "Bracing", "Chilly", "Cool", "Mild", "Warm", "Balmy", "Hot", "Sweltering", "Scorching" }; private readonly ILogger<WeatherForecast Controller> _logger; public WeatherForecastController(ILogger<WeatherForecast Controller> logger) { _logger = logger; } [HttpGet] public IEnumerable<WeatherForecast> Get() { _logger.LogInformation("Logging Information for testing"); _logger.LogWarning("Logging Warning for testing"); _logger.LogError("Logging Error for testing"); var rng = new Random(); return Enumerable.Range(1, 5).Select(index => new WeatherForecast { Date = DateTime.Now.AddDays(index), TemperatureC = rng.Next(-20, 55), Summary = Summaries[rng.Next(Summaries.Length)] }) .ToArray(); } } -
TestAppServiceForLoggingDemo。有关如何发布的更多信息,请参阅docs.microsoft.com/en-us/visualstudio/deployment/quickstart-deploy-to-azure?view=vs-2022。对于此示例,我们使用了基于 Windows 的 App Service 计划。 -
启用日志记录:转到 Azure 门户 | 您的订阅 | 资源组 | 应用服务(它在那里部署),然后在 监控 下选择 应用服务日志。您可以看到不同的日志选项,如下所示:
![Figure 7.3 – App Service 日志默认状态
![img/Figure_7.3_B18507.jpg]
图 7.3 – 应用服务日志默认状态
默认情况下,所有日志选项都已关闭,如前一个截图所示。让我们看看这些选项有哪些:
-
应用程序日志(文件系统):将应用程序的日志消息写入 Web 服务器的本地文件系统。一旦您打开它,这将启用 12 小时,之后将自动禁用。因此,此选项用于临时调试目的。
-
应用程序日志(Blob):将应用程序的日志消息写入 Blob 存储,以在配置的保留期内进行持久日志记录。Blob 中的日志记录用于长期调试目的。您需要一个 Blob 存储容器来写入日志。您可以在
docs.microsoft.com/en-us/azure/storage/blobs/storage-blobs-introduction上了解更多关于 Blob 存储容器的信息。一旦您选择开启,您将获得创建新存储账户或搜索现有存储账户以写入日志的选项。点击**+ 存储账户**并指定一个名称以创建新账户,如下截图所示:
图 7.4 – 存储账户配置
-
Web 服务器日志:IIS 日志记录在服务器上,提供诊断信息,如 HTTP 方法、资源 URI、客户端 IP、客户端端口、用户代理和响应代码。您可以将日志存储在 Blob 存储或文件系统中。在**保留期(天数)**中,您可以配置日志应保留的天数。
-
服务器返回的
400响应,这可以帮助您确定服务器为什么返回此错误。注意
由于安全原因,在生产环境中,我们不向客户端发送详细的错误页面,但每当应用程序发生 HTTP 代码
400或更高的错误时,应用服务都可以将此错误保存到文件系统中。 -
失败的请求跟踪:关于失败请求的详细信息,包括 IIS 跟踪等。对于每个失败的请求,都会生成一个包含 XML 日志文件和用于查看日志文件的 XSL 样式的文件夹。
以下截图显示了您打开并启用所有日志选项时的外观:
图 7.5 – 应用服务日志启用
- 您可以通过浏览托管在应用服务上的网站并导航到执行日志记录的控制器所在的页面来验证我们已启用的任何日志选项。例如,让我们通过访问配置了日志选项之一的 Blob 存储来检查应用程序日志(Blob),如下截图所示:
图 7.6 – 从 Blob 存储获取的应用服务日志
您还可以通过导航到监控下的日志流,从添加测试日志的控制器实时查看日志:
图 7.7 – 应用服务日志在日志流中
我们看到了如何在 Azure 应用服务中启用不同的日志并验证了应用程序日志(Blob)和日志流中的日志。在下一节中,我们将看到如何进行监控和设置警报。
使用度量值进行监控
您可以使用 Azure Monitor 中的度量值来监控您的应用服务计划和应用程序服务。
导航到您的应用服务计划并查看概述,如以下截图所示。您可以看到 CPU、内存、数据输入、数据输出等标准图表:
图 7.8 – 应用服务计划概述
现在,点击任何图表,例如,CPU 百分比图表。您将看到以下截图所示的视图(默认持续时间是 1 小时):
图 7.9 – 应用服务度量值概述
我在之前截图所示的图表中突出显示了三个重要部分。让我们来讨论它们:
- 本地时间:当您点击本地时间时,您将看到以下截图所示的选项。您可以更改此图表应表示的时间范围值:
图 7.10 – 应用服务度量值时间范围
- 添加度量值:当您点击添加度量值时,您将看到以下截图所示的选项。您可以选择图表要显示的度量值:
图 7.11 – 应用服务 – 添加度量值
- 固定到仪表板:您可以通过点击固定到仪表板将图表添加到仪表板,以便您登录 Azure 门户时可以看到更新。
当您点击左侧门户菜单时,您可以看到仪表板,您可以点击它以查看所有已固定的仪表板:
图 7.12 – 仪表板左侧门户菜单选项
在下一节中,让我们看看如何在 Azure 应用洞察中启用实时遥测。
Azure 应用洞察的实时遥测
Application Insights 是 Microsoft Azure 为开发人员和 DevOps 专业人员提供的最佳遥测服务之一,作为一个可扩展的**应用程序性能管理(APM)**服务,用于以下目的:
-
实时监控您的应用程序。
-
自动检测性能异常。
-
包含强大的分析工具以帮助您诊断问题。
-
了解用户如何使用您的应用程序。
-
帮助您持续改进性能和可用性。
Microsoft.Extensions.Logging.ApplicationInsights 作为 Microsoft.ApplicationInsights.AspNetCore 的依赖项被包含。Microsoft.ApplicationInsights.AspNetCore 包用于 ASP.NET Core 应用程序中的遥测,当您使用此包时,您不需要安装 Microsoft.Extensions.Logging.ApplicationInsights。
如下图所示,您可以在应用程序中安装此包以启用并写入遥测:
图 7.13 – 应用洞察遥测的仪器化
注意
这不会影响您的应用程序性能。对应用洞察的调用是非阻塞的,并且以批量形式在单独的线程中发送。
在应用洞察中启用应用程序日志记录
使用应用洞察启用应用程序日志记录的步骤如下:
- 使用
Install-Package <Package name> -version <Version number>命令安装Microsoft.ApplicationInsights.AspNetCore包:
图 7.14 - 从包管理控制台安装包
-
appsettings.json以确保所有遥测数据都写入您的 Azure 应用洞察资源。如果您没有 Azure 应用洞察资源,请继续创建一个,然后将其添加到appsettings.json:{ "Logging": { "LogLevel": { "Default": "Information", "Microsoft": "Warning", "Microsoft.Hosting.Lifetime": "Information" } }, "Telemetry": { "InstrumentationKey": "Your AppInsights Instrumentation Key " } -
在
Program.cs文件中,添加高亮代码:string InstrumentationKey = builder.Configuration["Telemetry:InstrumentationKey"]; // The following line enables Application Insights telemetry collection. builder.Services.AddApplicationInsightsTelemetry(InstrumentationKey);
现在,您可以构建和运行应用程序。默认情况下,您将获得大量的遥测数据。
导航到 应用洞察 | 概览,您可以看到任何失败的请求、服务器响应时间和服务器请求,如图下截图所示:
图 7.15 – 应用洞察概览
您可以导航到 应用洞察 | 实时指标 以获取实时性能计数器,如图下所示:
图 7.16 - 应用洞察实时指标
您可以导航到 应用洞察 | 指标 以获取不同的指标和图表,如图下所示:
图 7.17 – 应用洞察指标
您可以导航到 应用洞察 | 性能 以分析操作持续时间、依赖项响应时间等,如图下所示:
图 7.18 – 应用洞察性能
您可以导航到 应用洞察 | 失败 并分析操作、失败的请求、失败的依赖项、前三个响应代码、异常类型和依赖项失败,如图下所示:
图 7.19 – 应用洞察失败
我们已经看到了开箱即用的遥测和警报。我们如何添加信息、错误或警告的日志?您可以使用日志记录器(对象创建是通过 DI 实现的,这在 第五章,.NET 6 中的依赖注入)中已经介绍过)。DI 是通过前述步骤中看到的第二个(应用程序设置配置)和第三个(启用 Application Insights 遥测)步骤启用的。为了测试目的,为了查看它是否正常工作,您可以将以下代码添加到您的控制器中并运行应用程序:
[HttpGet]
public IEnumerable<WeatherForecast> Get()
{
//Removed code for brevity
_logger.LogWarning("Logging Warning for
testing");
_logger.LogError("Logging Error for testing");
//Removed code for brevity
您可以导航到 Application Insights | 日志 并检查跟踪,在那里您可以查看已记录的警告和错误,如图所示:
图 7.20 – Application Insights 日志
Application Insights 非常简单易用,是一个非常强大的日志提供程序。我们看到了它提供的丰富遥测数据,并添加了我们自己的日志。在下一节中,我们将开发一个自定义日志类库。.NET 6 中提供的默认日志记录器对于您的应用程序遥测来说已经足够了。如果您需要记录默认在 .NET 6 中提供的自定义指标和事件,您可以利用以下自定义日志记录器库。
创建 .NET 6 日志类库
我们将创建一个支持 Application Insights 日志记录并可扩展以支持其他来源日志记录的类库(DLL)。为此,请执行以下步骤:
-
创建一个新的名为
Logger的 .NET 6 类库。 -
安装
Microsoft.ApplicationInsights包。 -
创建一个名为
ICustomLogger.cs的新类,并添加以下代码:using System; using System.Collections.Generic; namespace Logger { public interface ICustomLogger { void Dependency(string dependencyTypeName, string dependencyName, string data, DateTimeOffset startTime, TimeSpan duration, bool success); void Error(string message, IDictionary<string, string> properties = null); void Event(string eventName, IDictionary<string, string> properties = null, IDictionary<string, double> metrics = null); void Metric(string name, long value, IDictionary<string, string> properties = null); void Exception(Exception exception, IDictionary<string, string> properties = null); void Information(string message, IDictionary<string, string> properties = null); void Request(string name, DateTimeOffset startTime, TimeSpan duration, string responseCode, bool success); void Verbose(string message, IDictionary<string, string> properties = null); void Warning(string message, IDictionary<string, string> properties = null); } } -
创建一个名为
AiLogger.cs的新类,并添加以下代码以记录自定义事件和指标:-
命名空间和构造函数:
using Microsoft.ApplicationInsights; using Microsoft.ApplicationInsights.DataContracts; using System; using System.Collections.Generic; namespace Logger { public class AiLogger : ICustomLogger { private TelemetryClient client; public AiLogger(TelemetryClient client) { if (client is null) { throw new ArgumentNullException(nameof(client)); } this.client = client; } -
代码用于记录警告、错误和异常:
public void Warning(string message, IDictionary<string, string> properties = null) { this.client.TrackTrace(message, SeverityLevel.Warning, properties); } public void Error(string message, IDictionary<string, string> properties = null) { this.client.TrackTrace(message, SeverityLevel.Error, properties); } public void Exception(Exception exception, IDictionary<string, string> properties = null) { this.client.TrackException(exception, properties); } -
代码用于记录自定义事件、指标、信息、请求和依赖:
public void Event(string eventName, IDictionary<string, string> properties = null, IDictionary<string, double> metrics = null) { this.client.TrackEvent(eventName, properties, metrics); } public void Metric(string name, long value, IDictionary<string, string> properties = null) { this.client.TrackMetric(name, value, properties); } public void Information(string message, IDictionary<string, string> properties = null) { this.client.TrackTrace(message, SeverityLevel.Information, properties); } public void Request(string name, DateTimeOffset startTime, TimeSpan duration, string responseCode, bool success) { this.client.TrackRequest(name, startTime, duration, responseCode, success); } } public void Dependency(string dependencyTypeName, string dependencyName, string data, DateTimeOffset startTime, TimeSpan duration, bool success) { this.client.TrackDependency(dependencyTypeName, dependencyName, data, startTime, duration, success); }
-
AiLogger 使用 TelemetryClient 类,该类将遥测数据发送到 Azure Application Insights。
- 构建库,您的自定义 .NET 6 日志记录器就绪,可以消费项目中的事件。
在接下来的章节中,我们将作为企业应用程序开发的一部分使用日志记录库。在本章提供的示例中,您可以看到我们如何动态地将此自定义日志记录器注入到 LoggerDemoService 项目中。
摘要
在本章中,我们学习了良好日志记录的特点,可用的不同日志提供程序,例如 Azure App Service 日志提供程序和 Azure Application Insights 日志提供程序,以及如何创建可重用的日志记录器库。
您现在拥有了必要的日志记录知识,这将帮助您在项目中实现可重用的记录器或扩展当前的记录器,以适当的日志级别和关键信息来调试问题并在生产数据上构建分析。
在下一章中,我们将学习如何在 .NET 6 应用程序中缓存数据的各种技术,以及可以与 .NET 应用程序集成的各种缓存组件和平台。
问题
- 哪些日志会在执行流程因失败而停止时突出显示?这些应该表明当前活动中的失败,而不是应用程序范围内的失败:
a. 警告
b. 错误
c. 严重
d. 信息
答案:b
- 对于在任何地方运行的应用程序和组件,包括 Azure、AWS、您自己的本地服务器或移动平台,用于日志记录可以依赖什么?
a. 应用洞察
b. Azure App Service
c. EventLog
d. Serilog
答案:a
- Azure App Service 中可用的日志记录选项有哪些?
a. 应用程序日志(文件系统)和应用程序日志(Blob)
b. Web 服务器日志和详细错误消息
c. 失败的请求跟踪
d. 所有以上选项
答案:d
- 应用洞察是一个可扩展的 APM 服务,可以执行以下哪些操作?
a. 监控您的实时应用程序。
b. 自动检测性能异常。
c. 包含强大的分析工具以帮助您诊断问题。
d. 所有以上选项。
答案:d
1088



被折叠的 条评论
为什么被折叠?



