ASP.NET Core Web API에서 포맷터 사용자 지정

ASP.NET Core MVC는 입력 및 출력 포맷터를 사용하여 Web API에서 데이터 교환을 지원합니다. 입력 포맷터는 모델 바인딩에서 사용됩니다. 출력 포맷터는 응답 형식을 지정하는 데 사용됩니다.

프레임워크는 JSON 및 XML에 대한 기본 제공 입력 및 출력 포맷터를 제공합니다. 일반 텍스트에 대한 기본 제공 출력 포맷터는 제공하지만 일반 텍스트에 대한 입력 포맷터는 제공하지 않습니다.

이 문서에서는 사용자 지정 포맷터를 만들어 추가 형식에 대한 지원을 추가하는 방법을 보여줍니다. 사용자 지정 일반 텍스트 입력 포맷터의 예제는 GitHub의 TextPlainInputFormatter를 참조하세요.

샘플 코드 보기 및 다운로드(다운로드 방법)

사용자 지정 포맷터를 사용하는 경우

사용자 지정 포맷터를 사용하여 기본 제공 포맷터에서 처리되지 않는 콘텐츠 형식에 대한 지원을 추가합니다.

사용자 지정 포맷터를 사용하는 방법 개요

사용자 지정 포맷터를 만들려면 다음을 수행합니다.

  • 클라이언트로 전송된 데이터를 직렬화하려면 출력 포맷터 클래스를 만듭니다.
  • 클라이언트에서 수신된 데이터를 역직렬화하려면 입력 포맷터 클래스를 만듭니다.
  • 포맷터 클래스의 인스턴스를 MvcOptionsInputFormattersOutputFormatters 컬렉션에 추가합니다.

사용자 지정 포맷터 만들기

포맷터를 만들려면:

다음 코드는 샘플VcardOutputFormatter 클래스를 보여 줍니다.

public class VcardOutputFormatter : TextOutputFormatter
{
    public VcardOutputFormatter()
    {
        SupportedMediaTypes.Add(MediaTypeHeaderValue.Parse("text/vcard"));

        SupportedEncodings.Add(Encoding.UTF8);
        SupportedEncodings.Add(Encoding.Unicode);
    }

    protected override bool CanWriteType(Type? type)
        => typeof(Contact).IsAssignableFrom(type)
            || typeof(IEnumerable<Contact>).IsAssignableFrom(type);

    public override async Task WriteResponseBodyAsync(
        OutputFormatterWriteContext context, Encoding selectedEncoding)
    {
        var httpContext = context.HttpContext;
        var serviceProvider = httpContext.RequestServices;

        var logger = serviceProvider.GetRequiredService<ILogger<VcardOutputFormatter>>();
        var buffer = new StringBuilder();

        if (context.Object is IEnumerable<Contact> contacts)
        {
            foreach (var contact in contacts)
            {
                FormatVcard(buffer, contact, logger);
            }
        }
        else
        {
            FormatVcard(buffer, (Contact)context.Object!, logger);
        }

        await httpContext.Response.WriteAsync(buffer.ToString(), selectedEncoding);
    }

    private static void FormatVcard(
        StringBuilder buffer, Contact contact, ILogger logger)
    {
        buffer.AppendLine("BEGIN:VCARD");
        buffer.AppendLine("VERSION:2.1");
        buffer.AppendLine($"N:{contact.LastName};{contact.FirstName}");
        buffer.AppendLine($"FN:{contact.FirstName} {contact.LastName}");
        buffer.AppendLine($"UID:{contact.Id}");
        buffer.AppendLine("END:VCARD");

        logger.LogInformation("Writing {FirstName} {LastName}",
            contact.FirstName, contact.LastName);
    }
}

적절한 기본 클래스에서 파생

텍스트 미디어 형식(예: vCard)의 경우 TextInputFormatter 또는 TextOutputFormatter 기본 클래스에서 파생합니다.

public class VcardOutputFormatter : TextOutputFormatter

