Rest API Template 만들기 - EP 07 (EF Core - Migration and Update)

2023. 1. 9. 00:00ASPNET/ASPNET 7

반응형

기존 solution 에 프로젝트를 하나 추가 하자.

.NET 또는 .NET Standard 용 library 가 있을 것이다. 추가하자

 

Project 명은 Infrastructure 라고 지정하자.

최신 .net 을 지원하도록 framework 를 설정하자 (현재 시점 .NET 7.0)

 

개발자 명령 프롬프트로 이동

Ctrl + ` 또는 View --> Terminal click --> 개발자 명령 프롬프트로 이동

개발자 명령 프롬프트에서 Infrastructure 프로젝트 폴더로 이동

**이미 Infrastructure 프로젝트 폴더에 있다면 이동하지 않아도 된다. **

(현재 [솔루션명].sln 파일이 있는 폴더라면 [프로젝트명].csproj 파일이 있는 폴더로 이동)

 

다음 실행

SQLServer 에 대한 DB 공급자 설치

dotnet add package Microsoft.EntityFrameworkCore.SqlServer

 

MySQL 에 대한 DB 공급자 설치

dotnet add package Pomelo.EntityFrameworkCore.MySql --prerelease

 

EF 관련도구 설치

dotnet tool install --global dotnet-ef

아래와 같은 결과가 나온다면 이미 설치되었다는 뜻이니 update 해보자

 ** 업데이트가 필요하다면 dotnet tool update 명령을 사용한다. **

dotnet tool update --global dotnet-ef

 

EF design 패키지 설치

dotnet add package Microsoft.EntityFrameworkCore.Design

 

ASP.NET Core EF Core에 대한 진단 캡처 및 에러 보고

dotnet add package Microsoft.AspNetCore.Diagnostics.EntityFrameworkCore

 

class1.cs 를 삭제 하자.

Models 폴더를 추가하고 다음 class 를 두개 추가 하자

 

로그인시 token 관련 정보를 저장하는 model

Models/Login.cs

using Microsoft.EntityFrameworkCore;
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;
using System.Diagnostics.CodeAnalysis;

namespace Infrastructure.Models;
[Index(nameof(RefreshToken), Name = "IX_Login_RefreshToken")]
public class Login
{
    [Key]
    public long Id { get; set; }
    [StringLength(1024, MinimumLength = 100)]
    public string AccessToken { get; set; }
    [StringLength(40, MinimumLength = 32)]
    public string RefreshToken { get; set; }
    [NotNull]
    public DateTime? RefreshTokenExpired { get; set; } = DateTime.UtcNow;
    [NotNull]
    public DateTime LoginTime { get; set; } = DateTime.UtcNow;
    public bool UseFlag { get; set; } = false;
    [ForeignKey("UserId")]
    public virtual User User { get; set; }
}

 

유저 관련 정보를 저장하는 model

Models/User.cs

using System.ComponentModel.DataAnnotations;
using System.Diagnostics.CodeAnalysis;

namespace Infrastructure.Models;
public class User
{
    [Key]
    [StringLength(20, MinimumLength = 5)]
    public string Id { get; set; }
    [StringLength(40, MinimumLength = 1)]
    public string Name { get; set; }
    [StringLength(256), NotNull]
    public string Password { get; set; }
    public bool UseFlag { get; set; } = false;
    public virtual List<Login> Logins { get; } = new();
}

 

ApiServerContext.cs 를 추가하자

ApiServerContext.cs

using Infrastructure.Models;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Configuration;

namespace Infrastructure;
public partial class ApiserverContext : DbContext
{
    protected readonly IConfiguration _configuration;

    public ApiserverContext(IConfiguration configuration)
    {
        _configuration = configuration;
    }

    protected ApiserverContext(IConfiguration configuration, DbContextOptions options) : base(options)
    {
        _configuration = configuration;
    }
    public ApiserverContext(IConfiguration configuration, DbContextOptions<ApiserverContext> options)
        : base(options)
    {
        _configuration = configuration;
    }

    public virtual DbSet<Login> Logins { get; set; }
    public virtual DbSet<User> Users { get; set; }

    /// <summary>
    /// Do not use Unicode as the default type for string
    /// </summary>
    /// <param name="modelBuilder"></param>
    private void DisableUnicodeToString(ModelBuilder modelBuilder)
    {
        foreach (var property in modelBuilder.Model.GetEntityTypes()
                .SelectMany(t => t.GetProperties())
                .Where(
                       p => p.ClrType == typeof(string)    // Entity is a string
                    && p.GetColumnType() == null           // No column type is set
                ))
        {
            property.SetIsUnicode(false);
        }
    }

    partial void OnModelCreatingPartial(ModelBuilder modelBuilder);
    /// <summary>
    /// database first 로 작업할 경우
    /// db 쪽 table 에 column 을 추가하거나 수정한 후
    /// scaffold 를 통해 모델을 생성하면 새로 모델이 생성되어
    /// 사용자가 추가한 몇가지 코드를 (OnModelCreating) 다시 복사후 옮겨줘야 한다.
    /// 이러한 문제를 해결하기 위해 OnModelCrateingPartial 에 사용자가 정의한
    /// 내용들을 처리 하도록 할 수 있다. 
    /// </summary>
    /// <param name="modelBuilder"></param>
    partial void OnModelCreatingPartial(ModelBuilder modelBuilder)
    {
        //DisableUnicodeToString(modelBuilder);
    }

}

 

이제 위 ApiServerContext 를 상속한 context 두개를 만들것이다. 

이유는 다음과 같다. 

DBMS 가 다를경우 (sqlserver, oracle, mysql, mariadb ...etc) 

model 은 ApiServerContext 에 DbSett 을 이용해 (User, Login)  정의 하여 함께 사용하고 

scheme 는 DBMS 별로 차이점이 있으므로 ApiserverContext 를 상속받아 각각이 구현하도록 하는 것이다. 

물론 여기서는 구조만 만들어 낼것이고 scheme 정의를 따로 하지는 않을것이다. 

 

여튼 (anyway) 이제 SQLServer 위한 SqlServerContext 와 MySql 을 위한 MySqlContext 를 생성하겠다.

SqlServerContext.cs

using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Configuration;

namespace Infrastructure;
public class SqlServerContext : ApiserverContext
{
    public SqlServerContext(IConfiguration configuration) : base(configuration)
    {
    }
    protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
    {
        if (!optionsBuilder.IsConfigured)
        {
            string? connectionString = _configuration.GetConnectionString("Server");
            optionsBuilder.UseSqlServer(connectionString);
        }
    }
}

MySqlContext.cs

using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Configuration;

namespace Infrastructure;
public class MySqlContext : ApiserverContext
{
	public MySqlContext(IConfiguration configuration) : base(configuration)
    {

	}
    protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
    {
        if (!optionsBuilder.IsConfigured)
        {
            string? connectionString = _configuration.GetConnectionString("Server");
            optionsBuilder.UseMySql(connectionString, ServerVersion.AutoDetect(connectionString));
        }
    }
}

DB 연결을 위한 extension method 추가

Extensions\DbContextConfigurationExtensions.cs

using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.DependencyInjection;

namespace Infrastructure.Extensions;
public static class DbContextConfigurationExtensions
{
    public static void AddDataAccessServices(this IServiceCollection self, string connectionString, string dbProvider = "SqlServer", int maxRetryCount = 3)
    {
        if (dbProvider.Equals("MySql", StringComparison.OrdinalIgnoreCase))
        {
            self.AddDbContext<ApiServerContext, MySqlContext>
                (options => options.UseMySql(connectionString, ServerVersion.AutoDetect(connectionString),
                            x => x.EnableRetryOnFailure(maxRetryCount: maxRetryCount)));
        }
        else
        {
            self.AddDbContext<ApiServerContext, SqlServerContext>
                (options => options.UseSqlServer(connectionString,
                            x => x.EnableRetryOnFailure(maxRetryCount: maxRetryCount)));
        }
    }
}

 

이제 APIServer 에 Infrastructure project 를 참조 함자

이제 DB 를 연결해 보자

 

ApiServer 프로젝트로 이동하자

 

DB 종류별로 구분하기 위해 DbProvider 정보를 추가하자.

ConnectionStrings 을 이용하여 DB 연결 정보를 추가하자.

 

Appsettings.json

{
...
"DbProvider": "MsSql",
"ConnectionStrings": {
    "Server": "Data Source=(localdb)\\MSSQLLocalDB;Database=ApiServer;Trusted_Connection=True;"
  }
..
}

이제 Program.cs 로 가서 db 를 연결 하도록 해보자

Program.cs

...
using Infrastructure.Extensions;
...
string connectionString = builder.Configuration.GetConnectionString("Server") ?? string.Empty;
builder.Services.AddDataAccessServices(connectionString);
...

 

자 이제 기본적인 준비는 되었다. 연결된 db 를 migration, create or update 해보자.

 

Ctrl + ` 또는 View --> Terminal click --> 개발자 명령 프롬프트로 이동

