다음을 통해 공유


EF Core 6.0의 새로운 기능

EF Core 6.0이 NuGet으로 배송되었습니다. 이 페이지에는 이 릴리스에 도입된 흥미로운 변경 내용의 개요가 포함되어 있습니다.

팁 (조언)

GitHub에서 샘플 코드를 다운로드하여 아래 표시된 샘플을 실행하고 디버그할 수 있습니다.

SQL Server 임시 테이블

GitHub 문제: #4693.

SQL Server temporal 테이블 은 해당 데이터가 업데이트되거나 삭제된 후에도 테이블에 저장된 모든 데이터를 자동으로 추적합니다. 이는 주 테이블을 변경할 때마다 타임스탬프가 지정된 기록 데이터가 저장되는 병렬 "기록 테이블"을 만들어서 수행됩니다. 이렇게 하면 실수로 변경되거나 삭제된 후 복구와 같이 감사 또는 복원과 같은 기록 데이터를 쿼리할 수 있습니다.

이제 EF Core에서 다음을 지원합니다.

  • 마이그레이션을 사용하여 임시 테이블 만들기
  • 마이그레이션을 사용하여 기존 테이블을 임시 테이블로 변환
  • 기록 데이터 쿼리
  • 과거의 특정 지점에서 데이터 복원

임시 테이블 구성

모델 작성기를 사용하여 테이블을 temporal로 구성할 수 있습니다. 다음은 그 예입니다.

modelBuilder
    .Entity<Employee>()
    .ToTable("Employees", b => b.IsTemporal());

EF Core를 사용하여 데이터베이스를 만들 때 새 테이블은 타임스탬프 및 기록 테이블에 대한 SQL Server 기본값을 사용하여 임시 테이블로 구성됩니다. 엔터티 유형 Employee을(를) 예로 들어 고려합니다.

public class Employee
{
    public Guid EmployeeId { get; set; }
    public string Name { get; set; }
    public string Position { get; set; }
    public string Department { get; set; }
    public string Address { get; set; }
    public decimal AnnualSalary { get; set; }
}

만든 임시 테이블은 다음과 같습니다.

DECLARE @historyTableSchema sysname = SCHEMA_NAME()
EXEC(N'CREATE TABLE [Employees] (
    [EmployeeId] uniqueidentifier NOT NULL,
    [Name] nvarchar(100) NULL,
    [Position] nvarchar(100) NULL,
    [Department] nvarchar(100) NULL,
    [Address] nvarchar(1024) NULL,
    [AnnualSalary] decimal(10,2) NOT NULL,
    [PeriodEnd] datetime2 GENERATED ALWAYS AS ROW END NOT NULL,
    [PeriodStart] datetime2 GENERATED ALWAYS AS ROW START NOT NULL,
    CONSTRAINT [PK_Employees] PRIMARY KEY ([EmployeeId]),
    PERIOD FOR SYSTEM_TIME([PeriodStart], [PeriodEnd])
) WITH (SYSTEM_VERSIONING = ON (HISTORY_TABLE = [' + @historyTableSchema + N'].[EmployeeHistory]))');

SQL Server가 두 개의 숨겨진 datetime2 열, PeriodEndPeriodStart을 만든다는 것을 확인하십시오. 이러한 "기간 열"은 행의 데이터가 있는 시간 범위를 나타냅니다. 이러한 열은 EF Core 모델의 섀도 속성 에 매핑되므로 나중에 표시된 대로 쿼리에서 사용할 수 있습니다.

중요합니다

이러한 열의 시간은 항상 SQL Server에서 생성된 UTC 시간입니다. UTC 시간은 아래 표시된 쿼리와 같이 임시 테이블과 관련된 모든 작업에 사용됩니다.

또한 EmployeeHistory라는 관련된 이력 테이블이 자동으로 생성됩니다. 모델 작성기에 대한 추가 구성을 사용하여 기간 열 및 기록 테이블의 이름을 변경할 수 있습니다. 다음은 그 예입니다.

modelBuilder
    .Entity<Employee>()
    .ToTable(
        "Employees",
        b => b.IsTemporal(
            b =>
            {
                b.HasPeriodStart("ValidFrom");
                b.HasPeriodEnd("ValidTo");
                b.UseHistoryTable("EmployeeHistoricalData");
            }));

이는 SQL Server에서 만든 테이블에 반영됩니다.

DECLARE @historyTableSchema sysname = SCHEMA_NAME()
EXEC(N'CREATE TABLE [Employees] (
    [EmployeeId] uniqueidentifier NOT NULL,
    [Name] nvarchar(100) NULL,
    [Position] nvarchar(100) NULL,
    [Department] nvarchar(100) NULL,
    [Address] nvarchar(1024) NULL,
    [AnnualSalary] decimal(10,2) NOT NULL,
    [ValidFrom] datetime2 GENERATED ALWAYS AS ROW START NOT NULL,
    [ValidTo] datetime2 GENERATED ALWAYS AS ROW END NOT NULL,
    CONSTRAINT [PK_Employees] PRIMARY KEY ([EmployeeId]),
    PERIOD FOR SYSTEM_TIME([ValidFrom], [ValidTo])
) WITH (SYSTEM_VERSIONING = ON (HISTORY_TABLE = [' + @historyTableSchema + N'].[EmployeeHistoricalData]))');

임시 테이블 사용

대부분의 경우 임시 테이블은 다른 테이블과 마찬가지로 사용됩니다. 즉, 기간 열 및 기록 데이터는 애플리케이션에서 무시할 수 있도록 SQL Server에서 투명하게 처리됩니다. 예를 들어 새 엔터티를 일반적인 방식으로 데이터베이스에 저장할 수 있습니다.

context.AddRange(
    new Employee
    {
        Name = "Pinky Pie",
        Address = "Sugarcube Corner, Ponyville, Equestria",
        Department = "DevDiv",
        Position = "Party Organizer",
        AnnualSalary = 100.0m
    },
    new Employee
    {
        Name = "Rainbow Dash",
        Address = "Cloudominium, Ponyville, Equestria",
        Department = "DevDiv",
        Position = "Ponyville weather patrol",
        AnnualSalary = 900.0m
    },
    new Employee
    {
        Name = "Fluttershy",
        Address = "Everfree Forest, Equestria",
        Department = "DevDiv",
        Position = "Animal caretaker",
        AnnualSalary = 30.0m
    });

await context.SaveChangesAsync();

그런 다음 이 데이터를 일반적인 방식으로 쿼리, 업데이트 및 삭제할 수 있습니다. 다음은 그 예입니다.

var employee = await context.Employees.SingleAsync(e => e.Name == "Rainbow Dash");
context.Remove(employee);
await context.SaveChangesAsync();

또한 일반 추적 쿼리 후에는 추적된 엔터티에서 현재 데이터의 기간 열 값에 액세스할 수 있습니다. 다음은 그 예입니다.

var employees = await context.Employees.ToListAsync();
foreach (var employee in employees)
{
    var employeeEntry = context.Entry(employee);
    var validFrom = employeeEntry.Property<DateTime>("ValidFrom").CurrentValue;
    var validTo = employeeEntry.Property<DateTime>("ValidTo").CurrentValue;

    Console.WriteLine($"  Employee {employee.Name} valid from {validFrom} to {validTo}");
}

그러면 다음이 출력됩니다.

Starting data:
  Employee Pinky Pie valid from 8/26/2021 4:38:58 PM to 12/31/9999 11:59:59 PM
  Employee Rainbow Dash valid from 8/26/2021 4:38:58 PM to 12/31/9999 11:59:59 PM
  Employee Fluttershy valid from 8/26/2021 4:38:58 PM to 12/31/9999 11:59:59 PM

ValidTo 열(기본적으로 PeriodEnd로 명명됨)에는 datetime2 최대값이 포함됩니다. 이것은 테이블의 현재 행에 항상 해당합니다. ValidFrom 열(기본적으로 PeriodStart로 불림)에는 행이 삽입된 UTC 시간이 포함됩니다.

기록 데이터 쿼리

EF Core는 다음과 같은 여러 새 쿼리 연산자를 통해 기록 데이터를 포함하는 쿼리를 지원합니다.

  • TemporalAsOf: 지정된 UTC 시간에 활성(현재)인 행을 반환합니다. 지정된 기본 키에 대한 현재 테이블 또는 기록 테이블의 단일 행입니다.
  • TemporalAll: 기록 데이터의 모든 행을 반환합니다. 일반적으로 기록 테이블 및/또는 지정된 기본 키에 대한 현재 테이블의 많은 행입니다.
  • TemporalFromTo: 지정된 두 UTC 시간 사이에 활성화된 모든 행을 반환합니다. 이는 기록 테이블 및/또는 지정된 기본 키에 대한 현재 테이블의 많은 행일 수 있습니다.
  • TemporalBetween: 위쪽 경계에서 활성화된 행이 포함된다는 점을 제외하고는 동일합니다 TemporalFromTo.
  • TemporalContainedIn: 활성화되기 시작하고 지정된 두 UTC 시간 사이에 활성 상태인 모든 행을 반환합니다. 이는 기록 테이블 및/또는 지정된 기본 키에 대한 현재 테이블의 많은 행일 수 있습니다.

비고

이러한 각 연산자마다 정확히 어떤 행이 포함되는지에 대한 자세한 내용은 SQL Server 임시 테이블 설명서를 참조하세요.

예를 들어 데이터를 일부 업데이트하고 삭제한 후에는 기록 데이터를 확인하는 데 사용하는 TemporalAll 쿼리를 실행할 수 있습니다.

var history = await context
    .Employees
    .TemporalAll()
    .Where(e => e.Name == "Rainbow Dash")
    .OrderBy(e => EF.Property<DateTime>(e, "ValidFrom"))
    .Select(
        e => new
        {
            Employee = e,
            ValidFrom = EF.Property<DateTime>(e, "ValidFrom"),
            ValidTo = EF.Property<DateTime>(e, "ValidTo")
        })
    .ToListAsync();

foreach (var pointInTime in history)
{
    Console.WriteLine(
        $"  Employee {pointInTime.Employee.Name} was '{pointInTime.Employee.Position}' from {pointInTime.ValidFrom} to {pointInTime.ValidTo}");
}

주목하십시오 EF.Property 메서드를 사용하여 기간 열의 값에 액세스할 수 있는 방법을. 이 값은 절에서 OrderBy 데이터를 정렬한 다음 반환된 데이터에 이러한 값을 포함하도록 프로젝션에 사용됩니다.

이 쿼리는 다음 데이터를 다시 가져옵니다.

Historical data for Rainbow Dash:
  Employee Rainbow Dash was 'Ponyville weather patrol' from 8/26/2021 4:38:58 PM to 8/26/2021 4:40:29 PM
  Employee Rainbow Dash was 'Wonderbolt Trainee' from 8/26/2021 4:40:29 PM to 8/26/2021 4:41:59 PM
  Employee Rainbow Dash was 'Wonderbolt Reservist' from 8/26/2021 4:41:59 PM to 8/26/2021 4:43:29 PM
  Employee Rainbow Dash was 'Wonderbolt' from 8/26/2021 4:43:29 PM to 8/26/2021 4:44:59 PM

반환된 마지막 행이 2021년 8월 26일 오후 4:44:59에 비활성 상태로 전환되었습니다. 그 당시 레인보우 대시의 행이 주 테이블에서 삭제되었기 때문입니다. 나중에 이 데이터를 복원하는 방법을 살펴보겠습니다.

유사한 쿼리는 TemporalFromTo, TemporalBetween, 또는 TemporalContainedIn를 사용하여 작성할 수 있습니다. 다음은 그 예입니다.

var history = await context
    .Employees
    .TemporalBetween(timeStamp2, timeStamp3)
    .Where(e => e.Name == "Rainbow Dash")
    .OrderBy(e => EF.Property<DateTime>(e, "ValidFrom"))
    .Select(
        e => new
        {
            Employee = e,
            ValidFrom = EF.Property<DateTime>(e, "ValidFrom"),
            ValidTo = EF.Property<DateTime>(e, "ValidTo")
        })
    .ToListAsync();

이 쿼리는 다음 행을 반환합니다.

Historical data for Rainbow Dash between 8/26/2021 4:41:14 PM and 8/26/2021 4:42:44 PM:
  Employee Rainbow Dash was 'Wonderbolt Trainee' from 8/26/2021 4:40:29 PM to 8/26/2021 4:41:59 PM
  Employee Rainbow Dash was 'Wonderbolt Reservist' from 8/26/2021 4:41:59 PM to 8/26/2021 4:43:29 PM

기록 데이터 복원

위에서 설명한 대로 레인보우 대시가 테이블에서 삭제되었습니다 Employees . 이것은 분명히 실수였기 때문에 특정 시점으로 돌아가서 그 때부터 누락된 행을 복원해 보겠습니다.

var employee = await context
    .Employees
    .TemporalAsOf(timeStamp2)
    .SingleAsync(e => e.Name == "Rainbow Dash");

context.Add(employee);
await context.SaveChangesAsync();

이 쿼리는 지정된 UTC 시간에 있었던 것처럼 레인보우 대시에 대한 단일 행을 반환합니다. 임시 연산자를 사용하는 모든 쿼리는 기본적으로 추적되지 않으므로 여기에서 반환된 엔터티는 추적되지 않습니다. 이는 현재 주 테이블에 존재하지 않기 때문에 의미가 있습니다. 주 테이블에 엔터티를 다시 삽입하려면 간단히 Added로 표시하고 SaveChanges를 호출합니다.

레인보우 대시 행을 다시 삽입한 후 기록 데이터를 쿼리하면 지정된 UTC 시간에 행이 복원되었음을 알 수 있습니다.

Historical data for Rainbow Dash:
  Employee Rainbow Dash was 'Ponyville weather patrol' from 8/26/2021 4:38:58 PM to 8/26/2021 4:40:29 PM
  Employee Rainbow Dash was 'Wonderbolt Trainee' from 8/26/2021 4:40:29 PM to 8/26/2021 4:41:59 PM
  Employee Rainbow Dash was 'Wonderbolt Reservist' from 8/26/2021 4:41:59 PM to 8/26/2021 4:43:29 PM
  Employee Rainbow Dash was 'Wonderbolt' from 8/26/2021 4:43:29 PM to 8/26/2021 4:44:59 PM
  Employee Rainbow Dash was 'Wonderbolt Trainee' from 8/26/2021 4:44:59 PM to 12/31/9999 11:59:59 PM

마이그레이션 번들

GitHub 문제: #19693.

EF Core 마이그레이션은 EF 모델의 변경 내용을 기반으로 데이터베이스 스키마 업데이트를 생성하는 데 사용됩니다. 이러한 스키마 업데이트는 종종 연속 통합/지속적인 배포(C.I./C.D.) 시스템의 일부로 애플리케이션 배포 시 적용되어야 합니다.

이제 EF Core는 이러한 스키마 업데이트를 적용하는 새로운 방법인 마이그레이션 번들을 포함합니다. 마이그레이션 번들은 마이그레이션을 포함하는 작은 실행 파일이며 이러한 마이그레이션을 데이터베이스에 적용하는 데 필요한 코드입니다.

비고

마이그레이션, 번들 및 배포에 대한 자세한 내용은 .NET 블로그에서 DevOps 친화적인 EF Core 마이그레이션 번들 소개 를 참조하세요.

마이그레이션 번들은 dotnet ef 명령줄 도구를 사용하여 만들어집니다. 계속하기 전의 최신 버전의 도구를 설치했는지 확인합니다.

번들에는 포함할 마이그레이션이 필요합니다. 이들은 dotnet ef migrations add에 설명된 대로 를 사용하여 만들어집니다. 마이그레이션을 배포할 준비가 되면 dotnet ef migrations bundle을 사용하여 번들을 만듭니다. 다음은 그 예입니다.

PS C:\local\AllTogetherNow\SixOh> dotnet ef migrations bundle
Build started...
Build succeeded.
Building bundle...
Done. Migrations Bundle: C:\local\AllTogetherNow\SixOh\efbundle.exe
PS C:\local\AllTogetherNow\SixOh>

출력은 대상 운영 체제에 적합한 실행 파일입니다. 내 경우에는 Windows x64이므로 로컬 폴더에 efbundle.exe가 삭제됩니다. 이 실행 파일을 실행하면 그 안에 포함된 마이그레이션이 적용됩니다.

PS C:\local\AllTogetherNow\SixOh> .\efbundle.exe
Applying migration '20210903083845_MyMigration'.
Done.
PS C:\local\AllTogetherNow\SixOh>

마이그레이션은 아직 적용되지 않은 경우에만 데이터베이스에 적용됩니다. 예를 들어, 동일한 번들을 다시 실행해도 적용되는 새 마이그레이션이 없으므로 아무 작업도 수행하지 않습니다.

PS C:\local\AllTogetherNow\SixOh> .\efbundle.exe
No migrations were applied. The database is already up to date.
Done.
PS C:\local\AllTogetherNow\SixOh>

그러나 모델이 변경되고 dotnet ef migrations add를 사용하여 더 많은 마이그레이션이 생성된 경우에는 적용할 준비가 된 새 실행 파일에 번들로 묶을 수 있습니다. 다음은 그 예입니다.

PS C:\local\AllTogetherNow\SixOh> dotnet ef migrations add SecondMigration
Build started...
Build succeeded.
Done. To undo this action, use 'ef migrations remove'
PS C:\local\AllTogetherNow\SixOh> dotnet ef migrations add Number3
Build started...
Build succeeded.
Done. To undo this action, use 'ef migrations remove'
PS C:\local\AllTogetherNow\SixOh> dotnet ef migrations bundle --force
Build started...
Build succeeded.
Building bundle...
Done. Migrations Bundle: C:\local\AllTogetherNow\SixOh\efbundle.exe
PS C:\local\AllTogetherNow\SixOh>

--force 옵션을 사용하여 기존 번들을 새 번들로 덮어쓸 수 있습니다.

이 새 번들을 실행하면 데이터베이스에 새로운 두 개의 마이그레이션이 적용됩니다.

PS C:\local\AllTogetherNow\SixOh> .\efbundle.exe
Applying migration '20210903084526_SecondMigration'.
Applying migration '20210903084538_Number3'.
Done.
PS C:\local\AllTogetherNow\SixOh>

