빠른 시작: ASP.NET Core 앱에서 Microsoft.Data.SqlClient 사용

이 퀵스타트에서는 Microsoft.Data.SqlClient 7.1.1을 사용하여 ASP.NET Core 최소 API를 구축합니다.

빈 폴더, .NET SDK, 그리고 컴퓨터에서 접근할 수 있는 SQL Server나 Azure SQL 데이터베이스로 시작합니다. 결과적으로 테이블에 메시지를 추가하고 ID로 다시 읽어오는 웹 API가 완성됩니다.

이 과정에서 앱은 다음을 수행합니다:

  • ASP.NET Core 구성에서 연결 문자열을 읽습니다. 로컬 개발의 경우, 값은 사용자 비밀에서 나오기 때문에 프로젝트 폴더에는 들어가지 않습니다.
  • 의존성 주입으로 데이터 클래스를 등록합니다.
  • 각 요청마다 연결을 열고 폐기하여 기본 연결 풀이 물리적 연결을 재사용할 수 있게 합니다.
  • 모든 데이터베이스 호출에 요청의 CancellationToken를 전달합니다.

이 앱은 Quickstart: Connect and query with Microsoft.Data.SqlClient와 동일한 dbo.SqlClientQuickstart 테이블을 사용합니다. 먼저 둘 중 어느 퀵스타트든 해도 됩니다.

Prerequisites

현재 ASP.NET Core는 .NET 프레임워크가 아닌 .NET에서 실행됩니다. 드라이버를 .NET Framework에서 사용하려면 .NET Framework 프로젝트를 참조하세요.

데이터베이스 정체성은 연결하고 테이블을 생성, 삽입, 선택하려면 권한이 필요합니다.

프로젝트를 만듭니다.

빈 폴더에서 다음 명령어를 실행하세요:

dotnet new web --framework net10.0 --name SqlClientWebQuickstart
cd SqlClientWebQuickstart
dotnet add package Microsoft.Data.SqlClient --version 7.1.1
dotnet add package Microsoft.Data.SqlClient.Extensions.Azure --version 7.1.1
dotnet user-secrets init

dotnet user-secrets init 프로젝트 파일에 UserSecretsId를 추가합니다. ASP.NET Core는 앱이 Development 환경에서 실행될 때만 사용자 비밀을 로드하며, 이는 dotnet run의 기본값입니다.

확장 패키지는 드라이버의 Microsoft Entra 인증 모드를 제공합니다. 만약 앱이 SQL 인증만 사용한다면, Microsoft.Data.SqlClient.Extensions.Azure를 생략할 수 있습니다. 두 패키지를 사용할 때는 버전은 동일하게 유지하세요.

연결 설정

사용자 비밀에 ConnectionStrings:Quickstart 연결 문자열을 저장하세요. 사용자 비밀은 프로젝트 폴더가 아니라 사용자 프로필에 저장되기 때문에 소스 관리에 커밋되지 않습니다. 비밀번호 또는 프로덕션 연결 문자열을 appsettings.json에 넣지 마세요.

이 시작 지점 중 하나를 선택하고 자리 표시자를 교체하세요. dotnet user-secrets set PowerShell과 Bash 명령어는 단일 따옴표를 사용합니다. 명령 프롬프트에서는 대신 이중 따옴표를 사용하세요. 명령 프롬프트는 키와 값의 일부로 단일 따옴표를 저장하기 때문입니다.

Azure SQL with Microsoft Entra ID

데이터베이스에 접근할 수 있는 Microsoft Entra 신원으로 로그인하세요. 로컬 개발을 위해서는 Azure CLI와 같은 개발자 도구를 사용하세요:

az login

그 다음 연결 문자열을 저장하세요. Azure SQL 데이터베이스에서 정확한 서버와 데이터베이스 이름을 복사하세요. 동일한 연결 문자열은 콘솔 퀵스타트에서 설명된 SQL 데이터베이스 항목의 서버 및 데이터베이스 이름을 사용하는 경우 Microsoft Fabric의 SQL 데이터베이스에서도 작동합니다.

dotnet user-secrets set 'ConnectionStrings:Quickstart' 'Server=tcp:<server>,1433;Database=<database>;Authentication=Active Directory Default;Encrypt=Strict;Connect Timeout=30'

