Skip to content
This repository has been archived by the owner. It is now read-only.
No description, website, or topics provided.
C# HTML
Branch: master
Clone or download
Fetching latest commit…
Cannot retrieve the latest commit at this time.
Permalink
Type Name Latest commit message Commit time
Failed to load latest commit information.
Src
Tests
.gitattributes
.gitignore
Baymax.sln
README.md
appveyor.yml

README.md

Baymax - ASP.NET Core MVC Utility Framework

Build status

codecov

sonarcloud


Baymax Nuget

Baymax.Tester Nuget



Baymax

Config

自動對應 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" }
    ]
}

Log

註冊 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

建立自定義的 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

自動解析註冊結尾是 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();
}

BackgroundService

可定期 (排程) 執行程式

實作 IBackgroundProcessService

把需要定期執行的程式碼寫在 DoWork,把停止時需要取消的程式碼寫在 StopWork

有時 StopWork 不一定會有程式碼

public class TestBackgroundService : IBackgroundProcessService
{
    public TestBackgroundService()
    {
    }
    
    public void DoWork()
    {
    }

    public void StopWork()
    {
    }
}

Config

在 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

實作了 UnitOfWork Pattern

建立 DBContext

基本上跟一般在使用的 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

建立 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>()
}

取得 Repository

注入 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 在裡會報錯

Commit

原本 DBContext 的 SaveChange,有同步和非同步的方法

unitOfWork.Commit();
unitOfWork.CommitAsync();

ExecuteSqlCommand

UnitOfWork 可以直接執行原始 SQL 語句,可以使用字串內插的方式或是使用 Parameter 的方式傳參數

unitOfWork.ExecuteSqlCommand($"delete from Phone where id = {1}");

unitOfWork.ExecuteSqlCommand("delete from Phone where id = @id", new SqlParameter("id", 1));

Entity Validation

可以在 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

實作了 Repository Pattern,並且封裝了一些對 Entity 的操作,需搭配上述的 UnitOfWork 使用

GetFirstOrDefault & GetFirstOrDefaultAsync

有兩個多載,主要的不同是返回物件是不是同一個 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);

GetAll

有兩個多載,使用方法同 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);

GetPagedList & GetPagedListAsync

有兩個多載,使用方法同 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; }
}

Find

傳入參數為 Key (Async 方法使用相同)

repo.Find(1);

repo.Find(1, "key");

Count

可以傳入 Predicate,取得數量

repo.Count();

repo.Count(a => a.Id > 1);

Any

可以傳入 Predicate,取得是否有資料

repo.Any();

repo.Any(a => a.Id > 1);

FromSql

執行原始 SQL 語句,有兩個多載,可以使用字串內插的方式或是使用 Parameter 的方式傳參數

repo.FromSql($"select * from Person where id = {1}");

repo.FromSql("select * from Person where id = @id", new SqlParameter("id", 1));

Insert & InsertAsync

有三個多載,可以傳入單一 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" }
});

Update

有三個多載,可以傳入單一 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);

Delete

有四個多載,可以傳入 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);

ViewRepository

基本上和 Repository 一樣,只是給 View 使用

GetAll

有兩個多載,使用方法同 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);

FromSql

執行原始 SQL 語句,有兩個多載,可以使用字串內插的方式或是使用 Parameter 的方式傳參數,使用方法同 Repository

repo.FromSql($"select * from PersonView where id = {1}");

repo.FromSql("select * from PersonView where id = @id", new SqlParameter("id", 1));

Util

Enumeration

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

JsonConvert

如果有使用 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; }
}


Baymax.Tester

Integration Test

可以讓你容昜的建立 Test Server 和 InMemoryDB 作測試

建立 TestBase

建立一個 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;
    }
}

DB Init Data

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();
}

使用 HttpClient

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");
}

使用 DBContext

在 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
});

Unit Test

可以讓你使用 Fluent 的方式測試 controller 的 action

取得 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());
    }
}

驗證 ActionResult

取得結果後使用 ShouldBeXXXResult 就可以驗證 ActionResult 的型別, XXX 取決 Action 真實的返回結果,以下面的程式碼來說就是返回 ViewResult

[Fact]
public void Test()
{
    new HomeController()
        .AsTester()
        .Action(c => c.Index())
        .ShouldBeViewResult();
}

支援非同步 ActionResult

驗證 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) 
}

目前支援的 ActionResult

相關的用法可以參考 測試

  • AcceptedResult
  • CreatedAtActionResult
  • CreatedAtRoutedResult
  • JsonResult
  • RedirectResult
  • RedirectToActionResult
  • RedirectToRouteResult
  • LocalRedirectResult
  • PartialViewResult
  • ViewResult
  • StatusCodeResult
  • ContentResult
You can’t perform that action at this time.