기본적으로 번들은 애플리케이션 구성의 데이터베이스 연결 문자열을 사용합니다. 그러나 명령줄에서 연결 문자열을 전달하여 다른 데이터베이스를 마이그레이션할 수 있습니다. 다음은 그 예입니다.

PS C:\local\AllTogetherNow\SixOh> .\efbundle.exe --connection "Data Source=(LocalDb)\MSSQLLocalDB;Database=SixOhProduction"
Applying migration '20210903083845_MyMigration'.
Applying migration '20210903084526_SecondMigration'.
Applying migration '20210903084538_Number3'.
Done.
PS C:\local\AllTogetherNow\SixOh>

이번에는 프로덕션 데이터베이스에 아직 적용되지 않았기 때문에 세 가지 마이그레이션이 모두 적용되었습니다.

다른 옵션은 명령줄에 전달할 수 있습니다. 몇 가지 일반 옵션은 다음과 같습니다.

  • --output 만들 실행 파일의 경로를 지정합니다.
  • --context 프로젝트에 여러 컨텍스트 형식이 포함될 때 사용할 DbContext 형식을 지정합니다.
  • --project 사용할 프로젝트를 지정합니다. 기본값은 현재 작업 디렉터리입니다.
  • --startup-project 사용할 시작 프로젝트를 지정합니다. 기본값은 현재 작업 디렉터리입니다.
  • --no-build 명령을 실행하기 전에 프로젝트가 빌드되지 않도록 합니다. 이 작업은 프로젝트가 up-to-date로 알려진 경우에만 사용해야 합니다.
  • --verbose 명령이 수행하는 작업에 대한 자세한 정보를 확인합니다. 버그 보고서에 정보를 포함할 때 이 옵션을 사용합니다.

사용 가능한 모든 옵션을 확인하는 데 사용합니다 dotnet ef migrations bundle --help .

기본적으로 각 마이그레이션은 자체 트랜잭션에 적용됩니다. 이 영역의 향후 개선 사항에 대한 자세한 내용은 GitHub 문제 #22616 을 참조하세요.

사전 컨벤션 모델 구성

GitHub 문제: #12229.

이전 버전의 EF Core에서는 해당 매핑이 기본값과 다를 때 지정된 형식의 모든 속성에 대한 매핑을 명시적으로 구성해야 합니다. 여기에는 문자열의 최대 길이 및 소수 자릿수와 같은 "패싯"과 속성 형식에 대한 값 변환이 포함됩니다.

이렇게 하려면 다음 중 하나가 필요했습니다.

  • 각 속성에 대한 모델 작성기 구성
  • 각 속성의 매핑 특성
  • 모든 엔터티 형식의 모든 속성에 대한 명시적 반복 및 모델을 빌드할 때 하위 수준 메타데이터 API 사용

이 반복이 발생할 때 엔터티 형식 및 매핑된 속성 목록이 최종적이지 않을 수 있으므로 명시적 반복은 오류가 발생하기 쉬우며 강력하게 실행하기 어렵습니다.

EF Core 6.0을 사용하면 지정된 형식에 대해 이 매핑 구성을 한 번 지정할 수 있습니다. 그런 다음 모델에서 해당 형식의 모든 속성에 적용됩니다. 이를 "사전 규칙 모델 구성"이라고 합니다. 모델 빌드 규칙에서 사용되는 모델의 측면을 구성하기 때문에 이 구성은 "사전 규칙 모델 구성"입니다. 이러한 구성은 ConfigureConventions을 재정의하여 DbContext에 적용됩니다.

public class SomeDbContext : DbContext
{
    protected override void ConfigureConventions(
        ModelConfigurationBuilder configurationBuilder)
    {
        // Pre-convention model configuration goes here
    }
}

예를 들어 다음 엔터티 형식을 고려합니다.

public class Customer
{
    public int Id { get; set; }
    public string Name { get; set; }
    public bool IsActive { get; set; }
    public Money AccountValue { get; set; }

    public Session CurrentSession { get; set; }

    public ICollection<Order> Orders { get; } = new List<Order>();
}

public class Order
{
    public int Id { get; set; }
    public string SpecialInstructions { get; set; }
    public DateTime OrderDate { get; set; }
    public bool IsComplete { get; set; }
    public Money Price { get; set; }
    public Money? Discount { get; set; }

    public Customer Customer { get; set; }
}

모든 문자열 속성은 ANSI(유니코드 대신)로 구성되고 최대 길이는 1024입니다.

configurationBuilder
    .Properties<string>()
    .AreUnicode(false)
    .HaveMaxLength(1024);

DateTime에서 longs로의 기본 변환을 사용하여 모든 DateTime 속성을 데이터베이스의 64비트 정수로 변환할 수 있습니다.

configurationBuilder
    .Properties<DateTime>()
    .HaveConversion<long>();

모든 bool 속성은 정 0 수로 변환하거나 1 기본 제공 값 변환기 중 하나를 사용할 수 있습니다.

configurationBuilder
    .Properties<bool>()
    .HaveConversion<BoolToZeroOneConverter<int>>();

엔터티의 임시 속성이며 유지해서는 안 된다고 가정 Session 하면 모델의 모든 위치에서 무시될 수 있습니다.

configurationBuilder
    .IgnoreAny<Session>();

관례 이전 모델 구성은 값 객체로 작업할 때 매우 유용합니다. 예를 들어 위의 모델의 형식 Money 은 읽기 전용 구조체로 표시됩니다.

public readonly struct Money
{
    [JsonConstructor]
    public Money(decimal amount, Currency currency)
    {
        Amount = amount;
        Currency = currency;
    }

    public override string ToString()
        => (Currency == Currency.UsDollars ? "$" : "£") + Amount;

    public decimal Amount { get; }
    public Currency Currency { get; }
}

public enum Currency
{
    UsDollars,
    PoundsSterling
}

그런 다음 사용자 지정 값 변환기를 사용하여 JSON과 직렬화됩니다.

public class MoneyConverter : ValueConverter<Money, string>
{
    public MoneyConverter()
        : base(
            v => JsonSerializer.Serialize(v, (JsonSerializerOptions)null),
            v => JsonSerializer.Deserialize<Money>(v, (JsonSerializerOptions)null))
    {
    }
}

이 값 변환기는 Money의 모든 용도에 대해 한 번 구성할 수 있습니다.

configurationBuilder
    .Properties<Money>()
    .HaveConversion<MoneyConverter>()
    .HaveMaxLength(64);

또한 직렬화된 JSON이 저장되는 문자열 열에 대해 추가 패싯을 지정할 수 있습니다. 이 경우 열의 최대 길이는 64로 제한됩니다.

마이그레이션을 사용하여 SQL Server용으로 만든 테이블은 구성이 매핑된 모든 열에 적용된 방법을 보여 줍니다.

CREATE TABLE [Customers] (
    [Id] int NOT NULL IDENTITY,
    [Name] varchar(1024) NULL,
    [IsActive] int NOT NULL,
    [AccountValue] nvarchar(64) NOT NULL,
    CONSTRAINT [PK_Customers] PRIMARY KEY ([Id])
);
CREATE TABLE [Order] (
    [Id] int NOT NULL IDENTITY,
    [SpecialInstructions] varchar(1024) NULL,
    [OrderDate] bigint NOT NULL,
    [IsComplete] int NOT NULL,
    [Price] nvarchar(64) NOT NULL,
    [Discount] nvarchar(64) NULL,
    [CustomerId] int NULL,
    CONSTRAINT [PK_Order] PRIMARY KEY ([Id]),
    CONSTRAINT [FK_Order_Customers_CustomerId] FOREIGN KEY ([CustomerId]) REFERENCES [Customers] ([Id])
);

지정된 형식에 대한 기본 형식 매핑을 지정할 수도 있습니다. 다음은 그 예입니다.

configurationBuilder
    .DefaultTypeMapping<string>()
    .IsUnicode(false);

이는 거의 필요하지 않지만 형식이 모델의 매핑된 속성과 관련이 없는 방식으로 쿼리에 사용되는 경우에 유용할 수 있습니다.

비고

Entity Framework Core 6.0 미리 보기 6 발표: 규칙 구성에 대한 논의 및 규칙 전 모델 구성의 예제는 .NET 블로그에서 확인하세요.

컴파일된 모델

GitHub 문제: #1906.

컴파일된 모델은 대형 모델이 있는 애플리케이션의 EF Core 시작 시간을 개선할 수 있습니다. 대용량 모델은 일반적으로 엔터티 형식 및 관계의 100~1000을 의미합니다.

시작 시간은 해당 DbContext 형식이 애플리케이션에서 처음으로 사용될 때 DbContext에서 첫 번째 작업을 수행하는 시간을 의미합니다. DbContext 인스턴스만 만들면 EF 모델이 초기화되지 않습니다. 대신 모델이 초기화되는 일반적인 첫 번째 작업에는 DbContext.Add 호출 또는 첫 번째 쿼리 실행이 포함됩니다.

컴파일된 모델은 dotnet ef 명령줄 도구를 사용하여 생성됩니다. 계속하기 전의 최신 버전의 도구를 설치했는지 확인합니다.

dbcontext optimize 명령은 컴파일된 모델을 생성하는 데 사용됩니다. 다음은 그 예입니다.

dotnet ef dbcontext optimize

--output-dir--namespace 옵션을 사용하여 컴파일된 모델이 생성될 디렉터리 및 네임스페이스를 지정할 수 있습니다. 다음은 그 예입니다.

PS C:\dotnet\efdocs\samples\core\Miscellaneous\CompiledModels> dotnet ef dbcontext optimize --output-dir MyCompiledModels --namespace MyCompiledModels
Build started...
Build succeeded.
Successfully generated a compiled model, to use it call 'options.UseModel(MyCompiledModels.BlogsContextModel.Instance)'. Run this command again when the model is modified.
PS C:\dotnet\efdocs\samples\core\Miscellaneous\CompiledModels>

이 명령을 실행하여 출력에는 DbContext 구성에 복사하여 붙여넣어 EF Core가 컴파일된 모델을 사용하도록 하는 코드 조각이 포함되어 있습니다. 다음은 그 예입니다.

protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
    => optionsBuilder
        .UseModel(MyCompiledModels.BlogsContextModel.Instance)
        .UseSqlite(@"Data Source=test.db");

컴파일된 모델 부트스트래핑

일반적으로 생성된 부트스트래핑 코드를 확인할 필요가 없습니다. 그러나 모델이나 모델의 로드를 사용자 지정하면 도움이 되는 경우도 있습니다. 부트스트래핑 코드는 다음과 같이 표시됩니다.

[DbContext(typeof(BlogsContext))]
partial class BlogsContextModel : RuntimeModel
{
    private static BlogsContextModel _instance;
    public static IModel Instance
    {
        get
        {
            if (_instance == null)
            {
                _instance = new BlogsContextModel();
                _instance.Initialize();
                _instance.Customize();
            }

            return _instance;
        }
    }

    partial void Initialize();

    partial void Customize();
}

필요에 따라 모델을 사용자 지정하기 위해 구현할 수 있는 부분 메서드가 있는 partial 클래스입니다.

또한 일부 런타임 구성에 따라 다른 모델을 사용할 수 있는 DbContext 형식에 대해 여러 컴파일된 모델을 생성할 수 있습니다. 이러한 모델은 위에 표시된 대로 서로 다른 폴더 및 네임스페이스에 배치되어야 합니다. 그러면 연결 문자열과 같은 런타임 정보를 검사하고 필요에 따라 올바른 모델을 반환할 수 있습니다. 다음은 그 예입니다.

public static class RuntimeModelCache
{
    private static readonly ConcurrentDictionary<string, IModel> _runtimeModels
        = new();

    public static IModel GetOrCreateModel(string connectionString)
        => _runtimeModels.GetOrAdd(
            connectionString, cs =>
            {
                if (cs.Contains("X"))
                {
                    return BlogsContextModel1.Instance;
                }

                if (cs.Contains("Y"))
                {
                    return BlogsContextModel2.Instance;
                }

                throw new InvalidOperationException("No appropriate compiled model found.");
            });
}

제한점

컴파일된 모델에는 몇 가지 제한 사항이 있습니다.

이러한 제한 사항 때문에 EF Core 시작 시간이 너무 느린 경우에만 컴파일된 모델을 사용해야 합니다. 작은 모델을 컴파일하는 것은 일반적으로 가치가 없습니다.

이러한 기능을 지원하는 것이 성공에 중요한 경우 위에 연결된 적절한 문제에 투표하세요.

벤치마크

팁 (조언)

GitHub에서 샘플 코드를 다운로드하여 큰 모델을 컴파일하고 벤치마크를 실행해 볼 수 있습니다.

위에서 참조한 GitHub 리포지토리의 모델에는 449개의 엔터티 형식, 6390개의 속성 및 720개의 관계가 포함됩니다. 이것은 적당히 큰 모델입니다. BenchmarkDotNet을 사용하여 측정하면 비교적 강력한 랩톱에서 처음 쿼리하는 평균 시간은 1.02초입니다. 컴파일된 모델을 사용하면 동일한 하드웨어에서 117밀리초까지 감소합니다. 이와 같은 8x~10배 개선은 모델 크기가 증가함에 따라 상대적으로 일정하게 유지됩니다.

컴파일된 모델 성능 향상

비고

EF Core 시작 성능 및 컴파일된 모델에 대한 자세한 내용은 .NET 블로그에서 Entity Framework Core 6.0 미리 보기 5: 컴파일 된 모델 발표를 참조하세요.

TechEmpower Fortunes의 성능 향상

GitHub 문제: #23611.

EF Core 6.0에 대한 쿼리 성능이 크게 향상되었습니다. 특히:

  • EF Core 6.0 성능은 이제 5.0에 비해 업계 표준 TechEmpower Fortunes 벤치마크에서 70% 더 빠릅니다.
    • 이는 벤치마크 코드, .NET 런타임 등의 향상된 기능을 포함하여 전체 스택 성능 향상입니다.
  • EF Core 6.0 자체는 추적되지 않은 쿼리를 더 빠르게 실행할% 31입니다.
  • 쿼리를 실행할 때 힙 할당이 43% 감소했습니다.

이러한 개선 후 TechEmpower Fortunes 벤치마크에서 인기 있는 "마이크로 ORM" Dapper와 EF Core 간의 격차는 55 %에서 5 % 미만으로 줄어들었습니다.

비고

EF Core 6.0의 쿼리 성능 향상에 대한 자세한 내용은 .NET 블로그의 Entity Framework Core 6.0 Preview 4: Performance Edition 발표 를 참조하세요.

Azure Cosmos DB 공급자 향상

EF Core 6.0에는 Azure Cosmos DB 데이터베이스 공급자에 대한 많은 개선 사항이 포함되어 있습니다.

팁 (조언)

GitHub에서 샘플 코드를 다운로드하여 모든 Cosmos 관련 샘플을 실행하고 디버그할 수 있습니다.

기본값을 암시적 소유권으로 설정

GitHub 문제: #24803.

Azure Cosmos DB 공급자에 대한 모델을 빌드할 때 EF Core 6.0은 기본적으로 자식 엔터티 형식을 부모 엔터티가 소유한 것으로 표시합니다. 이렇게 하면 Azure Cosmos DB 모델에서 대부분의 OwnsMany 호출 및 OwnsOne 호출이 필요하지 않습니다. 이렇게 하면 부모 형식에 대한 자식 형식을 문서에 더 쉽게 포함할 수 있습니다. 이는 일반적으로 문서 데이터베이스에서 부모 및 자식을 모델링하는 적절한 방법입니다.

예를 들어 다음 엔터티 형식을 고려합니다.

public class Family
{
    [JsonPropertyName("id")]
    public string Id { get; set; }

    public string LastName { get; set; }
    public bool IsRegistered { get; set; }

    public Address Address { get; set; }

    public IList<Parent> Parents { get; } = new List<Parent>();
    public IList<Child> Children { get; } = new List<Child>();
}

public class Parent
{
    public string FamilyName { get; set; }
    public string FirstName { get; set; }
}

public class Child
{
    public string FamilyName { get; set; }
    public string FirstName { get; set; }
    public int Grade { get; set; }

    public string Gender { get; set; }

    public IList<Pet> Pets { get; } = new List<Pet>();
}

EF Core 5.0에서 이러한 형식은 다음 구성을 사용하여 Azure Cosmos DB에 대해 모델링되었을 것입니다.

modelBuilder.Entity<Family>()
    .HasPartitionKey(e => e.LastName)
    .OwnsMany(f => f.Parents);

modelBuilder.Entity<Family>()
    .OwnsMany(f => f.Children)
    .OwnsMany(c => c.Pets);

modelBuilder.Entity<Family>()
    .OwnsOne(f => f.Address);

EF Core 6.0에서 소유권은 암시적이므로 모델 구성이 다음과 같이 줄어듭니다.

modelBuilder.Entity<Family>().HasPartitionKey(e => e.LastName);

결과 Azure Cosmos DB 문서에는 가족의 부모, 자녀, 애완 동물 및 주소가 가족 문서에 포함되어 있습니다. 다음은 그 예입니다.

{
  "Id": "Wakefield.7",
  "LastName": "Wakefield",
  "Discriminator": "Family",
  "IsRegistered": true,
  "id": "Family|Wakefield.7",
  "Address": {
    "City": "NY",
    "County": "Manhattan",
    "State": "NY"
  },
  "Children": [
    {
      "FamilyName": "Merriam",
      "FirstName": "Jesse",
      "Gender": "female",
      "Grade": 8,
      "Pets": [
        {
          "GivenName": "Goofy"
        },
        {
          "GivenName": "Shadow"
        }
      ]
    },
    {
      "FamilyName": "Miller",
      "FirstName": "Lisa",
      "Gender": "female",
      "Grade": 1,
      "Pets": []
    }
  ],
  "Parents": [
    {
      "FamilyName": "Wakefield",
      "FirstName": "Robin"
    },
    {
      "FamilyName": "Miller",
      "FirstName": "Ben"
    }
  ],
  "_rid": "x918AKh6p20CAAAAAAAAAA==",
  "_self": "dbs/x918AA==/colls/x918AKh6p20=/docs/x918AKh6p20CAAAAAAAAAA==/",
  "_etag": "\"00000000-0000-0000-adee-87f30c8c01d7\"",
  "_attachments": "attachments/",
  "_ts": 1632121802
}

