首頁 C# RestFul Web Api
文章
Cancel

C# RestFul Web Api

前言

這邊紀錄一下我學到的RestFul Web Api的基本技巧與知識

情境說明

假設有個儲存設備資訊的資料表, 如圖所示

備註:後面會用到DTO,所以這邊資料表若欄位不多,將無法彰顯DTO的意義

Desktop View

API基本觀念

新增Model資料夾,在裡面新增Device.cs

Desktop View

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
public class Device
{
    public int Id { get; set; }
    public int DeviceTypeId { get; set; } //設備類型
    public string ItemName { get; set; } //設備品名
    public string ItemDescription { get; set; } //物品描述
    public string PCName { get; set; } //電腦名稱
    public string AssetId { get; set; } //設備編號
    public int DeviceStatusId { get; set; }//狀態Id
    public int LocationId { get; set; } //設備ID
    public DateTime? WarehousingDate { get; set; } //入庫時間
    public string Custodian { get; set; } //保管人-代號
    public string CustodianName { get; set; }//保管人-姓名
    public string Department { get; set; }//部門編號
    public string DepartmentName { get; set; }//部門名稱
    public string Brand { get; set; }//設備品牌1
    public string Model { get; set; }//設備品牌2
    public string SerialNo { get; set; }//設備序號
    public string System { get; set; }//系統
    public string Ram { get; set; }//RAM
    public string Disk { get; set; }//硬碟
    public string OfficeVersion { get; set; }//Office版本
    public string Mac01 { get; set; }//無線Mac
    public string Mac02 { get; set; }//有線Mac
    public string Remark { get; set; }//備註
    public string Borrower { get; set; }//借用人工號
    public string BorrowerName { get; set; }//借用人姓名
    public DateTime? BorrowingDate { get; set; }//借用日期
}

建立靜態資料

建立靜態List,先用靜態資料用來代替資料庫撈取

因為我想要東西放在Git載下來的時候,不會需要再重新設定資料庫

Desktop View

1
2
3
4
5
6
7
8
9
10
public static class DeviceStore
{
    public static List<Device> GetDevices=new List<Device> {
        new Device() {Id=1,ItemName="Server 伺服器",ItemDescription="型號A00",AssetId="1001",LocationId=0,WarehousingDate=DateTime.Now,Custodian="A001",CustodianName="保管人A",Brand="",Model="",SerialNo="",System="",Ram="",Disk="",OfficeVersion="",Mac01="",Mac02="",Remark="",Borrower="",BorrowerName="",Department="",DepartmentName="",PCName="" },
        new Device() {Id=2,ItemName="筆電A",ItemDescription="型號A01",AssetId="1101",LocationId=0,WarehousingDate=DateTime.Now,Custodian="A001",CustodianName="保管人A",Brand="",Model="",SerialNo="",System="",Ram="",Disk="",OfficeVersion="",Mac01="",Mac02="",Remark="",Borrower="",BorrowerName="",Department="",DepartmentName="",PCName="" },
        new Device() {Id=3,ItemName="桌電A",ItemDescription="型號A02",AssetId="1010",LocationId=0,WarehousingDate=DateTime.Now,Custodian="A001",CustodianName="保管人A",Brand="",Model="",SerialNo="",System="",Ram="",Disk="",OfficeVersion="",Mac01="",Mac02="",Remark="",Borrower="",BorrowerName="",Department="",DepartmentName="",PCName="" },
        new Device() {Id=4,ItemName="顯示器A",ItemDescription="型號A03",AssetId="1011",LocationId=0,WarehousingDate=DateTime.Now,Custodian="A001",CustodianName="保管人A",Brand="",Model="",SerialNo="",System="",Ram="",Disk="",OfficeVersion="",Mac01="",Mac02="",Remark="",Borrower="",BorrowerName="",Department="",DepartmentName="",PCName="" },
        new Device() {Id=5,ItemName="投影機A",ItemDescription="型號A04",AssetId="1111",LocationId=0,WarehousingDate=DateTime.Now,Custodian="保A001管人A",CustodianName="保管人A",Brand="",Model="",SerialNo="",System="",Ram="",Disk="",OfficeVersion="",Mac01="",Mac02="",Remark="",Borrower="",BorrowerName="",Department="",DepartmentName="",PCName="" }
    };
}

建立DTO

新增DeviceDto.cs,在DeviceDto中,建立實際上要開放給外部進行CRUD的欄位,且屬性名稱與Device要一樣

Desktop View

1
2
3
4
5
6
7
8
9
10
11
public class DevicesDto
{
    public string AssetId { get; set; }// 財產編號
    public string ItemName { get; set; }// 描述
    public string ItemDescription { get; set; }// 部門
    public string DepartmentId { get; set; }// 部門ID
    public string Department { get; set; }// 部門
    public string Borrower { get; set; }//借用人工號
    public string BorrowerName { get; set; }//借用人姓名

}

新增API Controller

Desktop View Desktop View Desktop View

定義Route和Http Type

Controller上方要定義Route

Method上方要定義Http的類型

Desktop View

改用DTO儲存資料

DTO是篩選後,最終可以呈現給使用者看得欄位

Desktop View

Method have Parameter

假設有兩個一樣的Method名稱,但一個回傳陣列,一個回傳陣列內容

Desktop View

定義回傳狀態

使用ActionResult將類型包起來

Desktop View

有無使用ActionResult及ProducesResponseType的執行差別