이진 형식의 경우 InputFormatter 또는 OutputFormatter 기본 클래스에서 파생합니다.

지원되는 미디어 유형 및 인코딩 지정

생성자에서 SupportedMediaTypesSupportedEncodings 컬렉션에 추가하여 지원되는 미디어 형식 및 인코딩을 지정합니다.

public VcardOutputFormatter()
{
    SupportedMediaTypes.Add(MediaTypeHeaderValue.Parse("text/vcard"));

    SupportedEncodings.Add(Encoding.UTF8);
    SupportedEncodings.Add(Encoding.Unicode);
}

포맷터 클래스는 해당 종속성에 대해 생성자 주입을 사용할 수 없습니다. 예를 들어 ILogger<VcardOutputFormatter>는 생성자에 매개 변수로 추가할 수 없습니다. 서비스에 액세스하려면 메서드에 전달된 컨텍스트 개체를 사용합니다. 이 문서의 예제와 샘플에 이 작업을 수행하는 방법이 나와 있습니다.

CanReadType 및 CanWriteType 재정의

CanReadType 또는 CanWriteType 메서드를 재정의하여 역직렬화하거나 직렬화하는 형식을 지정합니다. 예를 들어 Contact 형식에서 vCard 텍스트를 만들고 그 반대로 만듭니다.

protected override bool CanWriteType(Type? type)
    => typeof(Contact).IsAssignableFrom(type)
        || typeof(IEnumerable<Contact>).IsAssignableFrom(type);

CanWriteResult 메서드

일부 시나리오에서 CanWriteResultCanWriteType 대신 재정의해야 합니다. 다음 조건이 true인 경우 CanWriteResult를 사용합니다.

  • 작업 메서드는 모델 클래스를 반환합니다.
  • 런타임 시 반환될 수 있는 파생 클래스가 있습니다.
  • 동작에서 반환된 파생 클래스는 런타임 시 알려야 합니다.

예를 들어 작업 메서드를 가정해 보겠습니다.

  • 시그니처는 Person 형식을 반환합니다.
  • Person에서 파생된 Student 또는 Instructor 형식을 반환할 수 있습니다.

포맷터에서 Student 개체만을 처리하려는 경우 CanWriteResult 메서드에 제공된 컨텍스트 개체에서 Object 형식을 확인합니다. 작업 메서드가 IActionResult를 반환하는 경우:

  • CanWriteResult를 사용할 필요가 없습니다.
  • CanWriteType 메서드는 런타임 형식을 받습니다.

ReadRequestBodyAsync 및 WriteResponseBodyAsync 재정의

deserialization 또는 serialization은 ReadRequestBodyAsync 또는 WriteResponseBodyAsync에서 수행됩니다. 다음 예제에서는 종속성 주입 컨테이너에서 서비스를 얻는 방법을 보여 줍니다. 생성자 매개 변수에서 서비스를 가져올 수 없습니다.

public override async Task WriteResponseBodyAsync(
    OutputFormatterWriteContext context, Encoding selectedEncoding)
{
    var httpContext = context.HttpContext;
    var serviceProvider = httpContext.RequestServices;

    var logger = serviceProvider.GetRequiredService<ILogger<VcardOutputFormatter>>();
    var buffer = new StringBuilder();

    if (context.Object is IEnumerable<Contact> contacts)
    {
        foreach (var contact in contacts)
        {
            FormatVcard(buffer, contact, logger);
        }
    }
    else
    {
        FormatVcard(buffer, (Contact)context.Object!, logger);
    }

    await httpContext.Response.WriteAsync(buffer.ToString(), selectedEncoding);
}

private static void FormatVcard(
    StringBuilder buffer, Contact contact, ILogger logger)
{
    buffer.AppendLine("BEGIN:VCARD");
    buffer.AppendLine("VERSION:2.1");
    buffer.AppendLine($"N:{contact.LastName};{contact.FirstName}");
    buffer.AppendLine($"FN:{contact.FirstName} {contact.LastName}");
    buffer.AppendLine($"UID:{contact.Id}");
    buffer.AppendLine("END:VCARD");

    logger.LogInformation("Writing {FirstName} {LastName}",
        contact.FirstName, contact.LastName);
}