비고

이러한 소유 형식을 추가로 구성해야 하는 경우, OwnsOne/OwnsMany 구성을 반드시 사용해야 합니다.

기본 형식의 컬렉션

GitHub 문제: #14762.

EF Core 6.0은 Azure Cosmos DB 데이터베이스 공급자를 사용할 때 기본 형식의 컬렉션을 기본적으로 매핑합니다. 예를 들어 다음 엔터티 형식을 고려합니다.

public class Book
{
    public Guid Id { get; set; }
    public string Title { get; set; }
    public IList<string> Quotes { get; set; }
    public IDictionary<string, string> Notes { get; set; }
}

목록과 사전을 모두 입력하고 일반적인 방법으로 데이터베이스에 삽입할 수 있습니다.

using var context = new BooksContext();

var book = new Book
{
    Title = "How It Works: Incredible History",
    Quotes = new List<string>
    {
        "Thomas (Tommy) Flowers was the British engineer behind the design of the Colossus computer.",
        "Invented originally for Guinness, plastic widgets are nitrogen-filled spheres.",
        "For 20 years after its introduction in 1979, the Walkman dominated the personal stereo market."
    },
    Notes = new Dictionary<string, string>
    {
        { "121", "Fridges" },
        { "144", "Peter Higgs" },
        { "48", "Saint Mark's Basilica" },
        { "36", "The Terracotta Army" }
    }
};

context.Add(book);
await context.SaveChangesAsync();

그러면 다음 JSON 문서가 생성됩니다.

{
    "Id": "0b32283e-22a8-4103-bb4f-6052604868bd",
    "Discriminator": "Book",
    "Notes": {
        "36": "The Terracotta Army",
        "48": "Saint Mark's Basilica",
        "121": "Fridges",
        "144": "Peter Higgs"
    },
    "Quotes": [
        "Thomas (Tommy) Flowers was the British engineer behind the design of the Colossus computer.",
        "Invented originally for Guinness, plastic widgets are nitrogen-filled spheres.",
        "For 20 years after its introduction in 1979, the Walkman dominated the personal stereo market."
    ],
    "Title": "How It Works: Incredible History",
    "id": "Book|0b32283e-22a8-4103-bb4f-6052604868bd",
    "_rid": "t-E3AIxaencBAAAAAAAAAA==",
    "_self": "dbs/t-E3AA==/colls/t-E3AIxaenc=/docs/t-E3AIxaencBAAAAAAAAAA==/",
    "_etag": "\"00000000-0000-0000-9b50-fc769dc901d7\"",
    "_attachments": "attachments/",
    "_ts": 1630075016
}

그런 다음, 이러한 컬렉션을 정상적인 방식으로 다시 업데이트할 수 있습니다.

book.Quotes.Add("Pressing the emergency button lowered the rods again.");
book.Notes["48"] = "Chiesa d'Oro";

await context.SaveChangesAsync();

제한 사항:

  • 문자열 키가 있는 사전만 지원됩니다.
  • 기본 컬렉션의 내용에 대한 쿼리는 현재 지원되지 않습니다. 이러한 기능이 중요한 경우 #16926, #25700#25701 에 투표하세요.

기본 제공 함수로 변환

GitHub 문제: #16143.

이제 Azure Cosmos DB 공급자는 더 많은 BCL(기본 클래스 라이브러리) 메서드를 Azure Cosmos DB 기본 제공 함수로 변환합니다. 다음 표에서는 EF Core 6.0의 새로운 번역을 보여 줍니다.

문자열 번역

BCL 메서드 기본 제공 함수 비고
String.Length LENGTH
String.ToLower LOWER
String.TrimStart LTRIM
String.TrimEnd RTRIM
String.Trim TRIM
String.ToUpper UPPER
String.Substring SUBSTRING
+ 연산자 CONCAT
String.IndexOf INDEX_OF
String.Replace REPLACE
String.Equals STRINGEQUALS 대/소문자를 구분하지 않는 호출만 허용됨

LOWER, LTRIM, RTRIM, TRIM, UPPER, 및 SUBSTRING에 대한 번역은 @Marusyk에 의해 기여되었습니다. 대단히 고맙습니다!

예를 들면 다음과 같습니다.

var stringResults = await context.Triangles.Where(
        e => e.Name.Length > 4
             && e.Name.Trim().ToLower() != "obtuse"
             && e.Name.TrimStart().Substring(2, 2).Equals("uT", StringComparison.OrdinalIgnoreCase))
    .ToListAsync();

이는 다음으로 변환됩니다.

SELECT c
FROM root c
WHERE ((c["Discriminator"] = "Triangle") AND (((LENGTH(c["Name"]) > 4) AND (LOWER(TRIM(c["Name"])) != "obtuse")) AND STRINGEQUALS(SUBSTRING(LTRIM(c["Name"]), 2, 2), "uT", true)))

수학 번역

BCL 메서드 기본 제공 함수
Math.Abs 또는 MathF.Abs ABS
Math.Acos 또는 MathF.Acos ACOS
Math.Asin 또는 MathF.Asin ASIN
Math.Atan 또는 MathF.Atan ATAN
Math.Atan2 또는 MathF.Atan2 ATN2
Math.Ceiling 또는 MathF.Ceiling CEILING
Math.Cos 또는 MathF.Cos COS
Math.Exp 또는 MathF.Exp EXP
Math.Floor 또는 MathF.Floor FLOOR
Math.Log 또는 MathF.Log LOG
Math.Log10 또는 MathF.Log10 LOG10
Math.Pow 또는 MathF.Pow POWER
Math.Round 또는 MathF.Round ROUND
Math.Sign 또는 MathF.Sign SIGN
Math.Sin 또는 MathF.Sin SIN
Math.Sqrt 또는 MathF.Sqrt SQRT
Math.Tan 또는 MathF.Tan TAN
Math.Truncate 또는 MathF.Truncate TRUNC
DbFunctions.Random RAND

이러한 번역은 @Marusyk 의해 기여되었다. 대단히 고맙습니다!

예를 들면 다음과 같습니다.

var hypotenuse = 42.42;
var mathResults = await context.Triangles.Where(
        e => (Math.Round(e.Angle1) == 90.0
              || Math.Round(e.Angle2) == 90.0)
             && (hypotenuse * Math.Sin(e.Angle1) > 30.0
                 || hypotenuse * Math.Cos(e.Angle2) > 30.0))
    .ToListAsync();

이는 다음으로 변환됩니다.

SELECT c
FROM root c
WHERE ((c["Discriminator"] = "Triangle") AND (((ROUND(c["Angle1"]) = 90.0) OR (ROUND(c["Angle2"]) = 90.0)) AND (((@__hypotenuse_0 * SIN(c["Angle1"])) > 30.0) OR ((@__hypotenuse_0 * COS(c["Angle2"])) > 30.0))))

DateTime 번역

BCL 메서드 기본 제공 함수
DateTime.UtcNow GetCurrentDateTime

이러한 번역은 @Marusyk 의해 기여되었다. 대단히 고맙습니다!

예를 들면 다음과 같습니다.

var timeResults = await context.Triangles.Where(
        e => e.InsertedOn <= DateTime.UtcNow)
    .ToListAsync();

이는 다음으로 변환됩니다.

SELECT c
FROM root c
WHERE ((c["Discriminator"] = "Triangle") AND (c["InsertedOn"] <= GetCurrentDateTime()))

FromSql을 사용하는 원시 SQL 쿼리

GitHub 문제: #17311.

LINQ를 사용하는 대신 원시 SQL 쿼리를 실행해야 하는 경우도 있습니다. Azure Cosmos DB 공급자에서 이제 FromSql 메서드를 사용하여 지원됩니다. 이는 관계형 공급자와 항상 동일한 방식으로 작동합니다. 다음은 그 예입니다.

var maxAngle = 60;
var results = await context.Triangles.FromSqlRaw(
        @"SELECT * FROM root c WHERE c[""Angle1""] <= {0} OR c[""Angle2""] <= {0}", maxAngle)
    .ToListAsync();

이렇게 실행됩니다.

SELECT c
FROM (
    SELECT * FROM root c WHERE c["Angle1"] <= @p0 OR c["Angle2"] <= @p0
) c

고유 쿼리

GitHub 문제: #16144.

Distinct를 사용하는 간단한 쿼리가 이제 번역됩니다. 다음은 그 예입니다.

var distinctResults = await context.Triangles
    .Select(e => e.Angle1).OrderBy(e => e).Distinct()
    .ToListAsync();

이는 다음으로 변환됩니다.

SELECT DISTINCT c["Angle1"]
FROM root c
WHERE (c["Discriminator"] = "Triangle")
ORDER BY c["Angle1"]

진단

GitHub 문제: #17298.

이제 Azure Cosmos DB 공급자는 데이터베이스에서 데이터를 삽입, 쿼리, 업데이트 및 삭제하기 위한 이벤트를 포함하여 더 많은 진단 정보를 기록합니다. 요청 단위(RU)는 적절한 경우 이러한 이벤트에 포함됩니다.

비고

여기에 표시된 로그는 ID 값이 표시되도록 사용합니다 EnableSensitiveDataLogging() .

Azure Cosmos DB 데이터베이스에 항목을 삽입하면 이벤트가 생성됩니다 CosmosEventId.ExecutedCreateItem . 예를 들어 다음 코드는 다음과 같습니다.

var triangle = new Triangle
{
    Name = "Impossible",
    PartitionKey = "TrianglesPartition",
    Angle1 = 90,
    Angle2 = 90,
    InsertedOn = DateTime.UtcNow
};
context.Add(triangle);
await context.SaveChangesAsync();

다음 진단 이벤트를 기록합니다.

info: 8/30/2021 14:41:13.356 CosmosEventId.ExecutedCreateItem[30104] (Microsoft.EntityFrameworkCore.Database.Command)
      Executed CreateItem (5 ms, 7.43 RU) ActivityId='417db46f-fcdd-49d9-a7f0-77210cd06f84', Container='Shapes', Id='Impossible', Partition='TrianglesPartition'

쿼리를 사용하여 Azure Cosmos DB 데이터베이스에서 항목을 검색하면 CosmosEventId.ExecutingSqlQuery 이벤트가 생성되고, 읽은 항목에 대해 하나 이상의 CosmosEventId.ExecutedReadNext 이벤트가 생성됩니다. 예를 들어 다음 코드는 다음과 같습니다.

var equilateral = await context.Triangles.SingleAsync(e => e.Name == "Equilateral");

다음 진단 이벤트를 기록합니다.

info: 8/30/2021 14:41:13.475 CosmosEventId.ExecutingSqlQuery[30100] (Microsoft.EntityFrameworkCore.Database.Command)
      Executing SQL query for container 'Shapes' in partition '(null)' [Parameters=[]]
      SELECT c
      FROM root c
      WHERE ((c["Discriminator"] = "Triangle") AND (c["id"] = "Equilateral"))
      OFFSET 0 LIMIT 2
info: 8/30/2021 14:41:13.651 CosmosEventId.ExecutedReadNext[30102] (Microsoft.EntityFrameworkCore.Database.Command)
      Executed ReadNext (169.6126 ms, 2.93 RU) ActivityId='4e465fae-3d49-4c1f-bd04-142bc5d0b0a1', Container='Shapes', Partition='(null)', Parameters=[]
      SELECT c
      FROM root c
      WHERE ((c["Discriminator"] = "Triangle") AND (c["id"] = "Equilateral"))
      OFFSET 0 LIMIT 2

파티션 키와 함께 Find를 사용하여 Azure Cosmos DB 데이터베이스에서 단일 항목을 검색하면 CosmosEventId.ExecutingReadItemCosmosEventId.ExecutedReadItem 이벤트가 생성됩니다. 예를 들어 다음 코드는 다음과 같습니다.

var isosceles = await context.Triangles.FindAsync("Isosceles", "TrianglesPartition");

다음 진단 이벤트를 기록합니다.

info: 8/30/2021 14:53:39.326 CosmosEventId.ExecutingReadItem[30101] (Microsoft.EntityFrameworkCore.Database.Command)
      Reading resource 'Isosceles' item from container 'Shapes' in partition 'TrianglesPartition'.
info: 8/30/2021 14:53:39.330 CosmosEventId.ExecutedReadItem[30103] (Microsoft.EntityFrameworkCore.Database.Command)
      Executed ReadItem (1 ms, 1 RU) ActivityId='3c278643-4e7f-4bb2-9953-6055b5f1288f', Container='Shapes', Id='Isosceles', Partition='TrianglesPartition'

업데이트된 항목을 Azure Cosmos DB 데이터베이스에 저장하면 CosmosEventId.ExecutedReplaceItem 이벤트가 생성됩니다. 예를 들어 다음 코드는 다음과 같습니다.

triangle.Angle2 = 89;
await context.SaveChangesAsync();

다음 진단 이벤트를 기록합니다.

info: 8/30/2021 14:53:39.343 CosmosEventId.ExecutedReplaceItem[30105] (Microsoft.EntityFrameworkCore.Database.Command)
      Executed ReplaceItem (6 ms, 10.67 RU) ActivityId='1525b958-fea1-49e8-89f9-d429d0351fdb', Container='Shapes', Id='Impossible', Partition='TrianglesPartition'

Azure Cosmos DB 데이터베이스에서 항목을 삭제하면 CosmosEventId.ExecutedDeleteItem 이벤트가 생성됩니다. 예를 들어 다음 코드는 다음과 같습니다.

context.Remove(triangle);
await context.SaveChangesAsync();

다음 진단 이벤트를 기록합니다.

info: 8/30/2021 14:53:39.359 CosmosEventId.ExecutedDeleteItem[30106] (Microsoft.EntityFrameworkCore.Database.Command)
      Executed DeleteItem (6 ms, 7.43 RU) ActivityId='cbc54463-405b-48e7-8c32-2c6502a4138f', Container='Shapes', Id='Impossible', Partition='TrianglesPartition'

처리량 구성

GitHub 문제: #17301.

이제 Azure Cosmos DB 모델을 수동 또는 자동 크기 조정 처리량으로 구성할 수 있습니다. 이러한 값은 데이터베이스에서 처리량을 프로비전합니다. 다음은 그 예입니다.

modelBuilder.HasManualThroughput(2000);
modelBuilder.HasAutoscaleThroughput(4000);

또한 개별 엔터티 형식을 구성하여 해당 컨테이너에 대한 처리량을 프로비전할 수 있습니다. 다음은 그 예입니다.

modelBuilder.Entity<Family>(
    entityTypeBuilder =>
    {
        entityTypeBuilder.HasManualThroughput(5000);
        entityTypeBuilder.HasAutoscaleThroughput(3000);
    });

TTL(수명 주기) 설정

GitHub 문제: #17307.

Azure Cosmos DB 모델에서 엔터티 유형은 이제 기본 Time-to-Live와 분석 저장소에 대한 Time-to-Live로 구성할 수 있습니다. 다음은 그 예입니다.

modelBuilder.Entity<Family>(
    entityTypeBuilder =>
    {
        entityTypeBuilder.HasDefaultTimeToLive(100);
        entityTypeBuilder.HasAnalyticalStoreTimeToLive(200);
    });

HTTP 클라이언트 팩터리 해결

GitHub 문제: #21274. 이 기능은 @dnperfors 기여했습니다. 대단히 고맙습니다!

HttpClientFactory 이제 Azure Cosmos DB 공급자가 사용하는 것을 명시적으로 설정할 수 있습니다. 이는 테스트 중에 특히 유용할 수 있습니다. 예를 들어 Linux에서 Azure Cosmos DB 에뮬레이터를 사용할 때 인증서 유효성 검사를 무시합니다.

optionsBuilder
    .EnableSensitiveDataLogging()
    .UseCosmos(
        "https://localhost:8081",
        "C2y6yDjf5/R+ob0N8A7Cgv30VRDJIWEHLM+4QDU5DE2nQ9nDuVTqobD4b8mGGyPMbIZnqyMsEcaGQy67XIw/Jw==",
        "PrimitiveCollections",
        cosmosOptionsBuilder =>
        {
            cosmosOptionsBuilder.HttpClientFactory(
                () => new HttpClient(
                    new HttpClientHandler
                    {
                        ServerCertificateCustomValidationCallback =
                            HttpClientHandler.DangerousAcceptAnyServerCertificateValidator
                    }));
        });

비고

기존 애플리케이션 에 Azure Cosmos DB 공급자 개선 사항을 적용하는 자세한 예제는 .NET 블로그에서 시험 사용을 위한 EF Core Azure Cosmos DB 공급자 사용을 참조하세요.

기존 데이터베이스에서 스캐폴딩 개선

EF Core 6.0에는 기존 데이터베이스에서 EF 모델을 리버스 엔지니어링할 때 몇 가지 개선 사항이 포함되어 있습니다.

다 대 다 관계 구축

GitHub 문제: #22475.

EF Core 6.0은 간단한 조인 테이블들을 탐지하고, 이를 자동으로 다대다 매핑으로 생성합니다. 예를 들어, Posts 테이블과 Tags 테이블을 연결하는 조인 테이블 PostTag를 고려해 보세요.

CREATE TABLE [Tags] (
  [Id] int NOT NULL IDENTITY,
  [Name] nvarchar(max) NOT NULL,
  [Description] nvarchar(max) NULL,
  CONSTRAINT [PK_Tags] PRIMARY KEY ([Id]));

CREATE TABLE [Posts] (
    [Id] int NOT NULL IDENTITY,
    [Title] nvarchar(max) NOT NULL,
    [Contents] nvarchar(max) NOT NULL,
    [PostedOn] datetime2 NOT NULL,
    [UpdatedOn] datetime2 NULL,
    CONSTRAINT [PK_Posts] PRIMARY KEY ([Id]));

CREATE TABLE [PostTag] (
    [PostsId] int NOT NULL,
    [TagsId] int NOT NULL,
    CONSTRAINT [PK_PostTag] PRIMARY KEY ([PostsId], [TagsId]),
    CONSTRAINT [FK_PostTag_Posts_TagsId] FOREIGN KEY ([TagsId]) REFERENCES [Tags] ([Id]) ON DELETE CASCADE,
    CONSTRAINT [FK_PostTag_Tags_PostsId] FOREIGN KEY ([PostsId]) REFERENCES [Posts] ([Id]) ON DELETE CASCADE);

