如何在使用IHttpActionResult时设置自定义标头?

48
在ASP.NET Web API 2中,IHttpActionResult 在简化控制器代码方面提供了很多价值,我不想停止使用它,但是我遇到了一个问题。我需要在发送的响应上设置ETag,但找不到任何可让我访问响应头的属性。目前,我正在使用来自ApiControllerOk<T>(T content) 帮助方法,它返回一个OkNegotiatedContentResult<T> 对象。然而,似乎没有暴露任何可以让我修改头文件的东西。

我是否遗漏了什么,或者在使用默认的IHttpActionResult类型时确实无法做到这一点?我考虑过消息处理程序,但那样我将不得不弄清如何将ETag从操作中传递出去(对于不同的操作,ETags以不同的方式生成,因此这不是为所有操作创建通用处理程序的问题)。

我想避免使用原始的HttpResponseMessage,但目前看起来有些困难。

7个回答

38

针对您的情况,您需要创建一个自定义的IHttpActionResult。下面是一个示例,其中我从OkNegotiatedContentResult<T>派生,因为它运行内容协商(Content-Negotiation)并设置Ok状态码。

public class CustomOkResult<T> : OkNegotiatedContentResult<T>
{
    public CustomOkResult(T content, ApiController controller)
        : base(content, controller) { }

    public CustomOkResult(T content, IContentNegotiator contentNegotiator, HttpRequestMessage request, IEnumerable<MediaTypeFormatter> formatters) 
        : base(content, contentNegotiator, request, formatters) { }

    public string ETagValue { get; set; }

    public override async Task<HttpResponseMessage> ExecuteAsync(CancellationToken cancellationToken)
    {
        HttpResponseMessage response = await base.ExecuteAsync(cancellationToken);

        response.Headers.ETag = new EntityTagHeaderValue(this.ETagValue);

        return response;
    }        
}

Controller:

public class ValuesController : ApiController
{
    public IHttpActionResult Get()
    {
        return new CustomOkResult<string>(content: "Hello World!", controller: this)
            {
                    ETagValue = "You ETag value"
            };
    }
}

请注意,您还可以从NegotiatedContentResult<T>继承,这种情况下您需要自己提供StatusCode。希望这能帮到你。
您可以找到OkNegotiatedContentResult<T>NegotiatedContentResult<T>的源代码,它们其实很简单。

我正在考虑采用不同的方法,因为我不想扩展所有内置响应。我的当前想法是拥有一个 HttpResponseHeaders 字典,我将其合并到 IHttpActionResult 中的一个消息处理程序中。这似乎可以工作,但是没有响应头的构造函数。 - ehdv
你可以查看我对另一个评论的回复。你的方法是不正确的。 - Kiran
1
在这个例子中,我一直收到“406不可接受”的错误,直到我用引号包装值:即用string.Format("\"{0}\"", this.ETagValue)替换this.ETagValue。实际上,我是在尝试添加Content-type参数(profile="http:/..." - href必须加引号)时偶然发现了这个解决方案。 - biscuit314
2
正确的答案是使用在这里找到的ResponseMessageResult:http://stackoverflow.com/a/41459475/1644019 - Vasily Sliounaiev

36

您可以创建一个HttpResponseMessage,根据需要添加标头,然后从中创建ResponseMessageResult

HttpResponseMessage response =new HttpResponseMessage(HttpStatusCode.OK);
response.Headers.Add("MyHeader", "MyHeaderValue");
return ResponseMessage(response);

12
海报明确要求使用 IHttpActionResult 来解决问题。 - Adrian Hope-Bailie
8
@AdrianHope-Bailie 这是一个 X/Y 问题,任何试图做帖子作者所要求的事情的人都是在做错事。这个解决方案是框架作者想让你使用的解决方案,用于实现对响应进行细粒度控制。 - Jimmy Hoffa

8

这是我的简单实现,没有使用ActionFilterAttributes,与AlexACD的回答类似。我的解决方案使用ResponseMessageResult来实现IHttpActionResult接口。

HttpResponseMessage responseMessage = new HttpResponseMessage(HttpStatusCode.OK);
responseMessage.Headers.Add("Headername", "Value");
ResponseMessageResult response = new ResponseMessageResult(responseMessage);
return response;

5
ResponseMessage 返回 ResponseMessageResult - Doug Domeny

7
public static class HttpExtentions
{
    public static IHttpActionResult AddHeader(this IHttpActionResult action,
        string headerName, IEnumerable<string> headerValues)
    {
        return new HeaderActionResult(action, headerName, headerValues);
    }

    public static IHttpActionResult AddHeader(this IHttpActionResult action,
        string headerName, string header)
    {
        return AddHeader(action, headerName, new[] {header});
    }

    private class HeaderActionResult : IHttpActionResult
    {
        private readonly IHttpActionResult action;

        private readonly Tuple<string, IEnumerable<string>> header;

