iT邦幫忙

第 12 屆 iThome 鐵人賽

DAY 4
1

本篇同步發文在個人Blog: 一袋.NET要扛幾樓?打造容器化的ASP.NET Core網站!系列文章 - (4) 建立商品服務的Api - 2

 1. 新增繼承DbContext的類別

 在CatalogApi專案新增資料夾Data,新增CatalogContext類別,並繼承DbContext,要與DB存取資料都是以此物件。

  前述的CatalogType和CatalogItem兩個Entity加入至CatalogContext的DbSet,才能對這兩個做CRUD的功能。

  在這使用Fluent API,可以宣告資料庫的表格規範,包含欄位的限制、Relation的設計等。

using CatalogApi.Domain;
using Microsoft.EntityFrameworkCore;

namespace CatalogApi.Data
{
    public class CatalogContext : DbContext
    {
        public CatalogContext(DbContextOptions options) : base(options)
        {

        }

        protected override void OnModelCreating(ModelBuilder builder)
        {
            builder.Entity<CatalogType>(x =>
            {
                x.ToTable("CatalogType");
                x.Property(c => c.Type)
                    .IsRequired()
                    .HasMaxLength(100);
            });

            builder.Entity<CatalogItem>(x =>
            {
                x.ToTable("Catalog");
                x.Property(c => c.Name)
                    .IsRequired()
                    .HasMaxLength(50);
                x.Property(c => c.Price)
                    .IsRequired();
                x.HasOne(c => c.CatalogType)
                    .WithMany()
                    .HasForeignKey(c => c.CatalogTypeId);
            });
        }

        public DbSet<CatalogType> CatalogTypes { get; set; }
        public DbSet<CatalogItem> CatalogItems { get; set; }
    }
}

 2. 新建Docker Compose文件,並啟用Sql Server

  在方案的根目錄,建立docker-compose.yml,並加入至方案。yml的內容如下,其中有幾個參數:

  • networks:將資料庫規範在backend的網路區域,後續其他系統會歸類在其他區域
  • image:從Docker Hub要安裝的SQL Server Image,預設用目前最新的
  • ports:本機端的Port 1445映射Docker container的1433 port,由於本機已經裝過SQL Server,所以要用另外的Port 1445
  • container_name:建立本專案SQL Server的container名稱
  • environment:啟用SQL Server必須給定的參數有ACCEPT_EULA和SA_PASSWORD,前者參數必須指定Y,否則不給用;後者是SQL Server預設管理者SA的密碼
    version: "3.4"
    
    networks:
       backend:
       
    services:
       mssqlserver:
          image: "microsoft/mssql-server-linux:latest"
          ports:
             - "1445:1433"
    
          container_name: rpgmssqlcontainer
          environment:
             - ACCEPT_EULA=Y
             - SA_PASSWORD=JustTest!
          networks:
             - backend

  在docker-compose.yml啟用cmd,並執行

    docker-compose up

  Docker將建立一個SQL Server的Container,此時開啟SSMS,登入的伺服器名稱為 localhost,1445 (要有逗號並串接port),如圖1,代表Docker Container的SQL Server可使用。

圖1

 3. 設定appsettings.json的連線字串

  在appsettings.json增加4個屬性

"DatabaseServer": "localhost,1445",
"DatabaseName": "CatalogDb",
"DatabaseUser": "sa",
"DatabasePassword": "JustTest!"

  將連線字串的主要參數拆解,可以區分用VS和Dokcer的測試參數。

 4. 設定Startup.cs

  在public void ConfigureServices(IServiceCollection services),添加註冊Sql Server的連線資訊和DbContext的Services:

    public void ConfigureServices(IServiceCollection services)
            {
                // other code...
                
                string server = Configuration["DatabaseServer"];
                string database = Configuration["DatabaseName"];
                string user = Configuration["DatabaseUser"];
                string password = Configuration["DatabasePassword"];
                string connectionString = string.Format("Server={0};Database={1};User={2};Password={3};", server, database, user, password);
    
                services.AddDbContext(options => options.UseSqlServer(connectionString));
                
                // other code...
            }

 5. 建立資料庫遷移

  開啟cmd,將路徑切換到CatalogApi.csproj的目錄。假如尚未安裝dotnet ef的工具,在cmd執行

    dotnet tool install --global dotnet-ef --version 3.1.7

  接著輸入第一次遷移的指令:

    dotnet ef migrations add Initialize

  在專案內將產生Migrations的資料夾和遷移相關的.cs檔,如圖2。

圖2

  輸入此遷移的更新資料庫指令:

    dotnet ef database update

  成功的話,用SSMS檢查,可看見資料庫CatalogDb和2個表格Catalog和CatalogType,如圖3。