Desktop View

重新定義ProducesResponseType

Desktop View

1
2
3
[ProducesResponseType(StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status400BadRequest)]
[ProducesResponseType(StatusCodes.Status404NotFound)]

HttpPost

Desktop View

CreatedAtRoute

若要使用CreatedAtRoute以便在HttpPost完成之後,導向到HttpGet的function

首先HttpGet的function要設定指向到其路徑的參數

Desktop View

首先HttpPost改成回傳CreatedAtRoute,並設定只向的HttpGet路徑

Desktop View

備註CreatedAtRoute成功會回傳Code201,添加ProducesResponseType

Desktop View

Post驗證資料的方式

首先在Model上面進行描述,例如限制一定要輸入就用Request,限制資料長度就用MaxLength

Desktop View

然後再Controller 使用ModelState.IsValid進行驗證

備註Controller有加上ApiController的描述會自動進行驗證,否則得用ModelState.IsValid觸發驗證

Desktop View

1
if (!ModelState.IsValid) { }

HttpDelete

備註1:刪除資料的時候可以用HttpDelete

備註2:因為刪除資料後不想返回任何資訊,可以用IActionResult與NoContent

Desktop View

更新資料的 HttpPut

使用httpPut可以紀錄完整的更新資訊

Desktop View

1
2
3
4
5
6
7
8
9
10
11
12
13
[HttpPut("{Num:int}", Name = "UpdateCompany")]
[ProducesResponseType(StatusCodes.Status204NoContent)]
[ProducesResponseType(StatusCodes.Status404NotFound)]
[ProducesResponseType(StatusCodes.Status400BadRequest)]
public IActionResult UpadteCompany(int Num,[FromBody]Company company )
{
    if(company==null || Num!=company.id) return BadRequest();
    //更新資料庫的商業邏輯
    var tempBU = CompanyStore.CompanyList.FirstOrDefault(c => c.id == Num);
    tempBU.Name= company.Name;
    tempBU.Description= company.Description;
    return NoContent();
}

更新資料的 HttpPatch

使用httpPatch只更新完整資料表中的其中一個欄位的資料

使用JsonPath

1
NuGet\Install-Package JsonPath.Net -Version 0.3.1

以及Mvc.NewtonsoftJson

啟動時追加AddNewtonsoftJson

Desktop View

實際調用Patch的方式

Desktop View

1
2
3
4
5
6
7
8
9
10
11
12
13
[HttpPut("{Num:int}", Name = "PatchCompany")]
[ProducesResponseType(StatusCodes.Status204NoContent)]
[ProducesResponseType(StatusCodes.Status404NotFound)]
[ProducesResponseType(StatusCodes.Status400BadRequest)]
public IActionResult UpadteByJsonPatchCompany(int Num, JsonPatchDocument<Company> PatchCompany)
{
    if (PatchCompany==null || Num==0) return BadRequest();
    var tempBU = CompanyStore.CompanyList.FirstOrDefault(c => c.id == Num);
    PatchCompany.ApplyTo(tempBU, ModelState);
    if (!ModelState.IsValid) return BadRequest(ModelState);

    return NoContent();
}

依據Update、Create需求,建立更多Dto

可以從原本的專案中,延伸出Creat或Update甚至是Read專用的Dto,因為

Creat或Update所需的欄位與驗證方式可能都不盡相同

Desktop View

Sync to Async

除了前贅需要+Async之外,要記得使用Task把原本的回傳型態包起來

Desktop View

AutoMapper And Dto

假設這是我的資料庫,可以透過function得到當前資料,並使用Add追加新的資料

加入await async 也是為了Demo使用非同步處理資料庫

Desktop View

實際使用過程

Desktop View

標準API回傳類型

建立標準API回傳類型

當使用Get/Post之後,總不可能每次都不告知Request是否正常

首先建立新的Class 用來儲存API Request

Class可以長這樣

Desktop View

1
2
3
4
5
6
7
public class APIResponse
{
    public HttpStatusCode HttpStatusCode { get; set; }
    public bool IsSuccess { get; set; } = true;
    public IEnumerable<string> ErrMessage { get; set; }
    public object Result { get; set; }
}

使用標準API回傳類型

先宣告變數

Desktop View

變更回傳類型

將原本的回傳資料,放到APIResponse的Result裡面

Desktop View

加入TryCatch

為了避免調用資料有甚麼意外狀況,所以要加入TryCatch

Desktop View

建立API專案中,並實現Token驗證

一般來說使用別人的API時,別人會提供Token以便進行身分驗證,畢竟毫無限制地讓任何都能操作CRUD

這邊DEMO自己生成Token並進行驗證的方式,以便了解相關知識

備註: 實際上需要與資料庫串接,但這邊我只想練習DEMO與Token相關的技巧,所以讀取資料庫的操作,會就直接用靜態List來代替

所以假設這List裡面的資料,是資料庫裡面的資料

Desktop View

建立API專案

選擇建立API

Desktop View

設定完後建立

Desktop View

建立商業邏輯Dto