사용자 지정 포맷터를 사용하도록 MVC 구성

사용자 지정 포맷터를 사용하려면 MvcOptions.InputFormatters 또는 MvcOptions.OutputFormatters 컬렉션에 포맷터 클래스의 인스턴스를 추가합니다.

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddControllers(options =>
{
    options.InputFormatters.Insert(0, new VcardInputFormatter());
    options.OutputFormatters.Insert(0, new VcardOutputFormatter());
});

포맷터는 삽입된 순서대로 평가되며, 첫 번째 포맷터가 우선됩니다.

전체 VcardInputFormatter 클래스

다음 코드는 샘플VcardInputFormatter 클래스를 보여 줍니다.

public class VcardInputFormatter : TextInputFormatter
{
    public VcardInputFormatter()
    {
        SupportedMediaTypes.Add(MediaTypeHeaderValue.Parse("text/vcard"));

        SupportedEncodings.Add(Encoding.UTF8);
        SupportedEncodings.Add(Encoding.Unicode);
    }

    protected override bool CanReadType(Type type)
        => type == typeof(Contact);

    public override async Task<InputFormatterResult> ReadRequestBodyAsync(
        InputFormatterContext context, Encoding effectiveEncoding)
    {
        var httpContext = context.HttpContext;
        var serviceProvider = httpContext.RequestServices;

        var logger = serviceProvider.GetRequiredService<ILogger<VcardInputFormatter>>();

        using var reader = new StreamReader(httpContext.Request.Body, effectiveEncoding);
        string? nameLine = null;

        try
        {
            await ReadLineAsync("BEGIN:VCARD", reader, context, logger);
            await ReadLineAsync("VERSION:", reader, context, logger);

            nameLine = await ReadLineAsync("N:", reader, context, logger);

            var split = nameLine.Split(";".ToCharArray());
            var contact = new Contact(FirstName: split[1], LastName: split[0].Substring(2));

            await ReadLineAsync("FN:", reader, context, logger);
            await ReadLineAsync("END:VCARD", reader, context, logger);

            logger.LogInformation("nameLine = {nameLine}", nameLine);

            return await InputFormatterResult.SuccessAsync(contact);
        }
        catch
        {
            logger.LogError("Read failed: nameLine = {nameLine}", nameLine);
            return await InputFormatterResult.FailureAsync();
        }
    }

    private static async Task<string> ReadLineAsync(
        string expectedText, StreamReader reader, InputFormatterContext context,
        ILogger logger)
    {
        var line = await reader.ReadLineAsync();

        if (line is null || !line.StartsWith(expectedText))
        {
            var errorMessage = $"Looked for '{expectedText}' and got '{line}'";

            context.ModelState.TryAddModelError(context.ModelName, errorMessage);
            logger.LogError(errorMessage);

            throw new Exception(errorMessage);
        }

        return line;
    }
}

앱 테스트

기본 vCard 입력 및 출력 포맷터를 구현하는 이 문서의 샘플 앱을 실행합니다. 앱은 다음 형식과 유사한 vCard를 읽고 씁니다.

BEGIN:VCARD
VERSION:2.1
N:Davolio;Nancy
FN:Nancy Davolio
END:VCARD

vCard 출력을 보려면 앱을 실행하고 Accept 헤더 text/vcard가 있는 Get 요청을 https://localhost:<port>/api/contacts로 보냅니다.

vCard를 연락처의 메모리 내 컬렉션에 추가하려면 다음을 수행합니다.

추가 리소스

ASP.NET Core MVC는 입력 및 출력 포맷터를 사용하여 Web API에서 데이터 교환을 지원합니다. 입력 포맷터는 모델 바인딩에서 사용됩니다. 출력 포맷터는 응답 형식을 지정하는 데 사용됩니다.

