1. 程式人生 > >嘗試從零開始構建我的商城 (一) :使用Abp vNext快速一個簡單的商城專案

嘗試從零開始構建我的商城 (一) :使用Abp vNext快速一個簡單的商城專案

# 嘗試從零開始構建我的商城 (一) :使用Abp vNext快速搭建一個簡單的專案 ## 前言 ### GitHub地址 > https://github.com/yingpanwang/MyShop ### 此文目的 本文將嘗試使用Abp vNext 搭建一個商城系統並儘可能從業務,架構相關方向優化升級專案結構,並會將每次升級所涉及的模組及特性以blog的方式展示出來。 ## 程式碼實踐 > Abp vNext 使用 DDD 領域驅動設計 是非常方便的,但是由於本人認為自身沒有足夠的功力玩轉DDD,所以開發中使用的是基於貧血模型的設計的開發,而不是遵照DDD的方式 ### Domain 領域層 #### 1.新增引用 > 通過 **Nuget** 安裝 **Volo.Abp.Ddd.Domain** #### 2.定義模組 建立 **MyShopDomainModule.cs** 作為Domain的模組,Domain不依賴任何外部模組,本體也沒有什麼相關配置,所以只需要繼承AbpModule即可 ``` csharp namespace MyShop.Domain { public class MyShopDomainModule:AbpModule { } } ``` #### 3.定義實體 ###### BaseEntity 實體基類 定義BaseEntity並繼承由**Volo.Abp.Ddd.Domain**提供的Entity並新增**CreationTime**屬性 ``` csharp public class BaseEntity :Entity { public DateTime CreationTime { get; set; } = DateTime.Now; } ``` ###### Product 商品 繼承**BaseEntity**類 新增相關屬性 ``` csharp /// /// 商品資訊 ///
public class Product :BaseEntity { /// /// 名稱 /// public string Name { get; set; } /// /// 分類id /// public long CategoryId { get; set; } /// /// 價格 /// public decimal? Price { get; set; } /// /// 描述 ///
public string Description { get; set; } /// /// 庫存 /// public decimal Stock { get; set; } } ``` ###### Order 訂單 繼承**BaseEntity**類 新增相關屬性 ``` csharp /// /// 訂單 /// public class Order:BaseEntity { /// /// 訂單號 /// public Guid OrderNo { get; set; } = Guid.NewGuid(); /// /// 使用者 ///
public long UserId { get; set; } /// /// 訂單狀態 /// public OrderStatus OrderStatus { get; set; } /// /// 總金額 /// public decimal Total { get; set; } /// /// 地址 /// public string Address { get; set; } } public enum OrderStatus { Created, Cancel, Paid, } ``` ### Application 應用層實現 #### 1.新增引用 > 通過 **Nuget** 安裝 **Volo.Abp.Application** > 通過 **Nuget** 安裝 **Volo.Abp.AutoMapper** #### 2.定義模組 建立 **MyShopApplicationModule.cs** ,繼承自AbpModule,並且依賴Domain以及ApplicationContract層和後續使用的AutoMapper,所以對應DependsOn中需要新增對應依賴 ``` csharp /// /// 專案模組依賴 /// [DependsOn(typeof(MyShopApplicationContractModule), typeof(MyShopDomainModule))] /// 元件依賴 [DependsOn(typeof(AbpAutoMapperModule))] public class MyShopApplicationModule:AbpModule { public override void ConfigureServices(ServiceConfigurationContext context) { //新增ObjectMapper注入 context.Services.AddAutoMapperObjectMapper(); // Abp AutoMapper設定 Configure(config => { // 新增對應依賴關係Profile config.AddMaps(); }); } } ``` #### 3.實現相關服務 這裡由於我們使用AutoMapper所以需要建立Profile並新增相關對映關係 ##### Profile定義 ``` csharp namespace MyShop.Application.AutoMapper.Profiles { public class MyShopApplicationProfile:Profile { public MyShopApplicationProfile() { CreateMap().ReverseMap(); CreateMap().ReverseMap(); } } } ``` ##### 服務實現 Abp 可以很方便的將我們的服務向外暴露,通過簡單配置可以自動生成對應介面,只需要需要暴露的服務 繼承ApplicationService。 > **Abp**在確定服務方法的**HTTP Method**時使用命名約定: **Get**: 如果方法名稱以**GetList**,**GetAll**或**Get**開頭. **Put**: 如果方法名稱以**Put**或**Update**開頭. **Delete**: 如果方法名稱以**Delete**或**Remove**開頭. **Post**: 如果方法名稱以**Create**,**Add**,**Insert**或**Post**開頭. **Patch**: 如果方法名稱以**Patch**開頭. **其他情況**, **Post** 為 預設方式. ###### OrderApplicationService ``` csharp public class OrderApplicationService : ApplicationService, IOrderApplicationService { private readonly IRepository _orderRepository; public OrderApplicationService(IRepository orderRepository) { _orderRepository = orderRepository; } public async Task GetAsync(long id) { var order = await _orderRepository.GetAsync(g => g.Id == id); return ObjectMapper.Map(order); } public async Task> GetListAsync() { var orders = await _orderRepository.GetListAsync(); return ObjectMapper.Map, List>(orders); } } ``` ###### ProductApplicationService ``` csharp /// /// 商品服務 /// public class ProductApplicationService : ApplicationService, IProductApplicationService { /// /// 自定義商品倉儲 /// private readonly IProductRepository _productRepository; /// /// 商品類別倉儲 /// private readonly IRepository _categoryRepository; /// /// 構造 /// /// 自定義商品倉儲 /// 商品類別倉儲 public ProductApplicationService(IProductRepository productRepository, IRepository categoryRepository) { _productRepository = productRepository; _categoryRepository = categoryRepository; } /// /// 獲取商品資訊 /// /// 商品id /// public async Task GetAsync(long id) { return await Task.FromResult(Query(p => p.Id == id).FirstOrDefault(p =>p.Id == id)); } /// /// 獲取分頁商品列表 /// /// 分頁資訊 /// public IPagedResult GetPage(BasePageInput page) { var query = Query() .WhereIf(!string.IsNullOrEmpty(page.Keyword), w => w.Name.StartsWith(page.Keyword)); var count = query.Count(); var products = query.PageBy(page).ToList(); return new PagedResultDto(count,products); } /// /// 獲取商品列表 /// /// public async Task> GetListAsync() { var products = await _productRepository.GetListAsync(); return ObjectMapper.Map, List>(products); } private IQueryable Query(Expression> expression = null) { var products = _productRepository.WhereIf(expression != null, expression); var categories = _categoryRepository; var data = from product in products join category in categories on product.CategoryId equals category.Id into temp from result in temp.DefaultIfEmpty() select new ProductItemDto { Id = product.Id, Description = product.Description, Name = product.Name, Price = product.Price, Stock = product.Stock, CategoryName = result.CategoryName, }; return data; } } ``` ### Application.Contract 應用層抽象 #### 1.新增引用 > 通過 **Nuget** 安裝 **Volo.Abp.Application.Contract** #### 2.定義模組 建立 **MyShopApplicationContractModule.cs** ,繼承自AbpModule,並且沒有對外依賴 ``` csharp public class MyShopApplicationContractModule:AbpModule { } ``` #### 3.定義Contract ###### IOrderApplicationService ``` csharp public interface IOrderApplicationService:IApplicationService { Task GetAsync(long id); Task> GetListAsync(); } ``` ###### IProductApplicationService ``` csharp public interface IProductApplicationService :IApplicationService { Task> GetListAsync(); IPagedResult GetPage(BasePageInput page); Task GetAsync(long id); } ``` ### Admin.Application 管理後臺應用層 這裡和Application層差不多,但是為了方便編寫程式碼所以Admin.Contract和Admin.Application 放在了一個專案中,這裡我定義了**IBaseAdminCRUDApplicationService**介面並且實現了一個**BaseAdminCRUDApplicationService**實現了一些簡單的CRUD的方法暫時作為後臺資料管理的方法,並且為了區分後臺管理介面和平臺介面在自動註冊為api時加上**admin**字首 ``` csharp service.Configure((AbpAspNetCoreMvcOptions options) => { // 平臺介面和後臺管理介面暫時放在一個站點下 options.ConventionalControllers.Create(typeof(Application.ProductApplicationService).Assembly); options.ConventionalControllers.Create(typeof(Application.OrderApplicationService).Assembly); // 區分介面 請求路徑加上admin options.ConventionalControllers.Create(typeof(Admin.Application.Services.ProductApplicationService).Assembly, options => { options.RootPath = "admin"; }); }); ``` #### 1.新增引用 > 通過 **Nuget** 安裝 **Volo.Abp.Application** > 通過 **Nuget** 安裝 **Volo.Abp.AutoMapper** #### 2.定義模組 建立 **MyShopAdminApplicationModule.cs** ,繼承自AbpModule,並且依賴Domain以及ApplicationContract層和後續使用的AutoMapper,所以對應DependsOn中需要新增對應依賴 ``` csharp [DependsOn(typeof(MyShopDomainModule))] [DependsOn(typeof(AbpAutoMapperModule))] public class MyShopAdminApplicationModule:AbpModule { public override void ConfigureServices(ServiceConfigurationContext context) { context.Services.AddAutoMapperObjectMapper(); Configure(config => { config.AddMaps(); }); } } ``` ### EntityFreaworkCore資料訪問層中實現Repository > 通過Nuget新增 **Volo.Abp.EntityFrameworkCore.MySQL** > ###### 定義DbContext ``` csharp [ConnectionStringName("Default")] public class MyShopDbContext:AbpDbContext { public MyShopDbContext(DbContextOptions options) : base(options) { } protected override void OnModelCreating(ModelBuilder builder) { builder.ConfigureProductStore(); builder.ConfigureOrderStore(); builder.ConfigureOrderItemStore(); builder.ConfigureCategoryStore(); } public DbSet Products { get; set; } public DbSet Orders { get; set; } public DbSet OrderItems { get; set; } public DbSet Categories { get; set; } } ``` 這裡通過擴充套件方法分開了實體的一些定義 ###### ConfigureProductStore ``` csharp public static class ProductCreatingExtension { public static void ConfigureProductStore(this ModelBuilder builder) { Check.NotNull(builder, nameof(builder)); builder.Entity(option => { option.ToTable("Product"); option.ConfigureByConvention(); }); } } ``` ###### ConfigureOrderStore ``` csharp public static class OrderCreatingExtension { public static void ConfigureOrderStore(this ModelBuilder builder) { Check.NotNull(builder, nameof(builder)); builder.Entity(option => { option.ToTable("Order"); option.ConfigureByConvention(); }); } } ``` ###### ConfigureOrderItemStore ``` csharp public static class OrderItemsCreatingExtension { public static void ConfigureOrderItemStore(this ModelBuilder builder) { Check.NotNull(builder, nameof(builder)); builder.Entity(option => { option.ToTable("OrderItem"); option.ConfigureByConvention(); }); } } ``` ###### ConfigureCategoryStore ``` csharp public static class CategoryCreatingExtension { public static void ConfigureCategoryStore(this ModelBuilder builder) { Check.NotNull(builder, nameof(builder)); builder.Entity(option => { option.ToTable("Category"); option.ConfigureByConvention(); }); } } ``` 由於目前Abp vNext提供的 IRepository 介面已經滿足我們當前的使用需要,也提供了IQuaryable 介面,所以靈活組裝的能力也有,所以不另外實現自定義倉儲,不過這裡還是預先準備一個自定義的ProductRepsitory ###### 在Domain層中定義IProductRepository ``` csharp public interface IProductRepository : IRepository { IQueryable GetProducts(long id); } ``` ###### EntityFrameworkCore資料訪問層中實現IProductRepository 繼承EfCoreRepository以方便實現 IRepository介面中提供的基本方法和 DbContext的訪問,並實現IProductRepository ``` csharp public class ProductRepository : EfCoreRepository, IProductRepository { public ProductRepository(IDbContextProvider dbContextProvider) : base(dbContextProvider) { } public IQueryable GetProducts(long id) { return DbContext.Products.Where(p => p.Id == id); } } ``` #### 定義模組,注入預設倉儲 ``` csharp [DependsOn(typeof(AbpEntityFrameworkCoreMySQLModule))] public class MyShopEntityFrameworkCoreModule :AbpModule { public override void ConfigureServices(ServiceConfigurationContext context) { context.Services.AddAbpDbContext(option=> { // 如果只是用了 entity, 請將 includeAllEntities 設定為 true 否則會提示異常,導致所屬倉儲無法注入 option.AddDefaultRepositories(true); }); Configure(option=> { option.UseMySQL(); }); } } ``` ### Migration 遷移 #### 1.新增引用 ###### > 通過Nuget安裝 **Microsoft.EntityFrameworkCore.Tools** ###### 新增Domain,EntiyFrameworkCore資料訪問層的引用 ``` csharp ``` #### 2.定義DbContext 這裡我們使用了之前在EntifyFrameworkCore中定義的一些實體配置 ``` csharp [ConnectionStringName("Default")] public class DbMigrationsContext : AbpDbContext { public DbMigrationsContext(DbContextOptions options) : base(options) { } protected override void OnModelCreating(ModelBuilder builder) { base.OnModelCreating(builder); builder.ConfigureProductStore(); builder.ConfigureOrderStore(); builder.ConfigureOrderItemStore(); builder.ConfigureCategoryStore(); } } ``` #### 3.定義模組 ``` csharp [DependsOn(typeof(MyShopEntityFrameworkCoreModule))] public class MyShopEntityFrameworkCoreMigrationModule:AbpModule { public override void ConfigureServices(ServiceConfigurationContext context) { context.Services.AddAbpDbContext(); } } ``` #### 4.執行遷移 開啟程式包管理器控制檯,由於我們的連線字串定義在Api層,我已我們需要將Api設定為啟動專案,並將程式包管理器控制檯預設程式切換為我們的遷移層Migration並執行一下指令 1.新增遷移檔案 > Add-Migration "Init" 執行完後我們會在Migrations資料夾目錄下得到相關的遷移檔案 2.執行遷移 >Update-Database 執行完成後就可以開啟資料庫檢視我們生成的表了 ### Api 介面層 #### 1.建立專案 接下來建立AspNetCore WebAPI 專案 並命名為MyShop.Api作為我們對外暴露的API,並新增Abp Vnext AspNetCore.MVC包 > 通過Nuget 安裝 Volo.Abp.AspNetCore.Mvc ###### StartUp ``` csharp namespace MyShop.Api { public class Startup { // This method gets called by the runtime. Use this method to add services to the container. // For more information on how to configure your application, visit https://go.microsoft.com/fwlink/?LinkID=398940 public void ConfigureServices(IServiceCollection services) { // 註冊模組 services.AddApplication(); } // This method gets called by the runtime. Use this method to configure the HTTP request pipeline. public void Configure(IApplicationBuilder app, IWebHostEnvironment env) { // 初始化 app.InitializeApplication(); } } } ``` ###### Program 由於我們使用了 Abp vNext 中的Autofac 所以需要新增 Abp vNext Autofac 模組 並註冊Autofac > 通過Nuget 安裝 Volo.Abp.Autofac ``` csharp namespace MyShop.Api { public class Program { public static void Main(string[] args) { CreateHostBuilder(args).Build().Run(); } public static IHostBuilder CreateHostBuilder(string[] args) => Host.CreateDefaultBuilder(args) .ConfigureWebHostDefaults(webBuilder => { webBuilder.UseStartup(); }) .UseAutofac(); } } ``` ###### MyShopApiModule 這裡我們使用了Swagger 作為Api文件自動生成,所以需要新增相關Nuget包,並且依賴了MyShopEntityFrameworkCoreModule和 MyShopApplicationModule,MyShopAdminApplicationModule 所以需要新增相關依賴及專案引用, 並且使用了Api作為遷移連線字串所在專案,所以還需要新增EF的Design包 ###### Nuget > 通過Nuget 安裝 Swashbuckle.AspNetCore > 通過Nuget 安裝 Microsoft.EntityFrameworkCore.Design ###### 專案引用 > MyShop.AdminApplication > MyShop.Application > MyShop.EntitFrameworkCore > MyShop.EntitFrameworkCore.DbMigration ``` csharp namespace MyShop.Api { // 注意是依賴於AspNetCoreMvc 而不是 AspNetCore [DependsOn(typeof(AbpAspNetCoreMvcModule),typeof(AbpAutofacModule))] [DependsOn(typeof(MyShopApplicationModule),typeof(MyShopEntityFrameworkCoreModule),typeof(MyShopAdminApplicationModule))] public class MyShopApiModule :AbpModule { public override void ConfigureServices(ServiceConfigurationContext context) { var service = context.Services; // 跨域 context.Services.AddCors(options => { options.AddPolicy("AllowAll", builder => { builder.AllowAnyOrigin() .SetIsOriginAllowedToAllowWildcardSubdomains() .AllowAnyHeader() .AllowAnyMethod(); }); }); // 自動生成控制器 service.Configure((AbpAspNetCoreMvcOptions options) => { options.ConventionalControllers.Create(typeof(Application.ProductApplicationService).Assembly); options.ConventionalControllers.Create(typeof(Application.OrderApplicationService).Assembly); options.ConventionalControllers.Create(typeof(Admin.Application.Services.ProductApplicationService).Assembly, options => { options.RootPath = "admin"; }); }); // swagger service.AddSwaggerGen(options => { options.SwaggerDoc("v1", new Microsoft.OpenApi.Models.OpenApiInfo() { Title = "MyShopApi", Version = "v0.1" }); options.DocInclusionPredicate((docName, predicate) => true); options.CustomSchemaIds(type => type.FullName); }); } public override void OnApplicationInitialization(ApplicationInitializationContext context) { var env = context.GetEnvironment(); var app = context.GetApplicationBuilder(); if (env.IsDevelopment()) { app.UseDeveloperExceptionPage(); } app.UseCors("AllowAll"); app.UseSwagger(); app.UseSwaggerUI(options => { options.SwaggerEndpoint("/swagger/v1/swagger.json", "MyShopApi"); }); app.UseRouting(); app.UseConfiguredEndpoints(); } } } ``` 至此我們的簡單的專案就搭建好了 點選執行 檢視 https://localhost:5001/swagger/index.html 就可以檢視我們Application和Admin.Application中定義的介面了.