如下

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
//CreatDevicesDto.cs
public class CreatDevicesDto
{
    public int Id { get; set; }
    public int DeviceTypeId { get; set; } //設備類型
    public string ItemName { get; set; } //設備品名
    public string ItemDescription { get; set; } //物品描述
}
//Devices.cs
public class Devices
{
    public int Id { get; set; }
    public int DeviceTypeId { get; set; } //設備類型
    public string ItemName { get; set; } //設備品名
    public string ItemDescription { get; set; } //物品描述
    public string PCName { get; set; } //電腦名稱
    public string AssetId { get; set; } //設備編號
    public int DeviceStatusId { get; set; }//狀態Id
    public int LocationId { get; set; } //設備ID
    public DateTime? WarehousingDate { get; set; } //入庫時間
    public string Custodian { get; set; } //保管人-代號
    public string CustodianName { get; set; }//保管人-姓名
    public string Department { get; set; }//部門編號
    public string DepartmentName { get; set; }//部門名稱
    public string Brand { get; set; }//設備品牌1
    public string Model { get; set; }//設備品牌2
    public string SerialNo { get; set; }//設備序號
    public string System { get; set; }//系統
    public string Ram { get; set; }//RAM
    public string Disk { get; set; }//硬碟
    public string OfficeVersion { get; set; }//Office版本
    public string Mac01 { get; set; }//無線Mac
    public string Mac02 { get; set; }//有線Mac
    public string Remark { get; set; }//備註
    public string Borrower { get; set; }//借用人工號
    public string BorrowerName { get; set; }//借用人姓名
    public DateTime? BorrowingDate { get; set; }//借用日期
}
//DevicesDto.cs
public class DevicesDto
{
    public string AssetId { get; set; }// 財產編號
    public string ItemName { get; set; }// 描述
    public string ItemDescription { get; set; }// 部門
    public string DepartmentId { get; set; }// 部門ID
    public string Department { get; set; }// 部門
    public string Borrower { get; set; }//借用人工號
    public string BorrowerName { get; set; }//借用人姓名
}

建立商業邏輯假資料

如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
//DeviceStore.cs
public class DeviceList
{
    public async Task<List<DevicesDto>> GetDevices()
    {
        return  new List<DevicesDto> {
        new DevicesDto() {ItemName="Server 伺服器",ItemDescription="型號A00",AssetId="1001",Borrower="",BorrowerName="",Department=""},
        new DevicesDto() {ItemName="筆電A",ItemDescription="型號A01",AssetId="1101",Borrower="",BorrowerName="",Department="" },
        new DevicesDto() {ItemName="桌電A",ItemDescription="型號A02",AssetId="1010",Borrower="",BorrowerName="",Department="" },
        new DevicesDto() {ItemName="顯示器A",ItemDescription="型號A03",AssetId="1011",Borrower="",BorrowerName="",Department="" },
        new DevicesDto() {ItemName="投影機A",ItemDescription="型號A04",AssetId="1111",Borrower="",BorrowerName="",Department="" }
        };
    }
}

建立登入系統

Desktop View

1
2
3
4
5
6
7
8
public class LocalUser
{
    public int id { get; set; }
    public string UserName { get; set; }
    public string Name { get; set; }
    public string Password { get; set; }
    public string Role { get; set;  }
}

登入請求

Desktop View

1
2
3
4
5
public class LoginRequestDTO
{
    public string UserName { get; set; }
    public string Password { get; set; }
}

登入響應

Desktop View

1
2
3
4
5
public class LoginResponseDTO
{
    public LocalUser User { get; set; }
    public string Token { get; set; }
}

註冊請求

Desktop View

1
2
3
4
5
6
7
public class RegisterRequestDTO
{
    public string UserName { get; set; }
    public string Name { get; set; }
    public string Password { get; set; }
    public string Role { get; set; }
}

建立假帳號

之所以有這步驟是因為我不想真的使用資料庫驗證登入資訊

Desktop View

1
2
3
4
5
6
7
8
9
10
11
12
13
public class UserList
{
    public List<LocalUser> GetUser()
    {
        return  new List<LocalUser> {
        new LocalUser(){ id=1,Name="LILLY",Password="Pa",Role="Admin",UserName="LILLY"},
        new LocalUser(){ id=2,Name="CASEY ",Password="Pb",Role="RoleB",UserName="CASEY"},
        new LocalUser(){ id=3,Name="CHLOE",Password="Pc",Role="RoleC",UserName="CHLOE"},
        new LocalUser(){ id=4,Name="BENNETT",Password="Pd",Role="RoleD",UserName="BENNETT"},
        new LocalUser(){ id=5,Name="NIKOLAS",Password="Pe",Role="RoleE",UserName="NIKOLAS"},
    };
    }
}

建立API標準回傳

Desktop View

1
2
3
4
5
6
7
public class APIResponse
{
    public HttpStatusCode HttpStatusCode { get; set; }
    public bool IsSuccess { get; set; } = true;
    public IEnumerable<string> ErrMessage { get; set; }
    public object Result { get; set; }
}

Appsettings.json中,添加API金鑰字串

Desktop View

1
2
3
"ApiSettings": {
  "Secret": "this is my custom Secret key for authentication"
}

備註:字串不夠長,會跑出Error

解決方式

建立存取登入系統的Repository

建立interface

Desktop View

1
2
3
4
5
6
public interface IUserRepository
{
    bool IsUniqueUser(string username);
    Task<LoginResponseDTO> Login(LoginRequestDTO loginRequestDTO);
    Task<LocalUser> Register(RegisterRequestDTO registerRequestDTO);
}

安裝Tokens.Jwt

Desktop View

1
NuGet\Install-Package System.IdentityModel.Tokens.Jwt -Version 6.25.1

實現Class

