在以前的MVC引用程序中,控制器负责接收输入信息、执行、编排操作并返回响应,它是一个功能齐全的框架,它提供了过滤器、内置了模型绑定与验证,并提供了很多可扩展的管道,但它偏重,不像其它语言是通过更加简洁的方式来开启Web之旅的,因此在.Net6.0官方引入了MinimalAPIs,即最小API,与MVC相比,它足够的简洁,适合小型服务来使用,下面就让我们看看如何使用MinimalAPI来开发一个web应用程序

入门

下面我们来看一下官方提供的MinimalAPI是如何使用的

  1. 新建ASP.NET Core 空项目Assignment.MinimalApiDemo
dotnet new web -o Assignment.MinimalApiDemo
cd Assignment.MinimalApiDemo
  1. 增加一个Get请求,修改Program
app.MapGet("/test", () => "Test Success!");

根据需求,自行增加Get (MapGet)、Post (MapPost)、Put (MapPut)、Delete (MapDelete)方法即可,完整代码如下:

var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();
app.MapGet("/test", () => "Test Success!");

Masa版MinimalAPI

随着我们的服务变得越来越多,这些服务全部被堆积在Program中,这样岂不是变成流水账式的代码?那怎么做才能使得我们的代码更加美观呢?

下面我们就来看一下Masa提供的MinimalAPIs是如何来使用的

  1. 选中项目Assignment.MinimalApiDemo,并安装Masa.Contrib.Service.MinimalAPIs
dotnet add package Masa.Contrib.Service.MinimalAPIs --version 0.6.0-preview.13
  1. 注册Masa版的MinimalAPI,修改Program
var app = builder.AddServices();
  1. 新增加一个用户的服务,新增UserService
public class UserService : ServiceBase
{
public IResult Add(RegisterUserRequest request)
{
//模拟添加用户
return Results.Ok();
}
}

到这里已经结束了,可能会有小伙伴十分的疑惑,Masa提供的方案让我有点摸不着头脑,但项目运行后就会发现在Swagger上多了一个服务

细心的小伙伴发现了,这个服务好像是我们新增的添加用户服务,但链接地址为什么是api/v1/Users

进阶

通过快速入门我们了解到如何使用MinimalAPI,但我们也清楚流水账式编程的危害,我们不希望让项目中充斥着流水账式的代码,我们希望它是整洁的,并且是有迹可循的,这时候Masa提供的MinimalAPI方案进入了我们的视野,它上手难度极低,对我们来说它是很棒的,但如果我们不清楚它是如何设计的话,我们敢放心大胆的使用它吗?虽然它有些枯燥,但我们必须要掌握它是如何设计的,它都支持了什么样的功能

约定

当服务未禁用自动映射路由时,框架会自动扫描继承ServiceBase的非抽象子类并注册到服务集合中(IServiceCollection),并为满足以下要求的方法自动注册路由

  • 当前类的方法的访问级别为public(不包含父类方法)
  • 方法上未增加特性IgnoreRouteAttribute

路由规则

路由规则优先级:

自定义路由 > 约定生成路由

  1. 如何自定义路由?

通过RoutePattern特性我们可以为方法自定义路由

[RoutePattern("user/add")]
public IResult Add([FromBody]RegisterUserRequest request)
{
//模拟添加用户
return Results.Ok();
}
  1. 约定的生成路由规则为:

Pattern(路由) = BaseUri + RouteMethodName

  • BaseUri: 根地址,默认: null

    • BaseUri或者null时,则 BaseUri = Prefix/Version/ServiceName
  • RouteMethodName: 除非自定义RouteMethodName,否则RouteMethodName = GetMethodName(方法名)

GetMethodName:

  1. TrimStart:Get/Post/Create/Put/Update/Delete/Remove
  2. TrimEnd:Async

PS:/api/v1/User/Add,将会变成/api/v1/User

当方法的参数存在id并且id支持从Route中获取时,将会变成/api/v1/User/{id},如果id为可空或者存在默认值时,将会变成/api/v1/User/{id?}