        public HeaderActionResult(IHttpActionResult action, string headerName,
            IEnumerable<string> headerValues)
        {
            this.action = action;

            header = Tuple.Create(headerName, headerValues);
        }

        public async Task<HttpResponseMessage> ExecuteAsync(CancellationToken cancellationToken)
        {
            var response = await action.ExecuteAsync(cancellationToken);

            response.Headers.Add(header.Item1, header.Item2);

            return response;
        }
    }
}

5

这是一个很古老的问题。也许其他答案在当时是有道理的,但今天你可以简单地添加以下代码行,而无需更改或扩展 IHttpActionResult。它完美地在您的响应中添加了头部信息。还要确保按照以下RFC 1123标准进行格式化。否则,即使“Last-Modified”出现在头部信息中,客户端也无法使用 HttpClient 读取它。

    System.Web.HttpContext.Current.Response.Headers.
Add(Microsoft.Net.Http.Headers.HeaderNames.LastModified, DBdateModified.Value.ToString("r"));

3
这是我在常见的Web API 2库代码中使用的解决方案,可以轻松支持设置任何标题 - 或者在 ExecuteAsync 中提供的 HttpResponseMessage 上设置任何其他属性--而不会被限制在特定的衍生 NegotiatedContentResult 实现中。
public class FlexibleNegotiatedContentResult<T> : NegotiatedContentResult<T>
{
    private readonly Action<HttpResponseMessage> _responseMessageDelegate;

    public FlexibleNegotiatedContentResult(HttpStatusCode statusCode, T content, IContentNegotiator contentNegotiator, HttpRequestMessage request, IEnumerable<MediaTypeFormatter> formatters)
        : base(statusCode, content, contentNegotiator, request, formatters)
    {
    }

    public FlexibleNegotiatedContentResult(HttpStatusCode statusCode, T content, ApiController controller, Action<HttpResponseMessage> responseMessageDelegate = null)
        : base(statusCode, content, controller)
    {
        _responseMessageDelegate = responseMessageDelegate;
    }

    public override async Task<HttpResponseMessage> ExecuteAsync(CancellationToken cancellationToken)
    {
        HttpResponseMessage responseMessage = await base.ExecuteAsync(cancellationToken);

        if (_responseMessageDelegate != null)
        {
            _responseMessageDelegate(responseMessage);
        }

        return responseMessage;
    }
}

并且以下是使用示例:

new FlexibleNegotiatedContentResult<string>(HttpStatusCode.Created, "Entity created!", controller, response => response.Headers.Location = new Uri("https://myapp.com/api/entity/1"));

3
这可以通过一个ActionFilterAttribute实现,它会在控制器函数返回响应前检查响应,并且你可以将该属性设置在控制器方法上以添加此信息。以下是我的实现示例:
public class EnableETag : ActionFilterAttribute
{

    /// <summary>
    /// NOTE: a real production situation, especially when it involves a web garden
    ///       or a web farm deployment, the tags must be retrieved from the database or some other place common to all servers.
    /// </summary>
    private static ConcurrentDictionary<string, EntityTagHeaderValue> etags = new ConcurrentDictionary<string, EntityTagHeaderValue>();

    public override void OnActionExecuting(HttpActionContext context)
    {
        var request = context.Request;
        if (request.Method == HttpMethod.Get)
        {
            var key = GetKey(request);
            ICollection<EntityTagHeaderValue> etagsFromClient = request.Headers.IfNoneMatch;
            if (etagsFromClient.Count > 0)
            {
                EntityTagHeaderValue etag = null;
                if (etags.TryGetValue(key, out etag) && etagsFromClient.Any(t => t.Tag == etag.Tag))
                {
                    context.Response = new HttpResponseMessage(HttpStatusCode.NotModified);
                    SetCacheControl(context.Response);
                }
            }
        }
    }
    public override void OnActionExecuted(HttpActionExecutedContext context)
    {
        var request = context.Request;
        var key = GetKey(request);
        EntityTagHeaderValue etag;
        if (!etags.TryGetValue(key, out etag) || request.Method == HttpMethod.Put ||
        request.Method == HttpMethod.Post)
        {
            etag = new EntityTagHeaderValue("\"" + Guid.NewGuid().ToString() + "\"");
            etags.AddOrUpdate(key, etag, (k, val) => etag);
        }
        context.Response.Headers.ETag = etag;
        SetCacheControl(context.Response);
    }
    private string GetKey(HttpRequestMessage request)
    {
        return request.RequestUri.ToString();
    }

    /// <summary>
    /// Defines the time period to hold item in cache (currently 10 seconds)
    /// </summary>
    /// <param name="response"></param>
    private void SetCacheControl(HttpResponseMessage response)
    {
        response.Headers.CacheControl = new CacheControlHeaderValue()
        {
            MaxAge = TimeSpan.FromSeconds(10),
            MustRevalidate = true,
            Private = true
        };
    }
}

}


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