1. 程式人生 > 其它 >ASP.NET Core與RESTful API 開發實戰(三)

ASP.NET Core與RESTful API 開發實戰(三)

目錄

ASP.NET Core與RESTful API 開發實戰(三)

資源操作

大體操作流程:

  1. 建立倉儲介面

  2. 建立基於介面的具體倉儲實現

  3. 新增服務,Startup類的ConfigureServices方法中將它們新增到依賴注入容器中

  4. 建立Controller,建構函式注入得到倉儲介面,這裡使用建構函式注入。新增對應的CRUD

建立專案

建立一個ASP.NET Core WebApi專案,1.新增Models資料夾,建立AuthorDto和BookDto類。

public class AuthorDto
{
    public Guid Id { get; set; }
    public string Name { get; set; }
    public int Age { get; set; }
    public string Email { get; set; }
}

public class BookDto
{
    public Guid Id { get; set; }
    public string Title { get; set; }
    public string Description { get; set; }
    public int Pages { get; set; }
    public Guid AuthorId { get; set; }
}

使用記憶體資料

建立記憶體資料來源

建立Data資料夾,新增LibraryMockData類,該類包括一個建構函式、兩個集合屬性及一個靜態屬性、其中兩個集合屬性分別代表AuthorDto和BookDto集合,而靜態屬性Current則返回一個LibraryMockData例項,方便訪問該物件。

public class LibraryMockData
{
    //獲取LibraryMockData例項
    public static LibraryMockData Current { get; } = new LibraryMockData();

    public List<AuthorDto> Authors { get; set; }
    public List<BookDto> Books { get; set; }

    public LibraryMockData()
    {
        Authors = new List<AuthorDto>
        {
            new AuthorDto
            {
                Id = new Guid("9af7f46a-ea52-4aa3-b8c3-9fd484c2af12"),
                Age = 46,
                Email = "[email protected]",
                Name = "黃澤濤"
            },
            new AuthorDto
            {
                Id = new Guid("e0a953c3ee6040eaa9fae2b667060e09"),
                Name = "李策潤",
                Age = 16,
                Email = "[email protected]"
            }
        };

        Books = new List<BookDto>
        {
            new BookDto
            {
                Id = new Guid("9af7f46a-ea52-4aa3-b8c3-9fd484c2af12"),
                Title ="王蒙講孔孟老莊",
                Description="這是王蒙先生講解論語道德經等國學知識的書籍",
                Pages=12,
                AuthorId=new("9af7f46a-ea52-4aa3-b8c3-9fd484c2af12")
            },
            new BookDto
            {
                Id = new Guid("734fd453-a4f8-4c5d-9c98-3fe2d7079760"),
                Title ="終身成長",
                Description="終身學習成長的一本書籍",
                Pages = 244,
                AuthorId = new Guid("e0a953c3ee6040eaa9fae2b667060e09")
            },
            new BookDto
            {
                Id = new Guid("ade24d16-db0f-40af-8794-1e08e2040df3"),
                Title ="弟子規",
                Description ="一本國學古書",
                Pages = 32,
                AuthorId = new Guid("9af7f46a-ea52-4aa3-b8c3-9fd484c2af12")
            },
            new BookDto
            {
                Id =new Guid("9af7f46a-ea52-4aa3-b8c3-9fd484c2af11"),
                Title ="山海經",
                Description = "古人的奇物志",
                Pages = 90,
                AuthorId = new Guid("9af7f46a-ea52-4aa3-b8c3-9fd484c2af12")
            },
        };
    }
}

倉儲模式

倉儲模式作為領域驅動設計(DDD)的一部分,在系統設計中的使用非常廣泛。它主要用於解除業務邏輯與資料訪問層之間的耦合,使業務邏輯在儲存、訪問資料庫時無須關心資料的來源及儲存方式。

實現倉儲模式的方法有多種:

  1. 其中一種簡單的方法是對每一個與資料庫互動的業務物件建立一個倉儲介面及其實現。這樣做的好處是,對一種資料物件可以根據其實際情況來定義介面的成員,比如有些物件只需要讀,那麼在其倉儲介面中就不需要定義Update、Insert等成員。
  2. 還有一種也是比較常見的,就是建立一個通用倉儲介面,然後所有倉儲介面都繼承自這個介面。