配置

配置分为全局配置、局部配置(仅在当前服务生效),其中优先级为:局部配置 > 全局配置,默认局部配置的参数为null,我们约定局部参数未配置时,以全局配置为准

全局配置

  • DisableAutoMapRoute: 是否禁用自动映射路由,如果为true (禁用),则框架不会自动映射路由,默认:false
  • Prefix: 前缀,默认: api
  • Version: 版本,默认: v1
  • AutoAppendId: 是否追加Id,默认: true
  • PluralizeServiceName: 服务名称是否启用复数,默认: true
  • GetPrefixes: 用于识别当前方法类型为Get请求,默认: new List<string> { "Get", "Select" }
  • PostPrefixes: 用于识别当前方法类型为Post请求,默认: new List<string> { "Post", "Add", "Upsert", "Create" }
  • PutPrefixes: 用于识别当前方法类型为Put请求,默认: new List<string> { "Put", "Update", "Modify" }
  • DeletePrefixes: 用于识别当前方法类型为Delete请求,默认: new List<string> { "Delete", "Remove" }
  • DisableTrimMethodPrefix: 禁用移除方法前缀(Get/Post/Create/Put/Update/Delete/Remove 等), 默认: false
  • MapHttpMethodsForUnmatched: 匹配请求方式失败使用,默认: 支持Post、Get、Delete、Put
  • Assemblies: 用于扫描服务所在的程序集,默认: AppDomain.CurrentDomain.GetAssemblies()
  • RouteHandlerBuilder: 基于RouteHandlerBuilder的委托,可用于权限认证、Cors等

局部配置

  • BaseUri: 根地址,默认: null
  • ServiceName: 自定义服务名,默认: null
  • RouteHandlerBuilder:基于RouteHandlerBuilder的委托,可用于权限认证、Cors等
  • RouteOptions: 局部路由配置
    • DisableAutoMapRoute
    • Prefix
    • Version
    • AutoAppendId
    • PluralizeServiceName
    • GetPrefixes
    • PostPrefixes
    • PutPrefixes
    • DeletePrefixes
    • DisableTrimMethodPrefix
    • MapHttpMethodsForUnmatched

其中ServiceName为null时,ServiceName = 类名.TrimEnd("Service") //不区分大小写

特性

RoutePattern

用于自定义路由,支持参数

  • Pattern: 自定义路由或自定义方法名

    • 当StartWithBaseUri:true,Pattern为自定义方法名
    • 当StartWithBaseUri:false,Pattern为自定义路由
  • StartWithBaseUri: 是否基于BaseUri进行追加,默认: false
  • HttpMethod:请求类型,默认: null(根据方法名前缀自动识别),如果希望指定请求类型而非自动识别,则可手动指定:GetPostPutDelete

IgnoreRoute

用于忽略方法自动映射,例如;存在某个方法已经手动指定映射路由,不希望框架重复进行映射可使用IgnoreRoute, 例如:

public class User2Service : ServiceBase
{
public User2Service()
{
App.Map("/api/v2/user/add", Add);
} [IgnoreRoute]
public void Add([FromBody] RegisterUserRequest request, IData data)
{
data.Add(request.Name, request.Age);
}
}

场景

