測試 ProductAppService 類
ProductAppService
類的GetListAsync
方法寫單元測試代碼(構建自動化測試細節後續再議)。ProductAppService_Tests
類:using Shouldly; using System.Threading.Tasks; using Volo.Abp.Application.Dtos; using Xunit; namespace ProductManagement.Products { public class ProductAppService_Tests : ProductManagementApplicationTestBase { private readonly IProductAppService _productAppService; public ProductAppService_Tests() { _productAppService = GetRequiredService<IProductAppService>(); } /* TODO: Test methods */ } }
該類繼承自ProductManagementApplicationTestBase
,它默認集成 ABP 框架和其他基礎設施庫,這樣我們就可以直接使用內置的測試能力。另外,我們使用方法GetRequiredService
來解決測試代碼中的依賴關係,而不是構造函數注入(這在測試中是不可能的)。
ProductAppService_Tests
類中添加如下代碼:[Fact] public async Task Should_Get_Product_List() { //Act var output = await _productAppService.GetListAsync( new PagedAndSortedResultRequestDto() ); //Assert output.TotalCount.ShouldBe(3); output.Items.ShouldContain( x => x.Name.Contains("Acme Monochrome Laser Printer") ); }
該方法調用該GetListAsync
方法並檢查結果是否正確。如果您打開測試資源管理器窗口(在 Visual Studio 中的查看|測試資源管理器菜單下),您可以看到我們添加的測試方法。測試資源管理器用於顯示和運行解決方案中的測試:
運行測試到檢查它是否按預期工作。如果方法正常工作,將在測試方法名稱的左側看到一個綠色圖標。
自動 API 控制器和 Swagger UI
/swagger
URL,如圖所示:我們沒有創建ProductController接口。這個接口是如何出現的?
動態 JavaScript 代理
productManagement.products.product.getList({}).then(function(result) {
console.log(result);
});
執行此代碼後,將向服務器發出請求,並將返回結果記錄在Console選項卡中,如圖所示:
getList
的,您可以定位到/Abp/ServiceProxyScript
地址,查看由 ABP 框架動態創建的 JavaScript 代理函數。產品列表
Index.cshtml
。下圖顯示了我們添加的頁面的位置:編輯內容,Index.cshtml
如下代碼塊所示:
@page
@using ProductManagement.Web.Pages.Products
@model IndexModel
<h1>Products Page</h1>
在這裏,我放置一個h1
元素作爲頁眉。接下來我們在主菜單中添加一個菜單來打開這個頁面。
添加菜單項
ProductManagementMenuContributor
類,並在ConfigureMainMenuAsync
方法末尾添加以下代碼:context.Menu.AddItem( new ApplicationMenuItem( "ProductManagement", l["Menu:ProductManagement"], icon: "fas fa-shopping-cart" ).AddItem( new ApplicationMenuItem( "ProductManagement.Products", l["Menu:Products"], url: "/Products" ) ) );
此代碼添加了一個產品管理主菜單,其中包含產品菜單項。裏面的l["…"]
語法是用來獲取本地化的值。
en.json
文件,並將以下代碼添加到該texts
部分的末尾:"Menu:ProductManagement": "Product Management", "Menu:Products": "Products"
我們可以使用任意字符串值作爲本地化鍵。在本例中,我們使用Menu:
作爲菜單的本地化鍵的前綴,例如Menu:Products
。我們將在[第 8 章] 使用 ABP 的功能和服務中探討本地化主題。
創建產品數據表
Index.cshtml
頁面(在Pages/Products文件夾),並將其內容更改爲以下內容:@page @using ProductManagement.Web.Pages.Products @using Microsoft.Extensions.Localization @using ProductManagement.Localization @model IndexModel @inject IStringLocalizer<ProductManagementResource> L @section scripts { <abp-script src="/Pages/Products/Index.cshtml.js" /> } <abp-card> <abp-card-header> <h2>@L["Menu:Products"]</h2> </abp-card-header> <abp-card-body> <abp-table id="ProductsTable" striped-rows="true" /> </abp-card-body> </abp-card>
abp-script
是一個 ABP 標籤助手,用於將腳本文件添加到頁面,並具有自動捆綁、壓縮和版本控制功能。abp-card
是另一個標籤助手,以一種類型安全且簡單的方式渲染 Card 組件。
我們可以使用標準的 HTML 標籤。但是,ABP 標籤助手極大地簡化了 MVC/Razor 頁面中的 UI 創建。此外,它們支持智能感知和編譯時錯誤類型檢查。我們將在[第 12 章] 使用 MVC/Razor 頁面中研究標籤助手。
Index.cshtml.js
,內容如下:$(function () { var l = abp.localization.getResource('ProductManagement'); var dataTable = $('#ProductsTable').DataTable( abp.libs.datatables.normalizeConfiguration({ serverSide: true, paging: true, order: [[0, "asc"]], searching: false, scrollX: true, ajax: abp.libs.datatables.createAjax( productManagement.products.product.getList), columnDefs: [ /* TODO: Column definitions */ ] }) ); });
ABP 簡化了數據表配置並提供了內置集成:
-
abp.localization.getResource
返回一個本地化對象,ABP 允許您在 JS中重用服務器端定義的本地化。 -
abp.libs.datatables.normalizeConfiguration
是 ABP 框架定義的輔助函數。它通過爲缺失選項提供常規默認值來簡化數據表的配置。 -
abp.libs.datatables.createAjax
使 ABP 的動態 JS 客戶端代理來適配數據表的參數格式。 -
productManagement.products.product.getList
是動態JS代理方法。
columnDefs
數組用於定義數據表中的列:{ title: l('Name'), data: "name" }, { title: l('CategoryName'), data: "categoryName", orderable: false }, { title: l('Price'), data: "price" }, { title: l('StockState'), data: "stockState", render: function (data) { return l('Enum:StockState:' + data); } }, { title: l('CreationTime'), data: "creationTime", dataFormat: 'date' }
通常,列有一個title
字段和一個data
字段。data
字段匹配ProductDto
類中的屬性名稱,格式爲駝峯式(一種命名風格,其中每個單詞的第一個字母大寫,第一個單詞除外;它是JavaScript 語言中常用的命名風格)。
render
選項用於精細控制如何顯示列數據。en.json
文件,並在該部分的末尾添加以下條目texts
:"Name": "Name", "CategoryName": "Category name", "Price": "Price", "StockState": "Stock state", "Enum:StockState:0": "Pre-order", "Enum:StockState:1": "In stock", "Enum:StockState:2": "Not available", "Enum:StockState:3": "Stopped", "CreationTime": "Creation time"
看一下實際的產品數據表:
創建產品
定義新的應用服務方法來獲取類別和創建產品。
-
定義應用服務的獲取類別和創建產品方法。
-
在 UI 部分,使用 ABP 的動態表單功能,基於 C# 類自動生成產品創建表單。
定義應用接口
IProductAppService
接口添加兩個新方法開始:GetCategoriesAsync
方法獲取產品類別的下拉數據。我們定義了兩個新的 DTO。CreateUpdateProductDto
用於創建和更新產品(我們將在編輯產品時候重複使用它)。我們在ProductManagement.Application.Contracts項目的Products文件夾中定義它:using System; using System.ComponentModel.DataAnnotations; namespace ProductManagement.Products { public class CreateUpdateProductDto { public Guid CategoryId { get; set; } [Required] [StringLength(ProductConsts.MaxNameLength)] public string Name { get; set; } public float Price { get; set; } public bool IsFreeCargo { get; set; } public DateTime ReleaseDate { get; set; } public ProductStockState StockState { get; set; } } }
接下來,在ProductManagement.Application.Contracts項目的Categories文件夾中定義一個CategoryLookupDto
類:
using System; namespace ProductManagement.Categories { public class CategoryLookupDto { public Guid Id { get; set; } public string Name { get; set; } } }
定了接口相關類,現在我們可以在應用層實現接口了。
實現應用服務
ProductAppService
中實現CreateAsync
和GetCategoriesAsync
方法(ProductManagement.Application項目中),如下代碼塊:public async Task CreateAsync(CreateUpdateProductDto input) { await _productRepository.InsertAsync( ObjectMapper.Map<CreateUpdateProductDto, Product>(input) ); } public async Task<ListResultDto<CategoryLookupDto>> GetCategoriesAsync() { var categories = await _categoryRepository.GetListAsync(); return new ListResultDto<CategoryLookupDto>( ObjectMapper.Map<List<Category>, List<CategoryLookupDto>>(categories) ); }
這裏,_categoryRepository
屬於IRepository<Category, Guid>
服務類型,通過構造函數注入,方法實現很簡單,無需解釋。
ProductManagementApplicationAutoMapperProfile.cs
文件(在ProductManagement.Application項目中),添加以下代碼:CreateMap<CreateUpdateProductDto, Product>();
CreateMap<Category, CategoryLookupDto>();
用戶界面
CreateProductModal.cshtml
Razor 頁面。打開CreateProductModal.cshtml.cs
文件,更改CreateProductModalModel
代碼:using System.Linq; using System.Threading.Tasks; using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc.Rendering; using ProductManagement.Products; namespace ProductManagement.Web.Pages.Products { Public class CreateProductModalModel:ProductManagementPageModel { [BindProperty] public CreateEditProductViewModel Product { get; set; } public SelectListItem[] Categories { get; set; } private readonly IProductAppService _productAppService; public CreateProductModalModel(IProductAppService productAppService) { _productAppService = productAppService; } public async Task OnGetAsync() { // TODO } public async Task<IActionResult> OnPostAsync() { // TODO } } }
這裏的ProductManagementPageModel
是基類。你可以繼承它來創建PageModel
類。[BindProperty]
是一個標準的 ASP.NET Core 屬性,在HTTP Post 請求時,會將數據綁定到Product
屬性。Categories
將用於顯示下拉列表中的類別。我們通過注入IProductAppService
接口以使用之前定義的方法。
CreateEditProductViewModel
還沒定義,我們將其定義在與CreateProductModal.cshtml
相同的文件夾下:using ProductManagement.Products; using System; using System.ComponentModel; using System.ComponentModel.DataAnnotations; using Volo.Abp.AspNetCore.Mvc.UI.Bootstrap.TagHelpers.Form; namespace ProductManagement.Web.Pages.Products { public class CreateEditProductViewModel { [SelectItems("Categories")] [DisplayName("Category")] public Guid CategoryId { get; set; } [Required] [StringLength(ProductConsts.MaxNameLength)] public string Name { get; set; } public float Price { get; set; } public bool IsFreeCargo { get; set; } [DataType(DataType.Date)] public DateTime ReleaseDate { get; set; } public ProductStockState StockState { get; set; } } }
SelectItems
告訴我們CategoryId
屬性將從Categories
列表中選擇。我們將在編輯模式對話框中重用此類。這就是我爲什麼命名它爲CreateEditProductViewModel
。DTO 與 ViewModel
CreateEditProductViewModel
似乎沒有必要,因爲它與 CreateUpdateProductDto
DTO非常相似。當然你也可以在視圖裏複用DTO。但是,考慮到這些類具有不同的用途,並且隨着時間的推移會向不同的方向發展,所更推薦的辦法是將每個關注點分開。例如,[SelectItems("Categories")]
屬性指向 Razor Page 模型,它在應用層沒有任何意義。CreateProductModalModel
類中實現OnGetAsync
方法:public async Task OnGetAsync() { Product = new CreateEditProductViewModel { ReleaseDate = Clock.Now, StockState = ProductStockState.PreOrder }; var categoryLookup = await _productAppService.GetCategoriesAsync(); Categories = categoryLookup.Items.Select(x => new SelectListItem(x.Name, x.Id.ToString())).ToArray(); }
我們使用默認值創建Product
類,然後使用產品應用服務填充Categories
列表。Clock
是 ABP 框架提供的服務,用於獲取當前時間(在不處理時區和本地/UTC 時間的情況下),這裏我們不再使用DateTime.Now
。具體內容這將在[第 8 章] 使用 ABP 的功能和服務中進行解釋。
OnPostAsync
代碼塊:public async Task<IActionResult> OnPostAsync() { await _productAppService.CreateAsync( ObjectMapper.Map<CreateEditProductViewModel,CreateUpdateProductDto> (Product) ); return NoContent(); }
由於我們要映射CreateEditProductViewModel
到CreateProductDto
,所以需要定義映射配置。我們在ProductManagement.Web項目中打開ProductManagementWebAutoMapperProfile
類,並更改以下代碼塊內容:
public class ProductManagementWebAutoMapperProfile : Profile { public ProductManagementWebAutoMapperProfile() { CreateMap<CreateEditProductViewModel, CreateUpdateProductDto>(); } }
我們已經完成了產品創建 UI 的 C# 端,接下來可以開始構建 UI 和 JavaScript 代碼。打開CreateProductModal.cshtml
文件,並將內容更改如下:
@page @using Microsoft.AspNetCore.Mvc.Localization @using ProductManagement.Localization @using Volo.Abp.AspNetCore.Mvc.UI.Bootstrap.TagHelpers.Modal @model ProductManagement.Web.Pages.Products.CreateProductModalModel @inject IHtmlLocalizer<ProductManagementResource> L @{ Layout = null; } <abp-dynamic-form abp-model="Product" asp-page="/Products/CreateProductModal"> <abp-modal> <abp-modal-header title="@L["NewProduct"].Value"></abp-modal-header> <abp-modal-body> <abp-form-content /> </abp-modal-body> <abp-modal-footer buttons="@(AbpModalButtons.Cancel|AbpModalButtons.Save)"></abp-modal-footer> </abp-modal> </abp-dynamic-form>
在這裏,abp-dynamic-form
會根據 C# 模型類自動創建表單元素。abp-form-content
是呈現表單元素的地方。abp-modal
用於創建模態對話框。
Index.cshtml
文件,然後將abp-card-header
部分更改如下:<abp-card-header> <abp-row> <abp-column size-md="_6"> <abp-card-title>@L["Menu:Products"]</abp-card-title> </abp-column> <abp-column size-md="_6" class="text-end"> <abp-button id="NewProductButton" text="@L["NewProduct"].Value" icon="plus" button-type="Primary"/> </abp-column> </abp-row> </abp-card-header>
我添加了 2 列,其中每列都有一個size-md="_6"
屬性(即 12 列 Bootstrap 網格的一半)。左側設置卡片標題,右側放置了一個按鈕。
Index.cshtml.js
文件末尾(在})
之前):var createModal = new abp.ModalManager(abp.appPath + 'Products/CreateProductModal'); createModal.onResult(function () { dataTable.ajax.reload(); }); $('#NewProductButton').click(function (e) { e.preventDefault(); createModal.open(); });
-
abp.ModalManager
用於在客戶端管理模式對話框。在內部,它使用 Twitter Bootstrap 的標準模態組件,封裝了很多細節,並提供了一個簡單的 API。當模型觸發保存時會返回一個回調函數createModal.onResult()
。 -
createModal.open()
用於打開模態對話框。
en.json
文件中定義一些本地化文本(.Domain.Shared項目的Localization/ProductManagement 文件夾下):"NewProduct": "New Product", "Category": "Category", "IsFreeCargo": "Free Cargo", "ReleaseDate": "Release Date"
再次運行 Web 嘗試創建新產品
ABP基於 C# 類模型自動創建表單字段。本地化和驗證也可以通過讀取屬性和使用約定來自動工作。我們將在[第 12 章] 使用 MVC/Razor 頁面 中更詳細地介紹驗證和本地化主題。
編輯產品
定義應用接口
IProductAppService
接口定義兩個新方法:Task<ProductDto> GetAsync(Guid id);
Task UpdateAsync(Guid id, CreateUpdateProductDto input);
第一種方法用於通過ID獲取產品。我們在UpdateAsync
方法中重用之前定義的CreateUpdateProductDto
。
實現應用接口
ProductAppService
類中:public async Task<ProductDto> GetAsync(Guid id) { return ObjectMapper.Map<Product, ProductDto>( await _productRepository.GetAsync(id) ); } public async Task UpdateAsync(Guid id, CreateUpdateProductDto input) { var product = await _productRepository.GetAsync(id); ObjectMapper.Map(input, product); }
GetAsync
方法用於從數據庫中獲取產品,並將其映射到ProductDto
對象後進行返回。UpdateAsync
方法獲取到一個產品後,將給定的DTO輸入映射到產品。通過這種方式,我們用新值覆蓋產品。
_productRepository.UpdateAsync
,因爲 EF Core有一個變更跟蹤系統。ABP 的工作單元如果沒有拋出異常,則在請求結束時會自動保存更改。我們將在[第 6 章] *使用數據訪問基礎架構”*中介紹工作單元系統。用戶界面
EditProductModal.cshtml
Razor 頁面(ProductManagement.Web項目的 Pages/Products文件夾下)。打開EditProductModal.cshtml.cs
,代碼更改如下:using System; using System.Linq; using System.Threading.Tasks; using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc.Rendering; using ProductManagement.Products; namespace ProductManagement.Web.Pages.Products { public class EditProductModalModel : ProductManagementPageModel { [HiddenInput] [BindProperty(SupportsGet = true)] public Guid Id { get; set; } [BindProperty] public CreateEditProductViewModel Product { get; set; } public SelectListItem[] Categories { get; set; } private readonly IProductAppService _productAppService; public EditProductModalModel(IProductAppService productAppService) { _productAppService = productAppService; } public async Task OnGetAsync() { // TODO } public async Task<IActionResult> OnPostAsync() { // TODO } } }
表單中Id
字段將被隱藏。
Product
和Categories
屬性類似於創建產品。我們還將
IProductAppService
接口注入到構造函數。OnGetAsync
方法,如下代碼塊所示:public async Task OnGetAsync() { var productDto = await _productAppService.GetAsync(Id); Product = ObjectMapper.Map<ProductDto, CreateEditProductViewModel>(productDto); var categoryLookup = await _productAppService.GetCategoriesAsync(); Categories = categoryLookup.Items .Select(x => new SelectListItem(x.Name, x.Id.ToString())) .ToArray(); }
首先,我們要先獲取一個產品 ( ProductDto
),再將其轉換爲CreateEditProductViewModel
,使用它在 UI 上來創建編輯表單。然後,我們在表單上選擇產品類別。
ProductDto
到CreateEditProductViewModel
,所以我們需要在ProductManagementWebAutoMapperProfile
類中定義配置映射(ProductManagement.Web項目中),這和我們之前操作是一樣的:CreateMap<ProductDto, CreateEditProductViewModel>();
我們再看下OnPostAsync()
方法:
public async Task<IActionResult> OnPostAsync() { await _productAppService.UpdateAsync(Id, ObjectMapper.Map<CreateEditProductViewModel, CreateUpdateProductDto>(Product) ); return NoContent(); }
OnPostAsync
方法很簡單,把CreateEditProductViewModel
轉換爲CreateUpdateProductDto
。
EditProductModal.cshtml
,內容更改如下:@page @using Microsoft.AspNetCore.Mvc.Localization @using ProductManagement.Localization @using Volo.Abp.AspNetCore.Mvc.UI.Bootstrap.TagHelpers.Modal @model ProductManagement.Web.Pages.Products.EditProductModalModel @inject IHtmlLocalizer<ProductManagementResource> L @{ Layout = null; } <abp-dynamic-form abp-model="Product" asp-page="/Products/EditProductModal"> <abp-modal> <abp-modal-header title="@Model.Product.Name"></abp-modal-header> <abp-modal-body> <abp-input asp-for="Id" /> <abp-form-content/> </abp-modal-body> <abp-modal-footer buttons="@(AbpModalButtons.Cancel|AbpModalButtons.Save)"></abp-modal-footer> </abp-modal> </abp-dynamic-form>
頁面與CreateProductModal.cshtml
非常相似。我剛剛將Id
字段作爲隱藏字段添加到表單,用來存儲Id
編輯的產品的屬性。
Index.cshtml.js
文件,並在dataTable
代碼的頭部添加一個ModalManager
對象:var editModal = new abp.ModalManager(abp.appPath + 'Products/EditProductModal');
然後,在dataTable
內部的columnDefs
數組中定義一個列(第一項):
{ title: l('Actions'), rowAction: { items: [ { text: l('Edit'), action: function (data) { editModal.open({ id: data.record.id }); } } ] } },
此代碼向數據表添加了一個新的Actions列,並添加了一個Edit操作按鈕,單擊即可打開編輯窗口。rowAction
是 ABP Framework 提供的一個特殊選項。它用於在表中的一行添加一個或多個操作按鈕。
dataTable
初始化代碼後添加如下:editModal.onResult(function () {
dataTable.ajax.reload();
});
在保存產品編輯對話框後刷新數據表,確保我們可以看到表上的最新數據。最終的 UI 類似於下圖:
我們現在可以查看、創建和編輯產品了。最後一部分將實現刪除產品。
刪除產品
IProductAppService
接口中添加一個新方法:Task DeleteAsync(Guid id);
然後,在ProductAppService
類中實現它:
public async Task DeleteAsync(Guid id) { await _productRepository.DeleteAsync(id); }
現在向產品列表添加一個新刪除按鈕。打開Index.cshtml.js
,並在Edit操作之後添加以下定義(在rowAction.items
數組中):
{ text: l('Delete'), confirmMessage: function (data) { return l('ProductDeletionConfirmationMessage',data.record.name); }, action: function (data) { productManagement.products.product .delete(data.record.id) .then(function() { abp.notify.info(l('SuccessfullyDeleted')); dataTable.ajax.reload(); }); } }
confirmMessage
用於在刪除之前獲得用戶確認。productManagement.products.product.delete
函數由 ABP 框架動態創建。通過這種方式,可以直接在 JS 代碼中調用服務器端方法。我們只需傳遞當前記錄的 ID。then
函數傳遞一個回調函數,用於刪除之後的操作。最後,我們使用abp.notify.info
通知用戶,最後刷新數據表。
en.json
文件中添加以下代碼:因爲現在有兩個操作按鈕,所以編輯按鈕會自動變成一個下拉選項。當您單擊刪除操作時,您會收到一條確認消息:
Product
實體派生於FullAuditedAggregateRoot
,所以它使用了軟刪除。刪除產品後檢查數據庫,您會看到它並沒有真正刪除,但是IsDeleted
字段已經設置爲true
(邏輯刪除不是物理刪除)。下次查詢商品時,已刪除的商品會自動過濾掉,不包含在查詢結果中。這是由 ABP 框架的數據過濾系統完成的。