使用 ASP.NET Core 进行现代全栈 Web 开发: 3 中间件与依赖注入

在 Web 开发领域,掌握 ASP.NET Core 的内部工作原理不仅能提升您的技能,还能为您提供构建强大动态 Web 应用所需的工具。 本章将深入探讨 ASP.NET Core 的两大核心支柱:中间件和依赖注入。 这些概念不仅是理论性的,更是开发具有适应性和可维护性应用程序的基石。

在接下来的课程中,您将通过实践探索并实现 ASP.NET Core 中的中间件,从而能够精确定制请求管道以满足应用程序需求。 您将学习如何创建、配置和注入中间件组件,理解它们在应用程序中的关键作用。 同时,您将深入探索依赖注入的世界,这一设计模式能高效优雅地管理应用程序依赖项。 通过掌握依赖注入,您将提升维护和扩展 ASP.NET Core 应用程序的能力,同时保持代码库的整洁 与模块化。

您在此掌握的知识与技能极具实用价值,能帮助您自信应对现实挑战。 本章结束时,您将能熟练运用中间件灵活处理 HTTP 请求与响应,并通过依赖注入以解耦且 可测试的方式管理依赖项。

本章我们将涵盖以下 主要内容:

  • 理解与 实现中间件
  • 依赖注入基础
  • ASP.NET Core 依赖注入常见问题排查
  • ASP.NET Core 中的依赖注入最佳实践

通过这种结构化探索,您将能够提升 ASP.NET Core 应用程序的性能,使其更具响应性、可维护性 和可扩展性。

20年工作经验,承接微信小程序,App,网站,网站后端开发。有意向私聊我哈。vx:akluse

理解并实现 ASP.NET Core 中的中间件

中间件 在 ASP.NET Core 中是 组装到应用程序管道中以处理请求和响应的软件。 每个组件可以选择是否将请求传递给管道中的下一个组件,并可以在调用下一个组件前后执行特定操作 。

中间件的核心本质是在每个 ASP.NET Core 应用程序请求中执行的构造。 它负责控制应用程序如何响应 HTTP 请求的逻辑。 每个 中间件组件都能处理传入请求,将其传递给管道中的下一个中间件,然后处理 输出响应。

中间件组件按照它们被添加到应用程序管道的顺序执行,这个顺序对于安全性、性能 和功能都至关重要。

配置中间件

.NET 9 中 新的最小托管模型通过使用 Program.cs 进行设置,进一步简化了中间件配置。 下面是一个展示如何 注册中间件组件的简单示例:

public static void Main(string[] args)
{
    var builder = WebApplication.CreateBuilder(args);
    builder.Services.AddControllersWithViews();
    var app = builder.Build();
    if (!app.Environment.IsDevelopment())
    {
        app.UseExceptionHandler("/Home/Error");
    }
    app.UseStaticFiles();
    app.UseRouting();
    app.UseAuthorization();
    app.MapControllerRoute(
        name: "default",
        pattern:
            "{controller=Home}/{action=Index}/{id?}"
    );
    app.Run();
}

在此配置中, 中间件组件如使用 单页应用程序  SPA  静态文件 和授权功能都通过 app 实例进行配置。

.NET 9 中的 自定义中间件遵循与之前版本类似的模式,但集成在 Program.cs 文件中。 下一节将展示如何使用 自定义中间件。

创建自定义中间件