이 연결 문자열에는 비밀 값이 포함되어 있지 않습니다. Active Directory Default는 사용자의 개발자 로그인을 로컬에서 사용합니다. Azure App Service 또는 Azure Container Apps에 배포할 때, 앱의 관리 ID에 데이터베이스 액세스 권한을 부여하고, 값을 Authentication=Active Directory Managed Identity로 변경한 다음 호스트의 구성에서 ConnectionStrings__Quickstart로 설정합니다. 다른 옵션에 대해서는 Microsoft.Data.SqlClient를 사용한 Microsoft Entra 인증을 참조하세요.

SQL 인증이 있는 로컬 SQL Server

신뢰할 수 있는 인증서가 없는 로컬 개발 인스턴스의 연결 문자열을 저장하세요. PowerShell과 Bash가 비밀번호에서 $ 문자를 확장하지 않도록 단일 따옴표를 사용하세요.

dotnet user-secrets set 'ConnectionStrings:Quickstart' 'Server=tcp:<server>,1433;Database=<database>;User ID=<user-id>;Password=<password>;Encrypt=true;TrustServerCertificate=true;Connect Timeout=30'

Caution

TrustServerCertificate=true 서버 인증서 검증을 건너뛸 수 있습니다. 로컬 개발 인스턴스에서만 사용하세요. 공유 또는 운영 SQL Server 인스턴스의 경우, 클라이언트가 신뢰하는 인증서를 설치하고 TrustServerCertificate=true를 제거하세요. 자세한 내용은 Microsoft.Data.SqlClient의 암호화 및 인증서 유효성 검사를 참조하세요.

코드 추가

Program.cs의 내용을 다음 코드로 바꿉니다.

using System.Data;
using Microsoft.Data.SqlClient;

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddSingleton<MessageStore>();

var app = builder.Build();

await app.Services.GetRequiredService<MessageStore>()
    .EnsureTableAsync(app.Lifetime.ApplicationStopping);

app.MapPost("/messages", async (
    NewMessage input, MessageStore store, CancellationToken cancellationToken) =>
{
    if (string.IsNullOrWhiteSpace(input.Text) || input.Text.Length > 200)
    {
        return Results.BadRequest("Text must be 1 to 200 characters.");
    }

    Message message = await store.AddAsync(input.Text, cancellationToken);
    return Results.Created($"/messages/{message.Id}", message);
});

app.MapGet("/messages/{id:int}", async (
    int id, MessageStore store, CancellationToken cancellationToken) =>
    await store.GetAsync(id, cancellationToken) is Message message
        ? Results.Ok(message)
        : Results.NotFound());

app.Run();

record NewMessage(string Text);

record Message(int Id, string Text, DateTimeOffset CreatedAt);

sealed class MessageStore(IConfiguration configuration)
{
    private readonly string _connectionString =
        configuration.GetConnectionString("Quickstart")
        ?? throw new InvalidOperationException(
            "Set the ConnectionStrings:Quickstart configuration value.");

    public async Task EnsureTableAsync(CancellationToken cancellationToken)
    {
        const string sql = """
            IF OBJECT_ID(N'dbo.SqlClientQuickstart', N'U') IS NULL
            BEGIN
                CREATE TABLE dbo.SqlClientQuickstart
                (
                    Id int IDENTITY(1, 1) PRIMARY KEY,
                    Message nvarchar(200) NOT NULL,
                    CreatedAt datetimeoffset NOT NULL
                        CONSTRAINT DF_SqlClientQuickstart_CreatedAt
                        DEFAULT sysdatetimeoffset()
                );
            END;
            """;

        await using var connection = new SqlConnection(_connectionString);
        await connection.OpenAsync(cancellationToken);
        await using var command =
            new SqlCommand(sql, connection) { CommandTimeout = 30 };
        await command.ExecuteNonQueryAsync(cancellationToken);
    }

    public async Task<Message> AddAsync(
        string text, CancellationToken cancellationToken)
    {
        const string sql = """
            INSERT INTO dbo.SqlClientQuickstart (Message)
            OUTPUT INSERTED.Id, INSERTED.Message, INSERTED.CreatedAt
            VALUES (@message);
            """;

        await using var connection = new SqlConnection(_connectionString);
        await connection.OpenAsync(cancellationToken);
        await using var command =
            new SqlCommand(sql, connection) { CommandTimeout = 30 };
        command.Parameters.Add("@message", SqlDbType.NVarChar, 200).Value = text;

        await using SqlDataReader reader =
            await command.ExecuteReaderAsync(cancellationToken);
        await reader.ReadAsync(cancellationToken);
        return new Message(
            reader.GetInt32(0), reader.GetString(1), reader.GetDateTimeOffset(2));
    }

