使用URL路由

URL Routing 是 ASP.NET Core 用来映射 HTTP 请求到相应的应用端点的系统机制。常用的应用端点包括 控制器(controllers),Razor 页面,中间件(middleware)。
URL 路由是 ASP.NET Core 的关键部件,不仅应用在 MVC 和 Razor Pages 中,而且还应用于中间件管道中定制的应用端点。

URL 路由的优势

  1. 灵活的 URL 结构:
    • 路由可以让我们定义具有描述性的用户友好的 URLs , 比如:/products/002
    • 简单的 URLs 有助于 SEO ,让应用的节点之间跳转更简单
  2. 分离内在逻辑:
    • 让 URL 和应用的物理文件结构不产生紧密联系,URL 更关注业务逻辑
    • 便于组织和管理应用逻辑和业务逻辑
  3. 动态路径匹配
    • 路由支持动态路径匹配 (比如: /products/{id} ),让我们更灵活地操作参数
  4. 支持 RESTful APIs
    • 路由对于 RESTful Web APIs 映射 HTTP 方法(GET,POST,PUT, DELETE)的路径到功能来说是至关重要的机制

我们接下来用两个中间件来介绍路由机制。
Listing 13-1 在 Middlewares 文件夹里新建 ProductQuantity.cs 文件

namespace ASPNETCoreInsight.Middlewares
{
    public class ProductQuantity
    {
        private readonly RequestDelegate? _next;
        public ProductQuantity()
        {}
        public ProductQuantity(RequestDelegate next )
        {
            _next = next;
        }

        public async Task Invoke(HttpContext context)
        {
            string[] pathParts = context.Request.Path
                .ToString()
                .Split('/', StringSplitOptions.RemoveEmptyEntries);

            if (pathParts.Length == 2 && pathParts[0] == "product")
            {
                string product = pathParts[1];
                int quantity = ProductQuantityService.GetProductQuantity(product);
                await context.Response.WriteAsync($"Product: {product}, Quantity: {quantity}");
            }
            if(_next != null)
            {
                await _next(context);
            }
        }
    }
    public static class ProductQuantityService
    {
        public static int GetProductQuantity(string? product)
        {
            product = string.IsNullOrWhiteSpace(product) ? null : product.ToLower();
            return product switch
            {
                "apple" => 100,
                "bread" => 200,
                "coke" => 300,
                _ => 0,
            };
        }
    }
}

这个中间件是响应 /product/<product name> 的请求,路径按 ‘/’ 拆分成 3 部分,第二部分如果是 product ,那么调用静态服务类以取得产品数量。

Listing 13-2 在 Middlewares 文件夹里新建 ProductCategory.cs 文件

namespace ASPNETCoreInsight.Middlewares
{
    public class ProductCategory
    {
        private readonly RequestDelegate? _next;
        public ProductCategory()
        {}
        public ProductCategory(RequestDelegate next)
        {
            _next = next;
        }

        public async Task Invoke(HttpContext context)
        {
            string[] pathParts = context.Request.Path
                .ToString()
                .Split('/', StringSplitOptions.RemoveEmptyEntries);

            if (pathParts.Length == 2 && pathParts[0] == "category")
            {
                string product = pathParts[1];
                string category = ProductCategoryService.GetProductCategory(product);
                if (category == "Unknown")
                {
                    context.Response.StatusCode = 404;
                    return;
                }
                await context.Response.WriteAsync($"Product: {product} is in Category: {category}");
            }
            if(_next != null)
            {
                await _next(context);
            }
        }
    }
    public static class ProductCategoryService
    {
        public static string GetProductCategory(string? product)
        {
            product = string.IsNullOrWhiteSpace(product) ? null : product.ToLower();
            return product switch
            {
                "apple" => "Fruit",
                "bread" => "Bakery",
                "coke" => "Beverage",
                _ => "Unknown",
            };
        }
    }
}