通过上面的学习我们已经了解到了Masa提供了哪些配置,那下面就让我们实战来演练一下,通过模拟不同的场景使用不同的配置,以确保我们正确掌握这些知识

  1. A: 我不是一个新手,从0.6.0版本以前的版本就开始使用Masa提供的MinimalAPI了,对新版的MinimalAPI很喜欢,但我暂时不希望更改手动注册的方式,我希望升级之后不会对我现有的项目造成影响,我不希望将升级导致原来的服务无法访问

    Q: 您希望继续使用最新版的MinimalAPI,但不希望对原来的项目造成影响,在当前服务中,希望能一如既往的使用手动注册,而不是自动注册,那你可以配置全局禁用自动注册,例如:

    var app = builder.AddServices(options =>
    {
    options.DisableAutoMapRoute = true;
    });

    当然如果您希望在某个特定的服务中开启自动映射,则可以在服务中配置:

    public class UserService: ServiceBase
    {
    public UserService()
    {
    RouteOptions.DisableAutoMapRoute = false;
    } public void Add([FromBody] RegisterUserRequest request, IData data)
    {
    data.Add(request.Name, request.Age);
    }
    }
  2. A: 我是一个新手,我觉得我的项目不需要使用前缀以及版本,我希望自动映射的路由可以帮助我删掉它们

    Q: 你需要的是全局配置,通过全局配置禁用前缀以及版本即可,例如:

    var app = builder.AddServices(options =>
    {
    options.Prefix = string.Empty;
    options.Version = string.Empty;
    });
  3. A: 我是一个新手,虽然我很想严格遵守Resetful标准来写服务,但遗憾的是我无法掌控全局,总是有人不按照标准对方法进行命名,我希望可以人为控制特定的方法的路由

    Q: 目前有两种方法可供选择,它们分别是:

    第一种:自定义路由并忽略自动映射

    public class UserService : ServiceBase
    {
    public UserService()
    {
    App.Map("/user/add", Add);
    } [IgnoreRoute]
    public void Add([FromBody] RegisterUserRequest request, IData data)
    {
    data.Add(request.Name, request.Age);
    }
    }

    第二种: 完整自定义路由:

    public class UserService : ServiceBase
    {
    [RoutePattern("/api/v2/user/add")]
    public void CreateUser([FromBody] RegisterUserRequest request, IData data)
    {
    data.Add(request.Name, request.Age);
    }
    }

    第三种: 仅修改请求方式

    public class UserService : ServiceBase
    {
    [RoutePattern(HttpMethod = "Post")]
    public void CreateUser([FromBody] RegisterUserRequest request, IData data)
    {
    data.Add(request.Name, request.Age);
    }
    }

如果您希望手动指定方法的请求类型,则可以使用[RoutePattern("/api/v2/user/add", HttpMethod = "Post")]

  1. A: 我希望为项目中所有的接口都必须授权才能访问,但我不希望在每个方法上增加Authorize特性,那样太恶心了

    Q: 您的项目是需要为全局服务来设置,则可通过全局配置的RouteHandlerBuilder参数来完成,例如:

    var app = builder.AddServices(options =>
    {
    options.RouteHandlerBuilder = routeHandlerBuilder => routeHandlerBuilder.RequireAuthorization();
    });

    如果您希望对某个服务增加特殊的授权策略,则可以:

    public class UserService : ServiceBase
    {
    public UserService()
    {
    RouteHandlerBuilder = routeHandlerBuilder => routeHandlerBuilder.RequireAuthorization("test");
    } public void CreateUser([FromBody] RegisterUserRequest request, IData data)
    {
    data.Add(request.Name, request.Age);
    }
    }

但是你必须知道的是,如果在服务内配置了RouteHandlerBuilder,那么全局配置的RouteHandlerBuilder将对当前服务失效,局部配置存在时,全局配置将不起作用

  1. A: 我希望某个服务不需要经过授权即可访问,那我该怎么做?

    Q: 只需要在方法上加AllowAnonymous特性即可, 它是MinimalAPI支持的,除了AllowAnonymousEnableCorsAuthorize等都是支持的, 但HttpGetHttpPostHttpPutHttpDelete特性是不支持的

    public class UserService : ServiceBase
    {
    [AllowAnonymous]
    public void CreateUser([FromBody] RegisterUserRequest request, IData data)
    {
    data.Add(request.Name, request.Age);
    }
    }

