嘗試從零開始構建我的商城 (一) :使用Abp vNext快速搭建一個簡單的專案
前言
GitHub地址
此文目的
本文將嘗試使用Abp vNext 搭建一個商城系統並儘可能從業務,架構相關方向優化升級專案結構,並會將每次升級所涉及的模組及特性以blog的方式展示出來。
程式碼實踐
Abp vNext 使用 DDD 領域驅動設計 是非常方便的,但是由於本人認為自身沒有足夠的功力玩轉DDD,所以開發中使用的是基於貧血模型的設計的開發,而不是遵照DDD的方式
Domain 領域層
1.新增引用
通過 Nuget 安裝 Volo.Abp.Ddd.Domain
2.定義模組
建立 MyShopDomainModule.cs 作為Domain的模組,Domain不依賴任何外部模組,本體也沒有什麼相關配置,所以只需要繼承AbpModule即可
namespace MyShop.Domain
{
public class MyShopDomainModule:AbpModule
{
}
}
3.定義實體
BaseEntity 實體基類
定義BaseEntity並繼承由Volo.Abp.Ddd.Domain提供的Entity並新增CreationTime屬性
public class BaseEntity :Entity<long>
{
public DateTime CreationTime { get; set; } = DateTime.Now;
}
Product 商品
繼承BaseEntity類 新增相關屬性
/// <summary>
/// 商品資訊
/// </summary>
public class Product :BaseEntity
{
/// <summary>
/// 名稱
/// </summary>
public string Name { get; set; }
/// <summary>
/// 分類id
/// </summary>
public long CategoryId { get; set; }
/// <summary>
/// 價格
/// </summary>
public decimal? Price { get; set; }
/// <summary>
/// 描述
/// </summary>
public string Description { get; set; }
/// <summary>
/// 庫存
/// </summary>
public decimal Stock { get; set; }
}
Order 訂單
繼承BaseEntity類 新增相關屬性
/// <summary>
/// 訂單
/// </summary>
public class Order:BaseEntity
{
/// <summary>
/// 訂單號
/// </summary>
public Guid OrderNo { get; set; } = Guid.NewGuid();
/// <summary>
/// 使用者
/// </summary>
public long UserId { get; set; }
/// <summary>
/// 訂單狀態
/// </summary>
public OrderStatus OrderStatus { get; set; }
/// <summary>
/// 總金額
/// </summary>
public decimal Total { get; set; }
/// <summary>
/// 地址
/// </summary>
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中需要新增對應依賴
/// <summary>
/// 專案模組依賴
/// </summary>
[DependsOn(typeof(MyShopApplicationContractModule),
typeof(MyShopDomainModule))]
/// 元件依賴
[DependsOn(typeof(AbpAutoMapperModule))]
public class MyShopApplicationModule:AbpModule
{
public override void ConfigureServices(ServiceConfigurationContext context)
{
//新增ObjectMapper注入
context.Services.AddAutoMapperObjectMapper<MyShopApplicationModule>();
// Abp AutoMapper設定
Configure<AbpAutoMapperOptions>(config =>
{
// 新增對應依賴關係Profile
config.AddMaps<MyShopApplicationProfile>();
});
}
}
3.實現相關服務
這裡由於我們使用AutoMapper所以需要建立Profile並新增相關對映關係
Profile定義
namespace MyShop.Application.AutoMapper.Profiles
{
public class MyShopApplicationProfile:Profile
{
public MyShopApplicationProfile()
{
CreateMap<Product, ProductItemDto>().ReverseMap();
CreateMap<Order, OrderInfoDto>().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
public class OrderApplicationService : ApplicationService, IOrderApplicationService
{
private readonly IRepository<Order> _orderRepository;
public OrderApplicationService(IRepository<Order> orderRepository)
{
_orderRepository = orderRepository;
}
public async Task<OrderInfoDto> GetAsync(long id)
{
var order = await _orderRepository.GetAsync(g => g.Id == id);
return ObjectMapper.Map<Order, OrderInfoDto>(order);
}
public async Task<List<OrderInfoDto>> GetListAsync()
{
var orders = await _orderRepository.GetListAsync();
return ObjectMapper.Map<List<Order>, List<OrderInfoDto>>(orders);
}
}
ProductApplicationService
/// <summary>
/// 商品服務
/// </summary>
public class ProductApplicationService : ApplicationService, IProductApplicationService
{
/// <summary>
/// 自定義商品倉儲
/// </summary>
private readonly IProductRepository _productRepository;
/// <summary>
/// 商品類別倉儲
/// </summary>
private readonly IRepository<Category> _categoryRepository;
/// <summary>
/// 構造
/// </summary>
/// <param name="productRepository">自定義商品倉儲</param>
/// <param name="categoryRepository">商品類別倉儲</param>
public ProductApplicationService(IProductRepository productRepository,
IRepository<Category> categoryRepository)
{
_productRepository = productRepository;
_categoryRepository = categoryRepository;
}
/// <summary>
/// 獲取商品資訊
/// </summary>
/// <param name="id">商品id</param>
/// <returns></returns>
public async Task<ProductItemDto> GetAsync(long id)
{
return await Task.FromResult(Query(p => p.Id == id).FirstOrDefault(p =>p.Id == id));
}
/// <summary>
/// 獲取分頁商品列表
/// </summary>
/// <param name="page">分頁資訊</param>
/// <returns></returns>
public IPagedResult<ProductItemDto> 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<ProductItemDto>(count,products);
}
/// <summary>
/// 獲取商品列表
/// </summary>
/// <returns></returns>
public async Task<List<ProductItemDto>> GetListAsync()
{
var products = await _productRepository.GetListAsync();
return ObjectMapper.Map<List<Product>, List<ProductItemDto>>(products);
}
private IQueryable<ProductItemDto> Query(Expression<Func<Product,bool>> 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,並且沒有對外依賴
public class MyShopApplicationContractModule:AbpModule
{
}
3.定義Contract
IOrderApplicationService
public interface IOrderApplicationService:IApplicationService
{
Task<OrderInfoDto> GetAsync(long id);
Task<List<OrderInfoDto>> GetListAsync();
}
IProductApplicationService
public interface IProductApplicationService :IApplicationService
{
Task<List<ProductItemDto>> GetListAsync();
IPagedResult<ProductItemDto> GetPage(BasePageInput page);
Task<ProductItemDto> GetAsync(long id);
}
Admin.Application 管理後臺應用層
這裡和Application層差不多,但是為了方便編寫程式碼所以Admin.Contract和Admin.Application 放在了一個專案中,這裡我定義了IBaseAdminCRUDApplicationService介面並且實現了一個BaseAdminCRUDApplicationService實現了一些簡單的CRUD的方法暫時作為後臺資料管理的方法,並且為了區分後臺管理介面和平臺介面在自動註冊為api時加上admin字首
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中需要新增對應依賴
[DependsOn(typeof(MyShopDomainModule))]
[DependsOn(typeof(AbpAutoMapperModule))]
public class MyShopAdminApplicationModule:AbpModule
{
public override void ConfigureServices(ServiceConfigurationContext context)
{
context.Services.AddAutoMapperObjectMapper<MyShopAdminApplicationModule>();
Configure<AbpAutoMapperOptions>(config =>
{
config.AddMaps<MyShopAdminApplicationProfile>();
});
}
}
EntityFreaworkCore資料訪問層中實現Repository
通過Nuget新增 Volo.Abp.EntityFrameworkCore.MySQL
定義DbContext
[ConnectionStringName("Default")]
public class MyShopDbContext:AbpDbContext<MyShopDbContext>
{
public MyShopDbContext(DbContextOptions<MyShopDbContext> options) : base(options)
{
}
protected override void OnModelCreating(ModelBuilder builder)
{
builder.ConfigureProductStore();
builder.ConfigureOrderStore();
builder.ConfigureOrderItemStore();
builder.ConfigureCategoryStore();
}
public DbSet<Product> Products { get; set; }
public DbSet<Order> Orders { get; set; }
public DbSet<OrderItem> OrderItems { get; set; }
public DbSet<Category> Categories { get; set; }
}
這裡通過擴充套件方法分開了實體的一些定義
ConfigureProductStore
public static class ProductCreatingExtension
{
public static void ConfigureProductStore(this ModelBuilder builder)
{
Check.NotNull(builder, nameof(builder));
builder.Entity<Product>(option =>
{
option.ToTable("Product");
option.ConfigureByConvention();
});
}
}
ConfigureOrderStore
public static class OrderCreatingExtension
{
public static void ConfigureOrderStore(this ModelBuilder builder)
{
Check.NotNull(builder, nameof(builder));
builder.Entity<Order>(option =>
{
option.ToTable("Order");
option.ConfigureByConvention();
});
}
}
ConfigureOrderItemStore
public static class OrderItemsCreatingExtension
{
public static void ConfigureOrderItemStore(this ModelBuilder builder)
{
Check.NotNull(builder, nameof(builder));
builder.Entity<OrderItem>(option =>
{
option.ToTable("OrderItem");
option.ConfigureByConvention();
});
}
}
ConfigureCategoryStore
public static class CategoryCreatingExtension
{
public static void ConfigureCategoryStore(this ModelBuilder builder)
{
Check.NotNull(builder, nameof(builder));
builder.Entity<Category>(option =>
{
option.ToTable("Category");
option.ConfigureByConvention();
});
}
}
由於目前Abp vNext提供的 IRepository<TEntity,TKey> 介面已經滿足我們當前的使用需要,也提供了IQuaryable 介面,所以靈活組裝的能力也有,所以不另外實現自定義倉儲,不過這裡還是預先準備一個自定義的ProductRepsitory
在Domain層中定義IProductRepository
public interface IProductRepository : IRepository<Product, long>
{
IQueryable<Product> GetProducts(long id);
}
EntityFrameworkCore資料訪問層中實現IProductRepository
繼承EfCoreRepository以方便實現 IRepository介面中提供的基本方法和 DbContext的訪問,並實現IProductRepository
public class ProductRepository : EfCoreRepository<MyShopDbContext, Product, long>, IProductRepository
{
public ProductRepository(IDbContextProvider<MyShopDbContext> dbContextProvider) : base(dbContextProvider)
{
}
public IQueryable<Product> GetProducts(long id)
{
return DbContext.Products.Where(p => p.Id == id);
}
}
定義模組,注入預設倉儲
[DependsOn(typeof(AbpEntityFrameworkCoreMySQLModule))]
public class MyShopEntityFrameworkCoreModule :AbpModule
{
public override void ConfigureServices(ServiceConfigurationContext context)
{
context.Services.AddAbpDbContext<MyShopDbContext>(option=>
{
// 如果只是用了 entity, 請將 includeAllEntities 設定為 true 否則會提示異常,導致所屬倉儲無法注入
option.AddDefaultRepositories(true);
});
Configure<AbpDbContextOptions>(option=>
{
option.UseMySQL();
});
}
}
Migration 遷移
1.新增引用
通過Nuget安裝 Microsoft.EntityFrameworkCore.Tools
新增Domain,EntiyFrameworkCore資料訪問層的引用
<ItemGroup>
<ProjectReference Include="..\MyShop.EntityFrameworkCore\MyShop.EntityFrameworkCore.csproj" />
<ProjectReference Include="..\MyShop.Entity\MyShop.Domain.csproj" />
</ItemGroup>
2.定義DbContext
這裡我們使用了之前在EntifyFrameworkCore中定義的一些實體配置
[ConnectionStringName("Default")]
public class DbMigrationsContext : AbpDbContext<DbMigrationsContext>
{
public DbMigrationsContext(DbContextOptions<DbMigrationsContext> options) : base(options)
{
}
protected override void OnModelCreating(ModelBuilder builder)
{
base.OnModelCreating(builder);
builder.ConfigureProductStore();
builder.ConfigureOrderStore();
builder.ConfigureOrderItemStore();
builder.ConfigureCategoryStore();
}
}
3.定義模組
[DependsOn(typeof(MyShopEntityFrameworkCoreModule))]
public class MyShopEntityFrameworkCoreMigrationModule:AbpModule
{
public override void ConfigureServices(ServiceConfigurationContext context)
{
context.Services.AddAbpDbContext<DbMigrationsContext>();
}
}
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
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<MyShopApiModule>();
}
// 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
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<Startup>();
})
.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
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中定義的介面了.