Desktop View

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
public class UserRepository : IUserRepository
{
    private string secretKey;
    public UserRepository(IConfiguration configuration)
    {
        this.secretKey = configuration.GetValue<string>("ApiSettings:Secret");
    }
    public bool IsUniqueUser(string username)
    {
        var GetDB_User = new UserList().GetUser(); /*與DB有關的部分*/
        var user = GetDB_User.FirstOrDefault(c=>c.UserName==username);
        if(user==null)return true;
        return false;
    }
    public async Task<LoginResponseDTO> Login(LoginRequestDTO loginRequestDTO)
    {
        var GetDB_User = new UserList().GetUser(); /*與DB有關的部分*/
        var user = GetDB_User.FirstOrDefault(c=>c.UserName.ToLower()== loginRequestDTO.UserName.ToLower()&&
        c.Password==loginRequestDTO.Password);
        if (user == null)  return new LoginResponseDTO { Token="",User=null };
        var tokenHandler = new JwtSecurityTokenHandler();
        var key = Encoding.ASCII.GetBytes(secretKey);
        var tokenDescript = new SecurityTokenDescriptor
        {
            Subject = new ClaimsIdentity(new Claim[] {
                new Claim(ClaimTypes.Name, user.id.ToString()),
                new Claim(ClaimTypes.Role,user.Role)
            }),
            Expires = DateTime.Now.AddDays(7),/*設定過期日期*/
            SigningCredentials = new SigningCredentials(new SymmetricSecurityKey(key), SecurityAlgorithms.HmacSha256Signature)
        };
        var token = tokenHandler.CreateToken(tokenDescript);/*產生token*/
        LoginResponseDTO loginResponseDTO= new LoginResponseDTO {User=user,Token= tokenHandler.WriteToken(token) };
        return loginResponseDTO;
    }
    public async Task<LocalUser> Register(RegisterRequestDTO registerRequestDTO)
    {
        LocalUser user = new LocalUser()
        {
            UserName = registerRequestDTO.UserName,
            Password = registerRequestDTO.Password,
            Name = registerRequestDTO.Name,
            Role = registerRequestDTO.Role
        };
        var GetDB_User = new UserList().GetUser(); /*與DB有關的部分*/
        GetDB_User.Add(user);
        return user;

    }
}

建立Repository的DI注入

Desktop View

1
builder.Services.AddScoped<IUserRepository, UserRepository>();

建立產生Token的APIController

API這裡新增UserController,用來DEMO登入成功後,產生Token

Desktop View

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
    [Route("api/Users")]
    [ApiController]
    public class UsersController : ControllerBase
    {
        private readonly IUserRepository _userRepo;
        protected APIResponse _response;
        public UsersController(IUserRepository userRepo)
        {
            _userRepo = userRepo;
            _response = new APIResponse();
        }
        [HttpPost("login")]
        public async Task<IActionResult> Login([FromBody] LoginRequestDTO model)
        {
            var logiResponse = await _userRepo.Login(model);

            _response.HttpStatusCode = HttpStatusCode.OK;
            _response.IsSuccess = true;
            _response.Result=logiResponse;
            return Ok(_response);
        }
    }

執行產生Token的APIController

執行API專案,並使用UserController API

Desktop View

API與角色權限

建立簡單的API

也可以使用下列語句測試

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
[Route("api/Device")]
[ApiController]
public class DeviceController : ControllerBase
{
    protected APIResponse _response;

    public DeviceController()
    {
        this._response = new APIResponse();
    }

    [HttpGet]
    [Authorize(Roles = "Admin")]
    public async Task<ActionResult<APIResponse>> GetDevices() 
    {
        try
        {
            var result = await new DeviceList().GetDevices();
            _response.Result = result;
            _response.HttpStatusCode = System.Net.HttpStatusCode.OK;
            return Ok(_response);
        }
        catch (Exception ex)
        {
            _response.IsSuccess= false;
            _response.ErrMessage =new List<string>() { ex.ToString()};
        }
        return _response;
    }
}

設定API存取權限

Desktop View

1
[Authorize(Roles ="Admin")]

安裝JwtBearer

Desktop View

1
NuGet\Install-Package Microsoft.AspNetCore.Authentication.JwtBearer -Version 7.0.2

設定API解密

因為前面追加了權限驗證,但這邊還沒寫驗證金鑰,所以會因為Token驗證問題被擋下來

Desktop View

加入驗證金鑰的方式

Desktop View

1
2
3
4
5
6
7
8
9
10
11
12
13
14
var key = builder.Configuration.GetValue<string>("ApiSettings:Secret");
builder.Services.AddAuthentication(x => { 
x.DefaultAuthenticateScheme= JwtBearerDefaults.AuthenticationScheme;
    x.DefaultChallengeScheme= JwtBearerDefaults.AuthenticationScheme;
}).AddJwtBearer(c => {
            c.RequireHttpsMetadata = false;
            c.SaveToken = true;
            c.TokenValidationParameters = new Microsoft.IdentityModel.Tokens.TokenValidationParameters { 
            IssuerSigningKey=new SymmetricSecurityKey(Encoding.ASCII.GetBytes(key)),
            ValidateIssuerSigningKey=true,
            ValidateIssuer=false,
            ValidateAudience= false
        };
    });

設定成必須使用有效Token才能使用API

將AddSwaggerGen改成以下語法,可以讓API變成必須先輸入Token,才能使用的介面

