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

有什麼不能一笑而過呢發表於2020-10-22

嘗試從零開始構建我的商城 (一) :使用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即可

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,GetAllGet開頭.
Put: 如果方法名稱以PutUpdate開頭.
Delete: 如果方法名稱以DeleteRemove開頭.
Post: 如果方法名稱以Create,Add,InsertPost開頭.
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中定義的介面了.

相關文章