프레임워크는 JSON 및 XML에 대한 기본 제공 입력 및 출력 포맷터를 제공합니다. 일반 텍스트에 대한 기본 제공 출력 포맷터는 제공하지만 일반 텍스트에 대한 입력 포맷터는 제공하지 않습니다.

이 문서에서는 사용자 지정 포맷터를 만들어 추가 형식에 대한 지원을 추가하는 방법을 보여줍니다. 사용자 지정 일반 텍스트 입력 포맷터의 예제는 GitHub의 TextPlainInputFormatter를 참조하세요.

샘플 코드 보기 및 다운로드(다운로드 방법)

사용자 지정 포맷터를 사용하는 경우

사용자 지정 포맷터를 사용하여 기본 제공 포맷터에서 처리되지 않는 콘텐츠 형식에 대한 지원을 추가합니다.

사용자 지정 포맷터를 사용하는 방법 개요

사용자 지정 포맷터를 만들려면 다음을 수행합니다.

  • 클라이언트로 전송된 데이터를 직렬화하려면 출력 포맷터 클래스를 만듭니다.
  • 클라이언트에서 수신된 데이터를 역직렬화하려면 입력 포맷터 클래스를 만듭니다.
  • 포맷터 클래스의 인스턴스를 MvcOptionsInputFormattersOutputFormatters 컬렉션에 추가합니다.

사용자 지정 포맷터 만들기

포맷터를 만들려면:

다음 코드는 샘플VcardOutputFormatter 클래스를 보여 줍니다.

public class VcardOutputFormatter : TextOutputFormatter
{
    public VcardOutputFormatter()
    {
        SupportedMediaTypes.Add(MediaTypeHeaderValue.Parse("text/vcard"));

        SupportedEncodings.Add(Encoding.UTF8);
        SupportedEncodings.Add(Encoding.Unicode);
    }

    protected override bool CanWriteType(Type type)
    {
        return typeof(Contact).IsAssignableFrom(type) ||
            typeof(IEnumerable<Contact>).IsAssignableFrom(type);
    }

    public override async Task WriteResponseBodyAsync(
        OutputFormatterWriteContext context, Encoding selectedEncoding)
    {
        var httpContext = context.HttpContext;
        var serviceProvider = httpContext.RequestServices;

        var logger = serviceProvider.GetRequiredService<ILogger<VcardOutputFormatter>>();
        var buffer = new StringBuilder();

        if (context.Object is IEnumerable<Contact> contacts)
        {
            foreach (var contact in contacts)
            {
                FormatVcard(buffer, contact, logger);
            }
        }
        else
        {
            FormatVcard(buffer, (Contact)context.Object, logger);
        }

        await httpContext.Response.WriteAsync(buffer.ToString(), selectedEncoding);
    }

    private static void FormatVcard(
        StringBuilder buffer, Contact contact, ILogger logger)
    {
        buffer.AppendLine("BEGIN:VCARD");
        buffer.AppendLine("VERSION:2.1");
        buffer.AppendLine($"N:{contact.LastName};{contact.FirstName}");
        buffer.AppendLine($"FN:{contact.FirstName} {contact.LastName}");
        buffer.AppendLine($"UID:{contact.Id}");
        buffer.AppendLine("END:VCARD");

        logger.LogInformation("Writing {FirstName} {LastName}",
            contact.FirstName, contact.LastName);
    }
}

적절한 기본 클래스에서 파생

텍스트 미디어 형식(예: vCard)의 경우 TextInputFormatter 또는 TextOutputFormatter 기본 클래스에서 파생합니다.

public class VcardOutputFormatter : TextOutputFormatter

이진 형식의 경우 InputFormatter 또는 OutputFormatter 기본 클래스에서 파생합니다.

지원되는 미디어 유형 및 인코딩 지정

