自動化CodeReview系列目錄
我個人比較懶,能自動做的事絕不手動做,最近在用ASP.NET Core寫一個項目,過程中會積累一些方便的工具類或框架,分享出來歡迎大家點評。
如果以后有時間的話,我打算寫一個系列的【實現BUG自動檢測】,本文將是第一篇。
如果你使用過ASP.NET Core那么對依賴注入一定不陌生。
使用流程為:
1. 先注冊Service,有3個方法AddTransient、AddScoped、AddSingleton
2. 再使用Service,通常在構造方法里聲明
先來說說產生BUG的場景
BUG場景一:
有的時候可能因為疏忽忘記注冊Service直接就使用了,使用那個Service時會報異常。這種情況項目都是可以編譯通過的,是一個不太容易發現的BUG,如果那個Service在測試時沒有覆蓋到這個BUG就會被帶到生產環境
BUG場景二:
通常有一些Service我們只希望它在請求作用域內被使用,例如:在服務端持有數據庫連接的Service通常都是請求作用域級別的,即:在請求內第一次使用數據庫時創建數據庫連接,請求內會復用連接,請求結束回收連接。
對應ASP.NET Core里的注冊方式如下:
services.AddScoped<IDbContext, DbContext>();
在ASP.NET Core中AddScoped注冊的Service在請求結束時會銷毀。
如果你在控制器中直接引用IDbContext一切正常,現在業務需要我們要封裝一個用戶管理類UserManager,它是單例的,注冊代碼:
services.AddScoped<IUserManager, UserManager>();
在寫UserManager類的時候要訪問數據庫,順手就引用了IDbContext(正常是不應該這么引用的但是忘記了),因為UserManager是單例會造成IDbContext永遠不會釋放,進而長期占用一個數據庫連接。並且在編譯時,運行時都不會報錯,很隱蔽的一個BUG
好了,場景說完了,本文的主角該登場了,解決方式如下:
在Startup類的ConfigureServices方法最后加入如下代碼:
public void ConfigureServices(IServiceCollection services){
//此處省略若干代碼...
//確保服務依賴的正確性,放到所有注冊服務代碼后調用
if (_env.IsDevelopment())
services.AssertDependencyValid();
}
對於“場景一”此方法會拋出異常:
throw new InvalidProgramException($"服務 {svceType.FullName} 的構造方法引用了未注冊的服務 {paramType.FullName}");
對於“場景二”此方法會拋出異常:
throw new InvalidProgramException($"Singleton的服務 {svceType.FullName} 的構造方法引用了Scoped的服務 {paramType.FullName}");
您可以根據異常的提示找到具體有問題的類並修改之
完整代碼如下:
using System;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
using System.Resources;
using System.Text.RegularExpressions;
using System.Threading.Tasks;
using Microsoft.Extensions.DependencyInjection;
namespace Microsoft.Extensions.DependencyInjection
{
public static class MondolServiceCollectionExtensions
{
/// <summary>
/// DUMP服務列表
/// </summary>
public static string Dump(this IServiceCollection services)
{
var sevList = new List<Tuple<string, string>>();
foreach (var sev in services)
{
sevList.Add(new Tuple<string, string>(sev.Lifetime.ToString(), sev.ServiceType.FullName));
}
sevList.Sort((x, y) =>
{
var cRs = string.CompareOrdinal(x.Item1, y.Item1);
return cRs != 0 ? cRs : string.CompareOrdinal(x.Item2, y.Item2);
});
return string.Join("\r\n", sevList.Select(p => $"{p.Item2} - {p.Item1}"));
}
/// <summary>
/// 確保當前注冊服務的依賴關系是正確的
/// </summary>
public static void AssertDependencyValid(this IServiceCollection services)
{
var ignoreTypes = new[]
{
"Microsoft.AspNetCore.Mvc.Internal.MvcRouteHandler",
"Microsoft.AspNetCore.Razor.Runtime.TagHelpers.TagHelperDescriptorResolver"
};
foreach (var svce in services)
{
if (svce.Lifetime == ServiceLifetime.Singleton)
{
//確保Singleton的服務不能依賴Scoped的服務
if (svce.ImplementationType != null)
{
var svceType = svce.ImplementationType;
if (ignoreTypes.Contains(svceType.FullName))
continue;
var ctors = svceType.GetConstructors();
foreach (var ctor in ctors)
{
var paramLst = ctor.GetParameters();
foreach (var param in paramLst)
{
var paramType = param.ParameterType;
var paramTypeInfo = paramType.GetTypeInfo();
if (paramTypeInfo.IsGenericType)
{
if (paramType.ToString().StartsWith("System.Collections.Generic.IEnumerable`1"))
{
paramType = paramTypeInfo.GetGenericArguments().First();
paramTypeInfo = paramType.GetTypeInfo();
}
}
if (paramType == typeof(IServiceProvider))
continue;
ServiceDescriptor pSvce;
if (paramTypeInfo.IsGenericType)
{
//泛型采用模糊識別,可能有遺漏
var prefix = Regex.Match(paramType.ToString(), @"^[^`]+`\d+\[").Value;
pSvce = services.FirstOrDefault(p => p.ServiceType.ToString().StartsWith(prefix));
}
else
{
pSvce = services.FirstOrDefault(p => p.ServiceType == paramType);
}
if (pSvce == null)
throw new InvalidProgramException($"服務 {svceType.FullName} 的構造方法引用了未注冊的服務 {paramType.FullName}");
if (pSvce.Lifetime == ServiceLifetime.Scoped)
throw new InvalidProgramException($"Singleton的服務 {svceType.FullName} 的構造方法引用了Scoped的服務 {paramType.FullName}");
}
}
}
}
}
}
}
}