常见问题

  1. 为何使用DbContext时总是提示DbContext已经被释放?

    UserService仅在项目启动时会被初始化一次,之后不再初始化,因此Service的构造函数参数仅支持SingletonTransient。如果您的服务的生命周期为Scoped,建议在对应的方法中增加参数,例如:

    public void Add([FromBody] RegisterUserRequest request, IData data)
    {
    data.Add(request.Name, request.Age);
    }
  2. 模型校验不起作用?

    目前版本的MinimalAPI并不支持模型绑定与验证,后续版本会增加支持

  3. Builder.AddServices()又为什么必须要放到最后?

    我们知道通过builder.Build()可以得到WebApplication,但在.Net6.0中新增加了限制,这个限制就是在Build后无法再次更新IServiceCollection,否则会提示Cannot modify ServiceCollection after application is built

  4. 为什么MinimalAPIs的生命周期是单例?

    目前AddServices方法中做了两件事,第一件事就是获取到所有的服务,并注册到服务集合中,第二件事就是触发服务并将对应服务的地址以及方法映射到到AppApp.Map类似App.Use,也是一个扩展方法,类似MVC的路由,其生命周期是单例,我们仅仅是将继承ServiceBase的服务映射到App中,并没有魔改MinimalAPI,因此并不存在性能问题,但同样其生命周期也无法改变

总结

MinimalAPIMVC我应该如何选择?

小型服务使用MinimalAPI,因为它是很轻量级的,但如果是大型服务或者功能特别复杂的,还是推荐使用MVCMinimalAPI的上手成本很低,但它不是银弹,选择适合自己的才是最好的

MinimalAPI还有一些特殊的地方,例如Get请求无法使用类对象来接收参数,如果希望使用类对象来接受,则需要使用自定义绑定,除此之外还有其他不一样的地方,完整文档可查看

本章源码

Assignment14

https://github.com/zhenlei520/MasaFramework.Practice

开源地址

MASA.Framework:https://github.com/masastack/MASA.Framework

MASA.EShop:https://github.com/masalabs/MASA.EShop

MASA.Blazor:https://github.com/BlazorComponent/MASA.Blazor

如果你对我们的 MASA Framework 感兴趣,无论是代码贡献、使用、提 Issue,欢迎联系我们

MasaFramework的MinimalAPI设计的更多相关文章

  1. MasaFramework -- 缓存入门与设计

    概念 什么是缓存,在项目中,为了提高数据的读取速度,我们会对不经常变更但访问频繁的数据做缓存处理,我们常用的缓存有: 本地缓存 内存缓存:IMemoryCache 分布式缓存 Redis: Stack ...

  2. MASA Framework - 整体设计思路

    源起 年初我们在找一款框架,希望它有如下几个特点: 学习成本低 只需要学.Net每年主推的技术栈和业务特性必须支持的中间件,给开发同学减负,只需要专注业务就好 个人见解:一款好用的框架应该是补充,而不 ...

  3. 如何一步一步用DDD设计一个电商网站(九)—— 小心陷入值对象持久化的坑

    阅读目录 前言 场景1的思考 场景2的思考 避坑方式 实践 结语 一.前言 在上一篇中(如何一步一步用DDD设计一个电商网站(八)—— 会员价的集成),有一行注释的代码: public interfa ...

  4. 如何一步一步用DDD设计一个电商网站(八)—— 会员价的集成

    阅读目录 前言 建模 实现 结语 一.前言 前面几篇已经实现了一个基本的购买+售价计算的过程,这次再让售价丰满一些,增加一个会员价的概念.会员价在现在的主流电商中,是一个不大常见的模式,其带来的问题是 ...

  5. 设计爬虫Hawk背后的故事

    本文写于圣诞节北京下午慵懒的午后.本文偏技术向,不过应该大部分人能看懂. 五年之痒 2016年,能记入个人年终总结的事情没几件,其中一个便是开源了Hawk.我花不少时间优化和推广它,得到的评价还算比较 ...

  6. 如何一步一步用DDD设计一个电商网站(十)—— 一个完整的购物车

     阅读目录 前言 回顾 梳理 实现 结语 一.前言 之前的文章中已经涉及到了购买商品加入购物车,购物车内购物项的金额计算等功能.本篇准备把剩下的购物车的基本概念一次处理完. 二.回顾 在动手之前我对之 ...

  7. 如何一步一步用DDD设计一个电商网站(一)—— 先理解核心概念

    一.前言     DDD(领域驱动设计)的一些介绍网上资料很多,这里就不继续描述了.自己使用领域驱动设计摸滚打爬也有2年多的时间,出于对知识的总结和分享,也是对自我理解的一个公开检验,介于博客园这个平 ...

  8. 如何一步一步用DDD设计一个电商网站(七)—— 实现售价上下文

    阅读目录 前言 明确业务细节 建模 实现 结语 一.前言 上一篇我们已经确立的购买上下文和销售上下文的交互方式,传送门在此:http://www.cnblogs.com/Zachary-Fan/p/D ...

  9. 如何一步一步用DDD设计一个电商网站(六)—— 给购物车加点料,集成售价上下文

    阅读目录 前言 如何在一个项目中实现多个上下文的业务 售价上下文与购买上下文的集成 结语 一.前言 前几篇已经实现了一个最简单的购买过程,这次开始往这个过程中增加一些东西.比如促销.会员价等,在我们的 ...

