如何将纯文本发布到 ASP.NET Web API 端点?

56

我有一个ASP.NET Web API端点,控制器动作定义如下:

[HttpPost]
public HttpResponseMessage Post([FromBody] object text)

如果我的POST请求体包含纯文本(即不应被解释为JSON、XML或任何其他特殊格式),那么我认为我只需要在请求中包含以下标头:

Content-Type: text/plain

然而,我收到了错误信息:
No MediaTypeFormatter is available to read an object of type 'Object' from content with media type 'text/plain'.

如果我将控制器操作方法签名更改为:
[HttpPost]
public HttpResponseMessage Post([FromBody] string text)

我收到了一个稍微不同的错误信息:

没有可用的MediaTypeFormatter来从媒体类型为“text/plain”的内容中读取类型为“String”的对象。


你不想让你的“text”参数成为字符串类型吗? - Marvin Smit
是的,我也想过那个想法,但它并没有太大帮助(请看我的修订问题)。 - BaltoStar
@BaltoStar 我知道你的问题已经8个月了,但我也遇到了同样的问题。请看下面我的回答。 - gwenzek
嗨BaltoStar,你能将@gzou的回答标记为答案吗?那会帮助社区看到它作为答案。谢谢。 - benjguin
7个回答

74

事实上,Web API 没有支持纯文本的 MediaTypeFormatter 真是遗憾。以下是我实现的一个可以用于发布内容的纯文本格式化器。

public class TextMediaTypeFormatter : MediaTypeFormatter
{
    public TextMediaTypeFormatter()
    {
        SupportedMediaTypes.Add(new MediaTypeHeaderValue("text/plain"));
    }

    public override Task<object> ReadFromStreamAsync(Type type, Stream readStream, HttpContent content, IFormatterLogger formatterLogger)
    {
        var taskCompletionSource = new TaskCompletionSource<object>();
        try
        {
            var memoryStream = new MemoryStream();
            readStream.CopyTo(memoryStream);
            var s = System.Text.Encoding.UTF8.GetString(memoryStream.ToArray());
            taskCompletionSource.SetResult(s);
        }
        catch (Exception e)
        {
            taskCompletionSource.SetException(e);
        }
        return taskCompletionSource.Task;
    }

    public override Task WriteToStreamAsync(Type type, object value, Stream writeStream, HttpContent content, System.Net.TransportContext transportContext, System.Threading.CancellationToken cancellationToken)
    {
        var buff = System.Text.Encoding.UTF8.GetBytes(value.ToString());
        return writeStream.WriteAsync(buff, 0, buff.Length, cancellationToken);
    }

    public override bool CanReadType(Type type)
    {
        return type == typeof(string);
    }

    public override bool CanWriteType(Type type)
    {
        return type == typeof(string);
    }
}
您需要通过类似以下的方式,在您的HttpConfig中“注册”此格式化程序:
config.Formatters.Insert(0, new TextMediaTypeFormatter());

正是我所需要的!太棒了! - Stephen Chung
太棒了!这是一个很棒的解决方案! - Erick Boshoff
我想知道为什么不将ReadFromStreamAsync方法简化为以下形式: return await new StreamReader(readStream).ReadToEndAsync(); - Marat Asadurian
我的MediaTypeFormatter在WriteToStreamAsync方法中没有Cancellation Token参数。当我删除该参数时,它可以正常工作。 - Shiasu-sama

24

由于 Web API 没有默认的格式化程序来处理 text/plain,以下是一些解决方案:

  1. 修改您的操作以不具有参数...原因是具有参数会触发请求体反序列化。现在,您可以通过执行 await Request.Content.ReadAsStringAsync() 来显式地读取请求内容以获取字符串。

  2. 编写自定义 MediaTypeFormatter 来处理 'text/plain'... 在这种情况下编写它非常简单,您可以保留操作上的参数。


谢谢您的回复,Kiran。实际上,我的操作签名有额外的[FromUri]参数,为了简化问题,我没有包含它们。所以我猜这意味着我无法避免请求体的隐式反序列化?如果是这样,那么我需要编写一个自定义的MediaTypeFormatter来处理'text/plain'.... - BaltoStar
请求反序列化只会发生在通常被认为从主体读取的参数上...例如:明确带有[FromBody]标记的参数,隐含地从主体读取(例如:复杂类型)...所以我上面的答案仅适用于那些从主体中读取的参数...因此您应该能够像往常一样使用FromUri参数...但无论如何,创建自定义格式化程序是最好的方法,因为它与Web API设计非常契合。 - Kiran
提供这个格式化程序不危险吗?text/plain是使用POST方法的HTML表单的有效enctype。请务必添加防伪令牌以防止跨站点请求伪造。 - yonexbat