Desktop View

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
builder.Services.AddSwaggerGen(c => {
    c.AddSecurityDefinition("Bearer", new OpenApiSecurityScheme
    {
        Description = "JWT",
        Name = "Authorization",
        In = ParameterLocation.Header,
        Scheme = "Bearer"
    });
    c.AddSecurityRequirement(new OpenApiSecurityRequirement()
    {
        {
        new OpenApiSecurityScheme{
            Reference=new OpenApiReference{ Type=ReferenceType.SecurityScheme,Id="Bearer"},
            Scheme="oauth2",
            Name="Bearer",
            In = ParameterLocation.Header
            },
        new List<string>()
        }
    });
});

啟用UseAuthentication

Desktop View

1
app.UseAuthentication();

手動測試有效Token

因為Token本身就是確保安全性的機制,所以若不照以下流程操作測試,直接使用API只會收到401或403錯誤代號

首先,先確定要使用的API的權限字串,如圖所示Admin

Desktop View

先確定要使用登入帳戶的Role,與Admin相符

Desktop View

介面中,產生Token並複製Token的方式 詳見~~~

輸入 Bearer + 空白 + 複製的Token

備註Bearer這個字串,是源自前面的設定

Desktop View

執行須有Admin權限的API

Desktop View

ASP.NET專案,建立Dto

新增ASP.NET專案

新增傳案

Desktop View Desktop View Desktop View Desktop View

建好後,順帶將DTO跟APIResponse 複製到專案中,調用時API,這些都是一定會用到的東西

因為是DEMO所以直接COPY比較快,

Desktop View

建立登入系統的Dto

Desktop View

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
//LoginRequestDTO.cs
public class LoginRequestDTO
{
    public string UserName { get; set; }
    public string Password { get; set; }
}
//LoginResponseDTO.cs
public class LoginResponseDTO
{
    public UserDTO User { get; set; }
    public string Token { get; set; }
}
//RegisterRequestDTO.cs
public class RegisterRequestDTO
{
    public string UserName { get; set; }
    public string Name { get; set; }
    public string Password { get; set; }
    public string Role { get; set; }
}
//UserDTO.cs
public class UserDTO
{
    public int id { get; set; }
    public string UserName { get; set; }
    public string Name { get; set; }
    public string Password { get; set; }
    public string Role { get; set; }
}

建立調用Https的enum

可以新增共用類別的Dll,將enum放進裡面

Desktop View

1
2
3
4
5
6
7
8
9
10
11
public static class SD
{
    public enum ApiType
    {
        Get,
        Post,
        Put,
        Delete
    }
    public static string TokenSession = "JWTToken";
}

建立標準API請求類型

Desktop View

1
2
3
4
5
6
7
public class APIRequest
{
    public ApiType ApiType { get; set; } = ApiType.Get;
    public string URL { get; set; }
    public object Data { get; set; }
    public string Token { get; set; }
}

建立標準API回傳類型

Desktop View

1
2
3
4
5
6
7
public class APIResponse
{
    public HttpStatusCode HttpStatusCode { get; set; }
    public bool IsSuccess { get; set; } = true;
    public IEnumerable<string> ErrMessage { get; set; }
    public object Result { get; set; }
}

安裝Newtonsoft.Json

Desktop View

1
NuGet\Install-Package Newtonsoft.Json -Version 13.0.2

ASP.NET專案,建立API Service

建立通用API Service

先建立Services資料夾,結構長這樣

Desktop View IBaseService.cs

1
2
3
4
5
public interface IBaseService
{
    APIRequest apiRequest { get; set; }
    Task<T> SendAsync<T>(APIRequest apiRequest);
}

BaseService.cs

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
51
52
53
54
55
56
57
58
59
60
61
62
public class BaseService : IBaseService
{

    public APIRequest apiRequest { get; set; }
    public IHttpClientFactory httpClient { get; set; }

    public BaseService(IHttpClientFactory httpClient)
    {
        this.apiRequest = new APIRequest();
        this.httpClient = httpClient;
    }

    public async Task<T> SendAsync<T>(APIRequest apiRequest)
    {
        try
        {
            var client = httpClient.CreateClient("API");
            HttpRequestMessage message = new HttpRequestMessage();
            message.Headers.Add("Accept", "application/json");
            message.RequestUri = new Uri(apiRequest.URL);
            if (apiRequest.Data != null)
            {
                message.Content = new StringContent(JsonConvert.SerializeObject(apiRequest.Data), Encoding.UTF8, "application/json");
            }
            switch (apiRequest.ApiType)
            {
                case ApiType.Get:
                    message.Method = HttpMethod.Get;
                    break;
                case ApiType.Post:
                    message.Method = HttpMethod.Post;
                    break;
                case ApiType.Put:
                    message.Method = HttpMethod.Put;
                    break;
                case ApiType.Delete:
                    message.Method = HttpMethod.Delete;
                    break;
            }
            HttpResponseMessage apiResponse = null;
            if (!string.IsNullOrEmpty(apiRequest.Token))
            {
                client.DefaultRequestHeaders.Authorization = new System.Net.Http.Headers.AuthenticationHeaderValue("Bearer",apiRequest.Token);
            }
            apiResponse = await client.SendAsync(message);
            var apiContent = await apiResponse.Content.ReadAsStringAsync();
            var APIResponse = JsonConvert.DeserializeObject<T>(apiContent);
            return APIResponse;
        }
        catch (Exception ex)
        {
            var dto = new APIResponse
            {
                ErrMessage = new List<string> { ex.Message.ToString() },
                IsSuccess = false
            };
            var res = JsonConvert.SerializeObject(dto);
            var APIResponse = JsonConvert.DeserializeObject<T>(res);
            return APIResponse;
        }
    }
}