記憶體資料測試先使用第一種倉儲模式:分別定義對於AuthorDto和BookDto的相關操作方法,目前所有方法都是為了獲取資料。後面對資料的其他操作(如新增、更新和刪除等)都會新增進來。

public interface IAuthorRepository
{
    IEnumerable<AuthorDto> GetAuthors();
    AuthorDto GetAuthor(Guid authorId);
    bool IsAuthorExists(Guid authorId);
}

public interface IBookRepository
{
    IEnumerable<BookDto> GetBooksForAuthor(Guid authorId);
    BookDto GetBookForAuthor(Guid authorId, Guid bookId);
}

建立上述兩個介面的具體倉儲實現:

public class AuthorMockRepository : IAuthorRepository
{
    public AuthorDto GetAuthor(Guid authorId)
    {
        var author = LibraryMockData.Current.Authors.FirstOrDefault(au => au.Id == authorId);
        return author;
    }

    public IEnumerable<AuthorDto> GetAuthors()
    {
        return LibraryMockData.Current.Authors;
    }

    public bool IsAuthorExists(Guid authorId)
    {
        return LibraryMockData.Current.Authors.Any(au => au.Id == authorId);
    }
}

public class BookMockRepository : IBookRepository
{
    public BookDto GetBookForAuthor(Guid authorId, Guid bookId)
    {
        return LibraryMockData.Current.Books.FirstOrDefault(b => b.AuthorId == authorId && b.Id == bookId);
    }

    public IEnumerable<BookDto> GetBooksForAuthor(Guid authorId)
    {
        return LibraryMockData.Current.Books.Where(b => b.AuthorId == authorId).ToList();
    }
}

為了在程式中使用上述兩個倉儲介面,還需要再Startup類的ConfigureServices方法中將它們新增到依賴注入容器中:

public void ConfigureServices(IServiceCollection services)
{
    ···
    services.AddScoped<IAuthorRepository, AuthorMockRepository>();
    services.AddScoped<IBookRepository, BookMockRepository>();
}

建立Controller

AuthorController類繼承自ControllerBase類,並且標有[Route]特性和[ApiController]特性,其中,

  1. [Route]特性設定類預設的路由值api/[Controller].由於WebApi作為向外公開的介面,其路由名稱應固定,為了防止由於類名重構後引起API路由發生變化,可以將這裡的預設路由值改為固定值。
  2. [ApiController]繼承自ControllerAttribute類,並且包括了一些在開發WebApi應用時極為方便的功能,如自動模型驗證以及Action引數來源推斷。因此ASP.NET Core2.1之前的Controller中,對於模型狀態判斷的程式碼可以刪除。

建構函式注入,獲取之前定義的倉儲介面,增加對應的CRUD。

//[Route("api/[Controller]")]		由於WebApi作為向外公開的介面,其路由名稱應固定,為了防止由於類名重構後引起API路由發生變化,可以將這裡的預設路由值改為固定值。
[Route("api/authors")]
[ApiController]
public class AuthorController : ControllerBase
{
    //建構函式注入 獲得倉儲介面
    public IAuthorRepository AuthorRepository { get; set; }
    public AuthorController(IAuthorRepository authorMockRepository)
    {        
        AuthorRepository = authorMockRepository;
    }
 	
    //獲取集合 所有作者的資訊 
    [HttpGet]
    public ActionResult<List<AuthorDto>> GetAuthors()
    {
        return AuthorRepository.GetAuthors().ToList();
    }

    //獲取單個資源 REST約束中規定每個資源應由一個URL代表,所以對於單個URL應使用api/authors/{authorId}
    //[HttpGet]設定了路有模板authorId,用於為當前Action提供引數。
    [HttpGet("{authorId}", Name = nameof(GetAuthor))]
    public ActionResult<AuthorDto> GetAuthor(Guid authorId)
    {
        var author = AuthorRepository.GetAuthor(authorId);
        if (author == null)
        {
            return NotFound();
        }
        else
        {
            return author;
        }
    }

}

