1. 程式人生 > >基於 abp vNext 和 .NET Core 開發部落格專案 - 使用Redis快取資料

基於 abp vNext 和 .NET Core 開發部落格專案 - 使用Redis快取資料

上一篇文章(https://www.cnblogs.com/meowv/p/12943699.html)完成了專案的全域性異常處理和日誌記錄。 在日誌記錄中使用的靜態方法有人指出寫法不是很優雅,遂優化一下上一篇中日誌記錄的方法,具體操作如下: 在`.ToolKits`層中新建擴充套件方法`Log4NetExtensions.cs`。 ```CSharp //Log4NetExtensions.cs using log4net; using log4net.Config; using Microsoft.Extensions.Hosting; using System.IO; using System.Reflection; namespace Meowv.Blog.ToolKits.Extensions { public static class Log4NetExtensions { public static IHostBuilder UseLog4Net(this IHostBuilder hostBuilder) { var log4netRepository = LogManager.GetRepository(Assembly.GetEntryAssembly()); XmlConfigurator.Configure(log4netRepository, new FileInfo("log4net.config")); return hostBuilder; } } } ``` 配置log4net,然後我們直接返回IHostBuilder物件,便於在`Main`方法中鏈式呼叫。 ```CSharp //Program.cs using Meowv.Blog.ToolKits.Extensions; using Microsoft.AspNetCore.Hosting; using Microsoft.Extensions.Hosting; using System.Threading.Tasks; namespace Meowv.Blog.HttpApi.Hosting { public class Program { public static async Task Main(string[] args) { await Host.CreateDefaultBuilder(args) .UseLog4Net() .ConfigureWebHostDefaults(builder => { builder.UseIISIntegration() .UseStartup(); }).UseAutofac().Build().RunAsync(); } } } ``` 然後修改`MeowvBlogExceptionFilter`過濾器,程式碼如下: ```CSharp //MeowvBlogExceptionFilter.cs using log4net; using Microsoft.AspNetCore.Mvc.Filters; namespace Meowv.Blog.HttpApi.Hosting.Filters { public class MeowvBlogExceptionFilter : IExceptionFilter { private readonly ILog _log; public MeowvBlogExceptionFilter() { _log = LogManager.GetLogger(typeof(MeowvBlogExceptionFilter)); } /// /// 異常處理 ///
/// /// public void OnException(ExceptionContext context) { // 錯誤日誌記錄 _log.Error($"{context.HttpContext.Request.Path}|{context.Exception.Message}", context.Exception); } } } ``` 可以刪掉之前新增的`LoggerHelper.cs`類,執行一下,同樣可以達到預期效果。 --- 本篇將整合Redis,使用Redis來快取資料,使用方法參考的微軟官方文件:https://docs.microsoft.com/zh-cn/aspnet/core/performance/caching/distributed 關於Redis的介紹這裡就不多說了,這裡有一篇快速入門的文章:[Redis快速入門及使用](https://www.cnblogs.com/meowv/p/11310452.html),對於不瞭解的同學可以看看。 直入主題,先在`appsettings.json`配置Redis的連線字串。 ```json //appsettings.json ... "Caching": { "RedisConnectionString": "127.0.0.1:6379,password=123456,ConnectTimeout=15000,SyncTimeout=5000" } ... ``` 對應的,在`AppSettings.cs`中讀取。 ```CSharp //AppSettings.cs ... /// /// Caching ///
public static class Caching { /// /// RedisConnectionString /// public static string RedisConnectionString => _config["Caching:RedisConnectionString"]; } ... ``` 在`.Application.Caching`層新增包`Microsoft.Extensions.Caching.StackExchangeRedis`,然後在模組類`MeowvBlogApplicationCachingModule`中新增配置快取實現。 ```CSharp //MeowvBlogApplicationCachingModule.cs using Meowv.Blog.Domain; using Meowv.Blog.Domain.Configurations; using Microsoft.Extensions.DependencyInjection; using Volo.Abp.Caching; using Volo.Abp.Modularity; namespace Meowv.Blog.Application.Caching { [DependsOn( typeof(AbpCachingModule), typeof(MeowvBlogDomainModule) )] public class MeowvBlogApplicationCachingModule : AbpModule { public override void ConfigureServices(ServiceConfigurationContext context) { context.Services.AddStackExchangeRedisCache(options =>
{ options.Configuration = AppSettings.Caching.RedisConnectionString; //options.InstanceName //options.ConfigurationOptions }); } } } ``` `options.Configuration`是 Redis 的連線字串。 `options.InstanceNam`是 Redis 例項名稱,這裡沒填。 `options.ConfigurationOptions`是 Redis 的配置屬性,如果配置了這個字,將優先於 Configuration 中的配置,同時它支援更多的選項。我這裡也沒填。 緊接著我們就可以直接使用了,直接將`IDistributedCache`介面依賴關係注入即可。 ![0](https://img2020.cnblogs.com/blog/891843/202005/891843-20200525143836366-1137949385.png) 可以看到預設已經實現了這麼多常用的介面,已經夠我這個小專案用的了,同時在`Microsoft.Extensions.Caching.Distributed.DistributedCacheExtensions`中微軟還給我們提供了很多擴充套件方法。 於是,我們我就想到寫一個新的擴充套件方法,可以同時處理獲取和新增快取的操作,當快取存在時,直接返回,不存在時,新增快取。 新建`MeowvBlogApplicationCachingExtensions.cs`擴充套件方法,如下: ```CSharp //MeowvBlogApplicationCachingExtensions.cs using Meowv.Blog.ToolKits.Extensions; using Microsoft.Extensions.Caching.Distributed; using System; using System.Threading.Tasks; namespace Meowv.Blog.Application.Caching { public static class MeowvBlogApplicationCachingExtensions { /// /// 獲取或新增快取 /// /// /// /// /// /// /// public static async Task GetOrAddAsync(this IDistributedCache cache, string key, Func> factory, int minutes) { TCacheItem cacheItem; var result = await cache.GetStringAsync(key); if (string.IsNullOrEmpty(result)) { cacheItem = await factory.Invoke(); var options = new DistributedCacheEntryOptions(); if (minutes != CacheStrategy.NEVER) { options.AbsoluteExpiration = DateTimeOffset.Now.AddMinutes(minutes); } await cache.SetStringAsync(key, cacheItem.ToJson(), options); } else { cacheItem = result.FromJson(); } return cacheItem; } } } ``` 我們可以在`DistributedCacheEntryOptions`中可以配置我們的快取過期時間,其中有一個判斷條件,就是當`minutes = -1`的時候,不指定過期時間,那麼我們的快取就不會過期了。 `GetStringAsync()`、`SetStringAsync()`是`DistributedCacheExtensions`的擴充套件方法,最終會將快取項`cacheItem`轉換成JSON格式進行儲存。 `CacheStrategy`是在`.Domain.Shared`層定義的快取過期時間策略常量。 ```CSharp //MeowvBlogConsts.cs ... /// /// 快取過期時間策略 /// public static class CacheStrategy { /// /// 一天過期24小時 /// public const int ONE_DAY = 1440; /// /// 12小時過期 /// public const int HALF_DAY = 720; /// /// 8小時過期 /// public const int EIGHT_HOURS = 480; /// /// 5小時過期 /// public const int FIVE_HOURS = 300; /// /// 3小時過期 /// public const int THREE_HOURS = 180; /// /// 2小時過期 /// public const int TWO_HOURS = 120; /// /// 1小時過期 /// public const int ONE_HOURS = 60; /// /// 半小時過期 /// public const int HALF_HOURS = 30; /// /// 5分鐘過期 /// public const int FIVE_MINUTES = 5; /// /// 1分鐘過期 /// public const int ONE_MINUTE = 1; /// /// 永不過期 /// public const int NEVER = -1; } ... ``` 接下來去建立快取介面類和實現類,然後再我們的引用服務層`.Application`中進行呼叫,拿上一篇中接入GitHub的幾個介面來做新增快取操作。 和`.Application`層格式一樣,在`.Application.Caching`中新建Authorize資料夾,新增快取介面`IAuthorizeCacheService`和實現類`AuthorizeCacheService`。 注意命名規範,實現類肯定要繼承一個公共的`CachingServiceBase`基類。在`.Application.Caching`層根目錄新增`MeowvBlogApplicationCachingServiceBase.cs`,繼承`ITransientDependency`。 ```CSharp //MeowvBlogApplicationCachingServiceBase.cs using Microsoft.Extensions.Caching.Distributed; using Volo.Abp.DependencyInjection; namespace Meowv.Blog.Application.Caching { public class CachingServiceBase : ITransientDependency { public IDistributedCache Cache { get; set; } } } ``` 然後使用屬性注入的方式,注入`IDistributedCache`。這樣我們只要繼承了基類:`CachingServiceBase`,就可以愉快的使用快取了。 新增要快取的介面到`IAuthorizeCacheService`,在這裡我們使用`Func()`方法,我們的介面返回什麼型別由`Func()`來決定,於是新增三個介面如下: ```CSharp //IAuthorizeCacheService.cs using Meowv.Blog.ToolKits.Base; using System; using System.Threading.Tasks; namespace Meowv.Blog.Application.Caching.Authorize { public interface IAuthorizeCacheService { /// /// 獲取登入地址(GitHub) /// /// Task> GetLoginAddressAsync(Func>> factory); /// /// 獲取AccessToken /// /// /// /// Task> GetAccessTokenAsync(string code, Func>> factory); /// /// 登入成功,生成Token /// /// /// /// Task> GenerateTokenAsync(string access_token, Func>> factory); } } ``` 是不是和`IAuthorizeService`程式碼很像,的確,我就是直接複製過來改的。 在`AuthorizeCacheService`中實現介面。 ```CSharp //AuthorizeCacheService.cs using Meowv.Blog.ToolKits.Base; using Meowv.Blog.ToolKits.Extensions; using System; using System.Threading.Tasks; using static Meowv.Blog.Domain.Shared.MeowvBlogConsts; namespace Meowv.Blog.Application.Caching.Authorize.Impl { public class AuthorizeCacheService : CachingServiceBase, IAuthorizeCacheService { private const string KEY_GetLoginAddress = "Authorize:GetLoginAddress"; private const string KEY_GetAccessToken = "Authorize:GetAccessToken-{0}"; private const string KEY_GenerateToken = "Authorize:GenerateToken-{0}"; /// /// 獲取登入地址(GitHub) /// /// /// public async Task> GetLoginAddressAsync(Func>> factory) { return await Cache.GetOrAddAsync(KEY_GetLoginAddress, factory, CacheStrategy.NEVER); } /// /// 獲取AccessToken /// /// /// /// public async Task> GetAccessTokenAsync(string code, Func>> factory) { return await Cache.GetOrAddAsync(KEY_GetAccessToken.FormatWith(code), factory, CacheStrategy.FIVE_MINUTES); } /// /// 登入成功,生成Token /// /// /// /// public async Task> GenerateTokenAsync(string access_token, Func>> factory) { return await Cache.GetOrAddAsync(KEY_GenerateToken.FormatWith(access_token), factory, CacheStrategy.ONE_HOURS); } } } ``` 程式碼很簡單,每個快取都有固定KEY值,根據引數生成KEY,然後呼叫前面寫的擴充套件方法,再給一個過期時間即可,可以看到KEY裡面包含了冒號 `:`,這個冒號 `:` 可以起到類似於資料夾的操作,在介面化管理工具中可以很友好的檢視。 這樣我們的快取就搞定了,然後在`.Application`層對應的Service中進行呼叫。程式碼如下: ```CSharp //AuthorizeService.cs using Meowv.Blog.Application.Caching.Authorize; using Meowv.Blog.Domain.Configurations; using Meowv.Blog.ToolKits.Base; using Meowv.Blog.ToolKits.Extensions; using Meowv.Blog.ToolKits.GitHub; using Microsoft.IdentityModel.Tokens; using System; using System.IdentityModel.Tokens.Jwt; using System.Linq; using System.Net; using System.Net.Http; using System.Net.Http.Headers; using System.Security.Claims; using System.Threading.Tasks; namespace Meowv.Blog.Application.Authorize.Impl { public class AuthorizeService : ServiceBase, IAuthorizeService { private readonly IAuthorizeCacheService _authorizeCacheService; private readonly IHttpClientFactory _httpClient; public AuthorizeService(IAuthorizeCacheService authorizeCacheService, IHttpClientFactory httpClient) { _authorizeCacheService = authorizeCacheService; _httpClient = httpClient; } /// /// 獲取登入地址(GitHub) /// /// public async Task> GetLoginAddressAsync() { return await _authorizeCacheService.GetLoginAddressAsync(async () => { var result = new ServiceResult(); var request = new AuthorizeRequest(); var address = string.Concat(new string[] { GitHubConfig.API_Authorize, "?client_id=", request.Client_ID, "&scope=", request.Scope, "&state=", request.State, "&redirect_uri=", request.Redirect_Uri }); result.IsSuccess(address); return await Task.FromResult(result); }); } /// /// 獲取AccessToken /// /// /// public async Task> GetAccessTokenAsync(string code) { var result = new ServiceResult(); if (string.IsNullOrEmpty(code)) { result.IsFailed("code為空"); return result; } return await _authorizeCacheService.GetAccessTokenAsync(code, async () => { var request = new AccessTokenRequest(); var content = new StringContent($"code={code}&client_id={request.Client_ID}&redirect_uri={request.Redirect_Uri}&client_secret={request.Client_Secret}"); content.Headers.ContentType = new MediaTypeHeaderValue("application/x-www-form-urlencoded"); using var client = _httpClient.CreateClient(); var httpResponse = await client.PostAsync(GitHubConfig.API_AccessToken, content); var response = await httpResponse.Content.ReadAsStringAsync(); if (response.StartsWith("access_token")) result.IsSuccess(response.Split("=")[1].Split("&").First()); else result.IsFailed("code不正確"); return result; }); } /// /// 登入成功,生成Token /// /// /// public async Task> GenerateTokenAsync(string access_token) { var result = new ServiceResult(); if (string.IsNullOrEmpty(access_token)) { result.IsFailed("access_token為空"); return result; } return await _authorizeCacheService.GenerateTokenAsync(access_token, async () => { var url = $"{GitHubConfig.API_User}?access_token={access_token}"; using var client = _httpClient.CreateClient(); client.DefaultRequestHeaders.Add("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/83.0.4103.14 Safari/537.36 Edg/83.0.478.13"); var httpResponse = await client.GetAsync(url); if (httpResponse.StatusCode != HttpStatusCode.OK) { result.IsFailed("access_token不正確"); return result; } var content = await httpResponse.Content.ReadAsStringAsync(); var user = content.FromJson(); if (user.IsNull()) { result.IsFailed("未獲取到使用者資料"); return result; } if (user.Id != GitHubConfig.UserId) { result.IsFailed("當前賬號未授權"); return result; } var claims = new[] { new Claim(ClaimTypes.Name, user.Name), new Claim(ClaimTypes.Email, user.Email), new Claim(JwtRegisteredClaimNames.Exp, $"{new DateTimeOffset(DateTime.Now.AddMinutes(AppSettings.JWT.Expires)).ToUnixTimeSeconds()}"), new Claim(JwtRegisteredClaimNames.Nbf, $"{new DateTimeOffset(DateTime.Now).ToUnixTimeSeconds()}") }; var key = new SymmetricSecurityKey(AppSettings.JWT.SecurityKey.SerializeUtf8()); var creds = new SigningCredentials(key, SecurityAlgorithms.HmacSha256); var securityToken = new JwtSecurityToken( issuer: AppSettings.JWT.Domain, audience: AppSettings.JWT.Domain, claims: claims, expires: DateTime.Now.AddMinutes(AppSettings.JWT.Expires), signingCredentials: creds); var token = new JwtSecurityTokenHandler().WriteToken(securityToken); result.IsSuccess(token); return await Task.FromResult(result); }); } } } ``` 直接return我們的快取介面,當查詢到Redis中存在KEY值的快取就不會再走我們的具體的實現方法了。 注意注意,千萬不要忘了在`.Application`層的模組類中新增依賴快取模組`MeowvBlogApplicationCachingModule`,不然就會報錯報錯報錯(我就是忘了新增...) ```CSharp //MeowvBlogApplicationCachingModule.cs using Meowv.Blog.Domain; using Meowv.Blog.Domain.Configurations; using Microsoft.Extensions.DependencyInjection; using Volo.Abp.Caching; using Volo.Abp.Modularity; namespace Meowv.Blog.Application.Caching { [DependsOn( typeof(AbpCachingModule), typeof(MeowvBlogDomainModule) )] public class MeowvBlogApplicationCachingModule : AbpModule { public override void ConfigureServices(ServiceConfigurationContext context) { context.Services.AddStackExchangeRedisCache(options => { options.Configuration = AppSettings.Caching.RedisConnectionString; }); } } } ``` 此時專案的層級目錄結構。 ![2](https://img2020.cnblogs.com/blog/891843/202005/891843-20200525160645669-1689293018.png) 好的,編譯執行專案,現在去呼叫介面看看效果,為了真實,這裡我先將我redis快取資料全部幹掉。 ![3](https://img2020.cnblogs.com/blog/891843/202005/891843-20200525152759522-1692025683.png) 訪問介面,.../auth/url,成功返回資料,現在再去看看我們的redis。 ![4](https://img2020.cnblogs.com/blog/891843/202005/891843-20200525153209228-630323052.png) 成功將KEY為:Authorize:GetLoginAddress 新增進去了,這裡直接使用RedisDesktopManager進行檢視。 ![5](https://img2020.cnblogs.com/blog/891843/202005/891843-20200525154927052-536134994.png) 那麼再次呼叫這個介面,只要沒有過期,就會直接返回資料了,除錯圖如下: ![6](https://img2020.cnblogs.com/blog/891843/202005/891843-20200525160919046-83009645.png) 可以看到,是可以直接取到快取資料的,其他介面大家自己試試吧,一樣的效果。 是不是很簡單,用最少的程式碼整合Redis進行資料快取,你學會了嗎?