建立商業邏輯API Service

如下

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
//CreatDevicesDto.cs
public class CreatDevicesDto
{
    public int Id { get; set; }
    public int DeviceTypeId { get; set; } //設備類型
    public string ItemName { get; set; } //設備品名
    public string ItemDescription { get; set; } //物品描述
}
//UpdataDevicesDto.cs
public class UpdataDevicesDto
{
    public int Id { get; set; }
    public string PCName { get; set; } //電腦名稱
    public string AssetId { get; set; } //設備編號
}
//DevicesDto.cs
public class DevicesDto
{
    public string AssetId { get; set; }// 財產編號
    public string ItemName { get; set; }// 描述
    public string ItemDescription { get; set; }// 部門
    public string DepartmentId { get; set; }// 部門ID
    public string Department { get; set; }// 部門
    public string Borrower { get; set; }//借用人工號
    public string BorrowerName { get; set; }//借用人姓名
}

建立商業邏輯API Service

建立操作資料庫CRUD相關的Service

建立interface,因為需要傳入token進行驗證,所以會多個傳入token的參數

備註:下面參數中的CreatDevicesDto與UpdataDevicesDto 要根據API自行替換成其他參數

Desktop View

1
2
3
4
5
6
7
8
9
public interface IDatabaseServices
{
    Task<T> GetAllAsync<T>(string token);
    Task<T> GetAsync<T>(string id, string token);
    Task<T> CreatAsync<T>(CreatDevicesDto creatDevicesDto, string token);
    Task<T> UpdateAsync<T>(UpdataDevicesDto updataDevicesDto, string token);
    Task<T> Delete<T>(int id, string token);

}

實現Class Desktop View

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
51
52
53
54
55
56
57
public class DatabaseServices:BaseService,IDatabaseServices
{
    private readonly IHttpClientFactory _clientFactory;
    private string _databaseURL;
    public DatabaseServices(IHttpClientFactory clientFactory,IConfiguration configuration):base(clientFactory)
    {
        _clientFactory = clientFactory;
        _databaseURL = configuration.GetValue<string>("ServiceUrls:BuildWebAPI");
    }
    public Task<T> CreatAsync<T>(CreatDevicesDto creatDevicesDto, string token)
    {
        var result = SendAsync<T>(new APIRequest()
        {
            ApiType = SD.ApiType.Get,
            Data = creatDevicesDto,
            URL = _databaseURL+ "/api/Device/A" ,
            Token = token

        });
        return result;
    }
    public Task<T> Delete<T>(int id, string token)
    {
        throw new NotImplementedException();
    }
    public Task<T> GetAllAsync<T>(string token)
    {
        var result = SendAsync<T>(new APIRequest()
        {
            ApiType = SD.ApiType.Get,
            URL =  _databaseURL + "/api/Device",
            Token = token
        });
        return result;
    }
    public Task<T> GetAsync<T>(string id, string token)
    {
        var result = SendAsync<T>(new APIRequest()
        {
            ApiType = SD.ApiType.Get,
            URL = _databaseURL + $"/api/Device/ItemName?ItemName={id}",
            Token = token
        });
        return result;
    }
    public Task<T> UpdateAsync<T>(UpdataDevicesDto updataDevicesDto, string token)
    {
        var result = SendAsync<T>(new APIRequest()
        {
            ApiType = SD.ApiType.Put,
            Data = updataDevicesDto,
            URL = Path.Combine(_databaseURL, "api", "Device"),
            Token = token
        });
        return result;
    }
}

補充說明:字串來源要客製化維護

Desktop View

注入DI:商業邏輯Services

如下

1
builder.Services.AddScoped<IDatabaseServices, DatabaseServices>();

建立驗證身分的Service

Desktop View Desktop View

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
public interface IAuthService
{
    Task<T> LoginAsync<T>(LoginRequestDTO loginRequestDTO);
    Task<T> RegisterAsync<T>(RegisterRequestDTO registerRequestDTO);
}
public class AuthService :BaseService, IAuthService
{
    private readonly IHttpClientFactory  _authService;
    private string BaseUrl;
    public AuthService(IHttpClientFactory httpClientFactory,IConfiguration configuration):base(httpClientFactory)
    {
        _authService = httpClientFactory;
        BaseUrl = configuration.GetValue<string>("ServiceUrls:BuildWebAPI");
    }

    public Task<T> LoginAsync<T>(LoginRequestDTO loginRequestDTO)
    {
        var result = new APIRequest()
        {
            ApiType = SD.ApiType.Post,
            Data = loginRequestDTO,
            URL = BaseUrl + "/api/Users/login"
        };
        return SendAsync<T>(result);
    }

    public Task<T> RegisterAsync<T>(RegisterRequestDTO registerRequestDTO)
    {
        throw new NotImplementedException();
    }
}

注入DI:驗證身分的Services

如下

1
2
builder.Services.AddHttpClient<IAuthService, AuthService>();
builder.Services.AddScoped<IAuthService, AuthService>();

ASP.NET專案,建立Controller與Razor

在appsettings.json定義要用的API網址

首先需要在ASP.NET網頁專案的JSON中,定義要使用的API的網址