    public async Task<Message?> GetAsync(
        int id, CancellationToken cancellationToken)
    {
        const string sql = """
            SELECT Id, Message, CreatedAt
            FROM dbo.SqlClientQuickstart
            WHERE Id = @id;
            """;

        await using var connection = new SqlConnection(_connectionString);
        await connection.OpenAsync(cancellationToken);
        await using var command =
            new SqlCommand(sql, connection) { CommandTimeout = 30 };
        command.Parameters.Add("@id", SqlDbType.Int).Value = id;

        await using SqlDataReader reader =
            await command.ExecuteReaderAsync(cancellationToken);
        return await reader.ReadAsync(cancellationToken)
            ? new Message(
                reader.GetInt32(0), reader.GetString(1), reader.GetDateTimeOffset(2))
            : null;
    }
}

앱 실행

앱을 고정 포트로 시작하세요:

dotnet run --urls http://localhost:5000

시작 시, 테이블이 없으면 앱이 그 테이블을 생성합니다. 앱이 준비되면 출력에는 다음과 같은 문장이 포함됩니다:

info: Microsoft.Hosting.Lifetime[14]
      Now listening on: http://localhost:5000
info: Microsoft.Hosting.Lifetime[0]
      Application started. Press Ctrl+C to shut down.
info: Microsoft.Hosting.Lifetime[0]
      Hosting environment: Development

앱이 데이터베이스에 도달하지 못하면 시작 시 SqlException와 함께 멈춥니다. 일반적인 원인에 대해서는 문제 해결을 참조하세요.

API 호출

앱을 켜둔 채 두 번째 터미널을 열어보세요. POST 요청으로 메시지를 추가한 뒤, GET 요청으로 ID로 다시 읽으세요. 테이블에 이미 행이 있다면(예: 콘솔 퀵스타트에서 생성된 행), GET 요청의 id를 POST 요청이 반환한 1로 바꾸세요.

PowerShell의 경우:

Invoke-RestMethod -Method Post -Uri http://localhost:5000/messages -ContentType application/json -Body (@{ text = 'Hello from ASP.NET Core' } | ConvertTo-Json)
Invoke-RestMethod http://localhost:5000/messages/1

PowerShell은 각 JSON 응답을 테이블로 포맷합니다. 첫 번째 행은 201 Created POST 요청에 대한 응답이고, 두 번째 행은 GET 응답입니다:

id text                    createdAt
-- ----                    ---------
 1 Hello from ASP.NET Core 2026-10-21T00:04:43.3588978+00:00
 1 Hello from ASP.NET Core 2026-10-21T00:04:43.3588978+00:00

Windows PowerShell 5.1에는 API가 반환하는 문자열로 createdAt이(가) 표시됩니다. PowerShell 7은 이를 현지 시간의 날짜(예: 10/20/2026 7:04:43 PM.)로 변환합니다.

Windows PowerShell에서는 curl는 curl 옵션을 받지 않는 Invoke-WebRequest의 별칭입니다. PowerShell 명령어를 사용하거나 명령 프롬프트에서 실행 curl.exe 하세요.

명령 프롬프트의 경우:

curl -X POST http://localhost:5000/messages -H "Content-Type: application/json" -d "{\"text\":\"Hello from ASP.NET Core\"}"
curl http://localhost:5000/messages/1

명령 프롬프트는 단일 따옴표를 따옴표 문자로 취급하지 않기 때문에, JSON 본문은 백슬래시로 이스케이프한 이중 따옴표를 사용합니다. 각 명령어는 JSON 응답을 출력합니다:

{"id":1,"text":"Hello from ASP.NET Core","createdAt":"2026-10-21T00:05:15.4936763+00:00"}
{"id":1,"text":"Hello from ASP.NET Core","createdAt":"2026-10-21T00:05:15.4936763+00:00"}

Bash의 경우:

curl -X POST http://localhost:5000/messages -H "Content-Type: application/json" -d '{"text":"Hello from ASP.NET Core"}'
curl http://localhost:5000/messages/1
{"id":1,"text":"Hello from ASP.NET Core","createdAt":"2026-10-20T20:44:17.8634925+00:00"}
{"id":1,"text":"Hello from ASP.NET Core","createdAt":"2026-10-20T20:44:17.8634925+00:00"}