개발자 명령 프롬프트에서 솔루션 폴더로 이동

**이미 솔루션(.sln) 폴더에 있다면 이동하지 않아도 된다.**

(현재 [프로젝트명].csproj 파일이 있는 폴더라면 [솔루션명].sln 파일이 있는 폴더로 이동)

dotnet ef migrations add InitialCreate --project Infrastructure --startup-project ApiServer --context SqlServerContext

위 명령 실행시 다음과 같은 error 가 날 수 있다. 

Your startup project 'ApiServer' doesn't reference Microsoft.EntityFrameworkCore.Design. 
This package is required for the Entity Framework Core Tools to work. 
Ensure your startup project is correct, install the package, and try again.

Infrastructure --> Edit Project File 

다음과 같이 주석 처리

명령 재 실행

 

이제 명령이 실행 되긴 하는데 아래와 같은 warning 이 있을 수 있다. 

우리는 try catch 로 host error 발생했을때 serilog 를 통해 log 를 작성하도록 하였다. 

log system 이 초기화 되기 전에 나는 오류를 처리하기 위해서 이다. 

그런데 ef migration 이 dotnet 6 부터 뭔가 error 를 throw 하고 있다. 이부분을 제대로 처리 못하고 있는 걸로 보인다.

ef 7 에서도 해결되지 않아고 이문제는 여전히 존재한다. 