Desktop View

1
2
3
"ServiceUrls": {
  "BuildWebAPI": "https://localhost:7038"
},

建立驗證身分的Controller

備註1:如果API跟ASP在同個方案總管底下,記得要同時啟動專案

備註2:Program.cs記得追加DI注入

1
2
3
4
builder.Services.AddControllersWithViews();
builder.Services.AddHttpClient<IAuthService, AuthService>();
builder.Services.AddScoped<IAuthService, AuthService>();
var app = builder.Build();

Desktop View

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
public class AuthController : Controller
{
    private readonly IAuthService _authService;
    public AuthController(IAuthService authService)
    {
        _authService = authService;
    }

    public IActionResult Index()
    {
        return View();
    }
    [HttpGet]
    public IActionResult Login()
    { 
        LoginRequestDTO loginRequestDTO = new LoginRequestDTO();
        return View(loginRequestDTO);
    }

    [HttpPost]
    [ValidateAntiForgeryToken]
    public async Task<IActionResult> Login(LoginRequestDTO loginRequestDTO)
    {
        APIResponse Response = await _authService.LoginAsync<APIResponse>(loginRequestDTO);
        if (Response != null && Response.IsSuccess == true)
        {
            LoginResponseDTO model = JsonConvert.DeserializeObject<LoginResponseDTO>($"{Response.Result}");
            
            var identity = new ClaimsIdentity(CookieAuthenticationDefaults.AuthenticationScheme);
            identity.AddClaim(new Claim(ClaimTypes.Name, model.User.UserName));
            identity.AddClaim(new Claim(ClaimTypes.Name, model.User.Role));
            var principal = new ClaimsPrincipal(identity);
            await HttpContext.SignInAsync(CookieAuthenticationDefaults.AuthenticationScheme, principal);
            HttpContext.Session.SetString(SD.TokenSession, model.Token);
            return RedirectToAction("Index","Home");
        }
        return View();
    }
    public async Task<IActionResult> Logout()
    {
        await HttpContext.SignOutAsync();
        HttpContext.Session.SetString(SD.TokenSession,string.Empty);
        return RedirectToAction("Index", "Home");
    }
    public IActionResult AccessDenied()
    {
        return View();
    }
}

建立驗證身分的Login頁面

Desktop View

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
@model CallWebAPI.Model.LoginRequestDTO 

<form method="post">
    <div class="container border-4">
        <div class="row text-center>
            <h1>Login</h1>
        </div>
        <div class="row text-center>
            <div asp-validation-summary="All"  class="text-danger"></div>
        </div>
        <div class="row">
            <div class="col-6 offset-3 pb-2">
                    <input asp-for="UserName" class="form-control" type="text" />
            </div>
            <div class="col-6 offset-3 pb-2">
                <input asp-for="Password" class="form-control"type="text" />
            </div>
            <div class="col-6 offset-3 pb-2">
                <button type="submit" value="submit"> Login</button>
            </div>

        </div>
    </div>
</form>

@section Scripts{
<partial name="_ValidationScriptsPartial">
}

建立商業邏輯Controller

如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
public class DatabaseController : Controller
{
    private IDatabaseServices _databaseServices;
    public DatabaseController(IDatabaseServices databaseServices)
    {
        _databaseServices = databaseServices;
    }
    public IActionResult Index()
    {
        return View();
    }
    public async Task<IActionResult> IndexDB()
    {
        List<DevicesDto> list = new List<DevicesDto>();
        var response = await _databaseServices.GetAllAsync<APIResponse>( HttpContext.Session.GetString(SD.TokenSession));
        if (response != null && response.IsSuccess)
        {
            list = JsonConvert.DeserializeObject<List<DevicesDto>>($"{response.Result}");
        }
        return View(list);  
    }
}

建立商業邏輯頁面

如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
@model IEnumerable<CallWebAPI.Model.DevicesDto> 
<table class="table">
    <thead>
        <tr>
            <th>
                ItemName
            </th>
            <th>
                AssetId
            </th>
        </tr>
    </thead>
    <tbody>
        @foreach (var item in Model)
        {
        <tr>
            <td>@item.ItemName</td>
            <td>@item.AssetId</td>
        </tr>
    
            }
    </tbody>
</table>

建立SessionToken字串

如下

1
public static string TokenSession = "JWTToken";

上方Menu追加Login與Logout

Desktop View

設定Layout_

Desktop View

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
@using Utility;
@inject Microsoft.AspNetCore.Http.IHttpContextAccessor httpAcc


@if (httpAcc.HttpContext.Session.GetString(SD.TokenSession) != null &&
      httpAcc.HttpContext.Session.GetString(SD.TokenSession).ToString().Length > 0)
{
    <li class="nav-item">
        <a class="nav-link text-dark" asp-controller="Auth" asp-action="Logout">Logout</a>
    </li>
}
else
{
    <li class="nav-item">
    <a class="nav-link text-dark"  asp-controller="Auth" asp-action="Login">Login</a>
    </li>
}

加入DI

Desktop View

1
builder.Services.AddSingleton<IHttpContextAccessor, HttpContextAccessor>();

為了避免Token短期重複產生,需要的設定Cookie驗證

Desktop View