이러한 테이블은 명령줄에서 스캐폴드할 수 있습니다. 다음은 그 예입니다.

dotnet ef dbcontext scaffold "Data Source=(localdb)\MSSQLLocalDB;Initial Catalog=BloggingWithNRTs" Microsoft.EntityFrameworkCore.SqlServer

그러면 Post에 대한 클래스가 생성됩니다.

public partial class Post
{
    public Post()
    {
        Tags = new HashSet<Tag>();
    }

    public int Id { get; set; }
    public string Title { get; set; } = null!;
    public string Contents { get; set; } = null!;
    public DateTime PostedOn { get; set; }
    public DateTime? UpdatedOn { get; set; }
    public int BlogId { get; set; }

    public virtual Blog Blog { get; set; } = null!;

    public virtual ICollection<Tag> Tags { get; set; }
}

태그에 대한 클래스:

public partial class Tag
{
    public Tag()
    {
        Posts = new HashSet<Post>();
    }

    public int Id { get; set; }
    public string Name { get; set; } = null!;
    public string? Description { get; set; }

    public virtual ICollection<Post> Posts { get; set; }
}

그러나 PostTag 테이블의 클래스는 없습니다. 대신 다대다 관계 설정이 자동으로 생성됩니다.

entity.HasMany(d => d.Tags)
    .WithMany(p => p.Posts)
    .UsingEntity<Dictionary<string, object>>(
        "PostTag",
        l => l.HasOne<Tag>().WithMany().HasForeignKey("PostsId"),
        r => r.HasOne<Post>().WithMany().HasForeignKey("TagsId"),
        j =>
            {
                j.HasKey("PostsId", "TagsId");
                j.ToTable("PostTag");
                j.HasIndex(new[] { "TagsId" }, "IX_PostTag_TagsId");
            });

스캐폴드 C# nullable 참조 형식

GitHub 문제: #15520.

이제 EF Core 6.0은 C# nullable 참조 형식(NRT)을 사용하는 EF 모델 및 엔터티 형식을 스캐폴드합니다. 코드가 스캐폴드되는 C# 프로젝트에서 NRT 지원을 사용하도록 설정하면 NRT 사용량이 자동으로 스캐폴드됩니다.

예를 들어 다음 Tags 표에는 null 허용과 미허용 문자열 열이 모두 포함되어 있습니다.

CREATE TABLE [Tags] (
  [Id] int NOT NULL IDENTITY,
  [Name] nvarchar(max) NOT NULL,
  [Description] nvarchar(max) NULL,
  CONSTRAINT [PK_Tags] PRIMARY KEY ([Id]));

이렇게 하면 생성된 클래스에서 해당 nullable 및 nullable이 아닌 문자열 속성이 생성됩니다.

public partial class Tag
{
    public Tag()
    {
        Posts = new HashSet<Post>();
    }

    public int Id { get; set; }
    public string Name { get; set; } = null!;
    public string? Description { get; set; }

    public virtual ICollection<Post> Posts { get; set; }
}

마찬가지로 다음 Posts 테이블에는 Blogs 테이블과의 필요한 관계가 포함되어 있습니다.

CREATE TABLE [Posts] (
    [Id] int NOT NULL IDENTITY,
    [Title] nvarchar(max) NOT NULL,
    [Contents] nvarchar(max) NOT NULL,
    [PostedOn] datetime2 NOT NULL,
    [UpdatedOn] datetime2 NULL,
    [BlogId] int NOT NULL,
    CONSTRAINT [PK_Posts] PRIMARY KEY ([Id]),
    CONSTRAINT [FK_Posts_Blogs_BlogId] FOREIGN KEY ([BlogId]) REFERENCES [Blogs] ([Id]));

이로 인해 블로그 간에 non-nullable(필수) 관계가 구축됩니다.

public partial class Blog
{
    public Blog()
    {
        Posts = new HashSet<Post>();
    }

    public int Id { get; set; }
    public string Name { get; set; } = null!;

    public virtual ICollection<Post> Posts { get; set; }
}

그리고 게시물:

public partial class Post
{
    public Post()
    {
        Tags = new HashSet<Tag>();
    }

    public int Id { get; set; }
    public string Title { get; set; } = null!;
    public string Contents { get; set; } = null!;
    public DateTime PostedOn { get; set; }
    public DateTime? UpdatedOn { get; set; }
    public int BlogId { get; set; }

    public virtual Blog Blog { get; set; } = null!;

    public virtual ICollection<Tag> Tags { get; set; }
}

마지막으로 생성된 DbContext의 DbSet 속성은 NRT에 친숙한 방식으로 만들어집니다. 다음은 그 예입니다.

public virtual DbSet<Blog> Blogs { get; set; } = null!;
public virtual DbSet<Post> Posts { get; set; } = null!;
public virtual DbSet<Tag> Tags { get; set; } = null!;

데이터베이스 주석은 코드 주석으로 스캐폴드됩니다.

GitHub 문제: #19113. 이 기능은 @ErikEJ 의해 제공되었습니다. 대단히 고맙습니다!

SQL 테이블 및 열에 대한 주석은 이제 기존 SQL Server 데이터베이스에서 EF Core 모델을 리버스 엔지니어링 할 때 생성된 엔터티 형식으로 스캐폴드됩니다.

/// <summary>
/// The Blog table.
/// </summary>
public partial class Blog
{
    /// <summary>
    /// The primary key.
    /// </summary>
    [Key]
    public int Id { get; set; }
}

LINQ 쿼리 개선 사항

EF Core 6.0에는 LINQ 쿼리의 번역 및 실행이 몇 가지 개선되었습니다.

향상된 GroupBy 지원

GitHub 문제: #12088, #13805#22609.

EF Core 6.0에는 쿼리에 대한 GroupBy 더 나은 지원이 포함되어 있습니다. 특히, EF Core는 이제 다음을 수행합니다.

  • GroupBy에 이어 FirstOrDefault (또는 그와 유사한 것)을 그룹에 적용
  • 그룹에서 상위 N개 결과 선택 지원
  • GroupBy 연산자가 적용된 후 탐색을 확장합니다.

다음은 고객 보고서의 쿼리 예제 및 SQL Server에서의 번역입니다.

예제 1:

var people = await context.People
    .Include(e => e.Shoes)
    .GroupBy(e => e.FirstName)
    .Select(
        g => g.OrderBy(e => e.FirstName)
            .ThenBy(e => e.LastName)
            .FirstOrDefault())
    .ToListAsync();
SELECT [t0].[Id], [t0].[Age], [t0].[FirstName], [t0].[LastName], [t0].[MiddleInitial], [t].[FirstName], [s].[Id], [s].[Age], [s].[PersonId], [s].[Style]
FROM (
    SELECT [p].[FirstName]
    FROM [People] AS [p]
    GROUP BY [p].[FirstName]
) AS [t]
LEFT JOIN (
    SELECT [t1].[Id], [t1].[Age], [t1].[FirstName], [t1].[LastName], [t1].[MiddleInitial]
    FROM (
        SELECT [p0].[Id], [p0].[Age], [p0].[FirstName], [p0].[LastName], [p0].[MiddleInitial], ROW_NUMBER() OVER(PARTITION BY [p0].[FirstName] ORDER BY [p0].[FirstName], [p0].[LastName]) AS [row]
        FROM [People] AS [p0]
    ) AS [t1]
    WHERE [t1].[row] <= 1
) AS [t0] ON [t].[FirstName] = [t0].[FirstName]
LEFT JOIN [Shoes] AS [s] ON [t0].[Id] = [s].[PersonId]
ORDER BY [t].[FirstName], [t0].[FirstName]

예제 2:

var group = await context.People
    .Select(
        p => new
        {
            p.FirstName,
            FullName = p.FirstName + " " + p.MiddleInitial + " " + p.LastName
        })
    .GroupBy(p => p.FirstName)
    .Select(g => g.First())
    .FirstAsync();
SELECT [t0].[FirstName], [t0].[FullName], [t0].[c]
FROM (
    SELECT TOP(1) [p].[FirstName]
    FROM [People] AS [p]
    GROUP BY [p].[FirstName]
) AS [t]
LEFT JOIN (
    SELECT [t1].[FirstName], [t1].[FullName], [t1].[c]
    FROM (
        SELECT [p0].[FirstName], (((COALESCE([p0].[FirstName], N'') + N' ') + COALESCE([p0].[MiddleInitial], N'')) + N' ') + COALESCE([p0].[LastName], N'') AS [FullName], 1 AS [c], ROW_NUMBER() OVER(PARTITION BY [p0].[FirstName] ORDER BY [p0].[FirstName]) AS [row]
        FROM [People] AS [p0]
    ) AS [t1]
    WHERE [t1].[row] <= 1
) AS [t0] ON [t].[FirstName] = [t0].[FirstName]

예제 3:

var people = await context.People
    .Where(e => e.MiddleInitial == "Q" && e.Age == 20)
    .GroupBy(e => e.LastName)
    .Select(g => g.First().LastName)
    .OrderBy(e => e.Length)
    .ToListAsync();
SELECT (
    SELECT TOP(1) [p1].[LastName]
    FROM [People] AS [p1]
    WHERE (([p1].[MiddleInitial] = N'Q') AND ([p1].[Age] = 20)) AND (([p].[LastName] = [p1].[LastName]) OR ([p].[LastName] IS NULL AND [p1].[LastName] IS NULL)))
FROM [People] AS [p]
WHERE ([p].[MiddleInitial] = N'Q') AND ([p].[Age] = 20)
GROUP BY [p].[LastName]
ORDER BY CAST(LEN((
    SELECT TOP(1) [p1].[LastName]
    FROM [People] AS [p1]
    WHERE (([p1].[MiddleInitial] = N'Q') AND ([p1].[Age] = 20)) AND (([p].[LastName] = [p1].[LastName]) OR ([p].[LastName] IS NULL AND [p1].[LastName] IS NULL)))) AS int)

예제 4:

var results = await (from person in context.People
               join shoes in context.Shoes on person.Age equals shoes.Age
               group shoes by shoes.Style
               into people
               select new
               {
                   people.Key,
                   Style = people.Select(p => p.Style).FirstOrDefault(),
                   Count = people.Count()
               })
    .ToListAsync();
SELECT [s].[Style] AS [Key], (
    SELECT TOP(1) [s0].[Style]
    FROM [People] AS [p0]
    INNER JOIN [Shoes] AS [s0] ON [p0].[Age] = [s0].[Age]
    WHERE ([s].[Style] = [s0].[Style]) OR ([s].[Style] IS NULL AND [s0].[Style] IS NULL)) AS [Style], COUNT(*) AS [Count]
FROM [People] AS [p]
INNER JOIN [Shoes] AS [s] ON [p].[Age] = [s].[Age]
GROUP BY [s].[Style]

예제 5:

var results = await context.People
    .GroupBy(e => e.FirstName)
    .Select(g => g.First().LastName)
    .OrderBy(e => e)
    .ToListAsync();
SELECT (
    SELECT TOP(1) [p1].[LastName]
    FROM [People] AS [p1]
    WHERE ([p].[FirstName] = [p1].[FirstName]) OR ([p].[FirstName] IS NULL AND [p1].[FirstName] IS NULL))
FROM [People] AS [p]
GROUP BY [p].[FirstName]
ORDER BY (
    SELECT TOP(1) [p1].[LastName]
    FROM [People] AS [p1]
    WHERE ([p].[FirstName] = [p1].[FirstName]) OR ([p].[FirstName] IS NULL AND [p1].[FirstName] IS NULL))

예제 6:

var results = await context.People
    .Where(e => e.Age == 20)
    .GroupBy(e => e.Id)
    .Select(g => g.First().MiddleInitial)
    .OrderBy(e => e)
    .ToListAsync();
SELECT (
    SELECT TOP(1) [p1].[MiddleInitial]
    FROM [People] AS [p1]
    WHERE ([p1].[Age] = 20) AND ([p].[Id] = [p1].[Id]))
FROM [People] AS [p]
WHERE [p].[Age] = 20
GROUP BY [p].[Id]
ORDER BY (
    SELECT TOP(1) [p1].[MiddleInitial]
    FROM [People] AS [p1]
    WHERE ([p1].[Age] = 20) AND ([p].[Id] = [p1].[Id]))

예제 7:

var size = 11;
var results
    = await context.People
        .Where(
            p => p.Feet.Size == size
                 && p.MiddleInitial != null
                 && p.Feet.Id != 1)
        .GroupBy(
            p => new
            {
                p.Feet.Size,
                p.Feet.Person.LastName
            })
        .Select(
            g => new
            {
                g.Key.LastName,
                g.Key.Size,
                Min = g.Min(p => p.Feet.Size),
            })
        .ToListAsync();
Executed DbCommand (12ms) [Parameters=[@__size_0='11'], CommandType='Text', CommandTimeout='30']
SELECT [p0].[LastName], [f].[Size], MIN([f0].[Size]) AS [Min]
FROM [People] AS [p]
LEFT JOIN [Feet] AS [f] ON [p].[Id] = [f].[Id]
LEFT JOIN [People] AS [p0] ON [f].[Id] = [p0].[Id]
LEFT JOIN [Feet] AS [f0] ON [p].[Id] = [f0].[Id]
WHERE (([f].[Size] = @__size_0) AND [p].[MiddleInitial] IS NOT NULL) AND (([f].[Id] <> 1) OR [f].[Id] IS NULL)
GROUP BY [f].[Size], [p0].[LastName]

예제 8:

var result = await context.People
    .Include(x => x.Shoes)
    .Include(x => x.Feet)
    .GroupBy(
        x => new
        {
            x.Feet.Id,
            x.Feet.Size
        })
    .Select(
        x => new
        {
            Key = x.Key.Id + x.Key.Size,
            Count = x.Count(),
            Sum = x.Sum(el => el.Id),
            SumOver60 = x.Sum(el => el.Id) / (decimal)60,
            TotalCallOutCharges = x.Sum(el => el.Feet.Size == 11 ? 1 : 0)
        })
    .CountAsync();
SELECT COUNT(*)
FROM (
    SELECT [f].[Id], [f].[Size]
    FROM [People] AS [p]
    LEFT JOIN [Feet] AS [f] ON [p].[Id] = [f].[Id]
    GROUP BY [f].[Id], [f].[Size]
) AS [t]

예제 9:

var results = await context.People
    .GroupBy(n => n.FirstName)
    .Select(g => new
    {
        Feet = g.Key,
        Total = g.Sum(n => n.Feet.Size)
    })
    .ToListAsync();
SELECT [p].[FirstName] AS [Feet], COALESCE(SUM([f].[Size]), 0) AS [Total]
FROM [People] AS [p]
LEFT JOIN [Feet] AS [f] ON [p].[Id] = [f].[Id]
GROUP BY [p].[FirstName]

예제 10:

var results = from Person person1
                  in from Person person2
                         in context.People
                     select person2
              join Shoes shoes
                  in context.Shoes
                  on person1.Age equals shoes.Age
              group shoes by
                  new
                  {
                      person1.Id,
                      shoes.Style,
                      shoes.Age
                  }
              into temp
              select
                  new
                  {
                      temp.Key.Id,
                      temp.Key.Age,
                      temp.Key.Style,
                      Values = from t
                                   in temp
                               select
                                   new
                                   {
                                       t.Id,
                                       t.Style,
                                       t.Age
                                   }
                  };
SELECT [t].[Id], [t].[Age], [t].[Style], [t0].[Id], [t0].[Style], [t0].[Age], [t0].[Id0]
FROM (
    SELECT [p].[Id], [s].[Age], [s].[Style]
    FROM [People] AS [p]
    INNER JOIN [Shoes] AS [s] ON [p].[Age] = [s].[Age]
    GROUP BY [p].[Id], [s].[Style], [s].[Age]
) AS [t]
LEFT JOIN (
    SELECT [s0].[Id], [s0].[Style], [s0].[Age], [p0].[Id] AS [Id0]
    FROM [People] AS [p0]
    INNER JOIN [Shoes] AS [s0] ON [p0].[Age] = [s0].[Age]
) AS [t0] ON (([t].[Id] = [t0].[Id0]) AND (([t].[Style] = [t0].[Style]) OR ([t].[Style] IS NULL AND [t0].[Style] IS NULL))) AND ([t].[Age] = [t0].[Age])
ORDER BY [t].[Id], [t].[Style], [t].[Age], [t0].[Id0]

예제 11:

var grouping = await context.People
    .GroupBy(i => i.LastName)
    .Select(g => new { LastName = g.Key, Count = g.Count() , First = g.FirstOrDefault(), Take = g.Take(2)})
    .OrderByDescending(e => e.LastName)
    .ToListAsync();
SELECT [t].[LastName], [t].[c], [t0].[Id], [t2].[Id], [t2].[Age], [t2].[FirstName], [t2].[LastName], [t2].[MiddleInitial], [t0].[Age], [t0].[FirstName], [t0].[LastName], [t0].[MiddleInitial]
FROM (
    SELECT [p].[LastName], COUNT(*) AS [c]
    FROM [People] AS [p]
    GROUP BY [p].[LastName]
) AS [t]
LEFT JOIN (
    SELECT [t1].[Id], [t1].[Age], [t1].[FirstName], [t1].[LastName], [t1].[MiddleInitial]
    FROM (
        SELECT [p0].[Id], [p0].[Age], [p0].[FirstName], [p0].[LastName], [p0].[MiddleInitial], ROW_NUMBER() OVER(PARTITION BY [p0].[LastName] ORDER BY [p0].[Id]) AS [row]
        FROM [People] AS [p0]
    ) AS [t1]
    WHERE [t1].[row] <= 1
) AS [t0] ON [t].[LastName] = [t0].[LastName]
LEFT JOIN (
    SELECT [t3].[Id], [t3].[Age], [t3].[FirstName], [t3].[LastName], [t3].[MiddleInitial]
    FROM (
        SELECT [p1].[Id], [p1].[Age], [p1].[FirstName], [p1].[LastName], [p1].[MiddleInitial], ROW_NUMBER() OVER(PARTITION BY [p1].[LastName] ORDER BY [p1].[Id]) AS [row]
        FROM [People] AS [p1]
    ) AS [t3]
    WHERE [t3].[row] <= 2
) AS [t2] ON [t].[LastName] = [t2].[LastName]
ORDER BY [t].[LastName] DESC, [t0].[Id], [t2].[LastName], [t2].[Id]