这个中间件响应 /category/<proudct name> 的请求,如果产品没找到,返回 404 错误。

我们接着修改 Program.cs 文件,加入这两个中间件。
Listing 13-3 修改 Program.cs

...
app.UseMiddleware<SeeYouAgain>();

app.UseMiddleware<ProductCategory>();
app.UseMiddleware<ProductQuantity>();

app.MapGet("/", () => "Hello World!");
...

运行应用,输入附加的 URL 部分 /product/apple/category/apple ,将会显示如下 2 个内容:

Apple 库存

Figure 13-1

Apple 属于种类

Figure 13-2

我们得到了所要的结果,但是这样得到结果的做法不能算是路由,这是由两个中间件重复进行 URL 拆分,判断,才得出是否返回响应。如果使用真正的 URL 路由,就要用到 endpoints 中间组件,而这种端点和 URLs 之间的正确映射才是真正的路由。
ASP.NET Core 的路由中间件处理 URL,根据路径分类,找到对应的端点(endpoint)去处理请求,这样的整个流程机制成为路由(动词)。

添加路由中间件

路由中间件包括两个方法:UseRouting 方法和 UseEndpoints 方法。前者处理管道中的请求,后者用来定义路由映射。

Listing 13-4 修改 Program.cs ,加入路由组件

...

app.UseMiddleware<QueryMiddleware>();
app.UseMiddleware<SeeYouAgain>();

// app.UseMiddleware<ProductCategory>();
// app.UseMiddleware<ProductQuantity>();


app.UseRouting();

app.UseEndpoints(endpoints =>
{
    endpoints.MapGet("hello", async context =>
    {
        await context.Response.WriteAsync("Hello World!");
    });
    endpoints.MapGet("product/{product}", new ProductQuantity().Invoke);
});
app.MapGet("category/{product}", new ProductCategory().Invoke);

app.MapGet("/", async context =>
{
    await context.Response.WriteAsync("Hello World!");
});

app.Run();

我们先要添加第一个路由中间件,然后用第二个路由组件对每个路由路径进行设置,这样,每次请求先经过 UseRouting 中间件来判断是否映射到对应的端点,不用每次都要在由类的 Invoke 进行判断了,效率高,逻辑清晰。我们这里演示了两种 endpoint 路由代码,因为 Webapplication 对象 —— app 派生了 IEndpointRouteBuilder 接口,所以可以用更简化的 MapGet 方法实现 endpoint 路由。
IEndpointRouteBuilder 扩展方法:

方法名描述
MapGet(pattern, endpoint)匹配 HTTP GET 请求
MapPost(pattern, endpoint)匹配 HTTP POST 请求
MapPut(pattern, endpoint)匹配 HTTP PUT 请求
MapDelete(pattern, endpoint)匹配 HTTP DELETE 请求
MapMethods(pattern, methods, endpoint)按指定的 HTTP 请求方法匹配端点
Map(pattern, endpoint)匹配所有的 HTTP 请求到指定端点

URL 模式

对于我们在浏览器地址栏里输入的 http://localhost:5000/product/apple URL,路由中间件将把主地址后面的每个 ’/‘ 后面的字符串称为段,那么第一段就是 product ,第二段就是 apple ,而 MagGet 方法会把段都抽取出来解析到正确的端点。
我们可以用请求 Request 的 RouteValues 属性取得每个段的键-值。

接下来,我们可以重构前面的两个中间件,使它们成为端点(Endpoint)。

Listing 13-5 修改 ProductQuantity.cs 里的 ProductQuantity 类

...
    public class ProductQuantity
    {
        public static async Task Endpoint(HttpContext context)
        {
            string? product = context.Request.RouteValues["product"] as string;
            int quantity = ProductQuantityService.GetProductQuantity(product);
            await context.Response.WriteAsync($"Product: {product} has Quantity: {quantity}");
        }
    }
...

Listing 13-6 修改 ProductCategory.cs 里的 ProductCategory 类