생성자에서 SupportedMediaTypesSupportedEncodings 컬렉션에 추가하여 지원되는 미디어 형식 및 인코딩을 지정합니다.

public VcardOutputFormatter()
{
    SupportedMediaTypes.Add(MediaTypeHeaderValue.Parse("text/vcard"));

    SupportedEncodings.Add(Encoding.UTF8);
    SupportedEncodings.Add(Encoding.Unicode);
}

포맷터 클래스는 해당 종속성에 대해 생성자 주입을 사용할 수 없습니다. 예를 들어 ILogger<VcardOutputFormatter>는 생성자에 매개 변수로 추가할 수 없습니다. 서비스에 액세스하려면 메서드에 전달된 컨텍스트 개체를 사용합니다. 이 문서의 예제와 샘플에 이 작업을 수행하는 방법이 나와 있습니다.

CanReadType 및 CanWriteType 재정의

CanReadType 또는 CanWriteType 메서드를 재정의하여 역직렬화하거나 직렬화하는 형식을 지정합니다. 예를 들어 Contact 형식에서 vCard 텍스트를 만들고 그 반대로 만듭니다.

protected override bool CanWriteType(Type type)
{
    return typeof(Contact).IsAssignableFrom(type) ||
        typeof(IEnumerable<Contact>).IsAssignableFrom(type);
}

CanWriteResult 메서드

일부 시나리오에서 CanWriteResultCanWriteType 대신 재정의해야 합니다. 다음 조건이 true인 경우 CanWriteResult를 사용합니다.

  • 작업 메서드는 모델 클래스를 반환합니다.
  • 런타임 시 반환될 수 있는 파생 클래스가 있습니다.
  • 동작에서 반환된 파생 클래스는 런타임 시 알려야 합니다.

예를 들어 작업 메서드를 가정해 보겠습니다.

  • 시그니처는 Person 형식을 반환합니다.
  • Person에서 파생된 Student 또는 Instructor 형식을 반환할 수 있습니다.

포맷터에서 Student 개체만을 처리하려는 경우 CanWriteResult 메서드에 제공된 컨텍스트 개체에서 Object 형식을 확인합니다. 작업 메서드가 IActionResult를 반환하는 경우:

  • CanWriteResult를 사용할 필요가 없습니다.
  • CanWriteType 메서드는 런타임 형식을 받습니다.

ReadRequestBodyAsync 및 WriteResponseBodyAsync 재정의

deserialization 또는 serialization은 ReadRequestBodyAsync 또는 WriteResponseBodyAsync에서 수행됩니다. 다음 예제에서는 종속성 주입 컨테이너에서 서비스를 얻는 방법을 보여 줍니다. 생성자 매개 변수에서 서비스를 가져올 수 없습니다.

public override async Task WriteResponseBodyAsync(
    OutputFormatterWriteContext context, Encoding selectedEncoding)
{
    var httpContext = context.HttpContext;
    var serviceProvider = httpContext.RequestServices;

    var logger = serviceProvider.GetRequiredService<ILogger<VcardOutputFormatter>>();
    var buffer = new StringBuilder();

    if (context.Object is IEnumerable<Contact> contacts)
    {
        foreach (var contact in contacts)
        {
            FormatVcard(buffer, contact, logger);
        }
    }
    else
    {
        FormatVcard(buffer, (Contact)context.Object, logger);
    }

    await httpContext.Response.WriteAsync(buffer.ToString(), selectedEncoding);
}

private static void FormatVcard(
    StringBuilder buffer, Contact contact, ILogger logger)
{
    buffer.AppendLine("BEGIN:VCARD");
    buffer.AppendLine("VERSION:2.1");
    buffer.AppendLine($"N:{contact.LastName};{contact.FirstName}");
    buffer.AppendLine($"FN:{contact.FirstName} {contact.LastName}");
    buffer.AppendLine($"UID:{contact.Id}");
    buffer.AppendLine("END:VCARD");

    logger.LogInformation("Writing {FirstName} {LastName}",
        contact.FirstName, contact.LastName);
}

