首先,定義一個ISimplifiedDataAccessor接口,這個接口被放在了一個獨(dú)立的包(.NET中的Assembly)Stickers.Common下,這個接口定義了一套CRUD的基本方法,在另一個獨(dú)立的包Stickers.DataAccess.InMemory中,有一個實(shí)現(xiàn)了該接口的類:InMemoryDataAccessor,它包含了一個IEntity實(shí)體的列表數(shù)據(jù)結(jié)構(gòu),然后基于這個列表,實(shí)現(xiàn)了ISimplifiedDataAccessor下的所有方法。而Stickers.WebApi中的API控制器StickersController則依賴ISimplifiedDataAccessor接口,并由ASP.NET Core的依賴注入框架將InMemoryDataAccessor的實(shí)例注入到控制器中。

為了構(gòu)圖美觀,類圖中所有方法的參數(shù)和返回類型都進(jìn)行了簡化,在案例的代碼中,各個方法的參數(shù)和返回類型都比圖中所示稍許復(fù)雜一些。

這里我們引入了IEntity接口,所有能夠通過SDAC進(jìn)行數(shù)據(jù)訪問的數(shù)據(jù)對象,都需要實(shí)現(xiàn)這個接口。引入該接口的一個重要目的是為了實(shí)現(xiàn)泛型約束,以便可以在ISimplifiedDataAccessor接口上明確指定什么樣的對象才可以被用于數(shù)據(jù)訪問。另外,這里還引入了一個泛型類型:Paginated<TEntity>類型,它可以包含分頁信息,并且其中的Items屬性保存的是某一頁的數(shù)據(jù)(頁碼由PageIndex屬性指定),因?yàn)樵?code>StickersController控制器中,我們大概率會需要實(shí)現(xiàn)能夠支持分頁的“貼紙”查詢功能。

限于篇幅,就不對InMemoryDataAccessor中的每個方法的具體實(shí)現(xiàn)進(jìn)行介紹了,有興趣的話可以打開本文最后貼出的源代碼鏈接,直接打開代碼閱讀。這里著重解讀一下GetPaginatedEntitiesAsync方法的代碼:


public Task<Paginated<TEntity>> GetPaginatedEntitiesAsync<TEntity, TField>(
Expression<Func<TEntity, TField>> orderByExpression, bool sortAscending = true, int pageSize = 25,
int pageNumber = 0, Expression<Func<TEntity, bool>>? filterExpression = null,
CancellationToken cancellationToken = default) where TEntity : class, IEntity
{
var resultSet = filterExpression is not null
? _entities.Cast<TEntity>().Where(filterExpression.Compile())
: _entities.Cast<TEntity>();
var enumerableResultSet = resultSet.ToList();
var totalCount = enumerableResultSet.Count;
var orderedResultSet = sortAscending
? enumerableResultSet.OrderBy(orderByExpression.Compile())
: enumerableResultSet.OrderByDescending(orderByExpression.Compile());
return Task.FromResult(new Paginated<TEntity>
{
Items = orderedResultSet.Skip(pageNumber * pageSize).Take(pageSize).ToList(),
PageIndex = pageNumber,
PageSize = pageSize,
TotalCount = totalCount,
TotalPages = (totalCount + pageSize - 1) / pageSize
});
}

這個方法的目的就是為了返回某一頁的實(shí)體數(shù)據(jù),首先分頁是需要基于排序的,因此,orderByExpression參數(shù)通過Lambda表達(dá)式來指定排序的字段;sortAscending很好理解,它指定是否按升序排序;pageSizepageNumber指定分頁時每頁的數(shù)據(jù)記錄條數(shù)以及需要返回的數(shù)據(jù)頁碼;通過filterExpression Lambda表達(dá)式參數(shù),還可以指定查詢過濾條件,比如,只返回“創(chuàng)建日期”大于某一天的數(shù)據(jù)。在InMemoryDataAccessor中,都是直接對列表數(shù)據(jù)結(jié)構(gòu)進(jìn)行操作,所以這個函數(shù)的實(shí)現(xiàn)還是比較簡單易懂的:如果filterExpression有定義,則首先執(zhí)行過濾操作,然后再進(jìn)行排序,并構(gòu)建Paginated<TEntity>對象作為函數(shù)的返回值。在下一篇文章介紹PostgreSQL數(shù)據(jù)訪問的實(shí)現(xiàn)時,我們還會看到這個函數(shù)的另一個不同的實(shí)現(xiàn)。