随机推荐

  1. Netty 如何高效接收网络数据?一文聊透 ByteBuffer 动态自适应扩缩容机制

    本系列Netty源码解析文章基于 4.1.56.Final版本,公众号:bin的技术小屋 前文回顾 在前边的系列文章中,我们从内核如何收发网络数据开始以一个C10K的问题作为主线详细从内核角度阐述了网 ...

  2. Graph-Based Social Relation Reasoning

    title: Graph-Based Social Relation Reasoning, 2020 task: we propose a simpler, faster, and more accu ...

  3. Lambda表达式有参数有返回值的练习(自定义接口)和Lambda省略格式&Lambda使用前提

    给定一个计算器Calculator接口,内含抽象方法calc可以将两个int数字相加得到和值 使用L ambdo的标准格式调用invokeCalc方法,完成120和130的相加计算 public in ...

  4. 如何编写测试团队通用的Jmeter脚本

    平时学习.工作过程中,编写的一些jmeter脚本,相信大多数都遇到过这个问题.那就是:如果换一台电脑运行,文件路径不一样,会导致运行失败. 前不久,自己就真真切切遇到过一回,A同学写了个脚本用于压测, ...

  5. RK3568开发笔记(四):在虚拟机上使用SDK编译制作uboot、kernel和buildroot镜像

    前言   上一篇搭建好了ubuntu宿主机开发环境,本篇的目标系统主要是开发linux+qt,所以需要刷上billdroot+Qt创建的系统,为了更好的熟悉原理和整个开发过程,选择从零开始搭建rk35 ...

  6. VGA显示原理

    VGA显示是图像处理的基础,是一开始广泛使用的显示器,大部分机器采用VGA接口驱动,所以后来的显示器需要用VGA-xxx转接口来匹配. 用FPGA来驱动VGA,并不适用于显示动态(如手机显示,GUI) ...

  7. OpenMP入门

    OpenMP入门 前情提要:并行(parallel):需要多个运算核心同时完成 其中有多处理器和单处理器多核两种实现方式,其中差异如下: 同一芯片上的多核通信速度更快 同一芯片上的多核能耗更低 Ope ...

  8. WPF 制作 Windows 屏保

    分享如何使用WPF 制作 Windows 屏保 WPF 制作 Windows 屏保 作者:驚鏵 原文链接:https://github.com/yanjinhuagood/ScreenSaver 框架 ...

  9. [极客大挑战 2019]BabySQL-1|SQL注入

    1.打开题目之后,查看源代码信息,发现check.php文件,结果如下: 2.那就只能尝试登录,经测试当输入or.by.select.from.and.where等关键字时会被过滤且会被过滤为空(过滤 ...

  10. iframe 标签

    iframe是一个内联框架,可以在当前HTML页面中嵌入另一个文档,一般情况下使用iframe直接在页面嵌套iframe标签再指定src就可以了. iframe 的常用属性: name : 规定 &l ...