我前面幾篇隨筆介紹了關於幾篇關於SqlSugar的基礎封裝,已經可以直接應用在Winform項目開發上,並且基礎介面也通過了單元測試,同時測試通過了一些Winform功能頁面;本篇隨筆繼續深化應用開發,著手在在.net6框架的Web API上開發應用,也就是基於.net core的Web API應用... ...
我前面幾篇隨筆介紹了關於幾篇關於SqlSugar的基礎封裝,已經可以直接應用在Winform項目開發上,並且基礎介面也通過了單元測試,同時測試通過了一些Winform功能頁面;本篇隨筆繼續深化應用開發,著手在在.net6框架的Web API上開發應用,也就是基於.net core的Web API應用開發,這樣可以應用在不同的前端接入上。本篇隨筆主要介紹基於.net6框架的Web API的相關整合開發內容,內容涉及到Swagger的整合支持、SeriLog的支持、JWT鑒權和用戶身份信息緩存、基類控制器封裝、自動註入介面對象、統一結果封裝、統一異常處理等方面。
1、創建.netcore WebApi項目並添加相關支持
本篇隨筆主要從基礎框架開發創建,因此使用VS2022添加一個基於.net core6的WebAPI項目,如下所示。
我們在生成的項目中,看到有一個Program.cs的代碼文件,裡面代碼比較簡潔,我們逐步調整並添加自己的相關代碼即可。
在其中可以看到
builder.Services.AddSwaggerGen();
這個是簡單的Swagger註釋支持,我們如果需要定義更多的信息,可以採用下麵的代碼。
#region 添加swagger註釋 //builder.Services.AddSwaggerGen(); builder.Services.AddSwaggerGen(c => { c.SwaggerDoc("v1", new OpenApiInfo { Version = "v1", Title = "Api" }); c.IncludeXmlComments(Path.Combine(basePath, "SugarWebApi.xml"), true);//WebAPI項目XML文件 c.IncludeXmlComments(Path.Combine(basePath, "SugarProjectCore.xml"), true);//其他項目所需的XML文件 c.AddSecurityDefinition("Bearer", new OpenApiSecurityScheme { Description = "Value: Bearer {token}", Name = "Authorization", In = ParameterLocation.Header, Type = SecuritySchemeType.ApiKey, 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>() } }); }); #endregion
上面的代碼除了添加對應控制器的介面信息外,還增加了一個相關服務類的介面定義,便於我們查看詳細的xml信息,如下所示得到很詳細的介面註釋。
然後調整Swagger UI支持的代碼如下所示。
// Configure the HTTP request pipeline. if (app.Environment.IsDevelopment()) { app.UseSwagger(); app.UseSwaggerUI(); }
另外,我們的Web API控制器,需要集成JWT Bear 認證的處理的,添加認證代碼如下所示。
//JWT Bear 認證 builder.Services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme).AddJwtBearer(options => { options.TokenValidationParameters = new TokenValidationParameters { //非固定可選可加 ValidateIssuer = true, ValidIssuer = builder.Configuration["Jwt:Issuer"], ValidateAudience = true, ValidAudience = builder.Configuration["Jwt:Audience"], ValidateLifetime = true,//時間 ClockSkew = TimeSpan.Zero, IssuerSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(builder.Configuration["Jwt:Secret"])) }; });
這裡面的代碼讀取配置信息的,我們可以在appSettings.json中配置JWT的一些鍵值。
"Jwt": { "Secret": "your-256-bit-secret", "Issuer": "iqidi.com", "Audience": "api" }
另外,為了在.net core中輸出日誌,可以使用SeriLog組件進行處理。
添加相關的nuget類庫,如下所示。
然後在Program.cs中添加初始化日誌代碼即可。
//初始化日誌 Log.Logger = new LoggerConfiguration() .MinimumLevel.Debug() //最小記錄級別 //對其他日誌進行重寫,除此之外,目前框架只有微軟自帶的日誌組件 .MinimumLevel.Override(source: "Microsoft", minimumLevel: Serilog.Events.LogEventLevel.Error) .Enrich.FromLogContext() //記錄相關上下文信息 .WriteTo.Console() //輸出到控制台 .WriteTo.File("logs/log.txt", rollingInterval: RollingInterval.Day) //輸出到本地文件 .CreateLogger();
2、統一結果封裝和異常處理
在Web API的控制器返回信息中,我們為了方便使用JSON信息,往往需要對返回結果進行封裝,讓它們返回指定格式的數據,如下所示。
正常結果200:
未授權結果401:
關於介面數據格式的統一封裝,我們定義一個WrapResultFilter,以及需要一個不封裝的屬性標識DontWrapResultAttribute,預設是統一封裝返回的結果。
/// <summary> /// 禁用封裝結果 /// </summary> [AttributeUsage(AttributeTargets.Class | AttributeTargets.Interface | AttributeTargets.Method, AllowMultiple = false, Inherited = false)] public class DontWrapResultAttribute : Attribute { }
而統一封裝的處理,需要繼承ActionFilterAttribute並重寫OnResultExecuting處理操作。
裡面的主要邏輯就是對結果內容進行統一的再次封裝,如下所示主要的邏輯代碼。
if (context.Result is ObjectResult objRst) { if (objRst.Value is AjaxResponse) return; context.Result = new ObjectResult(new AjaxResponse { Success = true, Error = null, TargetUrl = string.Empty, UnAuthorizedRequest = false, Result = objRst.Value }); }
除了常規的正常返回內容進行封裝,也需要對異常進行攔截,並對結果進行封裝,因此需要繼承ExceptionFilterAttribute並添加一個異常處理的過濾器進行處理,並重寫OnException的操作即可。
/// <summary> /// 自定義異常處理 /// </summary> public class GlobalExceptionFilter : ExceptionFilterAttribute { /// <summary> /// 異常處理封裝 /// </summary> /// <param name="context"></param> public override void OnException(ExceptionContext context) { if (!context.ExceptionHandled) { //異常返回結果包裝 var content = new AjaxResponse() { Success = false, Error = new ErrorInfo(0, context.Exception.Message, context.Exception.StackTrace), TargetUrl = string.Empty, UnAuthorizedRequest = false, Result = null }; //日誌記錄 Log.Error(context.Exception, context.Exception.Message); context.ExceptionHandled = true; context.Result = new ApplicationErrorResult(content); context.HttpContext.Response.StatusCode = (int)HttpStatusCode.InternalServerError; } }
因此為了攔截相關的處理,我們在Program.cs中添加以下代碼進行攔截。
//控制器添加自定義過濾器 builder.Services.AddControllers(options=> { options.Filters.Add<WrapResultFilter>(); //統一結果封裝處理 options.Filters.Add<GlobalExceptionFilter>();//自定義異常處理 }); //所有控制器啟動身份驗證 builder.Services.AddMvc(options => { options.Filters.Add(new AuthorizeFilter());//所有MVC服務預設添加授權標簽 });
並調整代碼,添加認證和授權驗證的代碼處理。
app.UseAuthentication();
app.UseAuthorization();
對於一些系統異常的處理(如401未授權、400未找到介面、500系統錯誤)等,預設是沒有進行處理的
我們如果要攔截,就另外需要添加一個中間件的方式來處理信息流,如下所示。
其中在Invoke的函數處理中,統一處理不同的異常即可。
public async Task Invoke(HttpContext context) { try { await next(context); } catch (Exception ex) { var statusCode = context.Response.StatusCode; if (ex is ArgumentException) { statusCode = 200; } await HandleExceptionAsync(context, statusCode, ex.Message); } finally { var statusCode = context.Response.StatusCode; var msg = ""; if (statusCode == 401) { msg = "未授權 " + context.Response.Headers["WWW-Authenticate"]; } else if (statusCode == 404) { msg = "未找到服務"; } else if(statusCode == 500) { msg = "系統錯誤"; } else if (statusCode == 502) { msg = "請求錯誤"; } else if (statusCode != 200) { msg = "未知錯誤"; } if (!string.IsNullOrWhiteSpace(msg)) { await HandleExceptionAsync(context, statusCode, msg); } } }
並添加一個擴展類方法,用於快速使用中間件方式調用。
/// <summary> /// 自定義錯誤處理的擴展方法 /// </summary> public static class ErrorHandlingExtensions { public static IApplicationBuilder UseErrorHandling(this IApplicationBuilder builder) { return builder.UseMiddleware<ErrorHandlingMiddleware>(); } }
最後在program.cs代碼中添加使用代碼即可,註意添加位置。
另外,為了把用戶身份信息緩存起來,我們可以使用Redis進行緩存處理,因此在項目中使用CRedis的封裝類庫進行操作Redis
通過連接字元串(讀取配置信息)初始化Redis的代碼如下所示。
//初始化Redis RedisHelper.Initialization(new CSRedisClient(builder.Configuration["CSRedis:ConnectString"]));
其中appSettings.json信息如下所示。
{
"ConnectionStrings": {
"Default": "Server=.; Database=WeixinBootstrap2; Trusted_Connection=True;",
"Oracle": "Data Source=(DESCRIPTION=(ADDRESS=(PROTOCOL=TCP)(HOST=localhost)(PORT=1521))(CONNECT_DATA=(SERVER=DEDICATED)(SERVICE_NAME=orcl)));User ID=C##ABP;Password=abp",
"MySql": "Server=localhost;Database=myprojectdb;Uid=root;Pwd=123456;",
"PostgreSQL": "Server=localhost;Port=5432;Database=myprojectdb;User Id=postgres;Password=123456"
},
"DbSetting": {
"DefaultDb": "Default",
"ComponentDbType": "sqlserver"
},
"CSRedis": {
"ConnectString": "127.0.0.1:6379"
},
"Jwt": {
"Secret": "your-256-bit-secret",
"Issuer": "iqidi.com",
"Audience": "api"
},
"Logging": {
"LogLevel": {
"Default": "Information",
"Microsoft.AspNetCore": "Warning"
}
},
"AllowedHosts": "*",
}
如果允許登錄授權請求成功的話,那麼對應的用戶省份緩存也就記錄在Redis中了。
3、介面對象的依賴註入處理
我們在.net core的Web API中調用相關處理,我們往往是使用介面來調用相關的處理的。
啟動Web API的時候通過手工或者自動註入介面對象的方式,然後在控制器裡面的構造函數中通過依賴註入方式使用介面即可。
如果是手工註入,那麼你確定在Web API項目中所有用到的業務訪問介面,都需要提取註入。
services.AddSingleton<IDictDataService, DictDataService>(); services.AddSingleton<IDictTypeService, DictTypeService>(); services.AddSingleton<ICustomerService, CustomerService>(); services.AddScoped<IUserService, UserService>();
但是這樣介面實現一旦很多,手工加入肯定繁瑣而且低效了,因此需要考慮自動註入所有相關的服務介面為佳。
為了實現這個自動註入的目標,首先我們先定義幾個不同生命周期的介面聲明。
//用於定義這三種生命周期的標識介面 public interface IDependency { } /// <summary> /// 瞬時(每次都重新實例) /// </summary> public interface ITransientDependency : IDependency { } /// <summary> /// 單例(全局唯一) /// </summary> public interface ISingletonDependency : IDependency { } /// <summary> /// 一個請求內唯一(線程內唯一) /// </summary> public interface IScopedDependency : IDependency { }
然後通過遍歷相關的DLL,然後實現所有實現指定介面的類對象,統一加入即可,如下代碼所示。
var baseType = typeof(IDependency); var path = AppDomain.CurrentDomain.RelativeSearchPath ?? AppDomain.CurrentDomain.BaseDirectory; var getFiles = Directory.GetFiles(path, "*.dll").Where(Match); //.Where(o=>o.Match()) var referencedAssemblies = getFiles.Select(Assembly.LoadFrom).ToList(); //.Select(o=> Assembly.LoadFrom(o)) var ss = referencedAssemblies.SelectMany(o => o.GetTypes());
然後進一步進行對介面的判斷,如下所示。
var types = referencedAssemblies .SelectMany(a => a.DefinedTypes) .Select(type => type.AsType()) .Where(x => x != baseType && baseType.IsAssignableFrom(x)).ToList(); var implementTypes = types.Where(x => x.IsClass).ToList(); var interfaceTypes = types.Where(x => x.IsInterface).ToList(); foreach (var implementType in implementTypes) { if (typeof(IScopedDependency).IsAssignableFrom(implementType)) { var interfaceType = interfaceTypes.FirstOrDefault(x => x.IsAssignableFrom(implementType)); if (interfaceType != null) services.AddScoped(interfaceType, implementType); } else if (typeof(ISingletonDependency).IsAssignableFrom(implementType)) { var interfaceType = interfaceTypes.FirstOrDefault(x => x.IsAssignableFrom(implementType)); if (interfaceType != null) services.AddSingleton(interfaceType, implementType); } else { var interfaceType = interfaceTypes.FirstOrDefault(x => x.IsAssignableFrom(implementType)); if (interfaceType != null) services.AddTransient(interfaceType, implementType); } }
然後統一調用即可。
//配置依賴註入訪問資料庫 ServiceInjection.ConfigureRepository(builder.Services);
這樣我們在對應的WebAPI 控制器中就可以方便的使用介面的構造函數註入方式了。
/// <summary> /// 客戶信息的控制器對象 /// </summary> public class CustomerController : BusinessController<CustomerInfo, string, CustomerPagedDto> { private ICustomerService _customerService; /// <summary> /// 構造函數,並註入基礎介面對象 /// </summary> /// <param name="customerService"></param> public CustomerController(ICustomerService customerService) :base(customerService) { this._customerService = customerService; } }
以上就是我們在創建.net Core項目的Web API項目中碰到的一些常見問題的總結,希望對大家有所幫助。
相關係類文章如下所示。
基於SqlSugar的資料庫訪問處理的封裝,在.net6框架的Web API上開發應用 (本隨筆) 基於SqlSugar的資料庫訪問處理的封裝,支持.net FrameWork和.net core的項目調用 基於SqlSugar的資料庫訪問處理的封裝,支持多資料庫並使之適應於實際業務開發中(2)基於SqlSugar的資料庫訪問處理的封裝,支持多資料庫並使之適應於實際業務開發中
主要研究技術:代碼生成工具、會員管理系統、客戶關係管理軟體、病人資料管理軟體、Visio二次開發、酒店管理系統、倉庫管理系統等共用軟體開發
專註於Winform開發框架/混合式開發框架、Web開發框架、Bootstrap開發框架、微信門戶開發框架的研究及應用。
轉載請註明出處:
撰寫人:伍華聰 http://www.iqidi.com