在接口定義上,GetPaginatedEntitiesAsync是一個異步方法,所以,我們應(yīng)該盡可能地傳入CancellationToken對象,以便在該方法中能夠支持取消操作。

現(xiàn)在我們已經(jīng)有了數(shù)據(jù)訪問層,就可以開始實(shí)現(xiàn)Sticker微服務(wù)的RESTful API了。

StickersController控制器

我們是使用ASP.NET Core Web API創(chuàng)建的StickersController控制器,所以也會默認(rèn)使用RESTful來實(shí)現(xiàn)微服務(wù)的API,RESTful API基于HTTP協(xié)議,是目前微服務(wù)間通信使用最為廣泛的協(xié)議之一,由于它主要基于JSON數(shù)據(jù)格式,因此對前端開發(fā)和實(shí)現(xiàn)也是特別友好。RESTful下對于被訪問的數(shù)據(jù)統(tǒng)一看成資源,是資源就有地址、所支持的訪問方式等屬性,不過這里我們就不深入討論這些內(nèi)容了,重點(diǎn)講一下StickersController實(shí)現(xiàn)的幾個要點(diǎn)。

ISimplifiedDataAccessor的注入

熟悉ASP.NET Core Web API開發(fā)的讀者,對于如何注入一個Service應(yīng)該是非常熟悉的,這里就簡單介紹下吧。在Stickers.Api項(xiàng)目的Program.cs文件里,直接加入下面這行代碼即可,注意加之前,先向項(xiàng)目添加對Stickers.DataAccess.InMemory項(xiàng)目的引用:


builder.Services.AddSingleton<ISimplifiedDataAccessor, InMemoryDataAccessor>();

在這里,我將InMemoryDataAccessor注冊為單例實(shí)例,雖然它是一個有狀態(tài)的對象,但使用它的目的也僅僅是讓整個應(yīng)用程序能夠運(yùn)行起來,后面是會用PostgreSQL進(jìn)行替換的(PostgreSQL的數(shù)據(jù)訪問層是無狀態(tài)的,因此在這里使用單例是合理的),所以在這里并不需要糾結(jié)它本身的實(shí)現(xiàn)是否合理、在單例下是否是線程安全。高內(nèi)聚低耦合的設(shè)計(jì)原則,讓問題變得更為簡單。

現(xiàn)在將Stickers.Api項(xiàng)目下的WeatherForecastController刪掉,然后新加一個Controller,命名為StickersController,基本代碼結(jié)構(gòu)如下:


namespace Stickers.WebApi.Controllers;


[ApiController]
[Route("[controller]")]
public class StickersController(ISimplifiedDataAccessor dac) : ControllerBase
{
// 其它代碼暫時省略
}

于是就可以在StickersController控制器中,通過dac實(shí)例來訪問數(shù)據(jù)存儲了。

控制器代碼的可測試性:由于StickersController僅依賴ISimplifiedDataAccessor接口,因此,在進(jìn)行單元測試時,完全可以通過Mock技術(shù),生成一個ISimplifiedDataAccessor的Mock對象,然后將其注入到StickersController中完成單元測試。

在控制器方法中返回合理的HTTP狀態(tài)碼

