Web API 2.2 - ApiController 到 ODataController(设置 Formatter 后更改内容类型)

Web API 2.2 - ApiController to ODataController (changing Content Type after setting Formatter)

我有一个正在转换为使用 OData v4 的 Web API 2.2 控制器。在 ApiController 中我可以这样做:

HttpResponseMessage response = Request.CreateResponse(HttpStatusCode.Created, MyObject);
response.Content.Headers.ContentType = new MediaTypeHeaderValue("text/html");
return ResponseMessage(response);

它发回 201 的 Http 状态代码 - 已创建,主体中的对象序列化为 JSON(Web API 2.2 的默认格式化程序),内容类型为 text/html.

之所以有效,是因为 Request.CreateResponse() 将格式化程序设置为 JSON,尽管我随后直接更改了内容类型,但它仍然存在。因此该对象被序列化为 JSON,并且返回的响应内容类型为 text/html。

我需要这个的原因是现有的前端利用 iframe 执行上传,然后从 iframe 主体中提取响应以将任何信息中继给用户。如果内容类型为 application/json,浏览器将尝试将其保存为文件。但是作为 text/html,它很容易被注入到 iframe 中。我们可以将其删除并反序列化为 javascript 对象。

现在尝试在使用 ODataController 时执行相同的中断,但出现以下错误:

{
  "error":{
    "code":"","message":"An error has occurred.","innererror":{
      "message":"The 'ObjectContent`1' type failed to serialize the response body for content type 'text/html'.","type":"System.InvalidOperationException","stacktrace":"","internalexception":{
        "message":"A supported MIME type could not be found that matches the content type of the response. None of the supported type(s) 'application/json;odata.metadata=minimal;odata.streaming=true;IEEE754Compatible=false, application/json;odata.metadata=minimal;odata.streaming=true;IEEE754Compatible=true, application/json;odata.metadata=minimal;odata.streaming=true, application/json;odata.metadata=minimal;odata.streaming=false;IEEE754Compatible=false, application/json;odata.metadata=minimal;odata.streaming=false;IEEE754Compatible=true, application/json;odata.metadata=minimal;odata.streaming=false, application/json;odata.metadata=minimal;IEEE754Compatible=false, application/json;odata.metadata=minimal;IEEE754Compatible=true, application/json;odata.metadata=minimal, application/json;odata.metadata=full;odata.streaming=true;IEEE754Compatible=false, application/json;odata.metadata=full;odata.streaming=true;IEEE754Compatible=true, application/json;odata.metadata=full;odata.streaming=true, application/json;odata.metadata=full;odata.streaming=false;IEEE754Compatible=false, application/json;odata.metadata=full;odata.streaming=false;IEEE754Compatib...' matches the content type 'text/html'.","type":"Microsoft.OData.Core.ODataContentTypeException","stacktrace":"   at Microsoft.OData.Core.MediaTypeUtils.GetFormatFromContentType(String contentTypeName, ODataPayloadKind[] supportedPayloadKinds, ODataMediaTypeResolver mediaTypeResolver, ODataMediaType& mediaType, Encoding& encoding, ODataPayloadKind& selectedPayloadKind)\r\n   at Microsoft.OData.Core.MediaTypeUtils.GetFormatFromContentType(String contentTypeHeader, ODataPayloadKind[] supportedPayloadKinds, ODataMediaTypeResolver mediaTypeResolver, ODataMediaType& mediaType, Encoding& encoding, ODataPayloadKind& selectedPayloadKind, String& batchBoundary)\r\n   at Microsoft.OData.Core.ODataMessageWriter.EnsureODataFormatAndContentType()\r\n   at Microsoft.OData.Core.ODataMessageWriter.SetHeaders(ODataPayloadKind payloadKind)\r\n   at Microsoft.OData.Core.ODataMessageWriter.SetOrVerifyHeaders(ODataPayloadKind payloadKind)\r\n   at Microsoft.OData.Core.ODataMessageWriter.WriteToOutput[TResult](ODataPayloadKind payloadKind, Action verifyHeaders, Func`2 writeFunc)\r\n   at Microsoft.OData.Core.ODataMessageWriter.CreateODataEntryWriter(IEdmNavigationSource navigationSource, IEdmEntityType entityType)\r\n   at System.Web.OData.Formatter.Serialization.ODataEntityTypeSerializer.WriteObject(Object graph, Type type, ODataMessageWriter messageWriter, ODataSerializerContext writeContext)\r\n   at System.Web.OData.Formatter.ODataMediaTypeFormatter.WriteToStream(Type type, Object value, Stream writeStream, HttpContent content, HttpContentHeaders contentHeaders)\r\n   at System.Web.OData.Formatter.ODataMediaTypeFormatter.WriteToStreamAsync(Type type, Object value, Stream writeStream, HttpContent content, TransportContext transportContext, CancellationToken cancellationToken)\r\n--- End of stack trace from previous location where exception was thrown ---\r\n   at System.Runtime.CompilerServices.TaskAwaiter.ThrowForNonSuccess(Task task)\r\n   at System.Runtime.CompilerServices.TaskAwaiter.HandleNonSuccessAndDebuggerNotification(Task task)\r\n   at System.Runtime.CompilerServices.TaskAwaiter.GetResult()\r\n   at System.Web.Http.WebHost.HttpControllerHandler.<WriteBufferedResponseContentAsync>d__1b.MoveNext()"
      }
    }
  }
}