使用EF Core 重構

使用EF Core的CodeFist重構專案,新增測試資料

  1. 建立實體類,新增資料夾Entities,建立Author類和Book類,[ForeignKey]特性,用於指明外來鍵的屬性名。

  2. 建立資料庫上下文的DbContext類,繼承DbContext類,DbSet類代表資料表

  3. 配置資料庫連線字串:appsettings.json中配置資料庫連線

  4. 新增服務,在Startup類中通過IServiceCollection介面的AddDbContext擴充套件方法將他作為服務新增到依賴注入容器中。而要呼叫這個方法,LibraryDbContext必須要有一個帶有DbContextOptions型別引數的建構函式。

  5. 新增遷移與建立資料庫:Ctrl+` 程式包管理控制檯命令列操作

  6. 首次遷移:Add-Migration InitialCreation

  7. 將遷移應用到資料庫中:Update-Database

  8. 新增資料:EF Core2.1增加了用於新增測試資料的API,ModelBuilder類的方法Entity()會返回一個EntityTypeBuilder物件,該物件提供了HasData方法,使用它可以將一個或多個實體物件新增到資料庫中。為了保證類的簡介清晰,可以為ModelBuilder建立擴充套件方法,在擴充套件方法內新增資料。

  9. 新增ModelBuilder擴充套件方法

  10. 將資料新增到資料庫,建立一個遷移:Add-Migration SeedData

  11. 將資料更新到資料庫中:Update-Database

  12. 刪除測試資料,刪除或註釋呼叫HasData方法程式碼,新增一個遷移:Add-Migration RemoveSeedeData

    //1.註釋掉HasData方法程式碼
    //modelBuilder.SeedData();
    //2.新增遷移
    Add-Migration RemoveSeedeData
    Update-Database
    

使用EF Core重構倉儲類

  1. 建立通用倉儲介面

  2. Services目錄下建立一個介面IRepositoryBase,併為它新增CRUD方法。

    1. 建立、更新、刪除三個同步方法
    2. 獲取、條件獲取、儲存三個非同步方法
  3. 新增介面IRepositoryBase2<T, TId>,兩個方法:

    1. 根據指定的實體Id獲取實體
    2. 檢查具有指定Id的實體是否存在
  4. 實現倉儲介面:Services檔案中新增RepositoryBase類,繼承兩個通用倉儲介面

  5. 建立每一個實體型別的倉儲介面:IAuthorRepository、IBookRepository、AuthorRrpository、BookRepository

並使其所建立的介面及實現分別繼承自IRepositoryBase、IRepositoryBase2。之所以為每個實體再建立自己的倉儲介面與實現類,是因為它們除了可以使用父介面和父類提供的方法以外,還可以根據自身的需要再單獨定義方法。

  1. 建立倉儲包裝器:IRepositoryWrapper介面及其實現。包裝器提供了對所有倉儲介面的統一訪問方式,避免單獨訪問每個倉儲介面,對倉儲的操作都是通過呼叫包裝器所提供的成員他們來完成的。IRepositoryWrapper介面中的兩個屬性分別代表IBookRepository、IAuthorRepository介面。

  2. 新增服務:Startup類ConfigureServices()方法中:services.AddScoped<IRepositoryWrapper, RepositoryWrapper>();

重構Controller和Action

設定物件對映

  1. 使用物件對映庫AutoMapper:NuGet搜尋:AutoMapper
  2. 新增AutoMapper服務,Startup類中的ConfigureServices().新增到依賴注入容器中。
  3. 建立對映規則:為了使AutoMapper能夠正確地執行物件對映,我們還需要建立一個Profile類的派生類,用以說明要對映的物件以及對映規則。Profile類位於AutoMapper名稱空間下,它是AutoMapper中用於配置物件對映關係的類。
  4. 建立Helpers資料夾,新增LibraryMappingProfile類,新增無參建構函式,建構函式中使用基類Profile的CreateMap方法來建立物件對映關係。

重構Controller

  1. Controller建構函式中將IRepositoryWrapper介面和IMapper介面注入進來,前者用於操作倉儲類,後來用於處理物件之間的對映關係。

  2. 新增過濾器,把MVC請求過程中一些特性階段(如執行Action)前後重複執行的一些程式碼提取出來。

  3. 新增Filters資料夾,新增CheckAuthorExistFilterAttribute,使它繼承ActionFilterAttribute類,並重寫基類的OnActionExecutionAsync方法。

  4. 新增服務:在Startup類的ConfigureServices方法中將CheckAuthorExistFilterAttribute類新增到容器中。

    services.AddScoped<CheckAuthorExistFilterAttribute>();
    
  5. 使用過濾器:在Controller中通過[ServiceFilter]特性使用CheckAuthorExistFilterAttribute了。

    [ServiceFilter(typeof(CheckAuthorExistFilterAttribute))]
    public class BookController : ControllerBase
    {
    }
    
  6. 新增對應的CRUD

建立實體類

建立實體類、資料庫上下文類

public class Author
{
    [Key, DatabaseGenerated(DatabaseGeneratedOption.Identity)]
    public Guid Id { get; set; }

    [Required]
    [MaxLength(20)]
    public string Name { get; set; }

    [Required]
    public DateTimeOffset BirthData { get; set; }

    [Required]
    [MaxLength(40)]
    public string BirthPlace { get; set; }

    [Required]
    [EmailAddress]
    public string Email { get; set; }

    public ICollection<Book> Books { get; set; } = new List<Book>();
}

public class Book
{
    [Key]
    public Guid Id { get; set; }

    [Required]
    [MaxLength(100)]
    public string Title { get; set; }

    [MaxLength(500)]
    public string Description { get; set; }

    public int Pages { get; set; }

    [ForeignKey("AuthorId")]
    public Author Author { get; set; }

    public Guid AuthorId { get; set; }
}

public class LibraryDbContext : DbContext
{
    public DbSet<Author> Authors { get; set; }
    public DbSet<Book> Books { get; set; }

    public LibraryDbContext(DbContextOptions<LibraryDbContext> options) : base(options)
    {
    }

    protected override void OnModelCreating(ModelBuilder modelBuilder)
    {
        base.OnModelCreating(modelBuilder);
        modelBuilder.SeedData();
    }
}

新增服務,在呼叫AddDbContext方法時通過DbContextOptionsBuilder(option變數的型別)物件配置資料庫。使用UseSqlServer方法來指定使用SQL Server資料庫,同時通過方法引數指定了資料庫連線字串。為了避免硬編碼資料庫連線字串,應將它放到配置檔案中,在appsettings.json檔案中的一級節點增加如下配置內容。

新增服務

services.AddDbContext<LibraryDbContext>(option => option.UseSqlServer(Configuration.GetConnectionString("DefaultConnection")));

配置連線字串

"ConnectionStrings": {
    "DefaultConnection": "Data Source = (local);Initial Catalog = Library;Integrated Security=SSPI"
},

新增測試資料:EF Core2.1增加了用於新增測試資料的API,ModelBuilder類的方法Entity()會返回一個EntityTypeBuilder物件,該物件提供了HasData方法,使用它可以將一個或多個實體物件新增到資料庫中。為了保證類的簡介清晰,可以為ModelBuilder建立擴充套件方法,在擴充套件方法內新增資料。

EntityTypeBuilder類還提供了Fluent API,這些API包括了幾類不同功能的方法,它們能夠設定欄位的屬性(如長度、預設值、列名、是否必需等)、主鍵、表與表之間的關係等。

public class LibraryDbContext : DbContext
{
    //ModelBuilder類的方法Entity<T>()會返回一個EntityTypeBuilder<T>物件,該物件提供了HasData方法,使用它可以將一個或多個實體物件新增到資料庫中
    protected override void OnModelCreating(ModelBuilder modelBuilder)
    {
        base.OnModelCreating(modelBuilder);
        modelBuilder.SeedData();
    }
}

//ModelBuilder的擴充套件方法
public static class ModelBuilderExtension
    {
        public static void SeedData(this ModelBuilder modelBuilder)
        {
            modelBuilder.Entity<Author>().HasData();
            modelBuilder.Entity<Book>().HasData();
        }
    }

刪除測試資料,刪除或註釋呼叫HasData方法程式碼,新增一個遷移:Add-Migration RemoveSeedeData

//1.註釋掉HasData方法程式碼
//modelBuilder.SeedData();
//2.新增遷移
Add-Migration RemoveSeedeData
Update-Database

重構倉儲類

建立通用的倉儲介面:

  1. Services目錄下建立一個介面IRepositoryBase,併為它新增CRUD方法。
    1. 建立、更新、刪除三個同步方法
    2. 獲取、條件獲取、儲存三個非同步方法
  2. 新增介面IRepositoryBase2<T, TId>,兩個方法:
    1. 根據指定的實體Id獲取實體
    2. 檢查具有指定Id的實體是否存在
public interface IRepositoryBase<T>
{
    Task<IEnumerable<T>> GetAllAsync();
    Task<IEnumerable<T>> GetByConditionAsync(Expression<Func<T, bool>> expression);
    void Create(T entity);
    void Update(T entity);
    void Delete(T entity);
    Task<bool> SaveAsync();
}

public interface IRepositoryBase2<T, TId>
{
    Task<T> GetByIdAsync(TId id);

    Task<bool> IsExistAsync(TId id);
}

實現倉儲介面:Services檔案中新增RepositoryBase類,繼承兩個通用倉儲介面:

在RepositoryBase類中包括一個帶有DbContext型別引數的建構函式,並有一個DbContext屬性用於接受傳入的引數。而在所有對介面定義方法的視線中,除了SaveAsync方法,其他方法均呼叫了DbContext.Set()的相應方法,以完成對應的操作。DbContext.Set()方法返回DbSet型別,它表示實體集合。

EF Core對於查詢的執行採用延遲執行的方式。當在程式中使用LINQ對資料庫進行查詢時,此時查詢並未實際執行,而是僅在相應的變數中儲存了查詢命令,只有遇到了實際需要結果的操作時,查詢才會執行,並返回給程式中定義的變數,這些操作包括以下幾種型別:

  • 對結果使用for 或foreach迴圈
  • 使用了ToList()、ToArray()、ToDictionary()等方法
  • 使用了Single()、Count()、Average()、First()和Max()等方法

使用延遲執行的好處是,EF Core在得到最終結果之前,能夠對集合進行篩選和排序,但並不會執行實際的操作,僅在遇到上面那些情況時才會執行。這要比獲取到所有結果之後再進行篩選和排序更高效。

public class RepositoryBase<T, TId> : IRepositoryBase<T>, IRepositoryBase2<T, TId> where T : class
{
    public DbContext DbContext { get; set; }

    public RepositoryBase(DbContext dbContext)
    {
        DbContext = dbContext;
    }

    public void Create(T entity)
    {
        DbContext.Set<T>().Add(entity);
    }

    public void Delete(T entity)
    {
        DbContext.Set<T>().Remove(entity);
    }

    public Task<IEnumerable<T>> GetAllAsync()
    {
        return Task.FromResult(DbContext.Set<T>().AsEnumerable());
    }

    public Task<IEnumerable<T>> GetByConditionAsync(Expression<Func<T, bool>> expression)
    {
        return Task.FromResult(DbContext.Set<T>().Where(expression).AsEnumerable());
    }

    public async Task<T> GetByIdAsync(TId id)
    {
        return await DbContext.Set<T>().FindAsync(id);
    }

    public async Task<bool> IsExistAsync(TId id)
    {
        return await DbContext.Set<T>().FindAsync(id) != null;
    }

    public async Task<bool> SaveAsync()
    {
        return await DbContext.SaveChangesAsync() > 0;
    }

    public void Update(T entity)
    {
        DbContext.Set<T>().Update(entity);
    }
}

建立每一個實體型別的倉儲介面並實現,並使其所建立的介面及實現分別繼承自IRepositoryBase、IRepositoryBase2。之所以為每個實體再建立自己的倉儲介面與實現類,是因為它們除了可以使用父介面和父類提供的方法以外,還可以根據自身的需要再單獨定義方法。

public interface IAuthorRepository : IRepositoryBase<Author>, IRepositoryBase2<Author, Guid>
{
}
public interface IBookRepository : IRepositoryBase<Book>, IRepositoryBase2<Book, Guid>
{
}

public class AuthorRepository : RepositoryBase<Author, Guid>, IAuthorRepository
{
    public AuthorRepository(DbContext dbContext) : base(dbContext)
    {
    }
}
public class BookRepository : RepositoryBase<Book, Guid>, IBookRepository
{
    public BookRepository(DbContext dbContext) : base(dbContext)
    {
    }
}

建立倉儲包裝器:IRepositoryWrapper介面及其實現。包裝器提供了對所有倉儲介面的統一訪問方式,避免單獨訪問每個倉儲介面,對倉儲的操作都是通過呼叫包裝器所提供的成員他們來完成的。

IRepositoryWrapper介面中的兩個屬性分別代表IBookRepository、IAuthorRepository介面。

public interface IRepositoryWrapper
{
    IBookRepository Book { get; }
    IAuthorRepository Author { get; }
}

public class RepositoryWrapper : IRepositoryWrapper
{
    private readonly IAuthorRepository _authorRepository = null;
    private readonly IBookRepository _bookRepository = null;

    public RepositoryWrapper(LibraryDbContext libraryDbContext)
    {
        LibraryDbContext = libraryDbContext;
    }

    public IAuthorRepository Author => _authorRepository ?? new AuthorRepository(LibraryDbContext);
    public IBookRepository Book => _bookRepository ?? new BookRepository(LibraryDbContext);
    public LibraryDbContext LibraryDbContext { get; }
}

新增服務:Startup的ConfigureServices中新增服務

services.AddScoped<IRepositoryWrapper, RepositoryWrapper>();

重構Controller和Action

使用AutoMapper

擋在專案中使用實體類以及EF Core時,應用程式將會從資料庫中讀取資料,並由EF Core返回實體物件。然而在Controller中,無論對GET請求返回資源,還是從POST、PUT、PATCH等請求接受正文,所有操作的物件都是DTO。對於實體物件,為了能夠建立一個相應的DTO,需要物件反轉,反之亦然。當實體類與DTO之間的對映屬性較多時,甚至存在更復雜的對映規則,如果不借助於類似對映庫之類的工具,使用手工轉換會很費力,並且極容易出錯。這裡我們選擇使用物件對映庫AutoMapper:

AutoMapper是一個物件對映的庫。在專案中,實體與DTO之間的轉換通常由物件對映庫完成。AutoMapper功能強大,簡單易用。

  1. 使用物件對映庫AutoMapper:NuGet搜尋:AutoMapper
  2. 新增AutoMapper服務,Startup類中的ConfigureServices().新增到依賴注入容器中。
services.AddAutoMapper(typeof(Startup));

為了使AutoMapper能夠正確地執行物件對映,我們還需要建立一個Profile類的派生類,用以說明要對映的物件以及對映規則。Profile類位於AutoMapper名稱空間下,它是AutoMapper中用於配置物件對映關係的類。

建立Helpers資料夾,新增LibraryMappingProfile類,新增無參建構函式,建構函式中使用基類Profile的CreateMap方法來建立物件對映關係。

public class LibraryMappingProfile : Profile
{
    public LibraryMappingProfile()
    {
        CreateMap<Author, AuthorDto>()
            .ForMember(dest => dest.Age, config =>
               config.MapFrom(src => src.BirthData.Year));
        CreateMap<Book, BookDto>();
        CreateMap<AuthorForCreationDto, Author>();
        CreateMap<BookForCreationDto, Book>();
        CreateMap<BookForUpdateDto, Book>();
    }

重構AuthorController

AuthorController建構函式中將IRepositoryWrapper介面和IMapper介面注入進來,前者用於操作倉儲類,後來用於處理物件之間的對映關係。

[Route("api/authors")]
[ApiController]
public class AuthorController : ControllerBase
{
    public IMapper Mapper { get; }
    public IRepositoryWrapper RepositoryWrapper { get; }

    public AuthorController(IMapper mapper, IRepositoryWrapper repositoryWrapper)
    {
        Mapper = mapper;
        RepositoryWrapper = repositoryWrapper;
    }
}

當上述服務注入後,在Controller中的各個方法就可以使用它們。以下是獲取作者列表重構後的程式碼。

  1. 首先由IRepositoryWrapper介面使用相應的倉儲介面從資料庫中獲取資料
  2. 使用IMapper介面的Map方法將實體物件集合轉化為DTO物件集合,並返回。

在RepositoryBase類中使用的延遲執行會在程式執行到“使用AutoMapper進行物件對映”這句程式碼時才實際去執行查詢。

var authorDtoList = Mapper.Map<IEnumerable<AuthorDto>>(authors);

[HttpGet]
public async Task<ActionResult<IEnumerable<AuthorDto>>> GetAuthorsAsync()
{
    var authors = (await RepositoryWrapper.Author.GetAllAsync()).OrderBy(author => author.Name);
    var authorDtoList = Mapper.Map<IEnumerable<AuthorDto>>(authors);
    return authorDtoList.ToList();
}

重構BookController

在BookController中,所有Action操作都是基於一個存在的Author資源,這可見於BookController類路由特性的定義中包含authorId,以及每個Action中都會檢測指定的authorId是否存在。因此在每個Action中,首先都應包含如下邏輯:

  if (!await RepositoryWrapper.Author.IsExistAsync(authorId))
  {
      return NotFound();
  }

然而,若在每個Action中都新增同樣的程式碼,則會造成程式碼多出重複,增加程式碼維護成本,因此可以考慮使用過濾器,在MVC請求過程中一些特定的階段(如執行Action)前後執行一些程式碼。

新增Filters資料夾,新增CheckAuthorExistFilterAttribute,使它繼承ActionFilterAttribute類,並重寫基類的OnActionExecutionAsync方法。

在OnActionExecutionAsync方法中,通過ActionExecutingContext物件的ActionAtguments屬效能夠得到所有將要傳入Action的引數。當得到authorId引數後,使用IAuthorRepository介面的.IsExistAsync方法來驗證是否存在具有指定authorId引數值的實體。而IAuthorRepository介面則是通過建構函式注入進來的IRepositoryWrapper介面的Author屬性得到的。如果檢查結果不存在,則通過設定ActionExecutingContext物件的Result屬性結束本次請求,並返回404 Not Found狀態碼;反之,則繼續完成MVC請求。

public class CheckAuthorExistFilterAttribute : ActionFilterAttribute
{
    public IRepositoryWrapper RepositoryWrapper { get; set; }

    public CheckAuthorExistFilterAttribute(IRepositoryWrapper repositoryWrapper)
    {
        RepositoryWrapper = repositoryWrapper;
    }

    public override async Task OnActionExecutionAsync(ActionExecutingContext context, ActionExecutionDelegate next)
    {
        var authorIdParameter = context.ActionArguments.Single(m => m.Key == "authorId");
        Guid authorId = (Guid)authorIdParameter.Value;
        var isExist = await RepositoryWrapper.Author.IsExistAsync(authorId);
        if (isExist) context.Result = new NotFoundResult();
        await base.OnActionExecutionAsync(context, next);
    }
}

新增服務:在Startup類的ConfigureServices方法中將CheckAuthorExistFilterAttribute類新增到容器中。

services.AddScoped<CheckAuthorExistFilterAttribute>();

使用過濾器:在Controller中通過[ServiceFilter]特性使用CheckAuthorExistFilterAttribute了。

[ServiceFilter(typeof(CheckAuthorExistFilterAttribute))]
public class BookController : ControllerBase
{
}
登峰造極的成就源於自律