對于不同的RESTful API,在不同的情況下應(yīng)該返回合理的HTTP狀態(tài)碼,這是RESTful API開發(fā)的一種最佳實(shí)踐。尤其是在微服務(wù)架構(gòu)下,合理定義API的返回代碼,對于多服務(wù)集成是有好處的。我認(rèn)為可以遵循以下幾個原則:

  1. 盡量避免直接返回500 Internal Server Error
  2. 由于客戶端傳入數(shù)據(jù)不符合要求而造成API無法順利執(zhí)行,應(yīng)該返回以“4”開頭的狀態(tài)碼(4XX),比如:
    1. 如果客戶端發(fā)出資源查詢請求,但實(shí)際上這個資源并不存在,則返回404 Not Found
    2. 如果希望創(chuàng)建的資源已經(jīng)存在,可以返回409 Conflict
    3. 如果客戶端傳入的資源中的某些數(shù)據(jù)存在問題,可以返回400 Bad Request
  3. POST方法一般用于資源的新建,所以通常返回201 Created,并在返回體(response body)中,指定新創(chuàng)建資源的地址。當(dāng)然,也有些情況下POST并不是用來創(chuàng)建新的資源,而是用來執(zhí)行某個任務(wù),此時也可以用200 OK或者204 No Content返回
  4. PUT、PATCH、DELETE方法,根據(jù)是否需要返回資源數(shù)據(jù),來決定是應(yīng)該返回200 OK還是204 No Content

以下面三個RESTful API方法為例:


[HttpGet("{id}")]
[ProducesResponseType(StatusCodes.Status200OK, Type = typeof(Sticker))]
[ProducesResponseType(StatusCodes.Status404NotFound)]
public async Task<IActionResult> GetByIdAsync(int id)
{
var sticker = await dac.GetByIdAsync<Sticker>(id);
if (sticker is null) return NotFound($"Sticker with id {id} was not found.");
return Ok(sticker);
}


[HttpPost]
[ProducesResponseType(StatusCodes.Status201Created)]
[ProducesResponseType(StatusCodes.Status409Conflict)]
[ProducesResponseType(StatusCodes.Status400BadRequest)]
public async Task<IActionResult> CreateAsync(Sticker sticker)
{
var exists = await dac.ExistsAsync<Sticker>(s => s.Title == sticker.Title);
if (exists) return Conflict($"Sticker {sticker.Title} already exists.");
var id = await dac.AddAsync(sticker);
return CreatedAtAction(nameof(GetByIdAsync), new { id }, sticker);
}


[HttpDelete("{id}")]
[ProducesResponseType(StatusCodes.Status204NoContent)]
[ProducesResponseType(StatusCodes.Status404NotFound)]
public async Task<IActionResult> DeleteByIdAsync(int id)
{
var result = await dac.RemoveByIdAsync<Sticker>(id);
if (!result) return NotFound($"Sticker with id {id} was not found.");
return NoContent();
}

這幾個方法都用到了Sticker類,這個類代表了“貼紙”對象,它其實(shí)是一個領(lǐng)域?qū)ο螅缟衔乃f,目前我們僅將其用作數(shù)據(jù)傳輸對象,它的定義如下:


public class Sticker(string title, string content) : IEntity
{
public int Id { get; set; }


[Required]
[StringLength(50)]
public string Title { get; set; } = title;


public string Content { get; set; } = content;


public DateTime CreatedOn { get; set; } = DateTime.UtcNow;


public DateTime? ModifiedOn { get; set; }
}

Sticker類實(shí)現(xiàn)了IEntity接口,它是Stickers.WebApi項(xiàng)目中的一個類,它被定義在了Stickers.WebApi項(xiàng)目中,而不是定義在Stickers.Common項(xiàng)目中,是因?yàn)閺腂ounded Context的劃分角度,它是Stickers.WebApi項(xiàng)目的一個內(nèi)部業(yè)務(wù)對象,并不會被其它微服務(wù)所使用。

CreateAsync方法中,它會首先判斷相同標(biāo)題的“貼紙”是否存在,如果存在,則返回409;否則就直接創(chuàng)建貼紙,并返回201,同時帶上創(chuàng)建成功后“貼紙”資源的地址(CreatedAtAction方法表示,資源創(chuàng)建成功,可以通過GetByIdAsync方法所在的HTTP路徑,帶上新建“貼紙”資源的Id來訪問到該資源)。而在DeleteByIdAsync方法中,API會直接嘗試刪除指定Id的“貼紙”,如果貼紙不存在,則返回404,否則就是成功刪除,返回204。

順便提一下在各個方法上所使用的ProducesResponseType特性,一般我們可以將當(dāng)前API方法能夠返回的HTTP狀態(tài)碼都用這個特性(Attribute)標(biāo)注一下,以便Swagger能夠生成更為詳細(xì)的文檔:

ASP.NET Core Web API中的模型驗(yàn)證

ASP.NET Core Web API在一個Controller方法被調(diào)用前,是可以自動完成模型驗(yàn)證的。比如在上面的CreateAsync方法中,為什么我沒有對“貼紙”的標(biāo)題(Title)字段判空?而在這個API的返回狀態(tài)定義中,卻明確表示它有可能返回400?因?yàn)椋赟ticker類的Title屬性上,我使用了RequiredStringLength這兩個特性:


[Required]
[StringLength(50)]
public string Title { get; set; } = title;

于是,在Sticker類被用于RESTful API的POST請求體(request body)時,ASP.NET Core Web API框架會自動根據(jù)這些特性來完成數(shù)據(jù)模型的驗(yàn)證,比如,在啟動程序后,執(zhí)行下面的命令:


$ curl -X POST http://localhost:5141/stickers \
-d '{"content": "hell world!"}' \
-H 'Content-Type: application/json' \
-v && echo

會得到下面的返回結(jié)果:

不僅如此,開發(fā)人員還可以擴(kuò)展System.ComponentModel.DataAnnotations.ValidationAttribute來實(shí)現(xiàn)自定義的驗(yàn)證邏輯。

PUT還是PATCH?

在開發(fā)RESTful API時,有個比較糾結(jié)的問題是,在修改資源時,是應(yīng)該用PUT還是PATCH?其實(shí)很簡單,PUT的定義是:使用數(shù)據(jù)相同的另一個資源來替換已有資源,而PATCH則是針對某個已有資源進(jìn)行修改。所以,單從修改對象的角度,PATCH要比PUT更高效,它不需要客戶端將需要修改的對象整個性地下載下來,修改之后又整個性地發(fā)送到后端進(jìn)行保存。于是,又產(chǎn)生另一個問題:服務(wù)端如何得知應(yīng)該修改資源的哪個屬性字段以及修改的方式是什么呢?一個比較直接的做法是,在服務(wù)端仍然接收來自客戶端由PATCH方法發(fā)送過來的Sticker對象,然后判斷這個對象中的每個字段的值是否有值,如果有,則表示客戶端希望修改這個字段,否則就跳過這個字段的修改。如果對象結(jié)構(gòu)比較簡單,這種做法可能也還行,但是如果對象中包含了大量屬性字段,或者它有一定的層次結(jié)構(gòu),那么這種做法就會顯得比較笨拙,不僅費(fèi)時費(fèi)力,而且容易出錯。

在RESTful API的實(shí)現(xiàn)中,一個比較好的做法是采用JSON Patch,它是一套國際標(biāo)準(zhǔn)(RFC6902),它定義了JSON文檔(JSON document)修改的基本格式和規(guī)范,而微軟的ASP.NET Core Web API原生支持JSON Patch。以下是StickersController控制器中使用JSON Patch的方法:


[HttpPatch("{id}")]
[ProducesResponseType(StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status404NotFound)]
[ProducesResponseType(StatusCodes.Status400BadRequest)]
public async Task<IActionResult> UpdateStickerAsync(int id, [FromBody] JsonPatchDocument<Sticker>? patchDocument)
{
if (patchDocument is null) return BadRequest();
var sticker = await dac.GetByIdAsync<Sticker>(id);
if (sticker is null) return NotFound();
sticker.ModifiedOn = DateTime.UtcNow;
patchDocument.ApplyTo(sticker, ModelState);
if (!ModelState.IsValid) return BadRequest(ModelState);
await dac.UpdateAsync(id, sticker);
return Ok(sticker);
}

代碼邏輯很簡單,首先通過Id獲得“貼紙”對象,然后使用patchDocument.ApplyTo方法,將客戶端的修改請求應(yīng)用到貼紙對象上,然后調(diào)用SDAC更新后端存儲中的數(shù)據(jù),最后返回修改后的貼紙對象。讓我們測試一下,首先新建一個貼紙:


$ curl -X POST http://localhost:5141/stickers \
> -H 'Content-Type: application/json' \
> -d '{"title": "Hello", "content": "Hello daxnet"}' -v
Note: Unnecessary use of -X or --request, POST is already inferred.
* Host localhost:5141 was resolved.
* IPv6: ::1
* IPv4: 127.0.0.1
* Trying [::1]:5141...
* Connected to localhost (::1) port 5141
> POST /stickers HTTP/1.1
> Host: localhost:5141
> User-Agent: curl/8.5.0
> Accept: */*
> Content-Type: application/json
> Content-Length: 45
>
< HTTP/1.1 201 Created
< Content-Type: application/json; charset=utf-8
< Date: Sat, 12 Oct 2024 07:50:00 GMT
< Server: Kestrel
< Location: http://localhost:5141/stickers/1
< Transfer-Encoding: chunked
<
* Connection #0 to host localhost left intact
{"id":1,"title":"Hello","content":"Hello daxnet","createdOn":"2024-10-12T07:50:00.9075598Z","modifiedOn":null}

然后,查看這個貼紙的數(shù)據(jù)是否正確:


$ curl http://localhost:5141/stickers/1 | jq
% Total % Received % Xferd Average Speed Time Time Time Current
Dload Upload Total Spent Left Speed
100 110 0 110 0 0 9650 0 --:--:-- --:--:-- --:--:-- 10000
{
"id": 1,
"title": "Hello",
"content": "Hello daxnet",
"createdOn": "2024-10-12T07:50:00.9075598Z",
"modifiedOn": null
}

現(xiàn)在,使用PATCH方法,將content改為”Hello World”:


$ curl -X PATCH http://localhost:5141/stickers/1 \
> -H 'Content-Type: application/json-patch+json' \
> -d '[{"op": "replace", "path": "/content", "value": "Hello World"}]' -v && echo
* Host localhost:5141 was resolved.
* IPv6: ::1
* IPv4: 127.0.0.1
* Trying [::1]:5141...
* Connected to localhost (::1) port 5141
> PATCH /stickers/1 HTTP/1.1
> Host: localhost:5141
> User-Agent: curl/8.5.0
> Accept: */*
> Content-Type: application/json-patch+json
> Content-Length: 63
>
< HTTP/1.1 200 OK
< Content-Type: application/json; charset=utf-8
< Date: Sat, 12 Oct 2024 07:56:04 GMT
< Server: Kestrel
< Transfer-Encoding: chunked
<
* Connection #0 to host localhost left intact
{"id":1,"title":"Hello","content":"Hello World","createdOn":"2024-10-12T07:50:00.9075598Z","modifiedOn":"2024-10-12T07:56:04.815507Z"}

注意上面命令中需要將Content-Type設(shè)置為application/json-patch+json,再執(zhí)行一次GET請求驗(yàn)證一下:


$ curl http://localhost:5141/stickers/1 | jq
% Total % Received % Xferd Average Speed Time Time Time Current
Dload Upload Total Spent Left Speed
100 134 0 134 0 0 43819 0 --:--:-- --:--:-- --:--:-- 44666
{
"id": 1,
"title": "Hello",
"content": "Hello World",
"createdOn": "2024-10-12T07:50:00.9075598Z",
"modifiedOn": "2024-10-12T07:56:04.815507Z"
}

可以看到,content已經(jīng)被改為了Hello World,同時modifiedOn字段也更新為了當(dāng)前資源被更改的UTC時間。

在服務(wù)端如果需要存儲時間信息,一般都應(yīng)該保存為UTC時間,或者本地時間+時區(qū)信息,這樣也能推斷出UTC時間,總之,在服務(wù)端,應(yīng)該以UTC時間作為標(biāo)準(zhǔn),這樣在不同時區(qū)的客戶端就可以根據(jù)服務(wù)端返回的UTC時間來計(jì)算并顯示本地時間,這樣就不會出現(xiàn)混亂。 

?在ASP.NET Core中使用JSON Patch還需要引入Newtonsoft JSON Input Formatter,請按照微軟官方文檔的步驟進(jìn)行設(shè)置即可。

在分頁查詢API上支持排序字段表達(dá)式