예제 12:

var grouping = await context.People
    .Include(e => e.Shoes)
    .OrderBy(e => e.FirstName)
    .ThenBy(e => e.LastName)
    .GroupBy(e => e.FirstName)
    .Select(g => new { Name = g.Key, People = g.ToList()})
    .ToListAsync();
SELECT [t].[FirstName], [t0].[Id], [t0].[Age], [t0].[FirstName], [t0].[LastName], [t0].[MiddleInitial], [t0].[Id0], [t0].[Age0], [t0].[PersonId], [t0].[Style]
FROM (
    SELECT [p].[FirstName]
    FROM [People] AS [p]
    GROUP BY [p].[FirstName]
) AS [t]
LEFT JOIN (
    SELECT [p0].[Id], [p0].[Age], [p0].[FirstName], [p0].[LastName], [p0].[MiddleInitial], [s].[Id] AS [Id0], [s].[Age] AS [Age0], [s].[PersonId], [s].[Style]
    FROM [People] AS [p0]
    LEFT JOIN [Shoes] AS [s] ON [p0].[Id] = [s].[PersonId]
) AS [t0] ON [t].[FirstName] = [t0].[FirstName]
ORDER BY [t].[FirstName], [t0].[Id]

예제 13:

var grouping = await context.People
    .GroupBy(m => new {m.FirstName, m.MiddleInitial })
    .Select(am => new
    {
        Key = am.Key,
        Items = am.ToList()
    })
    .ToListAsync();
SELECT [t].[FirstName], [t].[MiddleInitial], [p0].[Id], [p0].[Age], [p0].[FirstName], [p0].[LastName], [p0].[MiddleInitial]
FROM (
    SELECT [p].[FirstName], [p].[MiddleInitial]
    FROM [People] AS [p]
    GROUP BY [p].[FirstName], [p].[MiddleInitial]
) AS [t]
LEFT JOIN [People] AS [p0] ON (([t].[FirstName] = [p0].[FirstName]) OR ([t].[FirstName] IS NULL AND [p0].[FirstName] IS NULL)) AND (([t].[MiddleInitial] = [p0].[MiddleInitial]) OR ([t].[MiddleInitial] IS NULL AND [p0].[MiddleInitial] IS NULL))
ORDER BY [t].[FirstName], [t].[MiddleInitial]

모델

이러한 예제에 사용되는 엔터티 형식은 다음과 같습니다.

public class Person
{
    public int Id { get; set; }
    public int Age { get; set; }
    public string FirstName { get; set; }
    public string LastName { get; set; }
    public string MiddleInitial { get; set; }
    public Feet Feet { get; set; }
    public ICollection<Shoes> Shoes { get; } = new List<Shoes>();
}

public class Shoes
{
    public int Id { get; set; }
    public int Age { get; set; }
    public string Style { get; set; }
    public Person Person { get; set; }
}

public class Feet
{
    public int Id { get; set; }
    public int Size { get; set; }
    public Person Person { get; set; }
}

여러 인수를 사용하여 String.Concat 번역

GitHub 문제: #23859. 이 기능은 @wmeints 의해 제공되었습니다. 대단히 고맙습니다!

EF Core 6.0부터 여러 인수가 있는 String.Concat 호출이 이제 SQL로 변환됩니다. 예를 들어, 다음과 같은 쿼리가 있습니다.

var shards = await context.Shards
    .Where(e => string.Concat(e.Token1, e.Token2, e.Token3) != e.TokensProcessed).ToListAsync();

SQL Server를 사용하는 경우 다음 SQL로 변환됩니다.

SELECT [s].[Id], [s].[Token1], [s].[Token2], [s].[Token3], [s].[TokensProcessed]
FROM [Shards] AS [s]
WHERE (([s].[Token1] + ([s].[Token2] + [s].[Token3])) <> [s].[TokensProcessed]) OR [s].[TokensProcessed] IS NULL

System.Linq.Async와의 원활한 통합

GitHub 문제: #24041.

System.Linq.Async 패키지는 클라이언트 쪽 비동기 LINQ 처리를 추가합니다. 이전 버전의 EF Core에서 이 패키지를 사용하는 것은 비동기 LINQ 메서드의 네임스페이스 충돌로 인해 번거로웠습니다. EF Core 6.0에서는 노출된 EF Core IAsyncEnumerable<T> 가 인터페이스를 직접 구현할 필요가 없도록 C# 패턴 일치 DbSet<TEntity> 를 활용했습니다.

EF Core 쿼리는 일반적으로 서버에서 완전히 변환되므로 대부분의 애플리케이션은 System.Linq.Async를 사용할 필요가 없습니다.

GitHub 문제: #23921.

EF Core 6.0에서는 FreeText(DbFunctions, String, String)Contains에 대한 매개 변수 요구 사항을 완화했습니다. 이렇게 하면 이러한 함수를 이진 열 또는 값 변환기를 사용하여 매핑된 열과 함께 사용할 수 있습니다. 예를 들어 값 개체로 정의된 속성이 있는 Name 엔터티 형식을 고려합니다.

public class Customer
{
    public int Id { get; set; }

    public Name Name{ get; set; }
}

public class Name
{
    public string First { get; set; }
    public string MiddleInitial { get; set; }
    public string Last { get; set; }
}

데이터베이스의 JSON에 매핑됩니다.

modelBuilder.Entity<Customer>()
    .Property(e => e.Name)
    .HasConversion(
        v => JsonSerializer.Serialize(v, (JsonSerializerOptions)null),
        v => JsonSerializer.Deserialize<Name>(v, (JsonSerializerOptions)null));

이제 속성의 형식이 Contains가 아닌 FreeText인 경우에도 Name 또는 string를 사용하여 쿼리를 실행할 수 있습니다. 다음은 그 예입니다.

var result = await context.Customers.Where(e => EF.Functions.Contains(e.Name, "Martin")).ToListAsync();

이렇게 하면 SQL Server를 사용할 때 다음 SQL이 생성됩니다.

SELECT [c].[Id], [c].[Name]
FROM [Customers] AS [c]
WHERE CONTAINS([c].[Name], N'Martin')

SQLite에서 ToString 번역

GitHub 문제: #17223. 이 기능은 @ralmsdeveloper 의해 제공되었습니다. 대단히 고맙습니다!

ToString() 이제 SQLite 데이터베이스 공급자를 사용할 때 호출이 SQL로 변환됩니다. 이는 문자열이 아닌 열과 관련된 텍스트 검색에 유용할 수 있습니다. 예를 들어 전화 번호를 숫자 값으로 저장하는 엔터티 유형을 고려 User 합니다.

public class User
{
    public int Id { get; set; }
    public string Username { get; set; }
    public long PhoneNumber { get; set; }
}

ToString 을 사용하여 숫자를 데이터베이스의 문자열로 변환할 수 있습니다. 그런 다음 패턴과 일치하는 숫자를 찾는 등의 LIKE 함수와 함께 이 문자열을 사용할 수 있습니다. 예를 들어 555를 포함하는 모든 숫자를 찾으려면 다음을 수행합니다.

var users = await context.Users.Where(u => EF.Functions.Like(u.PhoneNumber.ToString(), "%555%")).ToListAsync();

이는 SQLite 데이터베이스를 사용하는 경우 다음 SQL로 변환됩니다.

SELECT "u"."Id", "u"."PhoneNumber", "u"."Username"
FROM "Users" AS "u"
WHERE CAST("u"."PhoneNumber" AS TEXT) LIKE '%555%'

SQL Server에 ToString() 대한 번역은 EF Core 5.0에서 이미 지원되며 다른 데이터베이스 공급자에서도 지원될 수 있습니다.

EF. Functions.Random

GitHub 문제: #16141. 이 기능은 @RaymondHuy 의해 제공되었습니다. 대단히 고맙습니다!

EF.Functions.Random는 0과 1 사이의 (0 포함, 1 제외) 의사 난수를 반환하는 데이터베이스 함수에 매핑됩니다. 번역은 SQL Server, SQLite 및 Azure Cosmos DB용 EF Core 리포지토리에서 구현되었습니다. 예를 들어, User 엔터티 형식에 Popularity 속성이 있는 경우를 고려해 보십시오.

public class User
{
    public int Id { get; set; }
    public string Username { get; set; }
    public int Popularity { get; set; }
}

Popularity 는 1에서 5까지의 값을 포함할 수 있습니다. 쿼리를 사용하여 EF.Functions.Random 임의로 선택한 인기도를 가진 모든 사용자를 반환할 수 있습니다.

var users = await context.Users.Where(u => u.Popularity == (int)(EF.Functions.Random() * 4.0) + 1).ToListAsync();

이는 SQL Server 데이터베이스를 사용하는 경우 다음 SQL로 변환됩니다.

SELECT [u].[Id], [u].[Popularity], [u].[Username]
FROM [Users] AS [u]
WHERE [u].[Popularity] = (CAST((RAND() * 4.0E0) AS int) + 1)

IsNullOrWhitespace에 대한 SQL Server 변환 개선

GitHub 문제: #22916. 이 기능은 @Marusyk 의해 제공되었습니다. 대단히 고맙습니다!

다음과 같은 쿼리를 고려해 보세요.

var users = await context.Users.Where(
    e => string.IsNullOrWhiteSpace(e.FirstName)
         || string.IsNullOrWhiteSpace(e.LastName)).ToListAsync();

EF Core 6.0 이전에는 SQL Server에서 다음으로 변환되었습니다.

SELECT [u].[Id], [u].[FirstName], [u].[LastName]
FROM [Users] AS [u]
WHERE ([u].[FirstName] IS NULL OR (LTRIM(RTRIM([u].[FirstName])) = N'')) OR ([u].[LastName] IS NULL OR (LTRIM(RTRIM([u].[LastName])) = N''))

이 변환은 EF Core 6.0에서 다음으로 향상되었습니다.

SELECT [u].[Id], [u].[FirstName], [u].[LastName]
FROM [Users] AS [u]
WHERE ([u].[FirstName] IS NULL OR ([u].[FirstName] = N'')) OR ([u].[LastName] IS NULL OR ([u].[LastName] = N''))

메모리 내 공급자에 대한 쿼리 정의

GitHub 문제: #24600.

새 메서드 ToInMemoryQuery 를 사용하여 지정된 엔터티 형식에 대한 메모리 내 데이터베이스에 대한 정의 쿼리를 작성할 수 있습니다. 이는 특히 해당 뷰가 키 없는 엔터티 형식을 반환하는 경우 메모리 내 데이터베이스에서 해당 뷰를 만드는 데 가장 유용합니다. 예를 들어 영국에 본사를 둔 고객의 고객 데이터베이스를 고려해 보세요. 각 고객에게는 다음 주소가 있습니다.

public class Customer
{
    public int Id { get; set; }
    public string Name { get; set; }
    public Address Address { get; set; }
}

public class Address
{
    public int Id { get; set; }
    public string House { get; set; }
    public string Street { get; set; }
    public string City { get; set; }
    public string Postcode { get; set; }
}

이제 각 우편 번호 영역에 얼마나 많은 고객이 있는지 보여주는 이 데이터에 대한 보기를 원한다고 상상해 보십시오. 다음을 나타내는 키 없는 엔터티 형식을 만들 수 있습니다.

public class CustomerDensity
{
    public string Postcode { get; set; }
    public int CustomerCount { get; set; }
}

DbContext에서 이를 위한 DbSet 속성과 다른 최상위 엔터티 형식에 대한 집합을 정의합니다.

public DbSet<Customer> Customers { get; set; }
public DbSet<CustomerDensity> CustomerDensities { get; set; }

그런 다음 OnModelCreating에서 반환할 데이터를 정의하는 LINQ 쿼리를 작성할 수 있습니다: CustomerDensities.

protected override void OnModelCreating(ModelBuilder modelBuilder)
{
    modelBuilder
        .Entity<CustomerDensity>()
        .HasNoKey()
        .ToInMemoryQuery(
            () => Customers
                .GroupBy(c => c.Address.Postcode.Substring(0, 3))
                .Select(
                    g =>
                        new CustomerDensity
                        {
                            Postcode = g.Key,
                            CustomerCount = g.Count()
                        }));
}

그러면 다른 DbSet 속성과 마찬가지로 쿼리할 수 있습니다.

var results = await context.CustomerDensities.ToListAsync();

단일 매개 변수를 사용하여 부분 문자열 번역

GitHub 문제: #20173. 이 기능은 @stevendarby 의해 제공되었습니다. 대단히 고맙습니다!

이제 EF Core 6.0은 단일 인수로 string.Substring 사용을 변환합니다. 다음은 그 예입니다.

var result = await context.Customers
    .Select(a => new { Name = a.Name.Substring(3) })
    .FirstOrDefaultAsync(a => a.Name == "hur");

SQL Server를 사용하는 경우 다음 SQL로 변환됩니다.

SELECT TOP(1) SUBSTRING([c].[Name], 3 + 1, LEN([c].[Name])) AS [Name]
FROM [Customers] AS [c]
WHERE SUBSTRING([c].[Name], 3 + 1, LEN([c].[Name])) = N'hur'

비 탐색형 컬렉션에 대한 분할 쿼리

GitHub 문제: #21234.

EF Core는 단일 LINQ 쿼리를 여러 SQL 쿼리로 분할하는 것을 지원합니다. EF Core 6.0에서는 비 탐색 컬렉션이 쿼리 프로젝션에 포함된 경우를 포함하도록 이 지원이 확장되었습니다.

다음은 SQL Server에서 단일 쿼리 또는 여러 쿼리로의 번역을 보여 주는 예제 쿼리입니다.

예제 1:

LINQ 쿼리:

await context.Customers
    .Select(
        c => new
        {
            c,
            Orders = c.Orders
                .Where(o => o.Id > 1)
        })
    .ToListAsync();

단일 SQL 쿼리:

SELECT [c].[Id], [t].[Id], [t].[CustomerId], [t].[OrderDate]
FROM [Customers] AS [c]
LEFT JOIN (
    SELECT [o].[Id], [o].[CustomerId], [o].[OrderDate]
    FROM [Order] AS [o]
    WHERE [o].[Id] > 1
) AS [t] ON [c].[Id] = [t].[CustomerId]
ORDER BY [c].[Id]

여러 SQL 쿼리:

SELECT [c].[Id]
FROM [Customers] AS [c]
ORDER BY [c].[Id]

SELECT [t].[Id], [t].[CustomerId], [t].[OrderDate], [c].[Id]
FROM [Customers] AS [c]
INNER JOIN (
    SELECT [o].[Id], [o].[CustomerId], [o].[OrderDate]
    FROM [Order] AS [o]
    WHERE [o].[Id] > 1
) AS [t] ON [c].[Id] = [t].[CustomerId]
ORDER BY [c].[Id]

예제 2:

LINQ 쿼리:

await context.Customers
    .Select(
        c => new
        {
            c,
            OrderDates = c.Orders
                .Where(o => o.Id > 1)
                .Select(o => o.OrderDate)
        })
    .ToListAsync();

단일 SQL 쿼리:

SELECT [c].[Id], [t].[OrderDate], [t].[Id]
FROM [Customers] AS [c]
  LEFT JOIN (
  SELECT [o].[OrderDate], [o].[Id], [o].[CustomerId]
  FROM [Order] AS [o]
  WHERE [o].[Id] > 1
  ) AS [t] ON [c].[Id] = [t].[CustomerId]
ORDER BY [c].[Id]

여러 SQL 쿼리:

SELECT [c].[Id]
FROM [Customers] AS [c]
ORDER BY [c].[Id]

SELECT [t].[Id], [t].[CustomerId], [t].[OrderDate], [c].[Id]
FROM [Customers] AS [c]
INNER JOIN (
    SELECT [o].[Id], [o].[CustomerId], [o].[OrderDate]
    FROM [Order] AS [o]
    WHERE [o].[Id] > 1
) AS [t] ON [c].[Id] = [t].[CustomerId]
ORDER BY [c].[Id]

예제 3:

LINQ 쿼리:

await context.Customers
    .Select(
        c => new
        {
            c,
            OrderDates = c.Orders
                .Where(o => o.Id > 1)
                .Select(o => o.OrderDate)
                .Distinct()
        })
    .ToListAsync();

단일 SQL 쿼리:

SELECT [c].[Id], [t].[OrderDate]
FROM [Customers] AS [c]
  OUTER APPLY (
  SELECT DISTINCT [o].[OrderDate]
  FROM [Order] AS [o]
  WHERE ([c].[Id] = [o].[CustomerId]) AND ([o].[Id] > 1)
  ) AS [t]
ORDER BY [c].[Id]

여러 SQL 쿼리:

SELECT [c].[Id]
FROM [Customers] AS [c]
ORDER BY [c].[Id]

SELECT [t].[OrderDate], [c].[Id]
FROM [Customers] AS [c]
  CROSS APPLY (
  SELECT DISTINCT [o].[OrderDate]
  FROM [Order] AS [o]
  WHERE ([c].[Id] = [o].[CustomerId]) AND ([o].[Id] > 1)
  ) AS [t]
ORDER BY [c].[Id]

컬렉션에 조인할 때 마지막 ORDER BY 절 제거

GitHub 문제: #19828.

관련된 일대다 엔티티를 로드할 때, EF Core는 지정된 엔티티에 대한 모든 관련 엔티티가 함께 그룹화되도록 하기 위해 ORDER BY 절을 추가합니다. 그러나 EF에서 필요한 그룹을 생성하는 데 마지막 ORDER BY 절은 필요하지 않으며 성능에 영향을 미칠 수 있습니다. 따라서 이 절은 EF Core 6.0에서 제거됩니다.

예를 들어 다음 쿼리를 고려합니다.

await context.Customers
    .Select(
        e => new
        {
            e.Id,
            FirstOrder = e.Orders.Where(i => i.Id == 1).ToList()
        })
    .ToListAsync();

SQL Server에서 EF Core 5.0을 사용하면 이 쿼리가 다음으로 변환됩니다.