1
2
3
4
5
6
7
8
9
10
11
12
13
builder.Services.AddAuthentication(CookieAuthenticationDefaults.AuthenticationScheme).AddCookie(c => { 
c.Cookie.HttpOnly= true;
    c.ExpireTimeSpan=TimeSpan.FromMinutes(5);
    c.SlidingExpiration = true;
    c.LoginPath = "/Auth/Login";//根據驗證身分的網址變更
});
builder.Services.AddSession(c => { 
    c.IdleTimeout=TimeSpan.FromMinutes(5);
    c.Cookie.HttpOnly= true;
    c.Cookie.IsEssential = true;
});

app.UseSession();

將Web專案產生的Token回傳

將APIRequest補上Token string

將Service補上Token參數

Desktop View

1
2
3
4
5
6
7
8
public interface IDatabaseServices
{
    Task<T> GetAllAsync<T>(string token);
    Task<T> GetAsync<T>(int id, string token);
    Task<T> CreatAsync<T>(CreatDevicesDto creatDevicesDto, string token);
    Task<T> UpdateAsync<T>(UpdataDevicesDto updataDevicesDto, string token);
    Task<T> Delete<T>(int id, string token);
}

API與版本控制

安裝套件

Microsoft.AspNetCore.Mvc.Versioning

Desktop View

1
NuGet\Install-Package Microsoft.AspNetCore.Mvc.Versioning -Version 5.0.0

Microsoft.AspNetCore.Mvc.Versioning.ApiExplorer

Desktop View

1
NuGet\Install-Package Microsoft.AspNetCore.Mvc.Versioning.ApiExplorer -Version 5.0.0

API的Program添加Servers設定

Desktop View

1
2
3
4
builder.Services.AddApiVersioning(c => {
    c.AssumeDefaultVersionWhenUnspecified = true; //預設版本啟動
    c.DefaultApiVersion = new Microsoft.AspNetCore.Mvc.ApiVersion(1,0);//設定次要版本
});

Desktop View

1
2
3
4
builder.Services.AddVersionedApiExplorer(c => {
    c.GroupNameFormat = "'v'VVV";
    c.SubstituteApiVersionInUrl = true;
});

Route

1
[Route("api/v{version:apiVersion}/Device")]

備註1:設計測試用的API的時候記得要用IEnumerable包住Class回傳,例如附圖這樣

Desktop View

備註2:安裝完套件之後,上面可以Copy的語法都要添加完畢,才能正常啟動API,備註1的方式在沒有使用版控API套件的情況下能正常運作

Controller添加版本描述

Desktop View

1
[ApiVersion("2.0")]

APIController多版本控制

建立一個新的Controller,並設定其為新版本

Desktop View

格式設定補充說明

添加不同API版本的描述文檔

說明達成下圖這種,多個下拉式選單切換不同版本API的方法

Desktop View

UseSwaggerUI中,設定SwaggerEndpoint

AddSwaggerGen中,設定SwaggerDoc

Desktop View

1
2
3
4
5
6
7
8
9
10
11
builder.Services.AddSwaggerGen(c => {

    c.SwaggerDoc("v1",new OpenApiInfo {  Version="描述版本",Title = "標題",Description = "描述" });
    c.SwaggerDoc("v2", new OpenApiInfo { Version = "描述版本", Title = "標題", Description = "描述" });

});

app.UseSwaggerUI(c => {
    c.SwaggerEndpoint("/swagger/v1/swagger.json","DEVICE_V1");//建立API版本1的文檔
    c.SwaggerEndpoint("/swagger/v2/swagger.json","DEVICE_V2");//建立API版本2的文檔
});

UseSwaggerUI補充說明

Desktop View

AddSwaggerGen和SwaggerEndpoint的補充說明

Desktop View

跨版本通用Controller

如果有個APIController底下的Method是不需要考慮版控,例如API登入系統的Controller,則用下圖的方式設計

Desktop View

1
[ApiVersionNeutral]

API與暫存

如下圖所示,在Program.cs設定暫存規則,並在Controller實施

備註:下圖的30指的是30秒

Desktop View

1
2
3
4
builder.Services.AddControllers(c =>
{
    c.CacheProfiles.Add("30sCache", new Microsoft.AspNetCore.Mvc.CacheProfile {Duration=30 });
});

Controller.cs

1
[ResponseCache(CacheProfileName = "30sCache")]

設定Post/Get

接著在演示 DTO and AutoMapper ->所有串接改成異步方法 -> 實際串接資料庫 -> 建立API Request AND Response Class -> 網頁專案中異步UnitOfWork

待學習FromBody的用途

參考網站

https://www.cnblogs.com/ypyp123/p/16198778.html https://blog.csdn.net/dawfwafaew/article/details/123753114 https://blog.csdn.net/weixin_52437470/article/details/113726646

加入Log紀錄資訊的方式

因為以內建DI 所以不需要額外再宣告新物件,使用方式如下

宣告方式

Desktop View

function中的使用方式

Desktop View

呈現Log資訊的位置

Desktop View

自定義DI注入的Mapping類別的方式

Desktop View

實體框架注入ConnectString的方式

.NET Core 與 .Net Framework不同,無法使用ADO.NET 快速建立已存在的資料庫模型,替代方案詳見以下網址參考

https://stackoverflow.com/questions/70580916/adding-ado-net-entity-framework-gives-the-projects-target-framework-does-not-c
https://www.entityframeworktutorial.net/efcore/create-model-for-existing-database-in-ef-core.aspx

後續API精進知識

https://ithelp.ithome.com.tw/articles/10267809

本文由作者按照 CC BY 4.0 進行授權