圖3

 6. 建立預設資料Seed

  商品的類型與列表,在系統剛啟動時會先檢查資料庫是否有資料,如果沒有的話,建立一個Seed程式(或者是SQL腳本),將預設的資料寫進資料庫。

  首先在Data資料夾建立CatalogSeed.cs,建立一個非同步的SeedAsync函式,先檢查CatalogItem和CatalogType是否有任何值,沒有的話則寫入預設的資料。

    using CatalogApi.Domain;
    using System.Collections.Generic;
    using System.Linq;
    using System.Threading.Tasks;
    
    namespace CatalogApi.Data
    {
        public class CatalogSeed
        {
            public static async Task SeedAsync(CatalogContext context)
            {
                if (!context.CatalogTypes.Any())
                {
                    context.CatalogTypes.AddRange(GetDefaultCatalogTypes());
                    await context.SaveChangesAsync();
                }
    
                if (!context.CatalogItems.Any())
                {
                    context.CatalogItems.AddRange(GetDefaultItems());
                    await context.SaveChangesAsync();
                }
            }
    
            static IEnumerable<CatalogType> GetDefaultCatalogTypes()
            {
                return new List<CatalogType>()
                {
                    new CatalogType() { Type = "Head"},
                    new CatalogType() { Type = "Chest" },
                    new CatalogType() { Type = "Hands" },
                    new CatalogType() { Type = "Legs" },
                    new CatalogType() { Type = "Feet" }
                };
            }
            static IEnumerable<CatalogItem> GetDefaultItems()
            {
                return new List<CatalogItem>()
                {
                    new CatalogItem() { CatalogTypeId=1, Description = "Normal Helmet", Name = "Normal Helmet", Price = 10M, PictureFileName = "1.png" },
                    new CatalogItem() { CatalogTypeId=1, Description = "Great Helmet", Name = "Great Helmet", Price= 50M, PictureFileName = "2.png" },
                    new CatalogItem() { CatalogTypeId=2, Description = "Normal Armor", Name = "Normal Armor", Price = 15M, PictureFileName = "3.png" },
                    new CatalogItem() { CatalogTypeId=2, Description = "Great Armor", Name = "Great Armor", Price = 42M, PictureFileName = "4.png" },
                    new CatalogItem() { CatalogTypeId=4, Description = "Normal Robe", Name = "Normal Robe", Price = 12M, PictureFileName = "5.png" },
                    new CatalogItem() { CatalogTypeId=4, Description = "Great Robe", Name = "Great Robe", Price = 44M, PictureFileName = "6.png" },
                    new CatalogItem() { CatalogTypeId=3, Description = "Normal Gloves", Name = "Normal Gloves", Price = 5M, PictureFileName = "7.png" },
                    new CatalogItem() { CatalogTypeId=3, Description = "Great Gloves", Name = "Great Gloves", Price = 69M, PictureFileName = "8.png" },
                    new CatalogItem() { CatalogTypeId=5, Description = "Normal Boots", Name = "Normal Boots", Price = 13M, PictureFileName = "9.png" },
                    new CatalogItem() { CatalogTypeId=5, Description = "Great Boots", Name = "Great Boots", Price = 55M, PictureFileName = "10.png" }
                };
            }
        }
    }

  接著在Program.cs,使用GetRequiredService取的CatalogContext,並呼叫CatalogSeed做寫資料的功能:

    using CatalogApi.Data;
    using Microsoft.AspNetCore.Hosting;
    using Microsoft.Extensions.DependencyInjection;
    using Microsoft.Extensions.Hosting;
    using Microsoft.Extensions.Logging;
    using System;
    
    namespace CatalogApi
    {
        public class Program
        {
            public static void Main(string[] args)
            {
                var host = CreateHostBuilder(args).Build();
                using (var scope = host.Services.CreateScope())
                {
                    var services = scope.ServiceProvider;
                    try
                    {
                        var context = services.GetRequiredService<CatalogContext>();
                        CatalogSeed.SeedAsync(context).Wait();
                    }
                    catch (Exception ex)
                    {
                        var logger = services.GetRequiredService<ILogger<Program>>();
                        logger.LogError(ex, "An error occured while seeding the database");
                    }
                }
    
                host.Run();
            }
    
            public static IHostBuilder CreateHostBuilder(string[] args) =>
                Host.CreateDefaultBuilder(args)
                    .ConfigureWebHostDefaults(webBuilder =>
                    {
                        webBuilder.UseStartup<Startup>();
                    });
        }
    }

  在VS啟用Debug,讓程式執行,再用SSMS確認有寫入預設資料,如圖4。

圖4

--------------------------------------------------------------

  下一篇將撰寫ApiController對商品的CRUD功能設計。


上一篇
[Day 3] 建立商品服務的Api - 1
下一篇
[Day 5] 建立商品服務的Api - 3
系列文
一袋.NET要扛幾樓?打造容器化的ASP.NET Core網站!30
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

1 則留言

0
usnp2001
iT邦新手 5 級 ‧ 2021-09-24 14:52:17

請問一下 [在docker-compose.yml啟用cmd,並執行] 這段該如何執行,yml 檔案是放在方案的目錄下是嗎?等於跟方案底下的專案目錄同層,再來是在visual studio 的方案選擇加入yml檔案是不是會顯示 Solution Items 這樣的資料夾顯示

glj8989332 iT邦研究生 4 級 ‧ 2021-09-25 00:04:55 檢舉

Hi , 可以參考我的github唷~
https://github.com/u8989332/RPGShopOnContainers

我要留言

立即登入留言