SELECT [c].[Id], [t].[Id], [t].[CustomerId], [t].[OrderDate]
FROM [Customers] AS [c]
LEFT JOIN (
    SELECT [o].[Id], [o].[CustomerId], [o].[OrderDate]
    FROM [Order] AS [o]
    WHERE [o].[Id] = 1
) AS [t] ON [c].[Id] = [t].[CustomerId]
ORDER BY [c].[Id], [t].[Id]

EF Core 6.0을 사용하면 대신 다음으로 변환됩니다.

SELECT [c].[Id], [t].[Id], [t].[CustomerId], [t].[OrderDate]
FROM [Customers] AS [c]
LEFT JOIN (
    SELECT [o].[Id], [o].[CustomerId], [o].[OrderDate]
    FROM [Order] AS [o]
    WHERE [o].[Id] = 1
) AS [t] ON [c].[Id] = [t].[CustomerId]
ORDER BY [c].[Id]

파일 이름 및 줄 번호를 사용하여 쿼리 태그 지정

GitHub 문제: #14176. 이 기능은 @michalczerwinski 의해 제공되었습니다. 대단히 고맙습니다!

쿼리 태그를 사용하면 LINQ 쿼리에 텍스트 태그를 추가하여 생성된 SQL에 포함할 수 있습니다. EF Core 6.0에서는 LINQ 코드의 파일 이름 및 줄 번호로 쿼리에 태그를 지정하는 데 사용할 수 있습니다. 다음은 그 예입니다.

var results1 = await context
    .Customers
    .TagWithCallSite()
    .Where(c => c.Name.StartsWith("A"))
    .ToListAsync();

그러면 SQL Server를 사용할 때 다음과 같은 SQL이 생성됩니다.

-- file: C:\dotnet\efdocs\samples\core\Miscellaneous\NewInEFCore6\TagWithFileAndLineSample.cs:21

SELECT [c].[Id], [c].[Name]
FROM [Customers] AS [c]
WHERE [c].[Name] IS NOT NULL AND ([c].[Name] LIKE N'A%')

소유된 선택적 종속 처리에 대한 변경 내용

GitHub 문제: #24558.

선택적 종속 엔터티가 주 엔터티와 테이블을 공유할 때 존재하는지 여부를 알기가 까다로워집니다. 보안 주체가 종속자가 존재하든 안 하든 종속자를 필요로 하기 때문에 테이블에 종속자에 대한 행이 존재합니다. 이 작업을 명확하게 처리하는 방법은 종속에 하나 이상의 필수 속성이 있는지 확인하는 것입니다. 필수 속성은 null일 수 없으므로 해당 속성에 대한 열의 값이 null이면 종속 엔터티가 존재하지 않습니다.

예를 들어, 각 고객이 소유한 Customer를 포함하는 Address 클래스를 생각해 보세요.

public class Customer
{
    public int Id { get; set; }
    public string Name { get; set; }
    public Address Address { get; set; }
}

public class Address
{
    public string House { get; set; }
    public string Street { get; set; }
    public string City { get; set; }

    [Required]
    public string Postcode { get; set; }
}

주소는 선택 사항입니다. 즉, 주소 없이 고객을 저장하는 것이 유효합니다.

context.Customers1.Add(
    new()
    {
        Name = "Foul Ole Ron"
    });

그러나 고객에게 주소가 있는 경우 해당 주소에는 Null이 아닌 우편 번호 이상이 있어야 합니다.

context.Customers1.Add(
    new()
    {
        Name = "Havelock Vetinari",
        Address = new()
        {
            Postcode = "AN1 1PL",
        }
    });

이는 Postcode 속성을 Required으로 표시하여 보장합니다.

이제 고객이 쿼리될 때 Postcode 열이 null이면, 이는 고객에게 주소가 없음을 의미하며, Customer.Address 탐색 속성이 null로 유지됩니다. 예를 들어 고객 목록을 반복하여 처리하고 주소가 null인지 확인합니다.

await foreach (var customer in context.Customers1.AsAsyncEnumerable())
{
    Console.Write(customer.Name);

    if (customer.Address == null)
    {
        Console.WriteLine(" has no address.");
    }
    else
    {
        Console.WriteLine($" has postcode {customer.Address.Postcode}.");
    }
}

다음 결과를 생성합니다.

Foul Ole Ron has no address.
Havelock Vetinari has postcode AN1 1PL.

대신 주소에서 벗어난 속성이 필요하지 않은 경우를 고려합니다.

public class Customer
{
    public int Id { get; set; }
    public string Name { get; set; }
    public Address Address { get; set; }
}

public class Address
{
    public string House { get; set; }
    public string Street { get; set; }
    public string City { get; set; }
    public string Postcode { get; set; }
}

이제 주소가 없는 고객과 모든 주소 속성이 null인 주소를 가진 고객을 모두 저장할 수 있습니다.

context.Customers2.Add(
    new()
    {
        Name = "Foul Ole Ron"
    });

context.Customers2.Add(
    new()
    {
        Name = "Havelock Vetinari",
        Address = new()
    });

그러나 데이터베이스에서 이러한 두 경우는 데이터베이스 열을 직접 쿼리하여 볼 수 있으므로 구별할 수 없습니다.

Id  Name               House   Street  City    Postcode
1   Foul Ole Ron       NULL    NULL    NULL    NULL
2   Havelock Vetinari  NULL    NULL    NULL    NULL

이러한 이유로 EF Core 6.0은 이제 모든 속성이 null인 선택적 종속성을 저장할 때 경고합니다. 다음은 그 예입니다.

경고: 2021년 9월 27일 09:25:01.338 RelationalEventId.OptionalDependentWithAllNullPropertiesWarning[20704](Microsoft.EntityFrameworkCore.Update) 기본 키 값이 {CustomerId: -2147482646}인 'Address' 엔터티는 테이블 공유를 사용하는 선택적 의존 항목입니다. 엔터티에는 엔터티의 존재 여부를 식별할 수 있는 기본값이 아닌 값을 가진 속성이 없습니다. 즉, 쿼리할 때 모든 속성이 기본값으로 설정된 인스턴스 대신 개체 인스턴스가 만들어지지 않습니다. 중첩된 종속성도 손실됩니다. 기본값만 있는 인스턴스를 저장하거나 들어오는 탐색을 모델에 필요한 대로 표시하지 마세요.

이는 선택적 종속 자체가 추가 선택적 종속의 주체로서 동일한 테이블에 매핑되는 경우 더욱 까다로워집니다. EF Core 6.0은 경고에 그치지 않고, 중첩된 선택적 종속 항목의 특정 사례를 허용하지 않습니다. 예를 들어 다음 모델을 고려해 보세요. 여기서 ContactInfoCustomer에 의해 소유되고, AddressContactInfo에 의해 소유됩니다.

public class Customer
{
    public int Id { get; set; }
    public string Name { get; set; }
    public ContactInfo ContactInfo { get; set; }
}

public class ContactInfo
{
    public string Phone { get; set; }
    public Address Address { get; set; }
}

public class Address
{
    public string House { get; set; }
    public string Street { get; set; }
    public string City { get; set; }
    public string Postcode { get; set; }
}

이제 null이면 ContactInfo.Phone 주소 자체에 데이터가 있을 수 있더라도 EF Core는 관계가 선택 사항인 경우 인스턴스 Address 를 만들지 않습니다. 이러한 종류의 모델의 경우 EF Core 6.0은 다음 예외를 throw합니다.

System.InvalidOperationException: 엔터티 형식 'ContactInfo'는 테이블 공유를 사용하고 엔터티가 있는지 여부를 식별하는 데 필요한 비공유 속성이 없는 다른 종속을 포함하는 선택적 종속입니다. 모든 nullable 속성이 데이터베이스에서 null 값을 포함하면 쿼리 내에서 개체 인스턴스가 생성되지 않아 중첩 종속성의 값이 손실됩니다. 다른 속성에 대해 null 값이 있는 인스턴스를 만들거나 항상 인스턴스를 만드는 데 필요한 대로 들어오는 탐색을 표시하는 데 필요한 속성을 추가합니다.

여기서 결론은 선택적 종속이 모든 nullable 속성 값을 포함할 수 있고 해당 주체와 테이블을 공유하는 경우를 방지하는 것입니다. 이를 방지하는 세 가지 쉬운 방법이 있습니다.

  1. 종속을 필수로 만듭니다. 즉, 모든 속성이 null인 경우에도 종속 엔터티는 쿼리된 후에 항상 값을 갖습니다.
  2. 위에서 설명한 대로 종속 항목에 하나 이상의 필수 속성이 포함되어 있는지 확인합니다.
  3. 주체와 테이블을 공유하는 대신 선택적 종속을 자신의 테이블에 저장합니다.

Required 속성을 해당 탐색에 사용하여 종속 항목을 필수로 만들 수 있습니다.

public class Customer
{
    public int Id { get; set; }
    public string Name { get; set; }

    [Required]
    public Address Address { get; set; }
}

public class Address
{
    public string House { get; set; }
    public string Street { get; set; }
    public string City { get; set; }
    public string Postcode { get; set; }
}

OnModelCreating에 필요하다고 지정함으로써 또는.

modelBuilder.Entity<WithRequiredNavigation.Customer>(
    b =>
        {
            b.OwnsOne(e => e.Address);
            b.Navigation(e => e.Address).IsRequired();
        });

사용할 테이블을 지정하여 종속 항목을 다른 테이블에 저장할 수 있습니다.OnModelCreating

modelBuilder
    .Entity<WithDifferentTable.Customer>(
        b =>
            {
                b.ToTable("Customers");
                b.OwnsOne(
                    e => e.Address,
                    b => b.ToTable("CustomerAddresses"));
            });

중첩된 선택적 종속이 있는 경우를 포함하여 선택적 종속에 대한 자세한 예제는 GitHub의 OptionalDependentsSample 을 참조하세요.

새 매핑 특성

EF Core 6.0에는 데이터베이스에 매핑되는 방식을 변경하기 위해 코드에 적용할 수 있는 몇 가지 새로운 특성이 포함되어 있습니다.

유니코드 속성

GitHub 문제: #19794. 이 기능은 @RaymondHuy 의해 제공되었습니다. 대단히 고맙습니다!

EF Core 6.0부터 데이터베이스 형식을 직접 지정하지 않고 매핑 특성을 사용하여 문자열 속성을 유니코드가 아닌 열에 매핑할 수 있습니다. 예를 들어, "ISBN 978-3-16-148410-0" 형식의 Book를 속성으로 가진 엔터티 유형을 고려해 보세요.

public class Book
{
    public int Id { get; set; }
    public string Title { get; set; }

    [Unicode(false)]
    [MaxLength(22)]
    public string Isbn { get; set; }
}

ISBN은 유니코드가 아닌 문자를 Unicode 포함할 수 없으므로 특성으로 인해 유니코드가 아닌 문자열 형식이 사용됩니다. 또한 MaxLength 데이터베이스 열의 크기를 제한하는 데 사용됩니다. 예를 들어 SQL Server를 사용하는 경우 다음과 같은 데이터베이스 열 varchar(22)이 생성됩니다.

CREATE TABLE [Book] (
    [Id] int NOT NULL IDENTITY,
    [Title] nvarchar(max) NULL,
    [Isbn] varchar(22) NULL,
    CONSTRAINT [PK_Book] PRIMARY KEY ([Id]));

비고

EF Core는 기본적으로 문자열 속성을 유니코드 열에 매핑합니다. UnicodeAttribute 는 데이터베이스 시스템에서 유니코드 형식만 지원하는 경우 무시됩니다.

정밀 속성

GitHub 문제: #17914. 이 기능은 @RaymondHuy 의해 제공되었습니다. 대단히 고맙습니다!

이제 데이터베이스 형식을 직접 지정하지 않고 매핑 특성을 사용하여 데이터베이스 열의 전체 자릿수와 크기를 구성할 수 있습니다. 예를 들어 10진 Product 수 속성이 있는 Price 엔터티 형식을 고려합니다.

public class Product
{
    public int Id { get; set; }

    [Precision(precision: 10, scale: 2)]
    public decimal Price { get; set; }
}

EF Core는 정밀도가 10이고 소수 자릿수가 2인 데이터베이스 열에 이 속성을 매핑합니다. 예를 들어 SQL Server에서는 다음과 같이 표시됩니다.

CREATE TABLE [Product] (
    [Id] int NOT NULL IDENTITY,
    [Price] decimal(10,2) NOT NULL,
    CONSTRAINT [PK_Product] PRIMARY KEY ([Id]));

EntityTypeConfigurationAttribute (엔티티 유형 구성 속성)

GitHub 문제: #23163. 이 기능은 @KaloyanIT 의해 제공되었습니다. 대단히 고맙습니다!

IEntityTypeConfiguration<TEntity> 인스턴스를 사용하면 ModelBuilder 각 엔터티 형식에 대한 구성을 자체 구성 클래스에 포함할 수 있습니다. 다음은 그 예입니다.

public class BookConfiguration : IEntityTypeConfiguration<Book>
{
    public void Configure(EntityTypeBuilder<Book> builder)
    {
        builder
            .Property(e => e.Isbn)
            .IsUnicode(false)
            .HasMaxLength(22);
    }
}

일반적으로 이 구성 클래스를 인스턴스화하고 .에서 DbContext.OnModelCreating호출해야 합니다. 다음은 그 예입니다.

protected override void OnModelCreating(ModelBuilder modelBuilder)
{
    new BookConfiguration().Configure(modelBuilder.Entity<Book>());
}

EF Core 6.0부터는 적절한 구성을 찾고 사용할 수 있도록 EntityTypeConfigurationAttribute를 엔터티 형식에 배치할 수 있습니다. 다음은 그 예입니다.

[EntityTypeConfiguration(typeof(BookConfiguration))]
public class Book
{
    public int Id { get; set; }
    public string Title { get; set; }
    public string Isbn { get; set; }
}

이 특성은 IEntityTypeConfiguration 엔터티 형식이 모델에 포함될 때마다 EF Core가 지정된 Book 구현을 사용한다는 것을 의미합니다. 엔터티 형식은 일반 메커니즘 중 하나를 사용하여 모델에 포함됩니다. 예를 들어 엔터티 형식에 DbSet<TEntity> 대한 속성을 만듭니다.

public class BooksContext : DbContext
{
    public DbSet<Book> Books { get; set; }

