使用 C# 的 .NET 6 极简 API






4.98/5 (24投票s)
使用 C# 的 .NET 6 创建和运行极简 API 的演练
引言
在本文中,我将展示并解释创建和运行 .NET 6 极简 API 的步骤。但在我们深入探讨 .NET 6 极简 API 之前,让我们回顾一下我们通常创建 API 的方式。特别是控制器、操作以及一些配置。
当您使用 .NET 5 创建 Web API 时,它会创建一个名为“Controllers”的文件夹。该文件夹包含包含操作的类。控制器和操作的组合构成了 API 的终结点。操作是一个可以执行逻辑工作或调用处理逻辑的另一个服务类的方法。它看起来是这样的
解决方案资源管理器显示“Controllers”文件夹,其中包含 WeatherForecastController.cs 文件。该文件包含一个同名的类和操作。它还包含 API 和路由的配置。
还有一个 Startup.cs 文件,其中包含项目的配置。例如,依赖注入、Entity Framework、Swagger 等等。
这已经是我们长期以来一直在做的方式。现在,Microsoft 推出了极简 API。
极简 API 详解
极简 API 基本上消除了 Startup.cs 文件,而是将代码放在 Program.cs 文件中。控制器及其操作被移除,并作为映射放在 Program.cs 文件中。它允许我们将 API 的编码保持在最低限度。
它为每个 API 调用使用 Lambda 表达式。您可以配置路由和请求类型。它看起来是这样的
app.MapGet("/movies", () => new List<string>() { "Shrek", "The Matrix", "Inception"});
app 在 Startup.cs 中定义,它是 WebApplication
。MapGet
是一个将路由“映射”到 Lambda 表达式的方法。在上面的示例中,我想映射路由 https://:12345/movies 并使其返回电影标题列表。
除了 MapGet
,还有 MapPost
、MapDelete
和 MapPut
。这些代表了我们在 .NET 5 API 中在操作上使用的 HTTPGet
、HTTPPost
、HTTPDelete
和 HTTPPut
属性。
这就是基本思想。让我们开始一个示例!
创建极简 API
既然我们已经了解了极简 API 的基本原理,让我们创建一个并看看它是如何工作的。在接下来的章节中,我将向您展示开发人员需要执行的最常见任务。简单的映射、异步映射以及依赖注入的使用。
让我们使用 Visual Studio 2022 创建一个新项目,然后选择 ASP.NET Core Web API 模板。给它一个好的项目名称。我将我的命名为“MinimalApiExample
”,解决方案名称相同。
附加信息屏幕需要一些关注。
Visual Studio 2022 中的每个项目模板都有其自己的附加信息。有些选项是相同的,例如框架。您在此处看到的大多数选项都相当常见,但如果仔细观察,您会发现两个新复选框
- 使用控制器(取消选中以使用极简 API)
- 不使用顶级语句
对于本文,我不在乎那些顶级语句。但我很在乎第一个。它默认勾选,它会根据我们习惯的“旧”方式来生成控制器和操作。如果您取消选中它,Visual Studio 将不会创建这些控制器,而这正是我们想要的。让我们取消选中它,然后按 **创建** 按钮。
当所有加载和创建基本文件完成后,您可能会注意到项目结构。我们缺少“Controllers”文件夹,而这正是我们想要的。
如果您打开 Program.cs,您可能会注意到一些差异。没有 app.MapControllers()
,并且突然出现了一个 app.MapGet(…)
。欢迎来到极简 API!
映射终结点
极简 API 的理念是您无需创建新的控制器和操作。这一切都通过 Lambda 表达式完成。通过映射您的终结点,您可以告诉 API 用户/客户端可以输入的地址。让我们看看 program.cs 中的示例
app.MapGet("/weatherforecast", () =>
{
var forecast = Enumerable.Range(1, 5).Select(index =>
new WeatherForecast
(
DateTime.Now.AddDays(index),
Random.Shared.Next(-20, 55),
summaries[Random.Shared.Next(summaries.Length)]
))
.ToArray();
return forecast;
})
.WithName("GetWeatherForecast");
此映射用于(HTTP)GET
请求。终结点是 (https://:12345)/weatherforecast。GET
方法的返回值是 JSON 格式的天气预报范围。很简单,对吧?
让我们删除所有示例数据,例如映射、变量摘要和内部记录 WeatherForecast()
。确保不要意外删除 app.Run()
,因为它在示例数据之间有点隐藏。
让我们创建一个新对象,称为 Movie
。将此对象放在新文件中或 program.cs 的底部。然后创建一个包含电影列表的新变量。请注意,在可以使用 movies
变量之前,需要先声明并填充它。我建议将其放在 Swagger 初始化下方。它可以看起来像这样
List<movie> movies = new()
{
new() { Id = 1, Rating = 5, Title = "Shrek" },
new() { Id = 2, Rating = 1, Title = "Inception" },
new() { Id = 3, Rating = 3, Title = "Jaws" },
new() { Id = 4, Rating = 1, Title = "The Green Latern" },
new() { Id = 5, Rating = 5, Title = "The Matrix" },
};
public class Movie
{
public int Id { get; set; }
public string Title { get; set; }
public int Rating { get; set; }
}
映射 GET
映射 GET
请求并不难。我们刚刚删除了示例。让我们创建一个新的映射来返回所有电影
app.MapGet("/api/movies/", () =>
{
return Results.Ok(movies);
});
当调用终结点 (https://:1234)/api/movies 时,这将返回完整的电影列表。
MapGet
表示您要创建一个 GET
终结点。使用 POST
到此终结点将不起作用。
如果您想通过 id 或其他参数获取特定电影,只需在映射 MapGet
的同时将其添加到终结点。
app.MapGet("/api/movies/", () =>
{
return Results.Ok(movies);
});
app.MapGet("/api/movies/{id:int}", (int id) =>
{
return Results.Ok(movies.Single(x => x.Id == id));
});
通过在终结点中添加 {id:int},您告诉 API 预计会收到一个数字。然后,添加变量声明,您可以在正文中使用了。
您也可以不指定类型来声明 id
变量,如下所示
app.MapGet("/api/movies/{id:int}", (id) =>
{
return Results.Ok(movies.Single(x => x.Id == id));
});
但这不起作用,因为此时假设第一个参数是 HttpContext
,而不是您试图使用的参数。使用 HttpContext
是管理对任何请求的响应的好方法。但这不是我们在这里的目标。
映射 POST
另一个常用的请求方法是 POST
。它允许客户端将数据发送到 API,API 可以消耗和处理这些数据。使用极简 API 创建 POST
请求与 GET
没什么不同,只是您需要引用一个对象来捕获来自客户端的发布数据。
在 .NET 5 的方式中,我们习惯于在操作的参数中声明一个类型。它仍然有点像这样工作。看看下面的代码
app.MapPost("/api/movies/", (Movie movie) =>
{
movies.Add(movie);
return Results.Ok(movies);
});
同样,我使用 app 来映射一个新终结点。在这种情况下,我使用 MapPost
,因为我想能够将数据发布到 API。然后,我将 Movie
movie 添加到参数列表中。这会导致 API 将正文中传入的数据映射到 Movie
对象。
在正文中,我将新电影添加到电影列表中。出于演示目的,我返回电影列表,包括新添加的电影。
映射 DELETE
我向您展示的最后一个映射是 DELETE
请求,或删除。其他请求类型基本上与 GET
、POST
或 DEL
相同。
删除需要一个键,或一些唯一的东西。否则,您最终可能会删除比您想要的更多的内容。所以我们需要一个查询参数,就像我在 MapGet
方法中展示的 id
一样。
app.MapDelete("/api/movies/{id:int}", (int id) =>
{
movies.Remove(movies.Single(x => x.Id == id));
return movies;
});
它看起来与 MapGet
结构非常相似,只是使用了 MapDelete
。请注意,我没有使用 Results.Ok(…)
。它不是必需的。像这样返回电影将导致 HTTP 200 代码,这是好的。
如果您测试 API 并想删除某些内容,您将在 URL 中添加要删除的项目的 id。但是,如果您发送的是 GET
请求而不是 DELETE
请求,API 将返回具有给定 id 的电影,而不是删除它。
极简 API 和依赖注入
您可能希望在 API 中使用依赖注入,这是一个常见的做法。在使用控制器时,您通过构造函数注入接口。但我们没有构造函数,我们只有带有 Lambda 表达式的映射。
对于这部分,我创建了一个包含接口的小类。我还将电影列表移到了新类中
public interface IMovies
{
List<Movie> GetAll();
Movie GetById(int id);
void Delete(int id);
void Insert(Movie movie);
}
public class Movies: IMovies
{
private List<Movie> _movies = new()
{
new() { Id = 1, Rating = 5, Title = "Shrek" },
new() { Id = 2, Rating = 1, Title = "Inception" },
new() { Id = 3, Rating = 3, Title = "Jaws" },
new() { Id = 4, Rating = 1, Title = "The Green Latern" },
new() { Id = 5, Rating = 5, Title = "The Matrix" },
};
public void Delete(int id)
{
_movies.Remove(_movies.Single(x => x.Id == id));
}
public List<Movie> GetAll()
{
return _movies;
}
public Movie GetById(int id)
{
return _movies.Single(x => x.Id == id);
}
public void Insert(Movie movie)
{
_movies.Add(movie);
}
}
现在我们可以配置接口和实现类。在 program.cs 文件中,您可以通过使用 builder.Services
来添加依赖注入。要添加 IMovies
和 Movies
,只需使用以下行
// Configuration for dependency injection
builder.Services.AddScoped<IMovies, Movies>();
将此行放在 builder.Services.AddSwaggerGen()
之后。我们已经配置好了,让我们使用它!MapGet
非常简单
app.MapGet("/api/movies/", (IMovies movies) =>
{
return Results.Ok(movies.GetAll());
})
只需在映射的参数列表中添加接口和变量名即可。
好的,让我们看看第二个 get 请求,即 URL 中带有 id 的那个。
app.MapGet("/api/movies/{id:int}", (int id, IMovies movies) =>
{
return Results.Ok(movies.GetById(id));
});
是的,就是这样!但是……如果我们交换 id 和 movies 的顺序会怎样?什么都不会!嗯,会发生一些事情。它就像上面的示例一样工作。.NET 会识别类型,命名约定也有帮助。但如果您想要我的建议:在参数列表中保持一致的顺序。我个人喜欢在注入之前添加查询参数。
让我们用 DI 修复最后两个映射
app.MapPost("/api/movies/", (Movie movie, IMovies movies) =>
{
movies.Insert(movie);
return Results.Ok(movies.GetAll());
});
app.MapDelete("/api/movies/{id:int}", (int id, IMovies movies) =>
{
movies.Delete(id);
return movies.GetAll();
});
使其变为 ASYNC
我们的许多操作都是异步的,以使 API 能够处理多个请求并提高性能。我向您展示的示例不是异步的。使映射异步并不难。
对于这部分,我将 Movies
类中的方法设为异步。这不是最好的例子,但这是关于 API 的,而不是关于某个基本示例类的逻辑。
public interface IMovies
{
Task<List<Movie>> GetAll();
Task<Movie> GetById(int id);
Task Delete(int id);
Task Insert(Movie movie);
}
public class Movies: IMovies
{
private List<Movie> _movies = new()
{
new() { Id = 1, Rating = 5, Title = "Shrek" },
new() { Id = 2, Rating = 1, Title = "Inception" },
new() { Id = 3, Rating = 3, Title = "Jaws" },
new() { Id = 4, Rating = 1, Title = "The Green Latern" },
new() { Id = 5, Rating = 5, Title = "The Matrix" },
};
public async Task Delete(int id)
{
_movies.Remove(_movies.Single(x => x.Id == id));
}
public async Task<List<Movie>> GetAll()
{
return _movies;
}
public async Task<Movie> GetById(int id)
{
return _movies.Single(x => x.Id == id);
}
public async Task Insert(Movie movie)
{
_movies.Add(movie);
}
}
我们现在要做的就是使映射异步。这并不难,就像在前面的章节中一样。让我们再次从 MapGet
开始。
app.MapGet("/api/movies/", async (IMovies movies) =>
{
return Results.Ok(await movies.GetAll());
});
看到了吗?不难。我将 Lambda 表达式标记为异步,并在 await movies.GetAll()
前面添加了 await,这是可以 await 的。
我也可以向您展示其他映射,但它们都一样,我认为您已经明白了。
关注点
使用极简 API 是处理小型 API 和少量终结点的绝佳方式。在我看来,当您有很多终结点时,它就会变得丑陋。您可能会迷失在所有映射中。对于小型项目,.NET 极简 API 更适合。如果您有更大的项目,可以切换回经典的控制器。
历史
- 2022 年 10 月 21 日:初始版本