여러분의 ID와 타임스탬프가 다릅니다. 콘솔 퀵스타트를 먼저 실행하면 ID가 해당 행의 ID부터 이어집니다. 존재하지 않는 ID를 요청하면 404 Not Found이 반환됩니다. 빈 메시지는 400 Bad Request를 반환합니다.

첫 번째 터미널에서 Ctrl+C를 눌러 앱을 멈추세요.

코드 작동 방식

  • MessageStore는 싱글턴으로 등록됩니다. 이는 연결 문자열만 저장하며, 한 번만 IConfiguration에서 읽습니다. 연결이 유지되지 않습니다.
  • 각 메서드는 개체 SqlConnection를 생성하고, 이를 열고, 한 가지 명령을 실행한 후 await using으로 해제합니다. Disposing은 물리적 연결을 연결 풀로 반환하며, 다음 요청에서 이를 재사용합니다. 연결 풀링은 기본적으로 켜져 있으니 따로 설정할 필요가 없습니다. 튜닝 옵션에 대해서는 Microsoft.Data.SqlClient를 사용한 SQL Server 연결 풀링을 참조하세요.
  • ASP.NET Core는 CancellationToken 매개변수를 요청에 바인딩합니다. 클라이언트가 연결을 끊으면 토큰은 열기 작업, 명령어, 읽기를 취소합니다.
  • @message 와 @id 매개변수의 유형과 크기는 열의 유형과 크기와 일치합니다. API는 값을 데이터베이스에 보내기 전에 메시지 길이를 검증합니다.
  • OUTPUT INSERTED.Id, INSERTED.Message, INSERTED.CreatedAt 서버가 생성한 ID와 타임스탬프를 포함한 삽입된 행을 두 번째 쿼리 없이 반환합니다.
  • 다른 환경에서는 ASP.NET Core가 환경 변수(예: ConnectionStrings:Quickstart) 또는 Azure Key Vault와 같은 다른 구성 공급자에서 동일한 ConnectionStrings__Quickstart 키를 읽습니다.

Troubleshoot

오류 또는 증상 확인할 사항 자세한 내용
Set the ConnectionStrings:Quickstart configuration value. 앱이 연결 문자열을 찾지 못했습니다. 프로젝트 폴더에서 dotnet user-secrets list을 실행하고, 앱이 Development 환경에서 실행되는지 확인하세요. ASP.NET Core 개발 시 앱 비밀의 안전한 저장
System.ArgumentException: Cannot find an authentication provider for 'ActiveDirectoryDefault' 시작 시 연결 문자열은 인증 모드Active Directory를 사용하지만, 프로젝트에서 Microsoft.Data.SqlClient.Extensions.Azure를 참조하지 않습니다. Microsoft.Data.SqlClient와 동일한 버전의 패키지를 추가하세요. Microsoft Entra 인증은 7.0에서 확장 패키지로 이동합니다
SqlException 시작 시 앱이 연결하지 못했거나 테이블을 생성하지 못했습니다. 오류 번호는 원인을 식별합니다. 예를 들어 로그인 실패의 경우 18456입니다. 콘솔 퀵스타트에서 문제 해결
415 Unsupported Media Type 요청이 Content-Type: application/json을 설정하지 않습니다. 최소 API 빠른 참조
Cannot bind parameter 'Headers' Windows PowerShell에서 curl 명령어를 실행했는데, 여기서 curl에서 Invoke-WebRequest이 실행됩니다. Call the API의 PowerShell 명령어를 사용하세요. Invoke-RestMethod
''' is an invalid start of a value 명령 프롬프트에서 Bash curl 명령을 실행했기 때문에, 그 결과 단일 따옴표가 본문의 일부로 전송됩니다. Call the API에 있는 명령 프롬프트 명령을 사용하세요. API를 호출하세요

자원을 정리하세요

쿼리 도구를 사용해 테이블을 드롭하세요:

DROP TABLE IF EXISTS dbo.SqlClientQuickstart;

저장된 연결 문자열을 제거하세요. 프로젝트 폴더에서 실행하세요:

dotnet user-secrets clear

그 다음 프로젝트 폴더를 삭제하세요. 상위 폴더에서 PowerShell로 다음 명령을 실행하세요:

Remove-Item -Recurse -Force SqlClientWebQuickstart

Bash의 경우:

rm -rf SqlClientWebQuickstart

다음 단계