前言
我可能有三年沒怎么碰C#了,目前的工作是在全職搞前端,最近有時間抽空看了一下Asp.net Core,Core版本號都到了5.0了,也越來越好用了,下面將記錄一下這幾天以來使用Asp.Net Core WebApi+Dapper+Mysql+Redis+Docker的一次開發過程。
項目結構
最終項目結構如下,CodeUin.Dapper數據訪問層,CodeUin.WebApi應用層,其中涉及到具體業務邏輯的我將直接寫在Controllers中,不再做過多分層。CodeUin.Helpers我將存放一些項目的通用幫助類,如果是只涉及到當前層的幫助類將直接在所在層級種的Helpers文件夾中存儲即可。
安裝環境
MySQL
# 下載鏡像 docker pull mysql # 運行 docker run -itd --name 容器名稱 -p 3306:3306 -e MYSQL_ROOT_PASSWORD=你的密碼 mysql
如果正在使用的客戶端工具連接MySQL提示1251,這是因為客戶端不支持新的加密方式造成的,解決辦法如下。
# 查看當前運行的容器 docker ps # 進入容器 docker exec -it 容器名稱 bash # 訪問MySQL mysql -u root -p # 查看加密規則 select host,user,plugin,authentication_string from mysql.user; # 對遠程連接進行授權 GRANT ALL PRIVILEGES ON *.* TO 'root'@'%' WITH GRANT OPTION; # 更改密碼加密規則 ALTER USER 'root'@'%' IDENTIFIED WITH mysql_native_password BY '你的密碼'; # 刷新權限 flush privileges;
最后,使用MySQL客戶端工具進行連接測試,我使用的工具是Navicat Premium。
Redis
# 下載鏡像 docker pull redis # 運行 docker run -itd -p 6379:6379 redis
使用Redis客戶端工具進行連接測試,我使用的工具是Another Redis DeskTop Manager。
.NET 環境
服務器我使用的是CentOS 8,使用的NET SDK版本5.0,下面將記錄我是如何在CentOS 8中安裝.NET SDK和.NET運行時的。
# 安裝SDK sudo dnf install dotnet-sdk-5.0 # 安裝運行時 sudo dnf install aspnetcore-runtime-5.0
檢查是否安裝成功,使用dotnet --info
命令查看安裝信息
創建項目
下面將實現一個用戶的登錄注冊,和獲取用戶信息的小功能。
數據服務層
該層設計參考了 玉龍雪山 的架構,我也比較喜歡這種結構,一看結構就知道是要做什么的,簡單清晰。
首先,新建一個項目命名為CodeUin.Dapper,只用來提供接口,為業務層服務。
- Entities
- 存放實體類
- IRepository
- 存放倉庫接口
- Repository
- 存放倉庫接口實現類
- BaseModel
- 實體類的基類,用來存放通用字段
- DataBaseConfig
- 數據訪問配置類
- IRepositoryBase
- 存放最基本的倉儲接口 增刪改查等
- RepositoryBase
- 基本倉儲接口的具體實現
創建BaseModel基類
該類存放在項目的根目錄下,主要作用是將數據庫實體類中都有的字段獨立出來。
1 using System; 2 3 namespace CodeUin.Dapper 4 { 5 /// <summary> 6 /// 基礎實體類 7 /// </summary> 8 public class BaseModel 9 { 10 /// <summary> 11 /// 主鍵Id 12 /// </summary> 13 public int Id { get; set; } 14 15 /// <summary> 16 /// 創建時間 17 /// </summary> 18 public DateTime CreateTime { get; set; } 19 } 20 }
創建DataBaseConfig類
該類存放在項目的根目錄下,我這里使用的是MySQL,需要安裝以下依賴包,如果使用的其他數據庫,自行安裝對應的依賴包即可。
該類具體代碼如下:
1 using MySql.Data.MySqlClient; 2 using System.Data; 3 4 namespace CodeUin.Dapper 5 { 6 public class DataBaseConfig 7 { 8 private static string MySqlConnectionString = @"Data Source=數據庫地址;Initial Catalog=codeuin;Charset=utf8mb4;User ID=root;Password=數據庫密碼;"; 9 10 public static IDbConnection GetMySqlConnection(string sqlConnectionString = null) 11 { 12 if (string.IsNullOrWhiteSpace(sqlConnectionString)) 13 { 14 sqlConnectionString = MySqlConnectionString; 15 } 16 IDbConnection conn = new MySqlConnection(sqlConnectionString); 17 conn.Open(); 18 return conn; 19 } 20 } 21 }
創建IRepositoryBase類
該類存放在項目的根目錄下,存放常用的倉儲接口。
1 using System; 2 using System.Collections.Generic; 3 using System.Threading.Tasks; 4 5 namespace CodeUin.Dapper 6 { 7 public interface IRepositoryBase<T> 8 { 9 Task<int> Insert(T entity, string insertSql); 10 11 Task Update(T entity, string updateSql); 12 13 Task Delete(int Id, string deleteSql); 14 15 Task<List<T>> Select(string selectSql); 16 17 Task<T> Detail(int Id, string detailSql); 18 } 19 }
創建RepositoryBase類
該類存放在項目的根目錄下,是IRepositoryBase類的具體實現。
1 using Dapper; 2 using System.Collections.Generic; 3 using System.Data; 4 using System.Linq; 5 using System.Threading.Tasks; 6 7 namespace CodeUin.Dapper 8 { 9 public class RepositoryBase<T> : IRepositoryBase<T> 10 { 11 public async Task Delete(int Id, string deleteSql) 12 { 13 using (IDbConnection conn = DataBaseConfig.GetMySqlConnection()) 14 { 15 await conn.ExecuteAsync(deleteSql, new { Id }); 16 } 17 } 18 19 public async Task<T> Detail(int Id, string detailSql) 20 { 21 using (IDbConnection conn = DataBaseConfig.GetMySqlConnection()) 22 { 23 return await conn.QueryFirstOrDefaultAsync<T>(detailSql, new { Id }); 24 } 25 } 26 27 public async Task<List<T>> ExecQuerySP(string SPName) 28 { 29 using (IDbConnection conn = DataBaseConfig.GetMySqlConnection()) 30 { 31 return await Task.Run(() => conn.Query<T>(SPName, null, null, true, null, CommandType.StoredProcedure).ToList()); 32 } 33 } 34 35 public async Task<int> Insert(T entity, string insertSql) 36 { 37 using (IDbConnection conn = DataBaseConfig.GetMySqlConnection()) 38 { 39 return await conn.ExecuteAsync(insertSql, entity); 40 } 41 } 42 43 public async Task<List<T>> Select(string selectSql) 44 { 45 using (IDbConnection conn = DataBaseConfig.GetMySqlConnection()) 46 { 47 return await Task.Run(() => conn.Query<T>(selectSql).ToList()); 48 } 49 } 50 51 public async Task Update(T entity, string updateSql) 52 { 53 using (IDbConnection conn = DataBaseConfig.GetMySqlConnection()) 54 { 55 await conn.ExecuteAsync(updateSql, entity); 56 } 57 } 58 } 59 }
好了,基礎類基本已經定義完成。下面將新建一個Users類,並定義幾個常用的接口。
創建Users實體類
該類存放在Entities文件夾中,該類繼承BaseModel。
1 namespace CodeUin.Dapper.Entities 2 { 3 /// <summary> 4 /// 用戶表 5 /// </summary> 6 public class Users : BaseModel 7 { 8 /// <summary> 9 /// 用戶名 10 /// </summary> 11 public string UserName { get; set; } 12 13 /// <summary> 14 /// 密碼 15 /// </summary> 16 public string Password { get; set; } 17 18 /// <summary> 19 /// 鹽 20 /// </summary> 21 public string Salt { get; set; } 22 23 /// <summary> 24 /// 郵箱 25 /// </summary> 26 public string Email { get; set; } 27 28 /// <summary> 29 /// 手機號 30 /// </summary> 31 public string Mobile { get; set; } 32 33 /// <summary> 34 /// 性別 35 /// </summary> 36 public int Gender { get; set; } 37 38 /// <summary> 39 /// 年齡 40 /// </summary> 41 public int Age { get; set; } 42 43 /// <summary> 44 /// 頭像 45 /// </summary> 46 public string Avatar { get; set; } 47 48 /// <summary> 49 /// 是否刪除 50 /// </summary> 51 public int IsDelete { get; set; } 52 } 53 }
創建IUserRepository類
該類存放在IRepository文件夾中,繼承IRepositoryBase,並定義了額外的接口。
1 using CodeUin.Dapper.Entities; 2 using System; 3 using System.Collections.Generic; 4 using System.Threading.Tasks; 5 6 namespace CodeUin.Dapper.IRepository 7 { 8 public interface IUserRepository : IRepositoryBase<Users> 9 { 10 Task<List<Users>> GetUsers(); 11 12 Task<int> AddUser(Users entity); 13 14 Task DeleteUser(int d); 15 16 Task<Users> GetUserDetail(int id); 17 18 Task<Users> GetUserDetailByEmail(string email); 19 } 20 }
創建UserRepository類
該類存放在Repository文件夾中,繼承RepositoryBase, IUserRepository ,是IUserRepository類的具體實現。
1 using CodeUin.Dapper.Entities; 2 using CodeUin.Dapper.IRepository; 3 using Dapper; 4 using System.Collections.Generic; 5 using System.Data; 6 using System.Threading.Tasks; 7 8 namespace CodeUin.Dapper.Repository 9 { 10 public class UserRepository : RepositoryBase<Users>, IUserRepository 11 { 12 public async Task DeleteUser(int id) 13 { 14 string deleteSql = "DELETE FROM [dbo].[Users] WHERE Id=@Id"; 15 await Delete(id, deleteSql); 16 } 17 18 19 public async Task<Users> GetUserDetail(int id) 20 { 21 string detailSql = @"SELECT Id, Email, UserName, Mobile, Password, Age, Gender, CreateTime,Salt, IsDelete FROM Users WHERE Id=@Id"; 22 return await Detail(id, detailSql); 23 } 24 25 public async Task<Users> GetUserDetailByEmail(string email) 26 { 27 string detailSql = @"SELECT Id, Email, UserName, Mobile, Password, Age, Gender, CreateTime, Salt, IsDelete FROM Users WHERE Email=@email"; 28 29 using (IDbConnection conn = DataBaseConfig.GetMySqlConnection()) 30 { 31 return await conn.QueryFirstOrDefaultAsync<Users>(detailSql, new { email }); 32 } 33 } 34 35 public async Task<List<Users>> GetUsers() 36 { 37 string selectSql = @"SELECT * FROM Users"; 38 return await Select(selectSql); 39 } 40 41 public async Task<int> AddUser(Users entity) 42 { 43 string insertSql = @"INSERT INTO Users (UserName, Gender, Avatar, Mobile, CreateTime, Password, Salt, IsDelete, Email) VALUES (@UserName, @Gender, @Avatar, @Mobile, now(),@Password, @Salt, @IsDelete,@Email);SELECT @id= LAST_INSERT_ID();"; 44 return await Insert(entity, insertSql); 45 } 46 } 47 }
大功告成,接下來需要手動創建數據庫和表結構,不能像使用EF那樣自動生成了,使用Dapper基本上是要純寫SQL的,如果想像EF那樣使用,就要額外的安裝一個擴展 Dapper.Contrib。
數據庫表結構如下,比較簡單。
DROP TABLE IF EXISTS `Users`; CREATE TABLE `Users` ( `Id` int(11) NOT NULL AUTO_INCREMENT COMMENT '主鍵', `Email` varchar(255) DEFAULT NULL COMMENT '郵箱', `UserName` varchar(20) DEFAULT NULL COMMENT '用戶名稱', `Mobile` varchar(11) DEFAULT NULL COMMENT '手機號', `Age` int(11) DEFAULT NULL COMMENT '年齡', `Gender` int(1) DEFAULT '0' COMMENT '性別', `Avatar` varchar(255) DEFAULT NULL COMMENT '頭像', `Salt` varchar(255) DEFAULT NULL COMMENT '加鹽', `Password` varchar(255) DEFAULT NULL COMMENT '密碼', `IsDelete` int(2) DEFAULT '0' COMMENT '0-正常 1-刪除', `CreateTime` datetime DEFAULT CURRENT_TIMESTAMP COMMENT '創建時間', PRIMARY KEY (`Id`), UNIQUE KEY `USER_MOBILE_INDEX` (`Mobile`) USING BTREE ) ENGINE=InnoDB AUTO_INCREMENT=10000 DEFAULT CHARSET=utf8mb4 COMMENT='用戶信息表';
好了,數據訪問層大概就這樣子了,下面來看看應用層的具體實現方式。
應用程序層
創建一個WebApi項目,主要對外提供Api接口服務,具體結構如下。
- Autofac
- 存放IOC 依賴注入的配置項
- AutoMapper
- 存放實體對象映射關系的配置項
- Controllers
- 控制器,具體業務邏輯也將寫在這
- Fliters
- 存放自定義的過濾器
- Helpers
- 存放本層中用到的一些幫助類
- Models
- 存放輸入/輸出/DTO等實體類
好了,結構大概就是這樣。錯誤優先,先處理程序異常,和集成日志程序吧。
自定義異常處理
在Helpers文件夾中創建一個ErrorHandingMiddleware中間件,添加擴展方法ErrorHandlingExtensions,在Startup中將會使用到。
1 using Microsoft.AspNetCore.Builder; 2 using Microsoft.AspNetCore.Http; 3 using Microsoft.Extensions.Logging; 4 using Newtonsoft.Json; 5 using System; 6 using System.Threading.Tasks; 7 8 namespace CodeUin.WebApi.Helpers 9 { 10 public class ErrorHandlingMiddleware 11 { 12 private readonly RequestDelegate next; 13 private readonly ILogger<ErrorHandlingMiddleware> _logger; 14 15 public ErrorHandlingMiddleware(RequestDelegate next, ILogger<ErrorHandlingMiddleware> logger) 16 { 17 this.next = next; 18 _logger = logger; 19 } 20 21 public async Task Invoke(HttpContext context) 22 { 23 try 24 { 25 await next(context); 26 } 27 catch (Exception ex) 28 { 29 _logger.LogError(ex.Message); 30 31 var statusCode = 500; 32 33 await HandleExceptionAsync(context, statusCode, ex.Message); 34 } 35 finally 36 { 37 var statusCode = context.Response.StatusCode; 38 var msg = ""; 39 40 if (statusCode == 401) 41 { 42 msg = "未授權"; 43 } 44 else if (statusCode == 404) 45 { 46 msg = "未找到服務"; 47 } 48 else if (statusCode == 502) 49 { 50 msg = "請求錯誤"; 51 } 52 else if (statusCode != 200) 53 { 54 msg = "未知錯誤"; 55 } 56 if (!string.IsNullOrWhiteSpace(msg)) 57 { 58 await HandleExceptionAsync(context, statusCode, msg); 59 } 60 } 61 } 62 63 // 異常錯誤信息捕獲,將錯誤信息用Json方式返回 64 private static Task HandleExceptionAsync(HttpContext context, int statusCode, string msg) 65 { 66 var result = JsonConvert.SerializeObject(new { Msg = msg, Code = statusCode }); 67 68 context.Response.ContentType = "application/json;charset=utf-8"; 69 70 return context.Response.WriteAsync(result); 71 } 72 } 73 74 // 擴展方法 75 public static class ErrorHandlingExtensions 76 { 77 public static IApplicationBuilder UseErrorHandling(this IApplicationBuilder builder) 78 { 79 return builder.UseMiddleware<ErrorHandlingMiddleware>(); 80 } 81 } 82 }
最后,在 Startup 的 Configure 方法中添加 app.UseErrorHandling() ,當程序發送異常時,會走我們的自定義異常處理。
1 public void Configure(IApplicationBuilder app, IWebHostEnvironment env) 2 { 3 if (env.IsDevelopment()) 4 { 5 app.UseDeveloperExceptionPage(); 6 } 7 8 app.UseHttpsRedirection(); 9 10 // 請求錯誤提示配置 11 app.UseErrorHandling(); 12 13 app.UseRouting(); 14 15 app.UseAuthorization(); 16 17 app.UseEndpoints(endpoints => 18 { 19 endpoints.MapControllers(); 20 }); 21 }
日志程序
我這里使用的是NLog,需要在項目中先安裝依賴包。
首先在項目根目錄創建一個 nlog.config 的配置文件,具體內容如下。
1 <?xml version="1.0" encoding="utf-8" ?> 2 <nlog xmlns="http://www.nlog-project.org/schemas/NLog.xsd" 3 xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" 4 autoReload="true" 5 internalLogLevel="Info" 6 internalLogFile="c:\temp\internal-nlog.txt"> 7 8 <!-- enable asp.net core layout renderers --> 9 <extensions> 10 <add assembly="NLog.Web.AspNetCore"/> 11 </extensions> 12 13 <!-- the targets to write to --> 14 <targets> 15 16 <target xsi:type="File" name="allfile" fileName="${currentdir}\logs\nlog-all-${shortdate}.log" 17 layout="${longdate}|${event-properties:item=EventId_Id}|${uppercase:${level}}|${aspnet-request-ip}|${logger}|${message} ${exception:format=tostring}" /> 18 19 <target xsi:type="Console" name="ownFile-web" 20 layout="${longdate}|${event-properties:item=EventId_Id}|${uppercase:${level}}|${logger}|${aspnet-request-ip}|${message} ${exception:format=tostring}|url: ${aspnet-request-url}|action: ${aspnet-mvc-action}" /> 21 </targets> 22 <!-- rules to map from logger name to target --> 23 <rules> 24 <!--All logs, including from Microsoft--> 25 <logger name="*" minlevel="Info" writeTo="allfile" /> 26 27 <!--Skip non-critical Microsoft logs and so log only own logs--> 28 <logger name="Microsoft.*" maxlevel="Info" final="true" /> 29 <!-- BlackHole without writeTo --> 30 <logger name="*" minlevel="Info" writeTo="ownFile-web" /> 31 </rules> 32 </nlog>
更多配置信息可以直接去官網查看 https://nlog-project.org
最后,在 Program 入口文件中集成 Nlog
1 using Autofac.Extensions.DependencyInjection; 2 using Microsoft.AspNetCore.Hosting; 3 using Microsoft.Extensions.Hosting; 4 using Microsoft.Extensions.Logging; 5 using NLog.Web; 6 7 namespace CodeUin.WebApi 8 { 9 public class Program 10 { 11 public static void Main(string[] args) 12 { 13 NLogBuilder.ConfigureNLog("nlog.config"); 14 CreateHostBuilder(args).Build().Run(); 15 } 16 17 public static IHostBuilder CreateHostBuilder(string[] args) => 18 Host.CreateDefaultBuilder(args) 19 .UseServiceProviderFactory(new AutofacServiceProviderFactory()) 20 .ConfigureLogging(logging => 21 { 22 logging.ClearProviders(); 23 logging.AddConsole(); 24 }) 25 .ConfigureWebHostDefaults(webBuilder => 26 { 27 webBuilder.UseStartup<Startup>(); 28 }) 29 .UseNLog(); 30 } 31 }
現在,我們可以直接使用NLog了,使用方法可以查看上面的 ErrorHandlingMiddleware 類中有使用到。
依賴注入
將使用 Autofac 來管理類之間的依賴關系,Autofac 是一款超級贊的.NET IoC 容器 。首先我們需要安裝依賴包。
在 項目根目錄的 Autofac 文件夾中新建一個 CustomAutofacModule 類,用來管理我們類之間的依賴關系。
1 using Autofac; 2 using CodeUin.Dapper.IRepository; 3 using CodeUin.Dapper.Repository; 4 5 namespace CodeUin.WebApi.Autofac 6 { 7 public class CustomAutofacModule:Module 8 { 9 protected override void Load(ContainerBuilder builder) 10 { 11 builder.RegisterType<UserRepository>().As<IUserRepository>(); 12 } 13 } 14 }
最后,在 Startup 類中添加方法
1 public void ConfigureContainer(ContainerBuilder builder) 2 { 3 // 依賴注入 4 builder.RegisterModule(new CustomAutofacModule()); 5 }
實體映射
將使用 Automapper 幫我們解決對象映射到另外一個對象中的問題,比如這種代碼。
// 如果有幾十個屬性是相當的可怕的 var users = new Users { Email = user.Email, Password = user.Password, UserName = user.UserName }; // 使用Automapper就容易多了 var model = _mapper.Map<Users>(user);
先安裝依賴包
在項目根目錄的 AutoMapper 文件夾中 新建 AutoMapperConfig 類,來管理我們的映射關系。
1 using AutoMapper; 2 using CodeUin.Dapper.Entities; 3 using CodeUin.WebApi.Models; 4 5 namespace CodeUin.WebApi.AutoMapper 6 { 7 public class AutoMapperConfig : Profile 8 { 9 public AutoMapperConfig() 10 { 11 CreateMap<UserRegisterModel, Users>().ReverseMap(); 12 CreateMap<UserLoginModel, Users>().ReverseMap(); 13 CreateMap<UserLoginModel, UserModel>().ReverseMap(); 14 CreateMap<UserModel, Users>().ReverseMap(); 15 } 16 } 17 }
在 Startup 文件的 ConfigureServices 方法中 添加 services.AddAutoMapper(AppDomain.CurrentDomain.GetAssemblies()) 即可。
使用JWT
下面將集成JWT,來處理授權等信息。首先,需要安裝依賴包。
修改 appsttings.json 文件,添加 Jwt 配置信息。
1 { 2 "Logging": { 3 "LogLevel": { 4 "Default": "Information", 5 "Microsoft": "Warning", 6 "Microsoft.Hosting.Lifetime": "Information" 7 } 8 }, 9 "AllowedHosts": "*", 10 "Jwt": { 11 "Key": "e816f4e9d7a7be785a", // 這個key必須大於16位數,非常生成的時候會報錯 12 "Issuer": "codeuin.com" 13 } 14 }
然后在 Startup 類的 ConfigureServices 方法中添加 Jwt 的使用。
1 services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme) 2 .AddJwtBearer(options => 3 { 4 options.TokenValidationParameters = new TokenValidationParameters 5 { 6 ValidateIssuer = true, 7 ValidateAudience = true, 8 ValidateLifetime = true, 9 ClockSkew = TimeSpan.FromMinutes(5), //緩沖過期時間 默認5分鍾 10 ValidateIssuerSigningKey = true, 11 ValidIssuer = Configuration["Jwt:Issuer"], 12 ValidAudience = Configuration["Jwt:Issuer"], 13 IssuerSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(Configuration["Jwt:Key"])) 14 }; 15 });
好了,最終我們的 Startup 類是這樣子的,關於自定義的參數驗證后面會講到。
1 using Autofac; 2 using AutoMapper; 3 using CodeUin.WebApi.Autofac; 4 using CodeUin.WebApi.Filters; 5 using CodeUin.WebApi.Helpers; 6 using Microsoft.AspNetCore.Authentication.JwtBearer; 7 using Microsoft.AspNetCore.Builder; 8 using Microsoft.AspNetCore.Hosting; 9 using Microsoft.AspNetCore.Mvc; 10 using Microsoft.Extensions.Configuration; 11 using Microsoft.Extensions.DependencyInjection; 12 using Microsoft.Extensions.Hosting; 13 using Microsoft.IdentityModel.Tokens; 14 using System; 15 using System.Text; 16 17 namespace CodeUin.WebApi 18 { 19 public class Startup 20 { 21 public Startup(IConfiguration configuration) 22 { 23 Configuration = configuration; 24 } 25 26 public IConfiguration Configuration { get; } 27 28 public void ConfigureContainer(ContainerBuilder builder) 29 { 30 // 依賴注入 31 builder.RegisterModule(new CustomAutofacModule()); 32 } 33 34 // This method gets called by the runtime. Use this method to add services to the container. 35 public void ConfigureServices(IServiceCollection services) 36 { 37 services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme) 38 .AddJwtBearer(options => 39 { 40 options.TokenValidationParameters = new TokenValidationParameters 41 { 42 ValidateIssuer = true, 43 ValidateAudience = true, 44 ValidateLifetime = true, 45 ClockSkew = TimeSpan.FromMinutes(5), //緩沖過期時間 默認5分鍾 46 ValidateIssuerSigningKey = true, 47 ValidIssuer = Configuration["Jwt:Issuer"], 48 ValidAudience = Configuration["Jwt:Issuer"], 49 IssuerSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(Configuration["Jwt:Key"])) 50 }; 51 }); 52 53 services.AddHttpContextAccessor(); 54 55 // 使用AutoMapper 56 services.AddAutoMapper(AppDomain.CurrentDomain.GetAssemblies()); 57 58 // 關閉參數自動校驗 59 services.Configure<ApiBehaviorOptions>((options) => 60 { 61 options.SuppressModelStateInvalidFilter = true; 62 }); 63 64 // 使用自定義驗證器 65 services.AddControllers(options => 66 { 67 options.Filters.Add<ValidateModelAttribute>(); 68 }). 69 AddJsonOptions(options => 70 { 71 // 忽略null值 72 options.JsonSerializerOptions.IgnoreNullValues = true; 73 }); 74 } 75 76 // This method gets called by the runtime. Use this method to configure the HTTP request pipeline. 77 public void Configure(IApplicationBuilder app, IWebHostEnvironment env) 78 { 79 if (env.IsDevelopment()) 80 { 81 app.UseDeveloperExceptionPage(); 82 } 83 84 app.UseHttpsRedirection(); 85 86 // 請求錯誤提示配置 87 app.UseErrorHandling(); 88 89 // 授權 90 app.UseAuthentication(); 91 92 app.UseRouting(); 93 94 app.UseAuthorization(); 95 96 app.UseEndpoints(endpoints => 97 { 98 endpoints.MapControllers(); 99 }); 100 } 101 } 102 }
新建實體類
我將新建三個實體類,分別是 UserLoginModel 用戶登錄,UserRegisterModel 用戶注冊,UserModel 用戶基本信息。
UserLoginModel 和 UserRegisterModel 將根據我們在屬性中配置的特性自動驗證合法性,就不需要在控制器中單獨寫驗證邏輯了,極大的節省了工作量。
1 using System; 2 using System.ComponentModel.DataAnnotations; 3 4 namespace CodeUin.WebApi.Models 5 { 6 /// <summary> 7 /// 用戶實體類 8 /// </summary> 9 public class UserModel 10 { 11 public int Id { get; set; } 12 13 public string Email { get; set; } 14 public string UserName { get; set; } 15 16 public string Mobile { get; set; } 17 18 public int Gender { get; set; } 19 20 public int Age { get; set; } 21 22 public string Avatar { get; set; } 23 } 24 25 public class UserLoginModel 26 { 27 [Required(ErrorMessage = "請輸入郵箱")] 28 public string Email { get; set; } 29 30 [Required(ErrorMessage = "請輸入密碼")] 31 public string Password { get; set; } 32 } 33 34 public class UserRegisterModel 35 { 36 [Required(ErrorMessage = "請輸入郵箱")] 37 [EmailAddress(ErrorMessage = "請輸入正確的郵箱地址")] 38 public string Email { get; set; } 39 40 [Required(ErrorMessage = "請輸入用戶名")] 41 [MaxLength(length: 12, ErrorMessage = "用戶名最大長度不能超過12")] 42 [MinLength(length: 2, ErrorMessage = "用戶名最小長度不能小於2")] 43 public string UserName { get; set; } 44 45 [Required(ErrorMessage = "請輸入密碼")] 46 [MaxLength(length: 20, ErrorMessage = "密碼最大長度不能超過20")] 47 [MinLength(length: 6, ErrorMessage = "密碼最小長度不能小於6")] 48 public string Password { get; set; } 49 } 50 }
驗證器
在項目根目錄的 Filters 文件夾中 添加 ValidateModelAttribute 文件夾,將在 Action 請求中先進入我們的過濾器,如果不符合我們定義的規則將直接輸出錯誤項。
具體代碼如下。
1 using Microsoft.AspNetCore.Mvc; 2 using Microsoft.AspNetCore.Mvc.Filters; 3 using System.Linq; 4 5 namespace CodeUin.WebApi.Filters 6 { 7 public class ValidateModelAttribute : ActionFilterAttribute 8 { 9 public override void OnActionExecuting(ActionExecutingContext context) 10 { 11 if (!context.ModelState.IsValid) 12 { 13 var item = context.ModelState.Keys.ToList().FirstOrDefault(); 14 15 //返回第一個驗證參數錯誤的信息 16 context.Result = new BadRequestObjectResult(new 17 { 18 Code = 400, 19 Msg = context.ModelState[item].Errors[0].ErrorMessage 20 }); 21 } 22 } 23 } 24 }
添加自定義驗證特性
有時候我們需要自己額外的擴展一些規則,只需要繼承 ValidationAttribute 類然后實現 IsValid 方法即可,比如我這里驗證了中國的手機號碼。
1 using System.ComponentModel.DataAnnotations; 2 using System.Text.RegularExpressions; 3 4 namespace CodeUin.WebApi.Filters 5 { 6 public class ChineMobileAttribute : ValidationAttribute 7 { 8 public override bool IsValid(object value) 9 { 10 if (!(value is string)) return false; 11 12 var val = (string)value; 13 14 return Regex.IsMatch(val, @"^[1]{1}[2,3,4,5,6,7,8,9]{1}\d{9}$"); 15 } 16 } 17 }
實現登錄注冊
我們來實現一個簡單的業務需求,用戶注冊,登錄,和獲取用戶信息,其他的功能都大同小異,無非就是CRUD!。
接口我們在數據服務層已經寫好了,接下來是處理業務邏輯的時候到了,將直接在 Controllers 中編寫。
新建一個控制器 UsersController ,業務很簡單,不過多介紹了,具體代碼如下。
1 using System; 2 using System.IdentityModel.Tokens.Jwt; 3 using System.Security.Claims; 4 using System.Text; 5 using System.Threading.Tasks; 6 using AutoMapper; 7 using CodeUin.Dapper.Entities; 8 using CodeUin.Dapper.IRepository; 9 using CodeUin.Helpers; 10 using CodeUin.WebApi.Models; 11 using Microsoft.AspNetCore.Authorization; 12 using Microsoft.AspNetCore.Http; 13 using Microsoft.AspNetCore.Mvc; 14 using Microsoft.Extensions.Configuration; 15 using Microsoft.Extensions.Logging; 16 using Microsoft.IdentityModel.Tokens; 17 18 namespace CodeUin.WebApi.Controllers 19 { 20 [Route("api/[controller]/[action]")] 21 [ApiController] 22 [Authorize] 23 public class UsersController : Controller 24 { 25 private readonly ILogger<UsersController> _logger; 26 private readonly IUserRepository _userRepository; 27 private readonly IMapper _mapper; 28 private readonly IConfiguration _config; 29 private readonly IHttpContextAccessor _httpContextAccessor; 30 31 public UsersController(ILogger<UsersController> logger, IUserRepository userRepository, IMapper mapper, IConfiguration config, IHttpContextAccessor httpContextAccessor) 32 { 33 _logger = logger; 34 _userRepository = userRepository; 35 _mapper = mapper; 36 _config = config; 37 _httpContextAccessor = httpContextAccessor; 38 } 39 40 [HttpGet] 41 public async Task<JsonResult> Get() 42 { 43 var userId = int.Parse(_httpContextAccessor.HttpContext.User.FindFirst(ClaimTypes.NameIdentifier).Value); 44 45 var userInfo = await _userRepository.GetUserDetail(userId); 46 47 if (userInfo == null) 48 { 49 return Json(new { Code = 200, Msg = "未找到該用戶的信息" }); 50 } 51 52 var outputModel = _mapper.Map<UserModel>(userInfo); 53 54 return Json(new { Code = 200, Data = outputModel }); ; 55 } 56 57 [HttpPost] 58 [AllowAnonymous] 59 public async Task<JsonResult> Login([FromBody] UserLoginModel user) 60 { 61 // 查詢用戶信息 62 var data = await _userRepository.GetUserDetailByEmail(user.Email); 63 64 // 賬號不存在 65 if (data == null) 66 { 67 return Json(new { Code = 200, Msg = "賬號或密碼錯誤" }); 68 } 69 70 user.Password = Encrypt.Md5(data.Salt + user.Password); 71 72 // 密碼不一致 73 if (!user.Password.Equals(data.Password)) 74 { 75 return Json(new { Code = 200, Msg = "賬號或密碼錯誤" }); 76 } 77 78 var userModel = _mapper.Map<UserModel>(data); 79 80 // 生成token 81 var token = GenerateJwtToken(userModel); 82 83 // 存入Redis 84 await new RedisHelper().StringSetAsync($"token:{data.Id}", token); 85 86 return Json(new 87 { 88 Code = 200, 89 Msg = "登錄成功", 90 Data = userModel, 91 Token = token 92 }); 93 } 94 95 [HttpPost] 96 [AllowAnonymous] 97 public async Task<JsonResult> Register([FromBody] UserRegisterModel user) 98 { 99 // 查詢用戶信息 100 var data = await _userRepository.GetUserDetailByEmail(user.Email); 101 102 if (data != null) 103 { 104 return Json(new { Code = 200, Msg = "該郵箱已被注冊" }); 105 } 106 107 var salt = Guid.NewGuid().ToString("N"); 108 109 user.Password = Encrypt.Md5(salt + user.Password); 110 111 var users = new Users 112 { 113 Email = user.Email, 114 Password = user.Password, 115 UserName = user.UserName 116 }; 117 118 var model = _mapper.Map<Users>(user); 119 120 model.Salt = salt; 121 122 await _userRepository.AddUser(model); 123 124 return Json(new { Code = 200, Msg = "注冊成功" }); 125 } 126 127 /// <summary> 128 /// 生成Token 129 /// </summary> 130 /// <param name="user">用戶信息</param> 131 /// <returns></returns> 132 private string GenerateJwtToken(UserModel user) 133 { 134 var securityKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(_config["Jwt:Key"])); 135 var credentials = new SigningCredentials(securityKey, SecurityAlgorithms.HmacSha256); 136 137 var claims = new[] { 138 new Claim(JwtRegisteredClaimNames.Email, user.Email), 139 new Claim(JwtRegisteredClaimNames.Gender, user.Gender.ToString()), 140 new Claim(ClaimTypes.NameIdentifier,user.Id.ToString()), 141 new Claim(ClaimTypes.Name,user.UserName), 142 new Claim(ClaimTypes.MobilePhone,user.Mobile??""), 143 }; 144 145 var token = new JwtSecurityToken(_config["Jwt:Issuer"], 146 _config["Jwt:Issuer"], 147 claims, 148 expires: DateTime.Now.AddMinutes(120), 149 signingCredentials: credentials); 150 151 return new JwtSecurityTokenHandler().WriteToken(token); 152 } 153 } 154 }
接下來測試一下我們的功能,首先是注冊。
先來驗證一下我們的傳入的參數是否符合我們定義的規則。
輸入一個錯誤的郵箱號試試看!
ok,沒有問題,和我們在 UserRegisterModel 中 添加的驗證特性返回結果一致,最后我們測試一下完全符合規則的情況。
最后,注冊成功了,查詢下數據庫也是存在的。
我們來試試登錄接口,在調用登錄接口之前我們先來測試一下我們的配置的權限驗證是否已經生效,在不登錄的情況下直接訪問獲取用戶信息接口。
直接訪問會返回未授權,那是因為我們沒有登錄,自然也就沒有 Token,目前來看是沒問題的,但要看看我們傳入正確的Token 是否能過權限驗證。
現在,我們需要調用登錄接口,登錄成功后會返回一個Token,后面的接口請求都需要用到,不然會無權限訪問。
先來測試一下密碼錯誤的情況。
返回正確,符合我們的預期結果,下面將試試正確的密碼登錄,看是否能夠返回我們想要的結果。
登錄成功,接口也返回了我們預期的結果,最后看看生成的 token 是否按照我們寫的邏輯那樣,存一份到 redis 當中。
也是沒有問題的,和我們預想的一樣。
下面將攜帶正確的 token 請求獲取用戶信息的接口,看看是否能夠正確返回。
獲取用戶信息的接口不會攜帶任何參數,只會在請求頭的 Headers 中 添加 Authorization ,將我們正確的 token 傳入其中。
能夠正確獲取到我們的用戶信息,也就是說我們的權限這一塊也是沒有問題的了,下面將使用 Docker 打包部署到 Linux 服務器中。
打包部署
在項目的根目錄下添加 Dockerfile 文件,內容如下。
1 #See https://aka.ms/containerfastmode to understand how Visual Studio uses this Dockerfile to build your images for faster debugging. 2 3 FROM mcr.microsoft.com/dotnet/aspnet:5.0-buster-slim AS base 4 WORKDIR /app 5 EXPOSE 80 6 EXPOSE 443 7 8 FROM mcr.microsoft.com/dotnet/sdk:5.0-buster-slim AS build 9 WORKDIR /src 10 COPY ["CodeUin.WebApi/CodeUin.WebApi.csproj", "CodeUin.WebApi/"] 11 COPY ["CodeUin.Helpers/CodeUin.Helpers.csproj", "CodeUin.Helpers/"] 12 COPY ["CodeUin.Dapper/CodeUin.Dapper.csproj", "CodeUin.Dapper/"] 13 RUN dotnet restore "CodeUin.WebApi/CodeUin.WebApi.csproj" 14 COPY . . 15 WORKDIR "/src/CodeUin.WebApi" 16 RUN dotnet build "CodeUin.WebApi.csproj" -c Release -o /app/build 17 18 FROM build AS publish 19 RUN dotnet publish "CodeUin.WebApi.csproj" -c Release -o /app/publish 20 21 FROM base AS final 22 WORKDIR /app 23 COPY --from=publish /app/publish . 24 ENTRYPOINT ["dotnet", "CodeUin.WebApi.dll"]
在 Dockerfile 文件的目錄下運行打包命令
# 在當前文件夾(末尾的句點)中查找 Dockerfile docker build -t codeuin-api . # 查看鏡像 docker images # 保存鏡像到本地 docker save -o codeuin-api.tar codeuin-api
最后,將我們保存的鏡像通過上傳的服務器后導入即可。
通過 ssh 命令 連接服務器,在剛上傳包的目錄下執行導入命令。
# 加載鏡像 docker load -i codeuin-api.tar # 運行鏡像 docker run -itd -p 8888:80 --name codeuin-api codeuin-api # 查看運行狀態 docker stats
到此為止,我們整個部署工作已經完成了,最后在請求服務器的接口測試一下是否ok。
最終的結果也是ok的,到此為止,我們所有基礎的工作都完成了,所有的代碼存儲在 https://github.com/xiazanzhang/dotnet5 中,如果對你有幫助的話可以參考一下。