    //...

또는 다음에서 등록합니다.OnModelCreating

protected override void OnModelCreating(ModelBuilder modelBuilder)
{
    modelBuilder.Entity<Book>();
}

비고

EntityTypeConfigurationAttribute 형식은 어셈블리에서 자동으로 검색되지 않습니다. 해당 엔터티 형식에서 특성을 검색하려면 먼저 모델에 엔터티 형식을 추가해야 합니다.

모델 빌드 개선 사항

EF Core 6.0에는 새로운 매핑 특성 외에도 모델 빌드 프로세스에 대한 몇 가지 다른 개선 사항이 포함되어 있습니다.

SQL Server 스파스 열 지원

GitHub 문제: #8023.

SQL Server 스파스 열은 null 값을 저장하도록 최적화된 일반 열입니다. 이는 거의 사용되지 않는 하위 형식의 속성이 테이블의 대부분의 행에 대해 null 열 값을 초래하는 TPH 상속 매핑 을 사용할 때 유용할 수 있습니다. 예를 들어, ForumModerator 클래스가 ForumUser에서 확장되는 것을 고려해 보십시오.

public class ForumUser
{
    public int Id { get; set; }
    public string Username { get; set; }
}

public class ForumModerator : ForumUser
{
    public string ForumName { get; set; }
}

수백만 명의 사용자가 있을 수 있으며, 이들 중 소수만이 중재자일 수 있습니다. 즉, 여기에서 ForumName를 스파스로 매핑하는 것이 합리적일 수 있습니다. 이제 IsSparse에서 OnModelCreating을(를) 사용하여 구성할 수 있습니다. 다음은 그 예입니다.

protected override void OnModelCreating(ModelBuilder modelBuilder)
{
    modelBuilder
        .Entity<ForumModerator>()
        .Property(e => e.ForumName)
        .IsSparse();
}

그런 다음 EF Core 마이그레이션은 열을 스파스로 표시합니다. 다음은 그 예입니다.

CREATE TABLE [ForumUser] (
    [Id] int NOT NULL IDENTITY,
    [Username] nvarchar(max) NULL,
    [Discriminator] nvarchar(max) NOT NULL,
    [ForumName] nvarchar(max) SPARSE NULL,
    CONSTRAINT [PK_ForumUser] PRIMARY KEY ([Id]));

비고

희소 열에는 한계가 있습니다. 스파스 열이 시나리오에 적합한 선택인지 확인하려면 SQL Server 스파스 열 설명서를 읽어야 합니다.

HasConversion API 개선 사항

GitHub 문제: #25468.

EF Core 6.0 이전에는 메서드의 HasConversion 제네릭 오버로드가 제네릭 매개 변수를 사용하여 변환할 형식을 지정했습니다. 예를 들어 Currency 열거형을 생각해봅시다.

public enum Currency
{
    UsDollars,
    PoundsSterling,
    Euros
}

를 사용하여 HasConversion<string>이 열거형의 값을 문자열 "UsDollars", "PoundsStirling" 및 "Euro"로 저장하도록 EF Core를 구성할 수 있습니다. 다음은 그 예입니다.

modelBuilder.Entity<TestEntity1>()
    .Property(e => e.Currency)
    .HasConversion<string>();

EF Core 6.0부터 제네릭 형식은 대신 값 변환기 형식을 지정할 수 있습니다. 기본 제공 값 변환기 중 하나일 수 있습니다. 예를 들어 열거형 값을 데이터베이스에 16비트 숫자로 저장하려면 다음을 수행합니다.

modelBuilder.Entity<TestEntity2>()
    .Property(e => e.Currency)
    .HasConversion<EnumToNumberConverter<Currency, short>>();

또는 사용자 지정 값 변환기 형식일 수 있습니다. 예를 들어 열거형 값을 통화 기호로 저장하는 변환기를 고려합니다.

public class CurrencyToSymbolConverter : ValueConverter<Currency, string>
{
    public CurrencyToSymbolConverter()
        : base(
            v => v == Currency.PoundsSterling ? "£" : v == Currency.Euros ? "€" : "$",
            v => v == "£" ? Currency.PoundsSterling : v == "€" ? Currency.Euros : Currency.UsDollars)
    {
    }
}

이제 제네릭 HasConversion 메서드를 사용하여 구성할 수 있습니다.

modelBuilder.Entity<TestEntity3>()
    .Property(e => e.Currency)
    .HasConversion<CurrencyToSymbolConverter>();

다 대 다 관계에 구성이 덜 필요함

GitHub 문제: #21535.

두 엔터티 유형 간의 명확한 다 대 다 관계는 관례에 의해 발견됩니다. 필요한 경우 또는 원하는 경우 탐색을 명시적으로 지정할 수 있습니다. 다음은 그 예입니다.

modelBuilder.Entity<Cat>()
    .HasMany(e => e.Humans)
    .WithMany(e => e.Cats);

두 경우 모두 EF Core는 두 형식 간의 조인 엔터티 역할을 하기 위해 형식 Dictionary<string, object> 화된 공유 엔터티를 만듭니다. EF Core 6.0 UsingEntity 부터 구성에 추가하여 추가 구성 없이 이 형식만 변경할 수 있습니다. 다음은 그 예입니다.

modelBuilder.Entity<Cat>()
    .HasMany(e => e.Humans)
    .WithMany(e => e.Cats)
    .UsingEntity<CatHuman>();

또한 왼쪽 및 오른쪽 관계를 명시적으로 지정하지 않고도 조인 엔터티 형식을 추가로 구성할 수 있습니다. 다음은 그 예입니다.

modelBuilder.Entity<Cat>()
    .HasMany(e => e.Humans)
    .WithMany(e => e.Cats)
    .UsingEntity<CatHuman>(
        e => e.HasKey(e => new { e.CatsId, e.HumansId }));

마지막으로 전체 구성을 제공할 수 있습니다. 다음은 그 예입니다.

modelBuilder.Entity<Cat>()
    .HasMany(e => e.Humans)
    .WithMany(e => e.Cats)
    .UsingEntity<CatHuman>(
        e => e.HasOne<Human>().WithMany().HasForeignKey(e => e.CatsId),
        e => e.HasOne<Cat>().WithMany().HasForeignKey(e => e.HumansId),
        e => e.HasKey(e => new { e.CatsId, e.HumansId }));

값 변환기가 null을 변환하도록 허용

GitHub 문제: #13850.

중요합니다

아래에 설명된 문제로 인해 null 변환을 허용하는 생성자는 EF Core 6.0 릴리스에서 ValueConverter로 표시되어 [EntityFrameworkInternal] 되었습니다. 이제 이러한 생성자를 사용하면 빌드 경고가 생성됩니다.

값 변환기는 일반적으로 null을 다른 값으로 변환하는 것을 허용하지 않습니다. nullable 형식과 nullable이 아닌 형식 모두에 동일한 값 변환기를 사용할 수 있기 때문입니다. 이는 FK가 null을 허용하고 PK가 null을 허용하지 않는 PK/FK 조합에 매우 유용하기 때문입니다.

EF Core 6.0부터 null을 변환하는 값 변환기를 만들 수 있습니다. 그러나 이 기능의 유효성 검증에서 실질적인 여러 문제점이 드러났습니다. 다음은 그 예입니다.

이러한 문제는 사소한 문제가 아니며 쿼리 문제의 경우 검색하기가 쉽지 않습니다. 따라서 이 기능을 EF Core 6.0의 내부 기능으로 표시했습니다. 계속 사용할 수 있지만 컴파일러 경고가 표시됩니다. 를 사용하여 #pragma warning disable EF1001경고를 사용하지 않도록 설정할 수 있습니다.

null 변환이 유용할 수 있는 한 가지 예는 데이터베이스에 null이 포함되어 있지만 엔터티 형식이 속성에 다른 기본값을 사용하려는 경우입니다. 예를 들어 기본값이 "알 수 없음"인 열거형을 고려합니다.

public enum Breed
{
    Unknown,
    Burmese,
    Tonkinese
}

그러나 품종을 알 수 없는 경우 데이터베이스에 null 값이 있을 수 있습니다. EF Core 6.0에서는 값 변환기를 사용하여 다음을 설명할 수 있습니다.

    public class BreedConverter : ValueConverter<Breed, string>
    {
#pragma warning disable EF1001
        public BreedConverter()
            : base(
                v => v == Breed.Unknown ? null : v.ToString(),
                v => v == null ? Breed.Unknown : Enum.Parse<Breed>(v),
                convertsNulls: true)
        {
        }
#pragma warning restore EF1001
    }

"알 수 없음"의 품종을 가진 고양이는 데이터베이스에서 열 Breed 이 null로 설정됩니다. 다음은 그 예입니다.

context.AddRange(
    new Cat { Name = "Mac", Breed = Breed.Unknown },
    new Cat { Name = "Clippy", Breed = Breed.Burmese },
    new Cat { Name = "Sid", Breed = Breed.Tonkinese });

await context.SaveChangesAsync();

SQL Server에서 다음 insert 문을 생성합니다.

info: 9/27/2021 19:43:55.966 RelationalEventId.CommandExecuted[20101] (Microsoft.EntityFrameworkCore.Database.Command)
      Executed DbCommand (16ms) [Parameters=[@p0=NULL (Size = 4000), @p1='Mac' (Size = 4000)], CommandType='Text', CommandTimeout='30']
      SET NOCOUNT ON;
      INSERT INTO [Cats] ([Breed], [Name])
      VALUES (@p0, @p1);
      SELECT [Id]
      FROM [Cats]
      WHERE @@ROWCOUNT = 1 AND [Id] = scope_identity();
info: 9/27/2021 19:43:55.983 RelationalEventId.CommandExecuted[20101] (Microsoft.EntityFrameworkCore.Database.Command)
      Executed DbCommand (0ms) [Parameters=[@p0='Burmese' (Size = 4000), @p1='Clippy' (Size = 4000)], CommandType='Text', CommandTimeout='30']
      SET NOCOUNT ON;
      INSERT INTO [Cats] ([Breed], [Name])
      VALUES (@p0, @p1);
      SELECT [Id]
      FROM [Cats]
      WHERE @@ROWCOUNT = 1 AND [Id] = scope_identity();
info: 9/27/2021 19:43:55.983 RelationalEventId.CommandExecuted[20101] (Microsoft.EntityFrameworkCore.Database.Command)
      Executed DbCommand (0ms) [Parameters=[@p0='Tonkinese' (Size = 4000), @p1='Sid' (Size = 4000)], CommandType='Text', CommandTimeout='30']
      SET NOCOUNT ON;
      INSERT INTO [Cats] ([Breed], [Name])
      VALUES (@p0, @p1);
      SELECT [Id]
      FROM [Cats]
      WHERE @@ROWCOUNT = 1 AND [Id] = scope_identity();

DbContext 팩터리 개선 사항

AddDbContextFactory도 DbContext를 직접 등록합니다.

GitHub 문제: #25164.

경우에 따라 DbContext 형식 해당 형식의 컨텍스트에 대한 팩터리를 둘 다 애플리케이션 D.I.(종속성 주입) 컨테이너에 등록하는 것이 유용합니다. 예를 들어 요청 범위에서 DbContext의 범위가 지정된 인스턴스를 해결할 수 있으며, 동시에 팩터리는 필요할 때 여러 독립적인 인스턴스를 생성하는 데 사용할 수 있습니다.

이를 AddDbContextFactory 지원하기 위해 이제 DbContext 형식도 범위가 지정된 서비스로 등록합니다. 예를 들어 애플리케이션의 D.I. 컨테이너에서 이 등록을 고려합니다.

var container = services
    .AddDbContextFactory<SomeDbContext>(
        builder => builder.UseSqlServer(@"Server=(localdb)\mssqllocaldb;Database=EFCoreSample;ConnectRetryCount=0"))
    .BuildServiceProvider();

이 등록을 사용하면 이전 버전과 마찬가지로 루트 D.I. 컨테이너에서 팩터리를 확인할 수 있습니다.

var factory = container.GetService<IDbContextFactory<SomeDbContext>>();
using (var context = factory.CreateDbContext())
{
    // Contexts obtained from the factory must be explicitly disposed
}

팩터리에서 만든 컨텍스트 인스턴스는 명시적으로 삭제해야 합니다.

또한 DbContext 인스턴스는 컨테이너 범위에서 직접 확인할 수 있습니다.

using (var scope = container.CreateScope())
{
    var context = scope.ServiceProvider.GetService<SomeDbContext>();
    // Context is disposed when the scope is disposed
}

이 경우 컨테이너 범위가 삭제될 때 컨텍스트 인스턴스가 삭제됩니다. 컨텍스트를 명시적으로 삭제해서는 안 됩니다.

더 높은 수준에서 이는 팩터리의 DbContext를 다른 D.I. 형식에 삽입할 수 있음을 의미합니다. 다음은 그 예입니다.

private class MyController2
{
    private readonly IDbContextFactory<SomeDbContext> _contextFactory;

    public MyController2(IDbContextFactory<SomeDbContext> contextFactory)
    {
        _contextFactory = contextFactory;
    }

    public async Task DoSomething()
    {
        using var context1 = _contextFactory.CreateDbContext();
        using var context2 = _contextFactory.CreateDbContext();

        var results1 = await context1.Blogs.ToListAsync();
        var results2 = await context2.Blogs.ToListAsync();

        // Contexts obtained from the factory must be explicitly disposed
    }
}

또는:

private class MyController1
{
    private readonly SomeDbContext _context;

    public MyController1(SomeDbContext context)
    {
        _context = context;
    }

    public async Task DoSomething()
    {
        var results = await _context.Blogs.ToListAsync();

        // Injected context is disposed when the request scope is disposed
    }
}

DbContextFactory에서 DbContext 매개 변수 없는 생성자를 무시합니다.

GitHub 문제: #24124.

이제 EF Core 6.0에서는 매개 변수가 없는 DbContext 생성자와 DbContextOptions를 받는 생성자 둘 다, 팩터리가 AddDbContextFactory로 등록될 때 동일한 컨텍스트 형식에서 사용할 수 있게 합니다. 예를 들어 위의 예제에 사용된 컨텍스트에는 두 생성자가 모두 포함됩니다.

public class SomeDbContext : DbContext
{
    public SomeDbContext()
    {
    }

    public SomeDbContext(DbContextOptions<SomeDbContext> options)
        : base(options)
    {
    }

    public DbSet<Blog> Blogs { get; set; }
}

DbContext 풀링을 종속성 주입 없이 사용할 수 있습니다.

GitHub 문제: #24137.

애플리케이션에 PooledDbContextFactory 종속성 주입 컨테이너가 없어도 DbContext 인스턴스에 대한 독립 실행형 풀로 사용할 수 있도록 형식이 공개되었습니다. 풀은 컨텍스트 인스턴스를 만드는 데 사용할 인스턴스 DbContextOptions 를 사용하여 만들어집니다.

var options = new DbContextOptionsBuilder<SomeDbContext>()
    .EnableSensitiveDataLogging()
    .UseSqlServer(@"Server=(localdb)\mssqllocaldb;Database=EFCoreSample;ConnectRetryCount=0")
    .Options;

var factory = new PooledDbContextFactory<SomeDbContext>(options);

그런 다음 팩터리를 사용하여 인스턴스를 만들고 풀할 수 있습니다. 다음은 그 예입니다.

for (var i = 0; i < 2; i++)
{
    using var context1 = factory.CreateDbContext();
    Console.WriteLine($"Created DbContext with ID {context1.ContextId}");

    using var context2 = factory.CreateDbContext();
    Console.WriteLine($"Created DbContext with ID {context2.ContextId}");
}

인스턴스는 삭제될 때 풀로 반환됩니다.

기타 개선 사항

마지막으로, EF Core에는 위에서 다루지 않은 영역에서 몇 가지 개선 사항이 포함되어 있습니다.

테이블을 만들 때 [ColumnAttribute.Order]를 사용합니다.

GitHub 문제: #10059.

이제 이 Order 속성을 ColumnAttribute 사용하여 마이그레이션을 사용하여 테이블을 만들 때 열을 정렬할 수 있습니다. 예를 들어 다음 모델을 고려합니다.

public class EntityBase
{
    public int Id { get; set; }
    public DateTime UpdatedOn { get; set; }
    public DateTime CreatedOn { get; set; }
}

public class PersonBase : EntityBase
{
    public string FirstName { get; set; }
    public string LastName { get; set; }
}

public class Employee : PersonBase
{
    public string Department { get; set; }
    public decimal AnnualSalary { get; set; }
    public Address Address { get; set; }
}

[Owned]
public class Address
{
    public string House { get; set; }
    public string Street { get; set; }
    public string City { get; set; }

    [Required]
    public string Postcode { get; set; }
}

기본적으로 EF Core는 먼저 엔터티 형식 및 소유 형식의 속성과 기본 형식의 속성에 따라 기본 키 열을 정렬합니다. 예를 들어 다음 테이블은 SQL Server에 만들어집니다.

CREATE TABLE [EmployeesWithoutOrdering] (
    [Id] int NOT NULL IDENTITY,
    [Department] nvarchar(max) NULL,
    [AnnualSalary] decimal(18,2) NOT NULL,
    [Address_House] nvarchar(max) NULL,
    [Address_Street] nvarchar(max) NULL,
    [Address_City] nvarchar(max) NULL,
    [Address_Postcode] nvarchar(max) NULL,
    [UpdatedOn] datetime2 NOT NULL,
    [CreatedOn] datetime2 NOT NULL,
    [FirstName] nvarchar(max) NULL,
    [LastName] nvarchar(max) NULL,
    CONSTRAINT [PK_EmployeesWithoutOrdering] PRIMARY KEY ([Id]));

EF Core 6.0 ColumnAttribute 에서 다른 열 순서를 지정하는 데 사용할 수 있습니다. 다음은 그 예입니다.

public class EntityBase
{
    [Column(Order = 1)]
    public int Id { get; set; }

    [Column(Order = 98)]
    public DateTime UpdatedOn { get; set; }

    [Column(Order = 99)]
    public DateTime CreatedOn { get; set; }
}

public class PersonBase : EntityBase
{
    [Column(Order = 2)]
    public string FirstName { get; set; }

    [Column(Order = 3)]
    public string LastName { get; set; }
}

public class Employee : PersonBase
{
    [Column(Order = 20)]
    public string Department { get; set; }

    [Column(Order = 21)]
    public decimal AnnualSalary { get; set; }

    public Address Address { get; set; }
}

[Owned]
public class Address
{
    [Column("House", Order = 10)]
    public string House { get; set; }

    [Column("Street", Order = 11)]
    public string Street { get; set; }

    [Column("City", Order = 12)]
    public string City { get; set; }

    [Required]
    [Column("Postcode", Order = 13)]
    public string Postcode { get; set; }
}

SQL Server에서 생성된 테이블은 다음과 같습니다.

CREATE TABLE [EmployeesWithOrdering] (
    [Id] int NOT NULL IDENTITY,
    [FirstName] nvarchar(max) NULL,
    [LastName] nvarchar(max) NULL,
    [House] nvarchar(max) NULL,
    [Street] nvarchar(max) NULL,
    [City] nvarchar(max) NULL,
    [Postcode] nvarchar(max) NULL,
    [Department] nvarchar(max) NULL,
    [AnnualSalary] decimal(18,2) NOT NULL,
    [UpdatedOn] datetime2 NOT NULL,
    [CreatedOn] datetime2 NOT NULL,
    CONSTRAINT [PK_EmployeesWithOrdering] PRIMARY KEY ([Id]));

기본 형식에 정의되어 있는 FistNameLastName 열은 위쪽으로 이동됩니다, 비록 그들이 기본 형식에 정의되어 있는 경우에도 말입니다. 열 순서 값에는 간격이 있을 수 있으므로 여러 파생 형식에서 사용하는 경우에도 항상 마지막에 열을 배치하는 데 범위를 사용할 수 있습니다.

이 예제에서는 열 이름과 순서를 모두 지정하는 데 동일한 ColumnAttribute 기능을 사용하는 방법도 보여줍니다.

열 정렬은 ModelBuilder API를 OnModelCreating에서 사용하여 구성할 수도 있습니다. 다음은 그 예입니다.

modelBuilder.Entity<UsingModelBuilder.Employee>(
    entityBuilder =>
    {
        entityBuilder.Property(e => e.Id).HasColumnOrder(1);
        entityBuilder.Property(e => e.FirstName).HasColumnOrder(2);
        entityBuilder.Property(e => e.LastName).HasColumnOrder(3);

        entityBuilder.OwnsOne(
            e => e.Address,
            ownedBuilder =>
            {
                ownedBuilder.Property(e => e.House).HasColumnName("House").HasColumnOrder(4);
                ownedBuilder.Property(e => e.Street).HasColumnName("Street").HasColumnOrder(5);
                ownedBuilder.Property(e => e.City).HasColumnName("City").HasColumnOrder(6);
                ownedBuilder.Property(e => e.Postcode).HasColumnName("Postcode").HasColumnOrder(7).IsRequired();
            });

        entityBuilder.Property(e => e.Department).HasColumnOrder(8);
        entityBuilder.Property(e => e.AnnualSalary).HasColumnOrder(9);
        entityBuilder.Property(e => e.UpdatedOn).HasColumnOrder(10);
        entityBuilder.Property(e => e.CreatedOn).HasColumnOrder(11);
    });

모델 작성기에서 HasColumnOrder를 사용하여 지정하는 순서는 ColumnAttribute로 지정된 순서보다 우선합니다. HasColumnOrder는 특성으로 이루어진 순서를 재정의하는 데 사용할 수 있으며, 이는 다른 속성에서 동일한 순서 번호를 지정할 때 발생할 수 있는 충돌을 해결하는 것을 포함합니다.

중요합니다

일반적으로 대부분의 데이터베이스는 테이블을 만들 때 열 순서 지정만 지원합니다. 즉, 열 순서 특성을 사용하여 기존 테이블의 열을 다시 정렬할 수 없습니다. 이에 대한 주목할 만한 예외 중 하나는 마이그레이션이 새 열 순서로 전체 테이블을 다시 빌드하는 SQLite입니다.

EF Core 최소 API

GitHub 문제: #25192.

.NET Core 6.0에는 .NET 애플리케이션에서 일반적으로 필요한 상용구 코드를 많이 제거하는 간소화된 "최소 API"를 특징으로 하는 업데이트된 템플릿이 포함되어 있습니다.

EF Core 6.0에는 DbContext 형식을 등록하고 데이터베이스 공급자에 대한 구성을 한 줄로 제공하는 새 확장 메서드가 포함되어 있습니다. 다음은 그 예입니다.

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddSqlite<MyDbContext>("Data Source=mydatabase.db");
var builder = WebApplication.CreateBuilder(args);

builder.Services.AddSqlServer<MyDbContext>(@"Server=(localdb)\mssqllocaldb;Database=MyDatabase");
var builder = WebApplication.CreateBuilder(args);

builder.Services.AddCosmos<MyDbContext>(
    "https://localhost:8081",
    "C2y6yDjf5/R+ob0N8A7Cgv30VRDJIWEHLM+4QDU5DE2nQ9nDuVTqobD4b8mGGyPMbIZnqyMsEcaGQy67XIw/Jw==");

이는 다음과 똑같습니다.

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddDbContext<MyDbContext>(
    options => options.UseSqlite("Data Source=mydatabase.db"));
var builder = WebApplication.CreateBuilder(args);

builder.Services.AddDbContext<MyDbContext>(
    options => options.UseSqlServer(@"Server=(localdb)\mssqllocaldb;Database=MyDatabase;ConnectRetryCount=0"));
var builder = WebApplication.CreateBuilder(args);

builder.Services.AddDbContext<MyDbContext>(
    options => options.UseCosmos(
        "https://localhost:8081",
        "C2y6yDjf5/R+ob0N8A7Cgv30VRDJIWEHLM+4QDU5DE2nQ9nDuVTqobD4b8mGGyPMbIZnqyMsEcaGQy67XIw/Jw=="));

비고

EF Core 최소 API는 DbContext 및 공급자의 매우 기본적인 등록 및 구성만 지원합니다. AddDbContext, AddDbContextPool, AddDbContextFactory 등을 사용하여 EF Core에서 제공하는 모든 유형의 등록 및 구성에 액세스합니다.

최소 API에 대해 자세히 알아보려면 다음 리소스를 확인하세요.

SaveChangesAsync에서 동기화 컨텍스트 유지

GitHub 문제: #23971.

5.0 릴리스에서 EF Core 코드를 비동기 코드를 사용하는 Task.ConfigureAwait 모든 위치에서 falseawait로 설정하도록 변경했습니다. 이는 일반적으로 EF Core 사용에 더 적합한 선택입니다. 그러나 SaveChangesAsync EF Core는 비동기 데이터베이스 작업이 완료된 후 생성된 값을 추적된 엔터티로 설정하기 때문에 특별한 경우입니다. 이러한 변경은 예를 들어 U.I. 스레드에서 실행해야 할 수 있는 알림을 트리거할 수 있습니다. 따라서 EF Core 6.0에서 SaveChangesAsync 메서드에 대해서만 이 변경 사항을 되돌리고 있습니다.

메모리 내 데이터베이스: 필수 속성이 null이 아닌지 확인

GitHub 문제: #10613. 이 기능은 @fagnercarvalho 의해 제공되었습니다. 대단히 고맙습니다!

이제 EF Core 메모리 내 데이터베이스는 필수로 표시된 속성에 대해 null 값을 저장하려고 하면 예외를 throw합니다. 예를 들어 필수 User 속성이 있는 Username 형식을 고려합니다.

public class User
{
    public int Id { get; set; }