물론 오류가 나타난다고 해서 ef migration 이 안되는것은 아닌다. 

[16:31:28 FTL] HostAbortedException Host terminated unexpectedly
Microsoft.Extensions.Hosting.HostAbortedException: The host was aborted.
   at Microsoft.Extensions.Hosting.HostFactoryResolver.HostingListener.ThrowHostAborted()
   at Microsoft.Extensions.Hosting.HostFactoryResolver.HostingListener.OnNext(KeyValuePair`2 value)
   at System.Diagnostics.DiagnosticListener.Write(String name, Object value)
   at Microsoft.Extensions.Hosting.HostBuilder.ResolveHost(IServiceProvider serviceProvider, DiagnosticListener diagnosticListener)
   at Microsoft.Extensions.Hosting.HostApplicationBuilder.Build()
   at Microsoft.AspNetCore.Builder.WebApplicationBuilder.Build()
   ...
Done. To undo this action, use 'ef migrations remove'

Program.cs 에서 try catch 구분을 다음과 같이 수정하자 (만약 try catch 하고 있다면)

catch (Exception ex)
{
    // ef migration 시에 나타나는 오류를 log 로 보내는 코드
    // https://github.com/dotnet/runtime/issues/60600
    string type = ex.GetType().Name;
    if (type.Equals("StopTheHostException", StringComparison.Ordinal))
        throw;
    if (type.Equals("HostAbortedException", StringComparison.Ordinal))
        throw;
    Log.Fatal(ex, $"{type} Host terminated unexpectedly");
}
finally
{
    Log.CloseAndFlush();
}

anyway 이제 migration 은 정상적으로 동작 하였고 db 에 실제로 update 해보자 (create db)

 

Ctrl + ` 또는 View --> Terminal click --> 개발자 명령 프롬프트로 이동

개발자 명령 프롬프트에서 솔루션 폴더로 이동

**이미 솔루션(.sln) 폴더에 있다면 이동하지 않아도 된다.**

(현재 [프로젝트명].csproj 파일이 있는 폴더라면 [솔루션명].sln 파일이 있는 폴더로 이동)

dotnet ef database update --project Infrastructure --startup-project ApiServer --context SqlServerContext

SQL Server Object Explorer 을 통해 제대로 create 되었는지 확인하자

View --> SQL Server Object Explorer

 

Model 을 수정하면 add migration 을 다시 실행하여 (새로운 이름 지정) 수정된 내용을 migration 파일에 적용하고 database update 를 통해 DB (Sqlserver, MySql) 에 적용하자

 

관련영상

https://youtu.be/A2qoW8Jpz64

반응형