사용자 지정 포맷터를 사용하도록 MVC 구성

사용자 지정 포맷터를 사용하려면 MvcOptions.InputFormatters 또는 MvcOptions.OutputFormatters 컬렉션에 포맷터 클래스의 인스턴스를 추가합니다.

public void ConfigureServices(IServiceCollection services)
{
    services.AddControllers(options =>
    {
        options.InputFormatters.Insert(0, new VcardInputFormatter());
        options.OutputFormatters.Insert(0, new VcardOutputFormatter());
    });
}

포맷터는 삽입한 순서 대로 평가됩니다. 첫 번째 포맷터가 우선됩니다.

전체 VcardInputFormatter 클래스

다음 코드는 샘플VcardInputFormatter 클래스를 보여 줍니다.

public class VcardInputFormatter : TextInputFormatter
{
    public VcardInputFormatter()
    {
        SupportedMediaTypes.Add(MediaTypeHeaderValue.Parse("text/vcard"));

        SupportedEncodings.Add(Encoding.UTF8);
        SupportedEncodings.Add(Encoding.Unicode);
    }

    protected override bool CanReadType(Type type)
    {
        return type == typeof(Contact);
    }

    public override async Task<InputFormatterResult> ReadRequestBodyAsync(
        InputFormatterContext context, Encoding effectiveEncoding)
    {
        var httpContext = context.HttpContext;
        var serviceProvider = httpContext.RequestServices;

        var logger = serviceProvider.GetRequiredService<ILogger<VcardInputFormatter>>();

        using var reader = new StreamReader(httpContext.Request.Body, effectiveEncoding);
        string nameLine = null;

        try
        {
            await ReadLineAsync("BEGIN:VCARD", reader, context, logger);
            await ReadLineAsync("VERSION:", reader, context, logger);

            nameLine = await ReadLineAsync("N:", reader, context, logger);

            var split = nameLine.Split(";".ToCharArray());
            var contact = new Contact
            {
                LastName = split[0].Substring(2),
                FirstName = split[1]
            };

            await ReadLineAsync("FN:", reader, context, logger);
            await ReadLineAsync("END:VCARD", reader, context, logger);

            logger.LogInformation("nameLine = {nameLine}", nameLine);

            return await InputFormatterResult.SuccessAsync(contact);
        }
        catch
        {
            logger.LogError("Read failed: nameLine = {nameLine}", nameLine);
            return await InputFormatterResult.FailureAsync();
        }
    }

    private static async Task<string> ReadLineAsync(
        string expectedText, StreamReader reader, InputFormatterContext context,
        ILogger logger)
    {
        var line = await reader.ReadLineAsync();

        if (!line.StartsWith(expectedText))
        {
            var errorMessage = $"Looked for '{expectedText}' and got '{line}'";

            context.ModelState.TryAddModelError(context.ModelName, errorMessage);
            logger.LogError(errorMessage);

            throw new Exception(errorMessage);
        }

        return line;
    }
}

앱 테스트

기본 vCard 입력 및 출력 포맷터를 구현하는 이 문서의 샘플 앱을 실행합니다. 앱은 다음 형식과 유사한 vCard를 읽고 씁니다.

BEGIN:VCARD
VERSION:2.1
N:Davolio;Nancy
FN:Nancy Davolio
END:VCARD

vCard 출력을 보려면 앱을 실행하고 Accept 헤더 text/vcard가 있는 Get 요청을 https://localhost:5001/api/contacts로 보냅니다.

vCard를 연락처의 메모리 내 컬렉션에 추가하려면 다음을 수행합니다.

  • Postman과 같은 도구를 사용하여 /api/contactsPost 요청을 보냅니다.
  • Content-Type 헤더를 text/vcard으로 설정합니다.
  • 앞의 예제와 같이 서식이 지정된 vCard 텍스트를 본문에 설정합니다.

추가 리소스