...
    public class ProductQuantity
    {
        public static async Task Endpoint(HttpContext context)
        {
            string? product = context.Request.RouteValues["product"] as string;
            int quantity = ProductQuantityService.GetProductQuantity(product);
            await context.Response.WriteAsync($"Product: {product} has Quantity: {quantity}");
        }
    }
...

接着我们可以把主入口里的两类路由中间件取消,直接用 MapGet 方法实现内置封装好的路由。

Listing 13-7 修改 Program.cs 文件里的两个 MapGet 方法

app.UseMiddleware<QueryMiddleware>();
app.UseMiddleware<SeeYouAgain>();

app.MapGet("product/{product}", ProductQuantity.Endpoint);
app.MapGet("category/{product}", ProductCategory.Endpoint);

app.MapGet("/", async context =>
{
    await context.Response.WriteAsync("Hello World!");
});

app.Run();

现在这三个文件里的代码复杂的是否已经大幅度减少了?并且可读性和逻辑性都更强了?是的!这是现在很流行并提倡使用的 ASP.NET Core 开发 Web APIs 里的设计模式,称为极简 API (Minimal API),我们以后会在 Web APIs 开发为主题的系列文章里面学习使用这种开发软件的方式。

URL 匹配

实际上,我们现实中的代码对 URL 路由会比前面说的要复杂一些,会用到一下几个常用的匹配模式。

  • 缺省匹配:
    可以在 URL 段里设置缺省值。例如,我们把第一个 MapGet 的第一个模式参数改成 /product/{product=apple} 。现在运行应用,如果我们在浏览器地址栏里只在主地址后面输入 /product ,那么我们会得到 apple 的数量。
  • 可选匹配:
    例如我们可以把模式参数名后面加 ? ,像这样,category/{product?} ,就是可选匹配,表示第二段是可选的。
  • 全选匹配:
    类似这样的 URL 段,app.MapGet("{firstseg}/{secondseg}/{*catchall}", ...) ,可以匹配所有 具有至少 2 个段的 URL 。
  • 类型限定匹配:
    我们可以给 URL 段限定数据类型,像 {firstseg:int}/{secondseg:file} 这样的参数模式必须匹配第一段是 int 类型,第二段是文件名的 URL 。顺带说一下,如果限定字符串,怎么办?用类似 alpha:length(5) 这样的限定,表示5个a-z字母的段。
  • 匹配范式:
    最有效的复杂匹配是范式模式匹配,我们可以把第一个 MapGet 方法写成这样的范式匹配,app.MapGet("/product/{product:regex(^apple|bread|coke$)| ,只允许具有 2 个段,第二个段必须符合范式的 URL 。
  • 备份路由:
    如果用户输入的 URL 不匹配任何端点,我们可以用备份路由让用户转到我们指定的端点。app.MapFallback(...) ,这个功能非常有用哦。

进阶路由

自定义限定: 我们可以在前面介绍的基本限定的基础上,通过实现 IRouteConstraint 接口来构造自定义的限定类。

Listing 13-8 在 Middlewares 文件夹里新建 ProductConstraint.cs 文件

namespace ASPNETCoreInsight.Middlewares;
public class ProductConstraint : IRouteConstraint
{
  private static readonly string[] _products = new[] { "apple", "bread", "coke" };
  public bool Match(HttpContext? httpContext, IRouter? route, 
      string routeKey, RouteValueDictionary values, RouteDirection routeDirection)
  {
      string? segmentValue = values[routeKey] as string;
      return _products.Contains(segmentValue, StringComparer.OrdinalIgnoreCase);
  }
}

Listing 13-9 修改 Program.cs

using ASPNETCoreInsight.Middlewares;
  
var builder = WebApplication.CreateBuilder(args);
  
builder.Services.Configure<RouteOptions>(options =>
{
  options.ConstraintMap.Add("productName", typeof(ProductConstraint));
});
  
var app = builder.Build();
  
app.MapGet("product/{product:productName}", ProductQuantity.Endpoint);
app.MapGet("category/{product}", ProductCategory.Endpoint);
  
app.MapGet("/", async context =>
{
  await context.Response.WriteAsync("Hello World!");
});
  
app.Run();

这样我们就可以对 URL 模式添加自定义的复杂限定了。

避免有歧义的路由: 假设我们有这样的两个端点方法,

app.MapGet("number:int", SomeEndpoint1.Endpoint);
app.MapGet("number:double", SomeEndpoint2.Endpoint);

如果我们请求 http://localhost:5000/12.34 ,那么路由会正确解析到第二个方法;但是,如果我们请求 http://localhost:5000/12 呢?路由机制就不知道要选择哪一个方法了,系统会抛出异常而中断执行。解决方式就是我们需要在每个方法后面都加一个附加方法 Add ,在其中设定路由端点的优先顺序。

app.MapGet("number:int", SomeEndpoint1.Endpoint)
  .Add(r => ((RouteEndpointBuilder)r).Order = 1);
app.MapGet("number:double", SomeEndpoint2.Endpoint)
  .Add(r => ((RouteEndpointBuilder)r).Order = 2);

依赖注入(Dependency Injection)

依赖注入(DI)也许是最重要也是最常用的设计模式,没有之一。可以说是一旦拥有,别无所求的设计模式。除了可以自定义依赖注入以外,ASP.NET Core 内建了功能超强的依赖注入支持。大部分情况下,使用内建的依赖注入方式足以应付所有实际情况。

为什么要在 ASP.NET Core 中使用 DI

  1. 提供更高的代码可维护性
    由于依赖是被注入到应用主体结构中的,意味着它容易管理,测试和修改,和应用主体代码无关。
  2. 提升代码松散耦合
    类和类直接不是紧密的耦合去完成任务,而是依靠抽象结合在一起。极大增加代码灵活性。
  3. 简化 DI 管理
    所有的 DI 都是在同一个文件里配置(在 Program.cs),管理方便,同时也精简了代码库。
  4. 增强单元测试能力
    所有的模拟数据都可以在单元测试时注入,有了 DI ,我想你应该不再会用静态数据来测试吧。

ASP.NET Core 内建了 IoC (Inversion of Control)。Ioc是一种机制,它不是传统的让类来创建它们的依赖,而是依赖被提供给类。内建的 IoC 容器支持以下功能:

  • 构造函数注入:依赖作为参数传给类的构造函数
  • 方法注入:直接把依赖传给方法
  • 属性注入:依赖被赋值给属性

我们最好用实例来了解 DI 在开发中的运用,先从一个简单的饮料售货机应用开始。

dotnet new globaljson --sdk-version 8.0.400 --output DIdemo
dotnet new web --outpu DIdemo --framework net8.0
dotnet new sln --output DIdemo
dotnet sln DIdemo add DIdemo

修改 Properties 文件夹里的 launchSettings.json 里的 http 端口为 5000,https 端口为 5001。
在项目文件夹里新建一个 Services 文件夹,在其中添加一个接口文件。
Listing 13-10 在 Services 文件夹里添加 IVendingMachine.cs 文件

namespace DIdemo.Services
{
    public interface IVendingMachine
    {
        Task OutPop(HttpContext context, string beverage);
    }
}

Listing 13-11 在 Services 文件夹里添加 VendingMachine.cs

namespace DIdemo.Services
{
    public class VendingMachine : IVendingMachine
    {
        private int _cupCount = 0;
        public async Task OutPop(HttpContext context, string beverage)
        {
            await context.Response.WriteAsync($"Vending {++_cupCount} cups of {beverage}");
        }
    }
}

Listing 13-12 修改 Program.cs 在其中添加注入 VendingMachine


using DIdemo.Services;

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddSingleton<IVendingMachine, VendingMachine>();

var app = builder.Build();

app.MapGet("vend/{beverage}", async (HttpContext context, IVendingMachine vendingMachine,    string beverage) =>
{
    await vendingMachine.OutPop(context, beverage);
});

app.MapGet("/", (HttpContext context) =>
{
    return context.Response.WriteAsync("Hello! Try your-favorite-beverage.");
});

app.Run();

我们的端点方法是一个异步方法,依赖通过方法的参数注入其中;在方法函数里,我们就可以直接调用接口中声明的方法 OUtPop 了。

dotnet run 运行应用,在地址栏输入 http://localhost:5000/vend/coffee ,页面将显示我们购买了一杯咖啡。

自动售货机服务

Figure 13-3

我们的自动售货机开始工作了!我们用内置的 AddSingleton 注册服务来注入 VendingMachine 的依赖。
ASP.NET Core 依赖注册服务的生命周期:

注册方式描述
AddTransientTransient,每一次服务请求时都会创建一个新实例,适用于轻量级无状态服务
AddScopedScoped,每一个 HTTP 请求(不是服务请求)时被创建,适用于在一个完整的 HTTP 请求中维持状态的服务
AddSingletonSingleton,一旦创建,就可以被整个应用的各个组件都共享的服务,适用于创建要求高或者全局状态的服务

‘ASP.NET Core’ 内置的 DI 容器虽然属于轻量级,但是对大多数应用来说,已经足够了。然而,如果我们需要一些高级的特性(属性注入,子容器等),也可以选择第三方的 DI 框架。

因为不管我们喜欢不喜欢,敲代码的人都非常需要喝咖啡,所以我们想要用手一拍自动售货机,叫一声“咖啡”,它就可以出咖啡。想要实现这个功能,我们可以给这个自动售货机添加一个特定的出咖啡的扩展方法。
Listing 13-13 在 Services 文件夹里添加一个 VendingMachineExtensions.cs 文件

namespace DIdemo.Services
{
    public static class VendingMachineExtensions
    {
        public static void MapCoffee(this IEndpointRouteBuilder app, string patten)
        {
            IVendingMachine vendingMachine = app.ServiceProvider.GetRequiredService<IVendingMachine>();
            app.MapGet(patten, async context =>
            {
                await vendingMachine.OutPop(context, "Coffee");
            });
        }
    }
}

接着在主入口 Program.cs 里添加新的取咖啡快捷方法。
Listing 13-14 修改 Program.cs ,添加新的扩展方法

using DIdemo.Services;

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddSingleton<IVendingMachine, VendingMachine>();

var app = builder.Build();

app.MapGet("vend/{beverage}", async (HttpContext context, IVendingMachine vendingMachine, string beverage) =>
{
    await vendingMachine.OutPop(context, beverage);
});

app.MapCoffee("coffee");

app.MapGet("/", (HttpContext context) =>
{
    return context.Response.WriteAsync("Hello! Try your-favorite-beverage.");
});

app.Run();

看,我们现在只用呼叫“咖啡”就可以了。

自动售货机-中间件-咖啡

Figure 13-4
因为比较方便,图中显示我已经喝到第四杯咖啡了。

在中间件里使用自动售货机服务

依赖注入机制允许我们在中间件 Middleware 里面使用自动售货机。
在项目文件夹里新建 Middlewares 文件夹,在其中添加文件 EspressoMiddleware.cs ,我们想让中间件给我们出意大利人的最爱喝的咖啡。
Listing 13-14 在 EspressoMiddleware 类中添加服务

using DIdemo.Services;

namespace DIdemo.Middlewares
{
    public class EspressoMiddleware
    {
        private readonly RequestDelegate _next;
        private readonly IVendingMachine _vendingMachine;

        public EspressoMiddleware(RequestDelegate next, IVendingMachine vendingMachine)
        {
            _next = next;
            _vendingMachine = vendingMachine;
        }

        public async Task Invoke(HttpContext context)
        {
            if (context.Request.Path == "/espresso")
            {
                await _vendingMachine.OutPop(context, "Espresso");
            }
            else
            {
                await _next(context);
            }
        }
    }
}

我们需要把接口作为构造方法的参数,在 Invoke 中检查请求路径是否符合,决定出意式浓缩还是转到下一个组件。接下来要把中间件在主入口里注册。
Listing 13-15 在 Program.cs 里添加 UseMiddleware 组件

...
var app = builder.Build();

app.UseMiddleware<EspressoMiddleware>();

app.MapGet("vend/{beverage}", async (HttpContext context, IVendingMachine vendingMachine, string beverage) =>
{
    await vendingMachine.OutPop(context, beverage);
});
...

运行应用,在地址栏的主地址后面加 espresso ,就可以有一杯浓浓的意式浓缩呈现给你了。

在端点里使用自动售货机

在端点里使用 DI 服务,相对比较复杂。先在项目主文件夹里新建一个 Endpoints 文件夹,在其中新建一个 LatteEndpoint.cs 文件,是的,我相信有很多人热爱拿铁。
Listing 13-16 类似中间件,在 LatteEndpoint.cs 里添加内容

using DIdemo.Services;

namespace DIdemo.Endpoints
{
    public class LatteEndpoint
    {
        private readonly IVendingMachine _vendingMachine;
        public LatteEndpoint(IVendingMachine vendingMachine)
        {
            _vendingMachine = vendingMachine;
        }
        public async Task GetLatte(HttpContext context)
        {
            await _vendingMachine.OutPop(context, "Latte");
        }
    }
}

现在看起来一切都还好,但是像这样定义的端点是不能在 Program.cs 中作为 app.MapGet 方法的第二个参数的,因为这个端点不是静态类和静态方法。那给这个类和方法加上 static 可以吗? 不行!因为静态类中不能有对象实例的成员。方法之一是用 HttpContext.RequestServices 属性的 GetREquiredService 方法在端点类的 GetLatte 方法中动态取得这个已经被注入的自动售货机依赖服务。方法之二是用 Reflection 工具来检查构造函数并解析和引入依赖注入的服务。我们再添加一个解析依赖服务的端点扩展方法。
Listing 13-17 在 Services 文件夹的 VendingMachineExtensions.cs 中添加 MapLatte 静态方法

using System.Reflection;

namespace DIdemo.Services
{
    public static class VendingMachineExtensions
    {
        public static void MapCoffee(this IEndpointRouteBuilder app, string patten)
        {
            IVendingMachine vendingMachine = app.ServiceProvider.GetRequiredService<IVendingMachine>();
            app.MapGet(patten, async context =>
            {
                await vendingMachine.OutPop(context, "Coffee");
            });
        }
        
        // 添加静态方法
        public static void MapLatte<T>(this IEndpointRouteBuilder app, string patten
            , string methodName = "GetLatte")
        {
            MethodInfo? methodInfo = typeof(T).GetMethod(methodName);
            if (methodInfo == null || methodInfo.ReturnType != typeof(Task))
            {
                throw new InvalidOperationException($"Method {methodName} not found in {typeof(T).Name}");
            }
            T endpointInstance = ActivatorUtilities.CreateInstance<T>(app.ServiceProvider);
            app.MapGet(patten, (RequestDelegate)methodInfo
                .CreateDelegate(typeof(RequestDelegate), endpointInstance));
        }
    }
}

这是一个泛型扩展方法,第一步先解析出泛型里对应方法名的方法引用,第二步检查有效性,第三步动态创建泛型的一个实例,最后用内置端点方法 MapGet 完成整个端点注入依赖的功能。虽然我们定义的端点类不是静态类,不满足 MapGet 的条件,但是我们通过套上静态方法的壳,并在静态方法运行时动态构建实例来使用依赖注入。

端点扩展方法-拿铁

Figure 13-5

我们成功地让喜欢拿铁的人开心了。

依赖注入服务的生命周期

为了更好的理解依赖注入服务的生命周期,我们先修改服务,给它加一个当类创建实例时的一个唯一的标识。
Listing 13-18 修改 Services 文件夹里的 VendingMachine.cs

namespace DIdemo.Services
{
    public class VendingMachine : IVendingMachine
    {
        private readonly string _serialNumber;
        public VendingMachine()
        {
            _serialNumber = Guid.NewGuid().ToString();
        }
        public async Task OutPop(HttpContext context, string beverage)
        {
            await context.Response
                .WriteAsync($"Vending a cup of {beverage} n Serial No. {_serialNumber} ");
        }

    }
}

我们给自动售货机添加一个 Guid 序列号,用这种方式可以通过辨别序列号知道是否创建了新的实例。
由于我们现在使用 Singleton 服务,当我们运行应用,在浏览器里输入 coffee,espresso,latte … ,显示的结果都是同一个序列号。表明 Singleton 的依赖服务是给整个应用共享的,也是应用开始运行就建立的一个依赖对象实例。

AddTransient

我们把服务改为 Transient 看看应用是否每次都会给我们提供新的自动售货机。
Listing 13-19 修改 Program.cs 的自动售货机服务为 AddTransient

...
builder.Services.AddTransient<IVendingMachine, VendingMachine>();
...

运行应用,在浏览器地址里主地址后面输入 latte 和 espresso ,我们会得到两个不同的序列号,说明这两杯咖啡是有两个不同的自动售货机对象生成的。如果,我们在任一个地址时,单击浏览器的重新载入按钮,那么序列号会是一样的。
有这样的表现是因为新的服务对象是仅仅在依赖服务被解析的时候创建,(生成这两种咖啡的方式,一种是通过中间件,一种是直接通过访问端点。在应用启动时,中间件和端点都会分别接受各自独立的依赖服务对象)而不是每次使用依赖服务都会创建。如果想让每次刷新网页都有新的对象,我们可以每次都用 Invoke 方法来重新请求新的对象。
Listing 13-20 修改 EspressoMiddleware.cs

using DIdemo.Services;

namespace DIdemo.Middlewares
{
    public class EspressoMiddleware
    {
        private readonly RequestDelegate _next;
        public EspressoMiddleware(RequestDelegate next, IVendingMachine vendingMachine)
        {
            _next = next;
        }
        
        // 每次 Invoke 才申请解析新的服务
        public async Task Invoke(HttpContext context, IVendingMachine vendingMachine)
        {
            if (context.Request.Path == "/espresso")
            {
                await vendingMachine.OutPop(context, "Espresso");
            }
            else
            {
                await _next(context);
            }
        }
    }
}

这样修改以后,我们在 espresso 页面时,每次刷新都可以看到序列号变更了。我们也可以通过添加 Invoke 方法在端点扩展方法里每次都去要求解析新的对象。
Listing 13-21 修改 LatteEndpoint.cs 文件,取消在构造函数里引入依赖

using DIdemo.Services;

namespace DIdemo.Endpoints
{
    public class LatteEndpoint
    {
        public async Task GetLatte(HttpContext context, IVendingMachine _vendingMachine)
        {
            await _vendingMachine.OutPop(context, "Latte");
        }
    }
}

Listing 13-22 修改 VendingMachineExtensions.cs 的扩展方法

...
        public static void MapLatte<T>(this IEndpointRouteBuilder app, string patten
            , string methodName = "GetLatte")
        {
            MethodInfo? methodInfo = typeof(T).GetMethod(methodName);
            if (methodInfo == null || methodInfo.ReturnType != typeof(Task))
            {
                throw new InvalidOperationException($"Method {methodName} not found in {typeof(T).Name}");
            }
            T endpointInstance = ActivatorUtilities.CreateInstance<T>(app.ServiceProvider);

            ParameterInfo[] parameters = methodInfo.GetParameters();
            app.MapGet(patten, context => (Task)methodInfo.Invoke(endpointInstance, 
                parameters.Select(p => p.ParameterType == typeof(HttpContext) ? 
                    context : app.ServiceProvider.GetService(p.ParameterType)).ToArray()));
        }
...

我们在 latte 页面刷新时,序列号每次也会不同了。

AddScoped

如果我们在中间件里放 3 台自动售货机,在当前服务设置下,每次都会有新的对象给我们输出意式浓缩。
Listing 13-23 修改 EspressoMiddleware.cs 文件,添加另外2个依赖引入

using DIdemo.Services;

namespace DIdemo.Middlewares
{
    public class EspressoMiddleware
    {
        private readonly RequestDelegate _next;
        public EspressoMiddleware(RequestDelegate next)
        {
            _next = next;
        }

        public async Task Invoke(HttpContext context, IVendingMachine vendingMachine1,
            IVendingMachine vendingMachine2, IVendingMachine vendingMachine3)
        {
            if (context.Request.Path == "/espresso")
            {
                await vendingMachine1.OutPop(context, "First Espresso");
                await vendingMachine2.OutPop(context, "Second Espresso");
                await vendingMachine3.OutPop(context, "Third Espresso");
            }
            else
            {
                await _next(context);
            }
        }
    }
}

注意我们取消了构造函数里的依赖参数,因为要在方法里面分别引用 3 个服务。如果运行应用的话,那么每次都有新的 3 个自动售货机服务输出,它们都有不同的序列号。

Transient服务结果

Figure 13-6

我们现在把主入口里的 AddTransient 改为 AddScoped。取消 MapCoffee ,因为这个方法不能收到主入口的依赖服务了。
Listing 13-24 修改 Program.cs 中的的服务为 Scoped

using DIdemo.Endpoints;
using DIdemo.Middlewares;
using DIdemo.Services;

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddScoped<IVendingMachine, VendingMachine>();

var app = builder.Build();

app.UseMiddleware<EspressoMiddleware>();

app.MapLatte<LatteEndpoint>("latte");

app.MapGet("vend/{beverage}", async (HttpContext context, IVendingMachine vendingMachine, string beverage) =>
{
    await vendingMachine.OutPop(context, beverage);
});

//app.MapCoffee("coffee");

app.MapGet("/", (HttpContext context) =>
{
    return context.Response.WriteAsync("Hello! Try your-favorite-beverage.");
});

app.Run();

运行应用,在 expresso 页面刷新的话,我们会发现 3 个输入都是同一个依赖服务。

Scoped服务结果

Figure 13-7

DI 的进阶主题

注册多个实现

对应一个服务接口,可以注册多个实现类。比如:

builder.Services.AddTransient<ITranslatingService, EnglishService>();
builder.Services.AddTransient<ITranslatingService, FrenchService>();

我们可以用 IEnumberable<ITranslatingService> 来解析所有的实现对象。

使用工厂模式

ASP.NET Core 支持工厂模式的依赖服务,其实很多系统都用工厂模式注册服务的:

builder.Services.AddSingleton<ITranslatingService>(provider =>
{
    return new TranslatingService(); // Custom logic here
});

泛型依赖服务

ASP.NET Core 支持泛型注册:

builder.Services.AddScoped(typeof(IRepository<>), typeof(Repository<>));

配置型依赖服务

我们可以把 appsettings.json 里的设置作为依赖注入到应用:

{
  "Translating": {
    "DefaultLanguage": "English"
  }
}

对于上面所示的 Json 内容,我们可以有相应的类用来注册:

public class TranslatingOptions
{
    public string DefaultLanguage { get; set; }
}

// Configure options
builder.Services.Configure<TranslatingOptions>(builder.Configuration.GetSection("Translating"));

我们使用 IOptions 注入依赖。

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注