在前端應(yīng)用中,通常都可以支持用戶自定義的數(shù)據(jù)排序,也就是用戶可以自己決定是按數(shù)據(jù)的哪個字段以升序還是降序的順序進(jìn)行排序,然后基于這樣的排序完成分頁功能。其實(shí)實(shí)現(xiàn)的基本原理我已經(jīng)在《在ASP.NET Core Web API上動態(tài)構(gòu)建Lambda表達(dá)式實(shí)現(xiàn)指定字段的數(shù)據(jù)排序》一文中介紹過了,思路就是根據(jù)輸入的字段名構(gòu)建Lambda表達(dá)式,然后將Lambda表達(dá)式應(yīng)用到對象列表的OrderBy/OrderByDescending方法,或者是應(yīng)用到數(shù)據(jù)庫訪問組件上,以實(shí)現(xiàn)排序功能。下面就是StickersController控制器中的相關(guān)代碼:


[HttpGet]
[ProducesResponseType(StatusCodes.Status200OK)]
public async Task<IActionResult> GetStickersAsync(
[FromQuery(Name = "sort")] string? sortField = null,
[FromQuery(Name = "asc")] bool ascending = true,
[FromQuery(Name = "size")] int pageSize = 20,
[FromQuery(Name = "page")] int pageNumber = 0)
{
Expression<Func<Sticker, object>> sortExpression = s => s.Id;
if (sortField is not null) sortExpression = ConvertToExpression<Sticker, object>(sortField);
return Ok(
await dac.GetPaginatedEntitiesAsync(sortExpression, ascending, pageSize, pageNumber)
);
}


private static Expression<Func<TEntity, TProperty>> ConvertToExpression<TEntity, TProperty>(string propertyName)
{
if (string.IsNullOrWhiteSpace(propertyName))
throw new ArgumentNullException($"{nameof(propertyName)} cannot be null or empty.");
var propertyInfo = typeof(TEntity).GetProperty(propertyName);
if (propertyInfo is null) throw new ArgumentNullException($"Property {propertyName} is not defined.");
var parameterExpression = Expression.Parameter(typeof(TEntity), "p");
var memberExpression = Expression.Property(parameterExpression, propertyInfo);
if (propertyInfo.PropertyType.IsValueType)
return Expression.Lambda<Func<TEntity, TProperty>>(
Expression.Convert(memberExpression, typeof(object)),
parameterExpression);
return Expression.Lambda<Func<TEntity, TProperty>>(memberExpression, parameterExpression);
}

下面展示了根據(jù)Id字段進(jìn)行降序排列的命令行以及API調(diào)用輸出:


$ curl 'http://localhost:5141/stickers?sort=Id&asc=false&size=20&page=0' | jq
% Total % Received % Xferd Average Speed Time Time Time Current
Dload Upload Total Spent Left Speed
100 453 0 453 0 0 205k 0 --:--:-- --:--:-- --:--:-- 221k
{
"items": [
{
"id": 4,
"title": "c",
"content": "5",
"createdOn": "2024-10-12T11:55:10.8708238Z",
"modifiedOn": null
},
{
"id": 3,
"title": "d",
"content": "1",
"createdOn": "2024-10-12T11:54:37.9055791Z",
"modifiedOn": null
},
{
"id": 2,
"title": "b",
"content": "7",
"createdOn": "2024-10-12T11:54:32.4162609Z",
"modifiedOn": null
},
{
"id": 1,
"title": "a",
"content": "3",
"createdOn": "2024-10-12T11:54:23.3103948Z",
"modifiedOn": null
}
],
"pageIndex": 0,
"pageSize": 20,
"totalCount": 4,
"totalPages": 1
}

Tip:在URL中使用小寫命名規(guī)范

由于C#編程規(guī)定對于標(biāo)識符都使用Pascal命名規(guī)范,而ASP.NET Core Web API在產(chǎn)生URL時,是根據(jù)Controller和Action的名稱來決定的,所以,在路徑中都是默認(rèn)使用Pascal命名規(guī)范,也就是第一個字符是大寫字母。比如:http://localhost:5141/Stickers,其中“Stickers”的“S”就是大寫。然而,實(shí)際中大多數(shù)情況下,都希望能夠跟前端開發(fā)保持一致,也就是希望開頭第一個字母是小寫,比如像http://localhost:5141/stickers這樣。ASP.NET Core Web API提供了解決方案,在Program.cs文件中加入如下代碼即可:


builder.Services.AddRouting(options =>
{
options.LowercaseUrls = true;
options.LowercaseQueryStrings = true;
});

Tip:讓控制器方法支持Async后綴

在StickersController控制器中,我們使用了async/await來實(shí)現(xiàn)每個API方法,根據(jù)C#編程規(guī)范,異步方法應(yīng)該以Async字樣作為后綴,但如果這樣做的話,那么在CreateAsync這個方法返回CreatedAtAction(nameof(GetByIdAsync), new { id }, sticker)時,就會報(bào)如下的錯誤:


System.InvalidOperationException: No route matches the supplied values.

 解決方案很簡單,在Program.cs文件中,調(diào)用builder.Services.AddControllers();方法時,將它改為:


builder.Services.AddControllers(options =>
{
options.SuppressAsyncSuffixInActionNames = false;
// 其它代碼省略...
});

至此,StickersController的基本部分已經(jīng)完成了,啟動整個項(xiàng)目,打開Swagger頁面,就可以看到我們所開發(fā)的幾個API。現(xiàn)在就可以直接在Swagger頁面中調(diào)用這些方法來體驗(yàn)我們的Sticker微服務(wù)所提供的這些RESTful API了:

總結(jié)

本文介紹了我們案例中Sticker微服務(wù)的基本實(shí)現(xiàn),包括數(shù)據(jù)訪問部分和Sticker RESTful API的設(shè)計(jì)與實(shí)現(xiàn),雖然目前我們只是使用一個InMemoryDataAccessor來模擬后端的數(shù)據(jù)存儲,但Sticker微服務(wù)的基本功能都已經(jīng)有了。然而,為了實(shí)現(xiàn)云原生,我們還需要向這個Sticker微服務(wù)加入一些與業(yè)務(wù)無關(guān)的東西,比如:加入日志功能以支持運(yùn)行時問題的追蹤和診斷;加入健康狀態(tài)檢測機(jī)制(health check)以支持服務(wù)狀態(tài)監(jiān)控和運(yùn)行實(shí)例調(diào)度,此外還有RESTful API Swagger文檔的完善、使用版本號和Git Hash來支持持續(xù)集成與持續(xù)部署等等,這些內(nèi)容看起來挺簡單,但也是需要花費(fèi)一定的時間和精力來遵循標(biāo)準(zhǔn)的最佳實(shí)踐。在我們真正完成了Sticker微服務(wù)后,我會使用獨(dú)立的篇幅來介紹這些內(nèi)容。

此外,ASP.NET Core Web API的功能也不僅僅局限于我們目前用到的這些,由于我們的重點(diǎn)不在ASP.NET Core Web API本身的學(xué)習(xí)上,所以這里也只會涵蓋用到的這些功能,對ASP.NET Core Web API整套體系知識結(jié)構(gòu)感興趣的讀者,建議閱讀微軟官方文檔。

下一講我將介紹如何使用PostgreSQL作為Sticker微服務(wù)的數(shù)據(jù)庫,從這一講開始,我將逐步引入容器技術(shù)。

源代碼

本章源代碼請參考這里:https://gitee.com/daxnet/stickers/tree/chapter_2/

文章轉(zhuǎn)自微信公眾號@DotNet NB

上一篇:

Gin系列二:Gin搭建Blog API's (二)

下一篇:

如何通過CD平臺如何將數(shù)據(jù)以API的方式同步到facebook
#你可能也喜歡這些API文章!

我們有何不同?

API服務(wù)商零注冊

多API并行試用

數(shù)據(jù)驅(qū)動選型,提升決策效率

查看全部API→
??

熱門場景實(shí)測,選對API

#AI文本生成大模型API

對比大模型API的內(nèi)容創(chuàng)意新穎性、情感共鳴力、商業(yè)轉(zhuǎn)化潛力

25個渠道
一鍵對比試用API 限時免費(fèi)

#AI深度推理大模型API

對比大模型API的邏輯推理準(zhǔn)確性、分析深度、可視化建議合理性

10個渠道
一鍵對比試用API 限時免費(fèi)