自動對應 config 區塊到實體型別,並註冊到 DI
在 service 註冊 Config,並且傳入 IConfiguration 當參數
public IConfiguration Configuration { get; }
public Startup(IConfiguration configuration)
{
Configuration = configuration;
}
public void ConfigureServices(IServiceCollection services)
{
services.AddConfig(Configuration);
}如果要指定特別的 Assemble 的話,可以傳入 Assembly 的 prefix 當參數
public void ConfigureServices(IServiceCollection services)
{
services.AddConfig(Configuration, "Baymax");
}新增一個類別實作 IConfig,並且加上 ConfigSection attribute,
第一個參數為 config 的物件名稱,類別的 property 對應到 config 的內容
[ConfigSection("Test")]
public TestConfig : IConfig
{
public int Id { get; set; }
}{
"Test" : {
"Id" : 1
}
}使用的話就直接注入 config 的類別就好了
public class IndexController : Controller
{
public IndexController(TestConfig testConfig){ ... }
} 預設注入的生命周期為 Scope,如果需要修改的話,請傳入第二個參數
[ConfigSection("Test", ServiceLifetime.Singleton)]
public TestConfig : IConfig
{
public int Id { get; set; }
}如果 config 是集合時,需要在 attribute 加上第三個參數 isCollections = true,和第四個參數 collectionType 集合的型別
[ConfigSection("Test", isCollections: true, collectionType: typeof(int)))]
public TestConfig : IConfig
{
}{
"Test" : [ 1, 2, 3]
}使用的話就直接注入 List of 集合的型別
public class IndexController : Controller
{
public IndexController(List<int> testConfig){ ... }
} 集合的型別也可以是 reference type
[ConfigSection("Test", isCollections: true, collectionType: typeof(TestConfig)))]
public TestConfig : IConfig
{
public int Id { get; set; }
public string Name { get; set; }
}{
"Test" : [
{ "Id" : 1, "Name" : "AA" },
{ "Id" : 2, "Name" : "BB" }
]
}註冊 LogService 到 DI,可以同時寫入多個 Log provider
在 service 註冊 LogService
public void ConfigureServices(IServiceCollection services)
{
services.AddLogService();
}如果要指定特別的 Assemble 的話,可以傳入 Assembly 的 prefix 當參數
public void ConfigureServices(IServiceCollection services)
{
services.AddLogService("Baymax");
}建立自定義的 Log 並且實作 ILogBase 除了 message 和 exception 之外,有多一個 EnvironmentName 的參數可以使用,例如某些特定的環境就不記錄 log
public class SlackLog : ILogBase
{
public Task LogAsync(System.Exception ex, string env)
{
return Task.CompletedTask;
}
public Task LogAsync(string msg, string env)
{
return Task.CompletedTask;
}
}
public class NLogProvider : ILogBase
{
public Task LogAsync(System.Exception ex, string env)
{
return Task.CompletedTask;
}
public Task LogAsync(string msg, string env)
{
return Task.CompletedTask;
}
}直接注入 ILogService 就可以使用
public class IndexController : Controller
{
public IndexController(ILogService logService){ ... }
}
public void Index(){
{
logServicr.Log("TEST");
logServicr.Log(new ArgumentException("TEST"));
}自動解析註冊結尾是 Service 的型別到 DI 需要注意 Service class 必需要有相對應名稱的 interface
public class TestService : ITestService
{
public void Handle()
{
}
}
public interface ITestService
{
void Handle();
}在 service 註冊 Service,傳入 Assembly 的 prefix 當參數
public void ConfigureServices(IServiceCollection services)
{
services.AddGeneralService("Baymax");
}預設注入 Service 的生命周期為 Scope,如果需要修改的話,請傳入第二個參數 Dictionary<Type, ServiceLifetime>, KEY 為 Service class 的 Type,VALUE 為 ServiceLifetime
public void ConfigureServices(IServiceCollection services)
{
var typeLifetimeDic = new Dictionary<Type, ServiceLifetime>
{
{ typeof(TestService), ServiceLifetime.Singleton }
};
services.AddGeneralService("Baymax", typeLifetimeDic);
}直接注入 Service 的 interface 就可以使用
public class IndexController : Controller
{
public IndexController(ITestService testService){ ... }
}
public void Index(){
{
testService.handle();
}可定期 (排程) 執行程式
把需要定期執行的程式碼寫在 DoWork,把停止時需要取消的程式碼寫在 StopWork
有時 StopWork 不一定會有程式碼
public class TestBackgroundService : IBackgroundProcessService
{
public TestBackgroundService()
{
}
public void DoWork()
{
}
public void StopWork()
{
}
}在 config 裡面新增一個 BackgroundService 的區段,裡面的 KEY 就是實作 IBackgroundProcessService 的類別名稱, Value 就是需要定期執行的周期
注意單位為
毫秒,以下面的程式為例就是 1 秒會執行一次
{
"BackgroundService" : {
"TestBackgroundService" : 1000
}
}在 service 註冊 BackgroundService,傳入實作 IBackgroundProcessService 的 type 當參數
public void ConfigureServices(IServiceCollection services)
{
services.AddBackgroundService(typeof(TestBackgroundService))
}可實作多個 IBackgroundProcessService 同時傳入
public void ConfigureServices(IServiceCollection services)
{
services.AddBackgroundService(typeof(TestBackgroundService1), typeof(TestBackgroundService2))
}無需寫任何程式碼,設定的時間周期就會定期執行,沒有設定時間的只會執行一次 DoWork
需注意的是第一次註冊 BackgroundService 時就會執行一次程式碼
實作了 UnitOfWork Pattern
基本上跟一般在使用的 DBContext 一樣,只是 DbSet 的 class 必需繼承 BaseEntity, DbQuery 的 class 必需繼承 QueryEntity, 這樣子才可以被 UnitOfWork 和 Repository 使用
public class AppDbContext : DbContext
{
public AppDbContext(DbContextOptions<AppDbContext> options)
: base(options)
{
}
public DbSet<Person> Person { get; set; }
public DbQuery<PersonView> PersonView { get; set; }
}
public class PersonView : QueryEntity
{
public int Id { get; set; }
public string Name { get; set; }
}
public class Person : BaseEntity
{
public int Id { get; set; }
public string Name { get; set; }
}建立 UnitOfWork interface 並且實作 IBaymaxUnitOfWork 然後把自己實作的 DbContext 當泛型參數傳入
public interface IAppUnitOfWork : IBaymaxUnitOfWork<AppDbContext>
{
}建立 UnitOfWork class 並且繼承 BaymaxUnitOfWork 和實作自己的 IUnitOfWork, 並把自己實作的 DbContext 當泛型參數傳入 BaymaxUnitOfWork 和 constructor 注入
public class AppUnitOfWork : BaymaxUnitOfWork<AppDbContext>, IAppUnitOfWork
{
public AppUnitOfWork(AppDbContext context)
: base(context)
{
}
}在 service 註冊自己的 UnitOfWork 為 Scoped (強烈建議註冊為 Scoped)
public void ConfigureServices(IServiceCollection services)
{
services.AddScoped<IAppUnitOfWork, AppUnitOfWork>()
}注入 UnitOfWork
public class IndexController : Controller
{
public IndexController(IAppUnitOfWork unitOfWork){ ... }
} 使用 DbContext 就可以拿到 DbContext 的實體
var db = unitOfWork.DbContext; 使用 GetRepository 並傳入 DbSet 的 class 當泛型參數就可以拿到 IBaymaxRepository<>
var repo = unitOfWork.GetRepository<Person>();使用 GetViewRepository 並傳入 DbQuery 的 class 當泛型參數就可以拿到 IBaymaxQueryRepository<>
var repoView = unitOfWork.GetViewRepository<PersonView>();需注意如果 DbSet 和 DbQuery 的 class 沒有繼承相對應的 class 在裡會報錯
原本 DBContext 的 SaveChange,有同步和非同步的方法
unitOfWork.Commit();
unitOfWork.CommitAsync();UnitOfWork 可以直接執行原始 SQL 語句,可以使用字串內插的方式或是使用 Parameter 的方式傳參數
unitOfWork.ExecuteSqlCommand($"delete from Phone where id = {1}");
unitOfWork.ExecuteSqlCommand("delete from Phone where id = @id", new SqlParameter("id", 1));可以在 Commit 之前針對 Insert 和 Update 的 Entity 作 Validation, 必需在 Service 註冊 AddEntityValidation,然後在裡面寫 Validation 的檢查條件, 注意 Func 的傳入參數為 object,傳出為 ValidationResult
一個 Entity 的型別必須要加一次 AddEntityValidation
public void ConfigureServices(IServiceCollection services)
{
services.AddEntityValidation<Person>(o => {
var p = o as Person;
if (p.Name.Length < 5)
{
return new ValidationResult("Name Error", new[]
{
"Name"
});
}
return ValidationResult.Success;
});
}之後在 Commit 的時候就會自動針對註冊的 Entity 作檢查,有問題的話會 throw EntityValidationException, 裡面的 Exception,會有所有 Entity 的 ValidationException
try
{
unitOfWork.Commit();
}
catch (EntityValidationException ex)
{
List<ValidationException> validationExceptions = ex.Exceptions;
}實作了 Repository Pattern,並且封裝了一些對 Entity 的操作,需搭配上述的 UnitOfWork 使用
有兩個多載,主要的不同是返回物件是不是同一個 Entity,傳入參數如下 (Async 方法使用相同)
- Expression<Func<TEntity, TResult>> selector (兩個多載主要差在這個參數)
- Expression<Func<TEntity, bool>> predicate = null
- Func<IQueryable, IOrderedQueryable> orderBy = null
- Func<IQueryable, IIncludableQueryable<TEntity, object>> include = null
- bool disableTracking = true
基本上所有的參數都有預設值 (selector 除外),建議使用具名引數的方式來呼叫
repo.GetFirstOrDefault(selector: a => a.Name,
predicate: a => a.Id == 2,
orderBy: a => a.OrderBy(b => b.Id),
include: a => a.Include(b => b.Phones),
disableTracking: true);
repo.GetFirstOrDefault(predicate: a => a.Id == 1,
orderBy: a => a.OrderBy(b => b.Id),
include: a => a.Include(b => b.Phones),
disableTracking: true);有兩個多載,使用方法同 GetFirstOrDefault,返回型態為 IQueryable
repo.GetAll(selector: a => a.Name,
predicate: a => a.Id > 1,
orderBy: a => a.OrderBy(b => b.Id),
include: a => a.Include(b => b.Phones),
disableTracking: true);
repo.GetAll(predicate: a => a.Id == 1,
orderBy: a => a.OrderBy(b => b.Id),
include: a => a.Include(b => b.Phones),
disableTracking: true);有兩個多載,使用方法同 GetFirstOrDefault,傳入參數多了 PageIndex 和 PageSize (Async 方法使用相同) PageIndex 從 0 開始,預設 PageSize 為 20
repo.GetPagedList(selector: a => a.Name,
predicate: a => a.Id > 1,
orderBy: a => a.OrderBy(b => b.Id),
include: a => a.Include(b => b.Phones),
pageIndex = 0,
pageSize = 10,
disableTracking: true);
repo.GetPagedList(predicate: a => a.Id > 1,
orderBy: a => a.OrderBy(b => b.Id),
include: a => a.Include(b => b.Phones),
pageIndex = 0,
pageSize = 10,
disableTracking: true);返回型態為 IPagedList,資料在 Items 裡面
public interface IPagedList<T>
{
int IndexFrom { get; }
int PageIndex { get; }
int PageSize { get; }
int TotalCount { get; }
int TotalPages { get; }
IList<T> Items { get; }
bool HasPreviousPage { get; }
bool HasNextPage { get; }
}傳入參數為 Key (Async 方法使用相同)
repo.Find(1);
repo.Find(1, "key");可以傳入 Predicate,取得數量
repo.Count();
repo.Count(a => a.Id > 1);可以傳入 Predicate,取得是否有資料
repo.Any();
repo.Any(a => a.Id > 1);執行原始 SQL 語句,有兩個多載,可以使用字串內插的方式或是使用 Parameter 的方式傳參數
repo.FromSql($"select * from Person where id = {1}");
repo.FromSql("select * from Person where id = @id", new SqlParameter("id", 1));有三個多載,可以傳入單一 Entity、多筆 Entity 或是一個集合 (Async 方法使用相同)
repo.Insert(new Person { Id = 1, Name = "a" });
repo.Insert(new Person { Id = 2, Name = "b" }, new Person { Id = 3, Name = "c" });
repo.Insert(new List<Person>
{
new Person { Id = 4, Name = "d" },
new Person { Id = 5, Name = "e" }
});有三個多載,可以傳入單一 Entity、多筆 Entity 或是一個集合
var persons = repo.GetAll();
persons[0].Name = "123";
persons[1].Name = "456";
repo.Update(persons[0]);
repo.Update(persons[0], person[1]);
repo.Insert(persons);有四個多載,可以傳入 Entity 的 Key、單一 Entity、多筆 Entity 或是一個集合
var persons = repo.GetAll();
persons[0].Name = "123";
persons[1].Name = "456";
repo.Delete(1);
repo.Delete(persons[0]);
repo.Delete(persons[0], person[1]);
repo.Delete(persons);基本上和 Repository 一樣,只是給 View 使用
有兩個多載,使用方法同 Repository,只是少了 include 的參數
repo.GetAll(selector: a => a.Name,
predicate: a => a.Id > 1,
orderBy: a => a.OrderBy(b => b.Id),
disableTracking: true);
repo.GetAll(predicate: a => a.Id == 1,
orderBy: a => a.OrderBy(b => b.Id),
disableTracking: true);執行原始 SQL 語句,有兩個多載,可以使用字串內插的方式或是使用 Parameter 的方式傳參數,使用方法同 Repository
repo.FromSql($"select * from PersonView where id = {1}");
repo.FromSql("select * from PersonView where id = @id", new SqlParameter("id", 1));Enum Object
建立一個 Enum 物件去繼承 Enumeration,並傳入建立的物件當泛型參數
public class EnumTest : Enumeration<EnumTest>
{
}建立 private constructor,並傳入 value 和 displayName 兩個參數
public class EnumTest : Enumeration<EnumTest>
{
private EnumTest(int value, string displayName)
: base(value, displayName)
{
}
}建立 static field 當成 Enum 的內容,Value 就是 Enum 的值,DisplayName 就是 Enum 的名稱
public class EnumTest : Enumeration<EnumTest>
{
public static readonly EnumTest A = new EnumTest(1, "A");
public static readonly EnumTest B = new EnumTest(2, "B");
private EnumTest(int value, string displayName)
: base(value, displayName)
{
}
}跟使用一般 Enum 一樣可以直接用物件點出
var a = EnumTest.A;
var b = EnumTest.B;如果要拿到名稱或是值的話,可以使用 DisplayName 和 Value,ToString 也可以直接拿到 DisplayName
var e = EnumTest.A;
e.Value; // 1
e.DisplayName; // "A"
e.ToString(); // "A"也可以使用 GetAll 拿到全部 Enum Object 的內容
EnumTest.GetAll(); // IEnumerable<EnumTest>可以透過名稱或是值來轉換成 Enum Object
EnumTest.FromValue(1); // EnumTest.A
EnumTest.FromDisplayName("A"); // EnumTest.A可以使用 Equals 或是 CompareTo 來比較是否相等
EnumTest.A.Equals(EnumTest.B) // false
EnumTest.A.CompareTo(EnumTest.A) // true如果有使用 Json.NET 來 Convert Enum Object 的話,需要在屬性加上 JsonConverter 的 attribute,
傳入的參數為 typeof(EnumerationJsonCovert),這樣子 SerializeObject 或是 DeserializeObject 的內容才會正確
public class Test
{
public int Id { get; set; }
[JsonConverter(typeof(EnumerationJsonCovert))]
public EnumTest EnumTest { get; set; }
}可以讓你容昜的建立 Test Server 和 InMemoryDB 作測試
建立一個 class 去實作 IClassFixture,並傳入 ApplicationFactory 當泛型參數, 而 ApplicationFactory 的泛型參數是你 Web App 的 Startup 和 DBContext 類別, constructor 需要注入 ApplicationFactory 並且建立一個 field 給外部 test class 使用
在這裡是使用 xUnit test framework
建立出來的測試 client EnvironmentName 為 Test,可以使用 IHostingEnvironmentExtensions 的 IsTest 來判斷
public class TestBase : IClassFixture<ApplicationFactory<Startup, AppDbContext>>
{
protected readonly ApplicationFactory<Startup, AppDbContext> Factory;
public TestBase(ApplicationFactory<Startup, AppDbContext> factory)
{
Factory = factory;
}
}ApplicationFactory 裡面有一個 InitDataEvent 可以使用, 可以在 TestBase 的 constructor 註冊事件塞初始資料
public TestBase(ApplicationFactory<Startup, AppDbContext> factory)
{
Factory.InitDataEvent += OnInitDataEvent;
}
private void OnInitDataEvent(object sender, InitDataEventArgs<AppDbContext> e)
{
var dbcontext = e.DbContext;
// insert data here
dbcontext.SaveChanges();
}test class 去繼承前面建立的 TestBase,並在 constructor 注入 ApplicationFactory
public class WebTests : TestBase
{
public WebTests(ApplicationFactory<Startup, AppDbContext> factory) : base(factory)
{
}
}在 Factory 裡面有 HttpClient 可以發送 http request 到網站,HttpClient 有基本的 Http Method Extension 可以讓你更方便的使用, 需要注意的是 url 是不包含 host
- PostHttpResult
- GetHttpResult
- PutHttpResult
- DeleteHttpResult
更多的使用方式請參考 測試
[Fact]
public void GetAll()
{
var result = Factory.HttpClient.GetHttpResult<List<Info>>("/api/values");
}在 Factory 裡面有 DbOperator method 可以使用,傳入參數為 DbContext 的 Action
在測試使用的是 InMemoryDatabase,所以有些 DB 的操作是不支援的
Factory.DbOperator(db =>
{
db.Info.Add(new Info { Id = 1, Name = "Test123" });
db.Info.Add(new Info { Id = 2, Name = "Test456" });
db.SaveChanges();
});使用 Factory 的 Operator method,傳入參數為泛型類別的 Action, 例如有一個 RedisService
Factor.Operator<RedisService>(redis =>
{
// do something here
});可以讓你使用 Fluent 的方式測試 controller 的 action
取得 controller 的實體之後使用擴充方法 .AsTester(),然後在用 Action 方法,
傳入 Func 呼叫 controller 的 action 就可以拿到 action 的執行結果
public class HomeController : Controller
{
public IActionResult Index()
{
return View();
}
}
public class Test
{
[Fact]
public void Test()
{
var result = new HomeController()
.AsTester()
.Action(c => c.Index());
}
}取得結果後使用 ShouldBeXXXResult 就可以驗證 ActionResult 的型別,
XXX 取決 Action 真實的返回結果,以下面的程式碼來說就是返回 ViewResult
[Fact]
public void Test()
{
new HomeController()
.AsTester()
.Action(c => c.Index())
.ShouldBeViewResult();
}支援非同步 ActionResult
可以用 WithXXX 來驗證 ActionResult 的公開屬性,
XXX 取決 ActionResult 的型別,以下面的程式碼來說 ViewResult 可以驗證
Model (WithModel)、ViewBag (WithViewBag)、ViewData (WithViewData) ...
[Fact]
public void Test()
{
new HomeController()
.AsTester()
.Action(c => c.Index())
.ShouldBeViewResult();
.WithModel(new { id = 1 });
.WithViewBag("viewbag", 123)
.WithViewData("viewdata", 456)
}相關的用法可以參考 測試
- AcceptedResult
- CreatedAtActionResult
- CreatedAtRoutedResult
- JsonResult
- RedirectResult
- RedirectToActionResult
- RedirectToRouteResult
- LocalRedirectResult
- PartialViewResult
- ViewResult
- StatusCodeResult
- ContentResult