21
在ASP.NET Core 2.0中,您只需要执行以下操作:-
using (var reader = new StreamReader(Request.Body))
{
      string plainText= reader.ReadToEnd();

      // Do something else

      return Ok(plainText);
}

1
简单而完美! - Fernando JS
1
在2020年帮了我很大的忙。本应该不难找到这个答案的,谢谢 :) - nandtrocity
非常好。为了使其工作清晰,必须从方法签名中移除 [frombody] 参数。 - John Arundell
非常有帮助,谢谢! - 3xGuy

17

使用 gwenzek 的格式化程序的精简版本,采用 async/await:

public class PlainTextFormatter : MediaTypeFormatter
{
    public PlainTextFormatter()
    {
        SupportedMediaTypes.Add(new MediaTypeHeaderValue("text/plain"));
    }

    public override bool CanReadType(Type type) =>
        type == typeof(string);

    public override bool CanWriteType(Type type) =>
        type == typeof(string);

    public override async Task<object> ReadFromStreamAsync(Type type, Stream readStream, HttpContent content, IFormatterLogger formatterLogger)
    {
        var streamReader = new StreamReader(readStream);
        return await streamReader.ReadToEndAsync();
    }

    public override async Task WriteToStreamAsync(Type type, object value, Stream writeStream, HttpContent content, TransportContext transportContext, CancellationToken cancellationToken)
    {
        var streamReader = new StreamWriter(writeStream);
        await streamReader.WriteAsync((string) value);
    }
}

请注意,我故意不释放 StreamReader/StreamWriter,因为这将释放底层流并破坏Web Api流程。 请参阅此处

"该方法的实现不应在完成时关闭readStream。当HttpContent实例被处理时,流将被独立地关闭。"

要使用它,请在构建HttpConfiguration时进行注册:

protected HttpConfiguration CreateHttpConfiguration()
{
    HttpConfiguration httpConfiguration = new HttpConfiguration();
    ...
    httpConfiguration.Formatters.Add(new PlainTextFormatter());
    ...
    return httpConfiguration;
}

1
释放资源会具体产生什么影响?我发现在处理异常时不释放资源会导致错误。 - Andrew

6

在某些情况下,让JsonMediaTypeFormatter自动处理可能更加简单:

var formatter = GlobalConfiguration.Configuration.Formatters.Where(f=>f is System.Net.Http.Formatting.JsonMediaTypeFormatter).FirstOrDefault();
if (!formatter.SupportedMediaTypes.Any( mt => mt.MediaType == "text/plain" ))
    formatter.SupportedMediaTypes.Add( new MediaTypeHeaderValue( "text/plain" ) );

如果我这样做,然后像这样设置主体:resp.Content = new StringContent(outputString, Encoding.UTF8, "text\plain");,它会只输出字符串本身(即使它不是JSON),并使用text-plain内容类型吗? - N K

0

这不是一个合适的答案,但是一个快速而简单的解决方法来解除开发的阻塞...

事实证明,一个被引号包围的字符串本身就是有效的JSON。因此,如果你确定内容始终非常简单,你可以将其用双引号括起来,并称之为application/json。

// TODO: Temporary, fix for production
HttpContent content = new StringContent($"\"{command}\"", UTF8Encoding.UTF8, "application/json");

嗨,我遇到了这个问题,即 asp.net web api (.net 5) 在 IIS 上可以正常工作,但在 Linux 服务器 (litespeed) 上无法正常工作。有什么想法吗? - Juan Pablo Gomez

0

对于这个问题,我来晚了,提供一个非常简化的解决方案。 在控制器方法中,我使用了以下代码并取得了成功:

       public HttpResponseMessage FileHandler()
       {
        HttpResponseMessage response = new HttpResponseMessage();

        using (var reader = new StreamReader(System.Web.HttpContext.Current.Request.GetBufferedInputStream()))
        {
            string plainText = reader.ReadToEnd();
        } .....}

而在客户端,这些是我使用的 Ajax 选项:

var ajaxOptions = {
url: 'api/fileupload/' + "?" + $.param({ "key": metaKey2, "File-Operation": "Remove", "removalFilePath": $removalFilePath, "Audit-Model": model, "Parent-Id": $parentId, "Audit-Id": $auditId }),
type: 'POST', processData: false, contentType: false, data: "BOB"
};


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接