您 通过创建一个包含 InvokeAsync 方法的类来定义中间件,具体遵循 以下步骤:

  1. 在 解决方案资源管理器 中,右键点击 项目名称。
  2. 选择 添加... 选项。
  3. 点击 文件夹 并将其命名为 Middlewares .
  4. 右键点击 Middlewares 文件夹,然后选择 添加 和类 .
  5. 将该类命名为 CustomLoggingMiddleware 并添加如下 代码片段所示代码:
    
    namespace PacktBook.Middlewares
    {
        public class CustomLoggingMiddleware
        {
            private readonly RequestDelegate _next;
            public CustomLoggingMiddleware(
                RequestDelegate next)
            {
                _next = next;
            }
            public async Task InvokeAsync(
                HttpContext context)
            {
                // Log the request path
                Console.WriteLine($"Request URL:
                    {context.Request.Path.Value}");
                await _next(context);
            }
        }
    }

    该方法应接收 HttpContext 并 返回 Task .

  6. 现在您 可以在 Program.cs 中注册此中间件到请求处理管道,如以下 代码所示:
    public static void Main(string[] args)
    {
        var builder = WebApplication.CreateBuilder(args);
        builder.Services.AddControllersWithViews();
        var app = builder.Build();
        if (!app.Environment.IsDevelopment())
        {
            app.UseExceptionHandler("/Home/Error");
        }
        app.UseStaticFiles();
        app.UseRouting();
        app.UseMiddleware<CustomLoggingMiddleware>();
        app.UseAuthorization();
        app.MapControllerRoute(
            name: "default",
            pattern:
                "{controller=Home}/{action=Index}/{id?}"
        );
        app.Run();
    }

要测试自定义中间件,您可以使用 Visual Studio 运行应用程序并浏览 ASP.NET MVC 应用的不同页面。 控制台日志应显示由自定义日志类配置打印的信息,如 图 3 .1 所示 :

 

图3.1 - 自定义中间件日志

在下一节中,您将学习如何创建一个用于 错误处理的自定义中间件。

错误处理中间件

优雅地处理错误对于任何应用程序都至关重要。 以下是如何在.NET 9 中实现一个简单的错误处理中间件。 为中间件创建一个类,如 以下代码所示:

namespace PacktBook.Middlewares
{
    public class ErrorHandlingMiddleware
    {
        private readonly RequestDelegate _next;
        public ErrorHandlingMiddleware(
            RequestDelegate next)
        {
            _next = next;
        }
        public async Task InvokeAsync(HttpContext context)
        {
            try
            {
                await _next(context);
            }
            catch (Exception ex)
            {
                context.Response.StatusCode = 500;
                await context.Response.WriteAsync("An
                    unexpected error occurred.");
                // Log the exception details here
            }
        }
    }
}

包含 中间件到管道的前端以捕获后续异常,如 以下代码所示:

public static void Main(string[] args)
{
    var builder = WebApplication.CreateBuilder(args);
    builder.Services.AddControllersWithViews();
    var app = builder.Build();
    if (!app.Environment.IsDevelopment())
    {
        app.UseExceptionHandler("/Home/Error");
    }
    app.UseStaticFiles();
    app.UseRouting();
    app.UseMiddleware<CustomLoggingMiddleware>();
<strong>    app.UseMiddleware<ErrorHandlingMiddleware>();</strong>
    app.UseAuthorization();
    app.MapControllerRoute(
        name: "default",
        pattern:
            "{controller=Home}/{action=Index}/{id?}"
    );
    app.Run();
}

要测试 中间件,您可以创建一个会抛出异常的控制器,如下所示代码中的测试控制器:

using Microsoft.AspNetCore.Mvc;
namespace PacktBook.Controllers
{
    [ApiController]
    [Route("[controller]")]
    public class TestController : ControllerBase
    {
        [HttpGet]
        public IActionResult Get()
        {
            throw new InvalidOperationException("This is a
                test exception.");
        }
    }
}

当您访问此端点时,中间件应当捕获异常,记录日志并返回通用错误响应,如图 3.2 所示 :

 

图3.2 – 测试控制器执行

当您访问 /test 端点时,不应看到具体的异常信息。 相反,您应该收到 HandleExceptionAsync 中定义的通用错误消息。 在您的日志存储(控制台、文件、数据库等)中,您应该能看到 中间件 记录的详细异常。

这个 自定义错误处理中间件通过集中异常处理和响应格式化,增强了 ASP.NET Core 应用程序的健壮性。 它向客户端隐藏敏感错误细节,有助于防止潜在的安全漏洞,并在 整个应用程序 中提供一致的错误处理策略。

下一节中,您将了解在 ASP.NET Core 中正确注册中间件顺序的重要性,以避免 复杂应用程序 中出现问题。

中间件排序

中间件顺序是 ASP.NET Core 应用程序请求处理管道中的关键环节。 中间件组件在管道中的注册顺序决定了请求和响应的处理与操作方式。 由于中间件组件可以在管道中下一个组件的前后执行操作,它们的排列顺序会显著影响应用程序的行为、安全性和性能。

当向 ASP.NET Core 应用程序发起请求时,该请求会按照中间件组件在 Program.cs 文件中的添加顺序流经中间件管道。 在到达管道末端或被某个中间件处理(不传递给下一个中间件)后,响应会以相反的顺序流经相同的中间件,使每个中间件都能对响应进行后处理。

以下是关于 中间件排序的关键要点:

  • 安全中间件 :安全相关的中间件,例如身份验证( UseAuthentication )和授权( UseAuthorization ),通常位于管道的前端。 这确保了请求在到达业务逻辑或端点处理中间件之前已完成身份验证和授权,这些中间件可能依赖于 用户身份 。
  • 静态文件中间件 :静态文件中间件( UseStaticFiles )通常被放置在处理请求和生成响应的组件之前。 这是因为提供静态文件是一个终端操作——如果找到匹配的文件,就不需要进一步处理,这样可以 节省资源。
  • 错误处理中间件 :用于错误处理的中间件,例如异常处理中间件( UseExceptionHandler ),通常注册在管道的最开始。 这确保它能够捕获由后续任何中间件或 应用程序代码 抛出的异常。
  • 路由中间件 :路由中间件( UseRouting )应当置于任何依赖路由选择终端的中间件之前,例如授权中间件。 但需位于错误处理和安全中间件之后,以确保请求在 被路由前 是有效且经过授权的。
  • 终端中间件 :终端相关中间件( UseEndpoints )位于管道末端。 它会根据 路由数据 将传入请求映射到特定终端(如控制器动作)。

以下是一个展示.NET 9 应用程序中中间件排序的简化示例:

public static void Main(string[] args)
{
    var builder = WebApplication.CreateBuilder(args);
    builder.Services.AddControllersWithViews();
    var app = builder.Build();
    if (!app.Environment.IsDevelopment())
    {
        app.UseExceptionHandler("/Home/Error");
    }
    app.UseStaticFiles();
    app.UseRouting();
    app.UseMiddleware<CustomLoggingMiddleware>();
    app.UseMiddleware<ErrorHandlingMiddleware>();
    <strong>app.UseAuthorization();</strong>
    app.MapControllerRoute(
        name: "default",
        pattern:
            "{controller=Home}/{action=Index}/{id?}"
    );
    app.Run();
}

在 上述代码片段中,安全中间件配置在控制器映射之前被调用,确保应用程序中加载的所有控制器都将应用授权。 因此,保持正确的顺序可以确保 预期结果。

中间件顺序配置不当可能导致 多种问题,例如 以下情况:

  • 安全漏洞 :例如,若将授权中间件置于端点中间件之后,未经授权的请求可能访问 受保护资源。
  • 性能低效问题 :在通过重量级中间件或端点处理后提供静态文件会浪费资源,因为当请求的是静态文件时,这些操作本可被绕过。
  • 功能错误 :某些中间件依赖于前序中间件的结果(如路由产生的端点数据)。 错误的排序可能导致运行时错误或 异常行为。

中间件顺序对 ASP.NET Core 应用程序的正确运行至关重要。 它影响请求处理方式、涉及安全与性能表现,并确保中间件依赖关系得到正确解析。 通过仔细考量每个中间件组件的作用和依赖关系,开发者能构建出高效、安全且功能完善的 Web 应用程序。

总之,ASP.NET Core 中的中间件是管理 HTTP 请求和响应的核心,确保错误处理、安全性以及与其他服务和组件的集成。 通过了解如何实现和利用中间件,您可以显著增强 ASP.NET Core 应用程序的健壮性、安全性和功能性。 无论是为特定任务创建自定义中间件、保护应用程序安全,还是与第三方工具集成,中间件都能提供灵活性和强大功能,使您的应用程序能够精确满足您的需求。

下一节中,我们将逐步介绍 ASP.NET Core 应用程序中的依赖注入概念,您将了解可测试性的最佳实践以及正确方法在可重用性方面带来的其他优势 (在.NET 中)。

ASP.NET Core 中的依赖注入基础

依赖注入 DI ) 是 一种设计模式,可促进松耦合并提升应用程序的可测试性和可维护性。 ASP.NET Core 内置了对 DI 的支持,使其成为该框架 的核心组成部分。 本节将探讨 ASP.NET Core 中依赖注入的基础知识,通过定义、优势、实现策略以及 实际示例 来提供全面的理解。

依赖注入(DI)是一种技术,其中一个对象为另一个对象提供其依赖项。 它将对象的使用与其创建解耦,促进松耦合、更易测试以及更好的 代码组织。

依赖注入(DI)的概念经过多年发展已显著演变,成为现代软件开发的基础要素。 了解其历史背景有助于认识其重要性 及其解决的问题。

在软件开发早期,应用程序通常采用紧耦合的组件设计。 这种紧耦合意味着应用程序某部分的变更可能导致其他部分需要连锁修改,使得维护和测试变得困难。 由于每个组件都严重依赖其他 组件的具体实现,因此难以对组件进行隔离单元测试。

这种对更好架构的需求催生了依赖注入(DI)模式的开发,该模式旨在通过消除组件对 具体实现的直接依赖来解耦组件。

术语 依赖注入 通常归功于 Martin Fowler,他在 2004 年的文章 《控制反转容器与依赖注入模式》 中对其进行了描述。 然而,DI 的原则在该术语普及之前就已得到应用。 这一 概念源于更广泛的 控制反转  IoC )原则,即程序的控制流与传统 过程式编程 相反。

DI 成为 IoC 的一种专门形式,其中依赖项被提供给对象而非由对象自身创建。 这种方法显著提高了模块化和可测试性,使开发者能够轻松替换和模拟 依赖项。

框架与语言的演进

面向对象编程语言和框架的兴起极大地推动了依赖注入(DI)模式的普及。 诸如 Java 的 Spring 框架和微软的.NET Framework 开始提供内置的 DI 支持,使开发者更易采用该模式,从而促进了 其广泛应用。

在 Java 领域,Spring 框架是采用 DI 的先驱之一,它提供了丰富的依赖管理功能集。 同样在.NET 生态中,Unity、Autofac 的引入,以及最终 ASP.NET Core 内置的 DI 支持,都凸显了 DI 在 应用开发中日益增长的重要性。

现代开发中的 DI

随着 ASP.NET Core 的出现,微软为 DI 提供了一流的支持,将其嵌入框架核心。 这一举措证明了该模式的重要性,表明现代应用需要灵活、可测试且 可维护的架构。

如今,依赖注入(DI)已不仅是一种模式,更是现代框架必备的核心特性。 它支持开发松耦合的组件,这对于构建可扩展、可维护且可测试的应用程序至关重要。

依赖注入的未来

随着软件开发不断演进,依赖注入背后的原理很可能仍将至关重要。 应用程序日益增长的复杂性以及向微服务和云原生架构的转变,进一步凸显了依赖注入这类促进解耦与灵活性模式的重要性。

总之,依赖注入的历史演变反映了软件开发向更模块化、可测试和可维护设计模式的广泛转变。 随着我们持续构建更复杂和分布式的系统,依赖注入的原则很可能仍将是软件设计和架构的核心,并适应软件开发领域的新范式与技术。

依赖注入如何促进松耦合

在传统的紧耦合系统中,组件通常直接实例化并管理其依赖项的生命周期。 这种方式导致高度相互依赖,使得在不 影响其他组件的情况下修改或替换组件变得困难。

依赖注入(DI)通过将对象的创建与其使用解耦来解决这个问题。 DI 不允件组件自行创建依赖项,而是要求从外部(通常由 DI 容器或框架)提供依赖项。 这种控制反转(IoC)意味着使用组件无需了解其依赖项的构建细节或它们 的来源。

考虑一个简单的应用场景,其中 NotificationService 被 UserController 用于发送通知,如以下代码所示:

namespace PacktBook.Services
{
    public class NotificationService
    {
        public void SendNotification(string message)
        {
            // Code to send notification
        }
    }
    public class UserController
    {
        private readonly
            NotificationService _notificationService;
        public UserController(
            NotificationService notificationService)
        {
            _notificationService = notificationService;
        }
        public void UpdateUser(User user)
        {
            // Code to update the user
            _notificationService.SendNotification("User
                updated");
        }
    }
}

在此示例中,UserController 直接创建并使用了 NotificationService。 如果我们决定更改通知逻辑或用不同实现替换 NotificationService,就必须修改 UserController,这违反了开闭原则 。

现在,让我们 使用依赖注入(DI)重构前面的示例,首先按照 以下代码中的修改建议:

namespace PacktBook.Services
{
    public interface INotificationService
    {
        void SendNotification(string message);
    }
    public class NotificationService : INotificationService
    {
        public void SendNotification(string message)
        {
            // Code to send notification
        }
    }
    public class UserController
    {
        private readonly
            INotificationService _notificationService;
        public UserController(INotificationService
            notificationService)
        {
            _notificationService = notificationService;
        }
        public void UpdateUser(User user)
        {
            // Code to update the user
            _notificationService.SendNotification("User
                updated");
        }
    }
}

在支持依赖注入的版本中,UserController 依赖于 INotificationService 接口,而非具体实现。 该依赖项(NotificationService)会被注入到 UserController 中,通常由 DI 容器完成。 这种方式允许我们在不修改 UserController 的情况下变更或替换 NotificationService。 例如在测试时,我们可以注入 INotificationService 的模拟或桩实现。 通过 DI 实现的松耦合具有诸多优势,具体体现在以下几点:

  • 增强的可测试性 :通过依赖注入,可以更轻松地在单元测试中用模拟对象或桩对象替换依赖项,从而实现更隔离且 可靠的测试
  • 提升的可维护性 :依赖项实现细节的变更不会影响消费组件,降低了 破坏性变更 的风险
  • 更高的灵活性 :通过替换实现而无需修改 依赖组件 ,可以更简单地引入新功能或变更现有行为
  • 可扩展性 :松耦合架构支持更具扩展性的应用,因为组件可以独立进行 开发、测试和部署

依赖注入(DI)是 实现软件设计中松耦合的强大模式。 通过解耦依赖项的创建与绑定,DI 使开发人员能够构建更具灵活性、可维护性和可测试性的系统。 通过示例演示,我们了解了 DI 如何促进松耦合,以及为何它成为现代应用开发(特别是在 ASP.NET Core 等框架中)的首选方法。

深入探索服务注册

服务注册 是 ASP.NET Core 中使用依赖注入(DI)的基础环节。 通过该过程,您可以定义哪些服务可供注入到组件中以及如何注入。 这个注册阶段至关重要,因为它会告知 DI 容器当请求接口或类时应提供哪些具体实现。 让我们深入探讨 ASP.NET Core 中的服务注册细节。

DI 的核心概念在于组件不应负责创建自己的依赖项。 相反,这些依赖项应该被提供给它们。 服务注册就是将依赖项(通常是接口)映射到其 具体实现 的步骤。

这一映射是在 ASP.NET Core 应用程序的 Program.cs 文件(对于.NET 5 及更早版本则是 Startup.cs )中完成的,使用的是内置服务 IServiceProvider 。 WebApplicationBuilder 实例用于配置和注册服务。 其 builder.Services 属性提供了 IServiceCollection 实例,用于注册具有 相应生命周期 的服务。

当您注册服务时,实际上是在告诉 DI 容器当请求特定接口或服务类型时应返回哪个实现。 以下是 注册服务的通用指南:

  • 识别依赖项 :确定您的应用程序需要哪些服务或组件。 这些可以是存储库、实用工具或任何自定义业务 逻辑类。
  • 定义接口 :为服务定义接口是良好的实践。 这有助于实现松耦合架构,使替换实现或 在测试时进行模拟更加容易。
  • 注册服务 :使用 IServiceCollection (由 WebApplicationBuilder 提供)来添加 你的服务。

以下是 ASP.NET Core 中服务注册的示例,位于 Program.cs 文件中:

builder.Services.AddSingleton<ILoggingService,
FileLoggingService>();
builder.Services.AddScoped<IUserService, UserService>();
builder.Services.AddTransient<IEmailSender, EmailSender>();

让我们理解 上述示例:

  • ILoggingService 被注册为 Singleton 生命周期,使用 FileLoggingService 作为 其实现
  • IUserService 被注册为 Scoped ,意味着每个用户会话 UserService 都会创建一个新的实例 或请求
  • IEmailSender 被注册为 Transient ,每次请求都会提供一个新的 EmailSender 实例

在下一节中,我们将了解 ASP.NET Core 应用程序中服务生命周期的详细信息。

服务生命周期

服务生命周期在 ASP.NET Core 的 DI 框架中是决定服务实例如何创建、共享和销毁的关键概念。 主要有三种服务生命周期: 单例(Singleton) 作用域(Scoped) 和瞬时(Transient)。 理解这些生命周期对于设计和维护一个健壮高效的 ASP.NETCore 应用程序至关重要。

单例(Singleton)

当你将服务注册为 Singleton 时,DI 容器会在首次请求时创建该服务的单一实例,随后在整个应用程序生命周期中所有后续请求都复用该实例。 以下是 Singleton 作用域在服务注册中的使用示例:

builder.Services.AddSingleton<ILoggingService,
FileLoggingService>();

这种情况下,FileLoggingService 的单一实例将被注入到所有配置了该注入的类中。 整个应用程序将共享 FileLoggingService 对象的同一个实例,并保持完全相同的状态。

单例模式 服务非常适合那些线程安全且不在请求间维护状态信息的共享资源或服务。 例如,一个向文件或控制台写入日志的服务可以设计为 单例 服务,因为它通常是无状态的,并且会在 整个应用程序中 使用。

然而,对于维护用户特定数据或依赖请求特定数据的服务使用 单例模式 , 可能导致不同请求和用户间 错误的数据共享问题。

作用域

作用域 生命周期服务会在每个客户端请求(在 Web 应用中即每个 HTTP 请求)时创建一次。 每个请求都会生成一个新实例,但在该请求范围内共享,确保处理该请求的所有组件都能访问相同的服务实例。 以下是使用 作用域 生命周期的 服务注册示例:

builder.Services.AddScoped<IUserService, UserService>();

需要在 HTTP 请求期间提供用户特定数据的用户服务,正是 作用域 服务的典型用例。 它能确保用户数据在不同 HTTP 请求间保持隔离且一致,同时在单个请求范围内共享,因此非常适合用户认证或 请求范围内的 数据检索等场景。

瞬时

Transient(瞬时) 服务每次从服务容器请求时都会创建新实例。 这确保每个需要该服务的组件都能获得全新实例。 以下是 Transient 生命周期在 服务注册中的使用示例:

builder.Services.AddTransient<IEmailSender, EmailSender>();

Transient 服务适用于轻量级、无状态的服务。 例如,一个在发送邮件间不保留任何状态信息的邮件发送服务,可以注册 为 Transient .

使用 瞬时(Transient) 服务可确保每个组件获取自己的实例,这对于需要维护状态或非线程安全的服务至关重要。 这种隔离机制避免了冲突,并确保服务状态不会在不同组件间共享 或跨请求传递。

理解 ASP.NET Core 依赖注入容器中 单例(Singleton)  作用域(Scoped) 与 瞬时(Transient) 生命周期之间的差异,是构建高效、健壮且无缺陷应用程序的基础。 选择正确的生命周期取决于服务设计及预期 使用场景,这将显著影响应用程序的行为、性能及 线程安全性。

下一节我们将逐步讲解如何在 ASP.NET Core 中注册复杂依赖项。

注册和配置复杂依赖项

在 ASP.NET Core 中注册 和配置复杂依赖项涉及处理依赖项本身还包含其他依赖项或需要特定配置才能正常工作的情况。 这类情况通常出现在高级服务配置、第三方库集成或为配置值设置选项模式时。 下面我们将深入探讨如何通过适当的 代码片段 来处理这些更复杂的依赖项注册和配置案例。

有时,某个 服务可能依赖于需要注入其中的其他服务或配置设置。 让我们考虑这样一个场景:您有一个服务需要配置对象和 另一个服务。

首先,定义您的服务及其可能依赖的任何配置类,如以下代码示例所示:

using Microsoft.Extensions.Options;
namespace PacktBook.Services
{
    public class ComplexService : IComplexService
    {
        private readonly IAnotherService _anotherService;
        private readonly ServiceConfig _config;
        public ComplexService(
            IAnotherService anotherService,
            IOptions<ServiceConfig> options
        )
        {
            _anotherService = anotherService;
            _config = options.Value;
        }
        public void PerformAction()
        {
            // Use _anotherService and _config to perform
            // actions
        }
    }
    public class AnotherService : IAnotherService
    {
        public void AnotherAction()
        {
            // Implementation
        }
    }
    public interface IAnotherService
    {
        void AnotherAction();
    }
    public class ServiceConfig
    {
        public string Url { get; set; }
        public int Timeout { get; set; }
    }
}

当 您 注册 ComplexService 时,还需要确保其所有依赖项都已正确注册到 DI 容器中。 此外,您需要为 ServiceConfig 类配置参数值,这些值通常从应用程序设置中加载,如 以下代码所示:

  builder.Services.Configure<AnotherService>(
    builder.Configuration.GetSection("ServiceConfig")
  );
  builder.Services.AddScoped<IComplexService,
    ComplexService>();

在此示例中,ComplexService 依赖于 IAnotherService 和 ServiceConfigIAnotherService 被注册为作用域服务,而 ServiceConfig 通过 IOptions 模式进行配置,从应用程序的配置中获取设置( 例如 appsettings.json)。

在某些情况下,您可能需要创建一个比简单构造函数注入允许的更复杂逻辑的服务。 为此,您可以使用工厂方法 在注册过程中。

考虑这样一个场景: ComplexService 需要传递运行时值或在运行时解析的服务到其构造函数中,如 以下示例所示:

builder.Services.AddScoped<IAnotherService,
AnotherService>();
builder.Services.AddScoped<IComplexService>(provider =>
{
    var anotherService =
        provider.GetRequiredService<IAnotherService>();
    var config = provider.GetRequiredService<IOptions<
        ServiceConfig
    >>();
    return new ComplexService(anotherService, config);
});

这种 工厂 方法允许您包含实例化 ComplexService 所需的任何逻辑,为处理复杂依赖链或 运行时值提供了灵活的方式。

在 ASP.NET Core 中注册和配置复杂依赖项是一项强大功能,能够支持应用程序开发中的高级场景。 通过掌握如何有效管理复杂服务注册(包括使用配置对象和工厂方法),您可以构建更灵活、健壮且易于维护的 ASP.NET Core 应用程序。

正如本节所述,ASP.NET Core 中的依赖注入(DI)是一种设计模式,它将依赖项的创建与使用解耦,从而提升代码的模块化程度、可测试性和可维护性。 从历史上看,DI 源于管理紧密耦合组件的需求,由此催生了 Java 的 Spring 框架以及 ASP.NET Core 的内置支持。 其主要优势包括更好的可测试性、可维护性、灵活性和可扩展性。 ASP.NET Core 中的 DI 涉及服务的生命周期注册,如 单例(Singleton)  作用域(Scoped) 和 瞬时(Transient) ,每种方式服务于不同目的。 高级应用场景包括使用工厂方法和配置对象来配置复杂依赖项,这对构建健壮且 可维护的应用程序至关重要。

下一节我们将了解如何排查 ASP.NET Core 应用程序中常见的 DI 问题。

ASP.NET Core 依赖注入常见问题排查

虽然 DI 是 ASP.NET Core 中一项 强大功能,但开发人员可能会遇到导致应用程序错误或意外行为的特定问题。 掌握 如何排查这些常见的 DI 相关问题对于维护健康稳健的应用程序至关重要。 本节将涵盖针对常见问题的排查策略,例如服务生命周期问题、循环依赖以及不当的 服务注册。

与服务生命周期相关的问题

服务生命周期问题 可能导致意外对象实例、内存泄漏或实例未正确释放等问题。 以下是 排查方法:

  • 单例服务引用作用域/瞬时服务 :单例服务不应依赖于作用域或瞬时服务。 这种依赖会导致作用域或瞬时服务表现出单例行为,从而引发数据不一致和内存泄漏问题。 解决方法:确保单例服务仅依赖于其他 单例服务。
  • 作用域服务生命周期 :不应在单例服务的构造函数或非请求作用域的方法中解析作用域服务。 这样做会导致应用程序抛出 InvalidOperationException 异常。 请确保在作用域服务的适用范围内(通常是在 HTTP 请求范围内 )解析它们。
  • 服务的释放处理 :对于实现了 IDisposable 接口的瞬时作用域服务,应谨慎管理以确保它们被正确释放。 监控应用程序是否存在内存泄漏,这可能表明实例 未被正确释放 。

解决循环依赖问题

循环依赖 发生在两个或多个服务相互依赖时,导致 DI 容器因无法构建这些服务而抛出异常。 要排查和解决循环依赖问题,您可以采取 以下措施:

  • 重构代码 :解决循环依赖的最佳方式是重构代码。 这可能涉及重新设计服务类使其不直接相互依赖,或引入中介服务来打破 循环链。
  • 属性注入 :虽然不推荐这种做法,但作为最后手段,您可以在其中一个依赖服务上使用属性注入而非构造函数注入。 但应尽可能避免这种方式,因为它会掩盖服务的依赖关系而非解决根本的 设计问题。

服务注册不当

服务注册中的错误 可能导致预期实现不可用,从而在运行时引发异常。 排查问题时,请遵循 以下建议:

  • 检查注册项 :确保所有必要服务都已注册在 Program.cs 文件的 ConfigureServices 方法中。 每个使用的服务都应具有 对应的注册项。
  • 正确实现 :验证每个服务接口是否注册了正确的实现类。 接口与实现不匹配会导致 运行时错误。
  • 歧义接口 :若服务存在多个实现,请确保在需要时注入正确的实现。 可能需要通过特性或名称限定服务注入,以区分 可用的实现方案。
  • 配置错误 :有时问题可能并非出在服务本身,而是其配置上。 请确保所有依赖配置设置的服务都能接收到 正确的参数值。

排查 ASP.NET Core 中的依赖注入问题需要采用系统化的方法识别并解决潜在问题。 通过理解与服务生命周期、循环依赖和错误注册相关的常见问题,开发者可以确保应用程序具备健壮性、可维护性且无错误。 当遇到 DI 相关问题时,检查 DI 配置、理解根本原因并应用 DI 设计的最佳实践,能帮助您 高效且有效地解决问题。

在本章关于依赖注入的最后部分,我们将学习 ASP.NET Core 应用程序中 DI 的最佳实践。

ASP.NET Core 中的 DI 最佳实践

DI 是 ASP.NET Core 的核心特性,它能促进更清晰、更模块化且可测试的代码。 遵循 DI 最佳实践不仅能提升代码质量和可维护性,还能帮助预防应用程序开发中可能出现的常见问题。 在接下来的小节中,我们将深入探讨在 ASP.NET Core 中有效使用 DI 的一些关键最佳实践。

优先使用构造函数注入

构造函数注入是 ASP.NET Core 中实现依赖注入(DI)的推荐方式。 它通过类的构造函数提供所需依赖项,确保类始终处于完全初始化且有效的状态,并遵循以下实践:

  • 显式依赖 :构造函数注入使类的依赖关系明确清晰。 它记录了类正常运行所需的依赖项,提高了代码可读性和可维护性。
  • 不可变性 :通过使用构造函数注入,依赖项通常被设置为只读,这确保它们在对象构造后不能被修改,从而增强了应用程序的健壮性。
  • 测试 :它通过允许轻松用模拟对象或桩替换类的依赖项来简化单元测试,便于 隔离测试。

避免使用服务定位器模式

服务定位器模式会引入隐藏依赖,使代码更难以理解和维护。 它通过直接从服务容器获取依赖项,实际上绕过了使用依赖注入的优势。 因此,您应当遵循以下实践:

  • 透明性 :避免使用服务定位器模式可保持类依赖关系的透明性,确保它们通过构造函数或属性显式声明和提供
  • 可测试性 :它通过防止隐藏依赖并确保提供所有所需服务来提高可测试性,从而实现更隔离和可控的测试

注意生命周期管理

理解并尊重依赖注入中服务的生命周期至关重要。 特别需要避免从单例中解析作用域服务,这可能导致意外行为和应用错误。 以下是必须避免的做法:

  • 在单例中使用作用域服务 :常见错误是将作用域服务注入单例服务,这会导致作用域服务像单例一样行为异常,从而引发数据过时和并发错误等问题
  • 处置与资源管理 :确保托管资源的服务注册了适当的生命周期,以避免内存泄漏并保证正确的 资源清理

使用接口

接口在 DI 模式中扮演核心角色,使您的应用程序更具模块化、可测试性和可维护性,因此您应遵循 以下实践:

  • 松耦合 :接口有助于实现组件间的松耦合,使得替换实现时不会影响 依赖代码
  • 可测试性 :使用接口可以方便地进行单元测试模拟,确保测试不依赖于 具体实现 的细节
  • 抽象性 :它们为具体实现提供了抽象层,使代码更具 灵活性和适应性

其他最佳实践

以下是您可以遵循的 一些额外实践:

  • 避免可选依赖 :尽量避免在类中使用可选依赖。 如果一个类需要某个依赖,就应该明确要求该依赖。 可选依赖会引入歧义,使代码更难以理解 和维护。
  • 为 DI 而设计 :从一开始就考虑 DI 来设计你的类。 这意味着要考虑它们将如何被实例化,并确保所有依赖都能 被注入。
  • 集中管理 DI 配置 :将依赖注入配置集中化并保持条理清晰,理想情况下应统一在 ConfigureServices 方法中配置,该方法位于 Program.cs 文件内,以便清晰掌控应用程序的 服务架构全景

遵循这些最佳实践,开发者能充分发挥 ASP.NET Core 中依赖注入的全部优势,从而构建出更易维护、可测试且健壮的 应用架构体系

摘要

本章深入探讨了 ASP.NET Core 中中间件与依赖注入的核心要素,提供了关于如何高效实现和运用这些功能的完整指南。 开篇首先介绍了中间件和 DI 的基本概念,为理解它们在 ASP.NET Core 框架中的作用奠定了坚实基础

关于中间件的讨论聚焦于其在 ASP.NET Core 请求管道中的关键作用,展示了如何利用中间件来定制和控制应用程序中的请求与响应流程。 本章通过实际案例和分步指导,详细说明了中间件的实现与定制方法,确保您能获得实践经验并清晰理解中间件在 ASP.NET Core 生态系统中的运作机制。

在转向依赖注入(DI)时,本章阐释了这一强大技术的基本原理,展示了其对于实现松耦合、可维护和可测试代码的重要性。 通过探讨服务容器、服务生命周期和服务注册机制,您将掌握在 ASP.NET Core 应用程序中有效管理和注入依赖项的知识。

本章中,理论讨论均配有代码示例,使您能够将所学知识直接应用到实际场景中。 现在您已全面掌握如何在 ASP.NET Core 项目中实现并利用中间件和依赖注入(DI),这将提升您的开发技能并提高 应用程序的质量。

本章通过聚焦这些领域,旨在提供一份全面指南,不仅教授中间件和依赖注入(DI)的理论知识,还提供实用的见解和示例,使您能够有效地将这些概念应用到自己的 ASP.NET Core 项目中。

在下一章中,您将学习如何处理 ASP.NET Core 项目中的配置与安全性,初步了解如何使 Web 应用具备可配置性和安全性,并遵循 行业既定的合规性最佳实践。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

akluse

失业老程序员求打赏,求买包子钱

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值