    [Required]
    public string Username { get; set; }
}

null Username 을 사용하여 엔터티를 저장하려고 하면 다음 예외가 발생합니다.

Microsoft.EntityFrameworkCore.DbUpdateException: 키 값이 '{Id: 1}'인 엔터티 형식 'User' 인스턴스에 필요한 속성 '{'Username'}'이(가) 없습니다.

필요한 경우 이 유효성 검사를 사용하지 않도록 설정할 수 있습니다. 다음은 그 예입니다.

protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
{
    optionsBuilder
        .LogTo(Console.WriteLine, new[] { InMemoryEventId.ChangesSaved })
        .UseInMemoryDatabase("UserContextWithNullCheckingDisabled", b => b.EnableNullChecks(false));
}

진단 및 인터셉터에 대한 명령 원본 정보

GitHub 문제: #23719. 이 기능은 @Giorgi 기여했습니다. 대단히 고맙습니다!

이제 CommandEventData는 진단 소스 및 인터셉터에 제공되며, 명령을 생성하는 책임이 있는 EF의 부분을 나타내는 열거형 값을 포함합니다. 진단 또는 인터셉터에서 필터로 사용할 수 있습니다. 예를 들어, 우리는 SaveChanges에서 오는 명령에만 적용되는 인터셉터를 원할 수 있습니다.

public class CommandSourceInterceptor : DbCommandInterceptor
{
    public override InterceptionResult<DbDataReader> ReaderExecuting(
        DbCommand command, CommandEventData eventData, InterceptionResult<DbDataReader> result)
    {
        if (eventData.CommandSource == CommandSource.SaveChanges)
        {
            Console.WriteLine($"Saving changes for {eventData.Context!.GetType().Name}:");
            Console.WriteLine();
            Console.WriteLine(command.CommandText);
        }

        return result;
    }
}

이렇게 하면 마이그레이션 및 쿼리를 생성하는 애플리케이션에서 사용되는 경우에만 SaveChanges 인터셉터를 이벤트로 필터링합니다. 다음은 그 예입니다.

Saving changes for CustomersContext:

SET NOCOUNT ON;
INSERT INTO [Customers] ([Name])
VALUES (@p0);
SELECT [Id]
FROM [Customers]
WHERE @@ROWCOUNT = 1 AND [Id] = scope_identity();

더 나은 임시 값 처리

GitHub 문제: #24245.

EF Core는 엔터티 형식 인스턴스에 임시 값을 노출하지 않습니다. 예를 들어 저장소 생성 키가 있는 Blog 엔터티 형식을 고려합니다.

public class Blog
{
    public int Id { get; set; }

    public ICollection<Post> Posts { get; } = new List<Post>();
}

Id 키 속성은 컨텍스트에서 Blog가 추적되면 즉시 임시 값을 가져옵니다. 예를 들어 호출할 때는 다음과 같습니다 DbContext.Add.

var blog = new Blog();
context.Add(blog);

임시 값은 컨텍스트 변경 추적기에서 가져올 수 있지만 엔터티 인스턴스로 설정되지는 않습니다. 예를 들어 다음 코드는 다음과 같습니다.

Console.WriteLine($"Blog.Id value on entity instance = {blog.Id}");
Console.WriteLine($"Blog.Id value tracked by EF = {context.Entry(blog).Property(e => e.Id).CurrentValue}");

다음 출력이 생성됩니다.

Blog.Id value on entity instance = 0
Blog.Id value tracked by EF = -2147482647

임시 값이 실수로 비 임시로 처리될 수 있는 애플리케이션 코드로 누출되는 것을 방지하므로 좋습니다. 그러나 임시 값을 직접 처리하는 것이 유용한 경우도 있습니다. 예를 들어 애플리케이션은 엔터티가 추적되기 전에 엔터티 그래프에 대한 자체 임시 값을 생성하여 외장 키를 사용하여 관계를 형성하는 데 사용할 수 있습니다. 이 작업은 값을 임시로 명시적으로 표시하여 수행할 수 있습니다. 다음은 그 예입니다.

var blog = new Blog { Id = -1 };
var post1 = new Post { Id = -1, BlogId = -1 };
var post2 = new Post { Id = -2, BlogId = -1 };

context.Add(blog).Property(e => e.Id).IsTemporary = true;
context.Add(post1).Property(e => e.Id).IsTemporary = true;
context.Add(post2).Property(e => e.Id).IsTemporary = true;

Console.WriteLine($"Blog has explicit temporary ID = {blog.Id}");
Console.WriteLine($"Post 1 has explicit temporary ID = {post1.Id} and FK to Blog = {post1.BlogId}");
Console.WriteLine($"Post 2 has explicit temporary ID = {post2.Id} and FK to Blog = {post2.BlogId}");

EF Core 6.0에서는 현재 임시로 표시된 경우에도 엔터티 인스턴스에 값이 유지됩니다. 예를 들어 위의 코드는 다음 출력을 생성합니다.

Blog has explicit temporary ID = -1
Post 1 has explicit temporary ID = -1 and FK to Blog = -1
Post 2 has explicit temporary ID = -2 and FK to Blog = -1

마찬가지로 EF Core에서 생성된 임시 값을 엔터티 인스턴스로 명시적으로 설정하고 임시 값으로 표시할 수 있습니다. 임시 키 값을 사용하여 새 엔터티 간의 관계를 명시적으로 설정하는 데 사용할 수 있습니다. 다음은 그 예입니다.

var post1 = new Post();
var post2 = new Post();

var blogIdEntry = context.Entry(blog).Property(e => e.Id);
blog.Id = blogIdEntry.CurrentValue;
blogIdEntry.IsTemporary = true;

var post1IdEntry = context.Add(post1).Property(e => e.Id);
post1.Id = post1IdEntry.CurrentValue;
post1IdEntry.IsTemporary = true;
post1.BlogId = blog.Id;

var post2IdEntry = context.Add(post2).Property(e => e.Id);
post2.Id = post2IdEntry.CurrentValue;
post2IdEntry.IsTemporary = true;
post2.BlogId = blog.Id;

Console.WriteLine($"Blog has generated temporary ID = {blog.Id}");
Console.WriteLine($"Post 1 has generated temporary ID = {post1.Id} and FK to Blog = {post1.BlogId}");
Console.WriteLine($"Post 2 has generated temporary ID = {post2.Id} and FK to Blog = {post2.BlogId}");

결과는 다음과 같습니다.

Blog has generated temporary ID = -2147482647
Post 1 has generated temporary ID = -2147482647 and FK to Blog = -2147482647
Post 2 has generated temporary ID = -2147482646 and FK to Blog = -2147482647

C# nullable 참조 타입에 주석이 추가된 EF Core

GitHub 문제: #19007.

이제 EF Core 코드베이스는 전체 C# NRT(nullable 참조 형식) 를 사용합니다. 즉, 사용자 고유의 코드에서 EF Core 6.0을 사용할 때 null 사용에 대한 올바른 컴파일러 표시가 표시됩니다.

Microsoft.Data.Sqlite 6.0

팁 (조언)

GitHub에서 샘플 코드를 다운로드하여 아래에 표시된 모든 샘플을 실행하고 디버그할 수 있습니다.

커넥션 풀링

GitHub 문제: #13837.

데이터베이스 연결을 가능한 한 적은 시간 동안 열어 두는 것이 일반적입니다. 이렇게 하면 연결 리소스에 대한 경합을 방지할 수 있습니다. 이 때문에 EF Core와 같은 라이브러리는 데이터베이스 작업을 수행하기 직전에 연결을 열고 즉시 다시 닫습니다. 예를 들어 다음 EF Core 코드를 고려합니다.

Console.WriteLine("Starting query...");
Console.WriteLine();

var users = await context.Users.ToListAsync();

Console.WriteLine();
Console.WriteLine("Query finished.");
Console.WriteLine();

foreach (var user in users)
{
    if (user.Username.Contains("microsoft"))
    {
        user.Username = "msft:" + user.Username;

        Console.WriteLine("Starting SaveChanges...");
        Console.WriteLine();

        await context.SaveChangesAsync();

        Console.WriteLine();
        Console.WriteLine("SaveChanges finished.");
    }
}

연결에 대한 로깅이 켜져 있는 이 코드의 출력은 다음과 같습니다.

Starting query...

dbug: 8/27/2021 09:26:57.810 RelationalEventId.ConnectionOpened[20001] (Microsoft.EntityFrameworkCore.Database.Connection)
      Opened connection to database 'main' on server 'C:\dotnet\efdocs\samples\core\Miscellaneous\NewInEFCore6\bin\Debug\net6.0\test.db'.
dbug: 8/27/2021 09:26:57.813 RelationalEventId.ConnectionClosed[20003] (Microsoft.EntityFrameworkCore.Database.Connection)
      Closed connection to database 'main' on server 'test.db'.

Query finished.

Starting SaveChanges...

dbug: 8/27/2021 09:26:57.813 RelationalEventId.ConnectionOpened[20001] (Microsoft.EntityFrameworkCore.Database.Connection)
      Opened connection to database 'main' on server 'C:\dotnet\efdocs\samples\core\Miscellaneous\NewInEFCore6\bin\Debug\net6.0\test.db'.
dbug: 8/27/2021 09:26:57.814 RelationalEventId.ConnectionClosed[20003] (Microsoft.EntityFrameworkCore.Database.Connection)
      Closed connection to database 'main' on server 'test.db'.

SaveChanges finished.

각 작업에 대해 연결이 빠르게 열리고 닫힙니다.

그러나 대부분의 데이터베이스 시스템의 경우 데이터베이스에 대한 물리적 연결을 여는 것은 비용이 많이 드는 작업입니다. 따라서 대부분의 ADO.NET 공급자는 물리적 연결 풀을 만들고 필요에 따라 DbConnection 인스턴스에 빌려줍니다.

SQLite는 데이터베이스 액세스가 일반적으로 파일에만 액세스하기 때문에 약간 다릅니다. 즉, SQLite 데이터베이스에 대한 연결을 여는 것이 일반적으로 매우 빠릅니다. 그러나 항상 그렇지는 않습니다. 예를 들어 암호화된 데이터베이스에 대한 연결을 여는 것은 매우 느릴 수 있습니다. 따라서 이제 Microsoft.Data.Sqlite 6.0을 사용할 때 SQLite 연결이 풀링됩니다.

DateOnly 및 TimeOnly 지원

GitHub 문제: #24506.

Microsoft.Data.Sqlite 6.0은 .NET 6의 새 DateOnly 형식과 TimeOnly 형식을 지원합니다. EF Core 6.0에서 SQLite 공급자와 함께 사용할 수도 있습니다. SQLite와 마찬가지로 네이티브 형식 시스템은 이러한 형식의 값을 지원되는 네 가지 형식 중 하나로 저장해야 한다는 것을 의미합니다. Microsoft.Data.Sqlite는 이를 .로 TEXT저장합니다. 예를 들어 다음 형식을 사용하는 엔터티:

public class User
{
    public int Id { get; set; }
    public string Username { get; set; }

    public DateOnly Birthday { get; set; }
    public TimeOnly TokensRenewed { get; set; }
}

SQLite 데이터베이스의 다음 표에 매핑됩니다.

CREATE TABLE "Users" (
    "Id" INTEGER NOT NULL CONSTRAINT "PK_Users" PRIMARY KEY AUTOINCREMENT,
    "Username" TEXT NULL,
    "Birthday" TEXT NOT NULL,
    "TokensRenewed" TEXT NOT NULL);

그런 다음 일반적인 방식으로 값을 저장, 쿼리 및 업데이트할 수 있습니다. 예를 들어 이 EF Core LINQ 쿼리는 다음과 같습니다.

var users = await context.Users.Where(u => u.Birthday < new DateOnly(1900, 1, 1)).ToListAsync();

SQLite에서 다음으로 변환됩니다.

SELECT "u"."Id", "u"."Birthday", "u"."TokensRenewed", "u"."Username"
FROM "Users" AS "u"
WHERE "u"."Birthday" < '1900-01-01'

그리고 1900년 이전 생일을 가진 경우에만 사용 반환:

Found 'ajcvickers'
Found 'wendy'

Savepoints API

GitHub 문제: #20228.

ADO.NET 공급자의 저장점에 대한 공통 API를 표준화하고 있습니다. 이제 Microsoft.Data.Sqlite는 다음을 포함하여 이 API를 지원합니다.

저장점을 사용하면 전체 트랜잭션을 롤백하지 않고 트랜잭션의 일부를 롤백할 수 있습니다. 예를 들어 아래 코드는 다음과 같습니다.

  • 트랜잭션을 만듭니다.
  • 데이터베이스에 업데이트를 보냅니다.
  • 저장점을 만듭니다.
  • 데이터베이스에 다른 업데이트 보내기
  • 이전에 만든 저장점으로 롤백
  • 트랜잭션 커밋
using var connection = new SqliteConnection("Command Timeout=60;DataSource=test.db");
await connection.OpenAsync();

await using var transaction = await connection.BeginTransactionAsync();

using (var command = connection.CreateCommand())
{
    command.CommandText = @"UPDATE Users SET Username = 'ajcvickers' WHERE Id = 1";
    await command.ExecuteNonQueryAsync();
}

await transaction.SaveAsync("MySavepoint");

using (var command = connection.CreateCommand())
{
    command.CommandText = @"UPDATE Users SET Username = 'wfvickers' WHERE Id = 2";
    await command.ExecuteNonQueryAsync();
}

await transaction.RollbackAsync("MySavepoint");

await transaction.CommitAsync();

이렇게 하면 첫 번째 업데이트가 데이터베이스에 커밋되지만 트랜잭션을 커밋하기 전에 저장점이 롤백된 이후 두 번째 업데이트는 커밋되지 않습니다.

연결 문자열의 명령 시간 제한

GitHub 문제: #22505. 이 기능은 @nmichels 의해 제공되었습니다. 대단히 고맙습니다!

ADO.NET 공급자는 두 가지 고유한 시간 제한을 지원합니다.

  • 데이터베이스에 연결할 때 대기할 최대 시간을 결정하는 연결 시간 제한입니다.
  • 명령이 실행이 완료되기를 기다리는 최대 시간을 결정하는 명령 시간 제한입니다.

코드에서 DbCommand.CommandTimeout를 사용하여 명령 시간 제한을 설정할 수 있습니다. 이제 많은 공급자가 연결 문자열에서 이 명령 시간 제한을 노출하고 있습니다. Microsoft.Data.Sqlite는 연결 문자열 키워드를 사용하여 Command Timeout 이러한 추세를 따르고 있습니다. 예를 들어 연결 "Command Timeout=60;DataSource=test.db" 에서 만든 명령에 대한 기본 시간 제한으로 60초를 사용합니다.

팁 (조언)

Sqlite는 Default Timeout 동의어 Command Timeout 로 취급되므로 원하는 경우 대신 사용할 수 있습니다.