ASP.NET Web API 最佳實踐專案整合 - 客製篇
在 ASP.NET Web API 2 基礎下,建立整合完備的應用程式專案範本。
- 整合套件或自行開發來提供
RESTFul API 最佳實踐
中的建議功能。 - 配合
開發規範
提供一致有效率的開發方式及程式品質。
這篇提供一些自行開發的程式範例,以達到對 RESTFul 最佳實踐的支援。
大標
內文
在 ASP.NET Web API 2 基礎下,建立整合完備的應用程式專案範本。
- 整合套件或自行開發來提供
RESTFul API 最佳實踐
中的建議功能。 - 配合
開發規範
提供一致有效率的開發方式及程式品質。
這篇提供一些自行開發的程式範例,以達到對 RESTFul 最佳實踐的支援。
版本資訊
視需求在 Route 中加入主版本號。
以 Owin Middleware 實作 Versioning Header
版本資訊,可用自動取得的 Assembly File Version 來表示,並保存起來,避免一直重覆讀取。進階上,可以配合參數,傳入指定的版本資訊或類型,提供更彈性的功能。
確認在 Web API 中啟用 Owin,並加入啟動類別。
類別 VersioningMiddleware1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48/// <summary>
/// 於 Response Header 中自動以 Assembly file version 提供 Api Version
/// </summary>
public class VersioningMiddleware
{
// 簡單保存在 static 中
private static string ApiVersion;
//
private const string HeaderName = "Api-Version";
//
private AppFunc next;
/// <param name="next"></param>
public VersioningMiddleware(AppFunc next)
{
this.next = next;
}
/// <param name="env"></param>
/// <returns></returns>
public async Task Invoke(IDictionary<string, object> env)
{
var ctx = new OwinContext(env);
var apiVersion = GetVersion();
ctx.Response.Headers.Add(HeaderName, new string[] { apiVersion });
await this.next(env);
}
private string GetVersion()
{
if(ApiVersion == null)
{
ApiVersion = GetVersionFromAssembly();
}
return ApiVersion;
}
private string GetVersionFromAssembly()
{
var fileVersion = FileVersionInfo.GetVersionInfo(Assembly.GetExecutingAssembly().Location);
// Major.Minor.Build
return string.Format("{0}.{1}.{2}", fileVersion.ProductMajorPart, fileVersion.ProductMinorPart, fileVersion.ProductBuildPart);
}
}
注意,取到的是哪一個 Assembly 會影響拿到的版本內容。Assembly.GetExecutingAssembly() 是取 Middleware 實作的這個 Assembly。如果 Middleware 實作在另外的專案中,完整範例 中有提供自動找到繼承 ApiController 的 Assembly 方法。
類別 VersioningMiddlewareExtensions1
2
3
4
5
6
7
8
9
10
11/// <summary>
/// 於 Response Header 中自動以 Assembly file version 提供 Api Version
/// </summary>
public static class VersioningMiddlewareExtensions
{
/// <param name="app"></param>
public static void UseVersioningMiddleware(this IAppBuilder app)
{
app.Use<VersioningMiddleware>();
}
}
在啟動類別中啟用1
app.UseVersioningMiddleware();
套用 snake_case
網址參數轉換
自行實作 ActionSelector,原理就是對 QueryString 做轉換和重寫。1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51public class SnakeCaseActionSelector : ApiControllerActionSelector
{
public override HttpActionDescriptor SelectAction(HttpControllerContext controllerContext)
{
var newUri = CreateNewUri(
controllerContext.Request.RequestUri,
controllerContext.Request.GetQueryNameValuePairs());
controllerContext.Request.RequestUri = newUri;
return base.SelectAction(controllerContext);
}
private Uri CreateNewUri(Uri requestUri, IEnumerable<KeyValuePair<string, string>> queryPairs)
{
var currentQuery = requestUri.Query;
if (!currentQuery.Any())
{
return requestUri;
}
var newQuery = ConvertQueryToCamelCase(queryPairs);
return new Uri(requestUri.ToString().Replace(currentQuery, newQuery));
}
private static string ConvertQueryToCamelCase(IEnumerable<KeyValuePair<string, string>> queryPairs)
{
queryPairs = queryPairs
.Select(x => new KeyValuePair<string, string>(x.Key.ToCamelCase(), x.Value));
return "?" + queryPairs
.Select(x => String.Format("{0}={1}", x.Key, x.Value))
.Aggregate((x, y) => x + "&" + y);
}
private static string ToCamelCase(string source)
{
var parts = source
.Split(new[] { '_' }, StringSplitOptions.RemoveEmptyEntries);
return parts
.First().ToLower() +
string.Join("", parts.Skip(1).Select(ToCapital));
}
private static string ToCapital(string source)
{
return string.Format("{0}{1}", char.ToUpper(source[0]), source.Substring(1).ToLower());
}
}
最後加入 WebApiConfig1
config.Services.Replace(typeof(IHttpActionSelector), new SnakeCaseActionSelector());
分頁
OData 有支援分頁
http://www.c-sharpcorner.com/article/paging-with-odata-and-Asp-Net-web-api/
自己實作產生 Link header、Paging header 及 Response 分頁資訊 (JSON Envelope) 的工具類別
分頁資訊: 提供頁碼及連結 Uri1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86public class PageDataBuilder
{
public Uri FirstPage { get; private set; }
public Uri LastPage { get; private set; }
public Uri NextPage { get; private set; }
public Uri PreviousPage { get; private set; }
public bool HasLink { get; set; }
//
public int PageNo { get; private set; }
public int PageSize { get; private set; }
public int TotalRecordCount { get; private set; }
public int PageCount { get; private set; }
/// <summary>
/// constructor
/// </summary>
/// <param name="pageNo">需求的頁碼</param>
/// <param name="pageSize">每頁的筆數</param>
/// <param name="totalRecordCount">資料總筆數</param>
/// <param name="requestPath">服務路徑 ex:Request.RequestUri.GetLeftPart(UriPartial.Path), 沒傳就不產生 Link</param>
/// <param name="routeValues">Link 的 Url 參數, 匿名物件</param>
public PageDataBuilder(int pageNo,
int pageSize,
int totalRecordCount,
string requestPath = null,
object routeValues = null)
{
// Determine total number of pages
var pageCount = totalRecordCount > 0
? (int)Math.Ceiling(totalRecordCount / (double)pageSize)
: 0;
PageNo = pageNo;
PageSize = pageSize;
PageCount = pageCount;
TotalRecordCount = totalRecordCount;
// create page links,有 requestPath 才產生
if (!string.IsNullOrEmpty(requestPath))
{
HasLink = true;
FirstPage = BuildUri(requestPath, new HttpRouteValueDictionary(routeValues)
{
{"pageNo", 1},
{"pageSize", pageSize}
});
LastPage = BuildUri(requestPath, new HttpRouteValueDictionary(routeValues)
{
{"pageNo", pageCount},
{"pageSize", pageSize}
});
if (pageNo > 1)
{
PreviousPage = BuildUri(requestPath, new HttpRouteValueDictionary(routeValues)
{
{"pageNo", pageNo - 1},
{"pageSize", pageSize}
});
}
if (pageNo < pageCount)
{
NextPage = BuildUri(requestPath, new HttpRouteValueDictionary(routeValues)
{
{"pageNo", pageNo + 1},
{"pageSize", pageSize}
});
}
}
}
private Uri BuildUri(string path, HttpRouteValueDictionary routeValues)
{
return new Uri(path + "?" + routeValues
.Select(x => string.Format("{0}={1}", ToSnakeCase(x.Key), x.Value))
.Aggregate((x, y) => x + "&" + y));
}
private string ToSnakeCase(string camelCase)
{
return string.Concat(camelCase.Select((x, i) => i > 0 && char.IsUpper(x) ? "_" + x.ToString() : x.ToString())).ToLower();
}
}
在 Response 中設定分頁相關 Header1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79public class PagingHeader
{
private const string PageNoHeaderName = "X-Paging-PageNo";
private const string PageSizeHeaderName = "X-Paging-PageSize";
private const string PageCountHeaderName = "X-Paging-PageCount";
private const string TotalRecordCountName = "X-Paging-TotalRecordCount";
private const string LinkHeaderName = "Link";
private const string LinkHeaderTemplate = "<{0}>; rel=\"{1}\"";
//
private HttpResponseMessage response;
public PagingHeader(HttpResponseMessage responseMessage)
{
this.response = responseMessage;
}
public PagingHeader AddPageNo(int pageNo)
{
response.Headers.Add(PageNoHeaderName, pageNo.ToString());
return this;
}
public PagingHeader AddPageSize(int pageSize)
{
response.Headers.Add(PageSizeHeaderName, pageSize.ToString());
return this;
}
public PagingHeader AddPageCount(int pageCount)
{
response.Headers.Add(PageCountHeaderName, pageCount.ToString());
return this;
}
public PagingHeader AddTotalRecordCount(int totalCount)
{
response.Headers.Add(TotalRecordCountName, totalCount.ToString());
return this;
}
public PagingHeader AddLink(PageDataBuilder dataBuilder)
{
if(!dataBuilder.HasLink)
{
return this;
}
List<string> links = new List<string>();
if (dataBuilder.FirstPage != null)
{
links.Add(string.Format(LinkHeaderTemplate, dataBuilder.FirstPage, "first"));
}
if (dataBuilder.PreviousPage != null)
{
links.Add(string.Format(LinkHeaderTemplate, dataBuilder.PreviousPage, "previous"));
}
if (dataBuilder.NextPage != null)
{
links.Add(string.Format(LinkHeaderTemplate, dataBuilder.NextPage, "next"));
}
if (dataBuilder.LastPage != null)
{
links.Add(string.Format(LinkHeaderTemplate, dataBuilder.LastPage, "last"));
}
// Set the page link header
response.Headers.Add(LinkHeaderName, string.Join(", ", links));
return this;
}
}
產生有分頁資訊的 Result1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105public class PagedResult<T>
{
private IEnumerable<T> pagedRows;
private PageDataBuilder dataBuilder;
/// <summary>
/// 用 totalRows 取得總筆數,做分頁後再附加分頁資訊
/// </summary>
/// <param name="totalRows">全部的資料列</param>
/// <param name="pageNo">需求的頁碼</param>
/// <param name="pageSize">每頁的筆數</param>
/// <param name="requestPath">服務路徑 ex:Request.RequestUri.GetLeftPart(UriPartial.Path), 沒傳就不產生 Link</param>
/// <param name="routeValues">Link 的 Url 參數, 匿名物件</param>
public PagedResult(IEnumerable<T> totalRows,
int pageNo,
int pageSize,
string requestPath = null,
object routeValues = null)
{
this.dataBuilder = new PageDataBuilder(pageNo, pageSize, totalRows.Count(), requestPath, routeValues);
//
var skipAmount = pageSize * (pageNo - 1);
this.pagedRows = totalRows.Skip(skipAmount).Take(pageSize);
}
/// <summary>
/// 使用者已分好頁,只附加分頁資訊
/// </summary>
/// <param name="requestPath">服務路徑 ex:Request.RequestUri.GetLeftPart(UriPartial.Path)</param>
/// <param name="routeValues">額外的 Url 參數</param>
/// <param name="pageRows">要回傳的單頁資料</param>
/// <param name="pageNo">需求的頁碼</param>
/// <param name="pageSize">每頁的筆數</param>
/// <param name="totalRecordCount">資料總筆數</param>
/// <param name="requestPath">服務路徑 ex:Request.RequestUri.GetLeftPart(UriPartial.Path), 沒傳就不產生 Link</param>
/// <param name="routeValues">Link 的 Url 參數, 匿名物件</param>
public PagedResult(IEnumerable<T> pageRows,
int pageNo,
int pageSize,
int totalRecordCount,
string requestPath = null,
object routeValues = null)
{
this.dataBuilder = new PageDataBuilder(pageNo, pageSize, totalRecordCount, requestPath, routeValues);
this.pagedRows = pageRows;
}
public ResultObject<T> CreateResult()
{
var result = new ResultObject<T>
{
Data = pagedRows,
Paging = new Paging
{
PageNo = dataBuilder.PageNo,
PageCount = dataBuilder.PageCount,
PageSize = dataBuilder.PageSize,
TotalRecordCount = dataBuilder.TotalRecordCount
}
};
if(dataBuilder.HasLink)
{
result.Paging.FirstPage = dataBuilder.FirstPage;
result.Paging.LastPage = dataBuilder.LastPage;
result.Paging.PreviousPage = dataBuilder.PreviousPage;
result.Paging.NextPage = dataBuilder.NextPage;
}
return result;
}
/// <summary>
/// 單取得分頁資料
/// </summary>
/// <returns></returns>
public PageDataBuilder GetPageDataBuilder()
{
return dataBuilder;
}
}
public class ResultObject<T>
{
public ResultObject()
{
Paging = new Paging();
}
public IEnumerable<T> Data { get; set; }
//
public Paging Paging { get; set; }
}
public class Paging
{
public int PageNo { get; set; }
public int PageSize { get; set; }
public int PageCount { get; set; }
public int TotalRecordCount { get; set; }
public Uri FirstPage { get; set; }
public Uri LastPage { get; set; }
public Uri PreviousPage { get; set; }
public Uri NextPage { get; set; }
}
用法範例:
產生分頁 header (X-Paging-*)1
2
3
4
5
6
7
8
9
10
11
12
13
14// 建立不產生連結的分頁資訊
var dataBuilder = new PageDataBuilder(1, 10, 1000);
// 建立 HttpResponseMessage
var responseMessage = Request.CreateResponse(HttpStatusCode.OK);
// 加入分頁資訊 header 到 responseMessage
new PagingHeader(responseMessage)
.AddPageNo(dataBuilder.PageNo)
.AddPageSize(dataBuilder.PageSize)
.AddPageCount(dataBuilder.PageCount)
.AddTotalRecordCount(dataBuilder.TotalRecordCount);
return ResponseMessage(responseMessage);
產生分頁 header 及 Link header1
2
3
4
5
6
7
8
9
10
11
12
13
14
15// 建立產生連結的分頁資訊 (有 requestPath 就會產生 Link)
var dataBuilder = new PageDataBuilder(1, 10, 1000, requestPath);
// 建立 HttpResponseMessage
var responseMessage = Request.CreateResponse(HttpStatusCode.OK);
// 加入分頁資訊 header 到 responseMessage
new PagingHeader(responseMessage)
.AddPageNo(dataBuilder.PageNo)
.AddPageSize(dataBuilder.PageSize)
.AddPageCount(dataBuilder.PageCount)
.AddTotalRecordCount(dataBuilder.TotalRecordCount)
.AddLink(dataBuilder);
return ResponseMessage(responseMessage);
產生分頁結果1
2
3
4
5
6
7
8// 建立分頁結果
var pagedResult = new PagedResult<Member>(members, pageNo, pageSize, requestPath);
// 有需要,可取得分頁資訊,做為輸出 header 用
var dataBuilder = pagedResult.GetPageDataBuilder();
// 在 Response 中加入 Result
var responseMessage = Request.CreateResponse(HttpStatusCode.OK, pagedResult.CreateResult());
return ResponseMessage(responseMessage);
輸出1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21{
"data": [
{
"num": 1,
"name": "MEM1"
},
{
"num": 2,
"name": "MEM2"
}
],
"paging": {
"page_no": 1,
"page_size": 2,
"page_count": 3,
"total_record_count": 5,
"first_page": "http://localhost:64568/api/members?page_no=1&page_size=2",
"last_page": "http://localhost:64568/api/members?page_no=3&page_size=2",
"next_page": "http://localhost:64568/api/members?page_no=2&page_size=2"
}
}
完整內容,可參考 GitHub 上的專案。