OData 格式化程序的工作方式似乎有点不同。在删除我的内容类型分配行时会发回正确序列化的 OData,它使用 application/json 的内容类型,然后浏览器会尝试将其保存到文件中...

关于如何使 ODataController 像 ApiController 一样工作的任何想法?我是否必须创建自己的格式化程序才能执行此操作?考虑到我唯一想改变的是返回的内容类型,这似乎有点傻。

在您的 startup/config 代码中调用它:

var odataFormatters = ODataMediaTypeFormatters.Create();
config.Formatters.Clear();
config.Formatters.AddRange(odataFormatters);

实际上,通过使用 OWIN 并编写我自己的中间件,我或多或少地按照我想要的方式工作。

基本上我想要一种强制内容类型的方法,而不必为每个实例编写特定的 oData 格式化程序。所以 oData 可以继续按照它被告知的方式进行格式化,我只会告诉浏览器将其视为纯文本(或我想要的任何其他方式)。

这样我就可以通过 iframe 发出请求(通常是上传或下载文件),如果发生错误,return 将是纯文本。哪个 Internet Explorer 然后会忽略(application/json 或 application/xml 响应被视为文件,并且总是启动下载提示)。大多数其他浏览器接受 application/json 或 application/xml 就好了,但 IE 总是想将其作为文件下载?!?!

我尝试使用 WebAPI 管道来执行此操作并不成功,因为看起来 MessageHandlers 实际上是在 oData 序列化过程之前被命中的,因为响应正在发出。

但看起来 OWIN 让我可以在 oData 序列化之后处理响应,这正是我想要的。下面是我拼凑的 "proof-of-case" OWIN 中间件 class,它采用特定的查询字符串值并使用它来强制响应的内容类型,然后再将其发送到下游。

它可以使用一些改进,但现在它完成了工作(它适用于任何请求,我有并排的 WebAPI 和 oData 控制器,两者都可以使用查询字符串来强制内容-类型).

public class OWINCustomMiddleware : OwinMiddleware {

  public OWINCustomMiddleware(OwinMiddleware next): base(next) {
  }

  public async override Task Invoke(IOwinContext context) {

  //Check if a content-type coercion querystring token is present
    if (context.Request.QueryString.HasValue) {
      NameValueCollection queryString = HttpUtility.ParseQueryString(context.Request.QueryString.Value);
      String coerce = queryString.Get("$coerce");
      if (!String.IsNullOrWhiteSpace(coerce)) {
      //Remove $coerce token so it doesn't impact future methods
        queryString.Remove("$coerce");
        context.Request.QueryString = new QueryString(queryString.ToString());
      //Append a header to the request, to be later used in Response Content-Type coercion
        context.Request.Headers.Add("coerce", new String[] { coerce });
      }
    }

    await Next.Invoke(context);

  //Coerce existing response content-type to value of coerce header (if present)
    if (context.Request.Headers != null) {
      if (context.Request.Headers["coerce"] != null) {
        Int32 index = context.Response.ContentType.IndexOf(';');
        if (index > 0) {
          context.Response.ContentType = context.Request.Headers["coerce"] + context.Response.ContentType.Substring(index, context.Response.ContentType.Length - index);
        } else {
          context.Response.ContentType = context.Request.Headers["coerce"];
        }
      }
    }

  }
}

如果有人知道直接使用 WebAPI 管道(不使用 OWIN)执行此操作的方法,我仍然想知道,所以请 post 如果你知道。