6

我有一个 WebApi 方法,它返回 RavenDB 文档的 IQueryable。调用者需要知道可能结果的数量(因为实际结果是有限的/分页的)。

所以,我在 WebApi 方法的末尾有这样的东西:

HttpContext.Current.Response.AddHeader("Total-Result-Count", 
    resultsStats.TotalResults.ToString())

不幸的是,这不起作用,因为 IQueryable 还没有实际执行 - 所以统计信息将为空。

如何将统计响应标头的人口推迟到查询执行后?

[更新]

我试图在控制器操作执行后应用 ActionFilter 来捕获结果......但似乎在实际枚举 IQueryable 之前调用了 ActionFilter......

public class CountQueryableResultsActionFilter : ActionFilterAttribute
{
    public override void OnActionExecuted(HttpActionExecutedContext filterContext)
    {
        var controllerStats = filterContext.ActionContext.ControllerContext.Controller as IControllerStatistics;
        System.Web.HttpContext.Current.Response.AddHeader("Total-Result-Count", controllerStats.TotalResults.ToString());
    }
}

如果,我在 WebApi 方法的末尾调用了“IQueryable.ToArray()”,然后 Linq 查询立即执行,它生成统计信息,一切正常 - 但这将阻止用户应用他们自己的 OData 过滤器ETC...

4

2 回答 2

3

好的 - 我想通了。

以下将导致仅发出一个 Raven 查询,该查询返回结果和结果计数。

感谢David Ruttka在这方面的实验。我已经修改了他的代码以与 RavenDb 一起使用。此代码将按照 RavenDB 的意图通过一个数据库查询返回结果和结果计数。

我在下面附加了我的代码 - 要使用它,您必须IRavenQueryable<T>从您的 WebApi 方法(不是IQueryable<T>)返回。然后,将 $inlinecount=allpages 附加到您的 Uri 将调用处理程序。此代码不会破坏其他 OData 查询扩展($take、$skip 等)

注意:此代码使用“内联”技术,因为统计信息会在消息正文中返回 - 如果您愿意,您可以更改代码以在标头中注入统计信息 - 我只是选择使用 OData 工作的标准方式.

您可以修改此代码以包含 Raven 生成的任何和所有统计信息。

使用以下代码向 ASP.NET 注册处理程序(在您的 Global.asax.cs 中)

注册码:

GlobalConfiguration.Configuration.MessageHandlers.Add(new WebApi.Extensions.InlineRavenCountHandler());

处理程序代码:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Web;
using System.Net.Http;
using System.Threading;
using System.Threading.Tasks;
using System.Reflection;
using System.Net.Http.Headers;
using System.Net;

namespace WebApi.Extensions
{
    public class InlineRavenCountHandler : DelegatingHandler
    {
        protected override Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
        {
            if (!ShouldInlineCount(request))
                return base.SendAsync(request, cancellationToken);

            // Otherwise, we have a continuation to work our magic...
            return base.SendAsync(request, cancellationToken).ContinueWith(
                t =>
                {
                    var response = t.Result;

                    // Is this a response we can work with?
                    if (!ResponseIsValid(response)) return response;

                    var pagedResultsValue = this.GetValueFromObjectContent(response.Content);
                    Type queriedType;

                    // Can we find the underlying type of the results?
                    if (pagedResultsValue is IQueryable)
                    {
                        queriedType = ((IQueryable)pagedResultsValue).ElementType;

                        // we need to work with an instance of IRavenQueryable to support statistics
                        var genericQueryableType = typeof(Raven.Client.Linq.IRavenQueryable<>).MakeGenericType(queriedType);

                        if (genericQueryableType.IsInstanceOfType(pagedResultsValue))
                        {
                            Raven.Client.Linq.RavenQueryStatistics stats = null;

                            // register our statistics object with the Raven query provider.
                            // After the query executes, this object will contain the appropriate stats data
                            dynamic dynamicResults = pagedResultsValue;
                            dynamicResults.Statistics(out stats);


                            // Create the return object.
                            var resultsValueMethod =
                                this.GetType().GetMethod(
                                    "CreateResultValue", BindingFlags.Instance | BindingFlags.NonPublic).MakeGenericMethod(
                                        new[] { queriedType });

                            // Create the result value with dynamic type
                            var resultValue = resultsValueMethod.Invoke(
                                this, new[] { stats, pagedResultsValue });

                            // Push the new content and return the response
                            response.Content = CreateObjectContent(
                                resultValue, response.Content.Headers.ContentType);
                            return response;

                        }
                        else
                            return response;
                    }
                    else
                        return response;
               });
        }

        private bool ResponseIsValid(HttpResponseMessage response)
        {
            // Only do work if the response is OK
            if (response == null || response.StatusCode != HttpStatusCode.OK) return false;

            // Only do work if we are an ObjectContent
            return response.Content is ObjectContent;
        }

        private bool ShouldInlineCount(HttpRequestMessage request)
        {
            var queryParams = request.RequestUri.ParseQueryString();
            var inlinecount = queryParams["$inlinecount"];
            return string.Compare(inlinecount, "allpages", true) == 0;
        }

    // Dynamically invoked for the T returned by the resulting ApiController
    private ResultValue<T> CreateResultValue<T>(Raven.Client.Linq.RavenQueryStatistics stats, IQueryable<T> pagedResults)
    {
        var genericType = typeof(ResultValue<>);
        var constructedType = genericType.MakeGenericType(new[] { typeof(T) });

        var ctor = constructedType
            .GetConstructors().First();

        var instance = ctor.Invoke(null);

        var resultsProperty = constructedType.GetProperty("Results");
        resultsProperty.SetValue(instance, pagedResults.ToArray(), null);

        var countProperty = constructedType.GetProperty("Count");
        countProperty.SetValue(instance, stats.TotalResults, null);

        return instance as ResultValue<T>;
    }

        // We need this because ObjectContent's Value property is internal
        private object GetValueFromObjectContent(HttpContent content)
        {
            if (!(content is ObjectContent)) return null;

            var valueProperty = typeof(ObjectContent).GetProperty("Value", BindingFlags.Instance | BindingFlags.NonPublic);
            if (valueProperty == null) return null;

            return valueProperty.GetValue(content, null);
        }

        // We need this because ObjectContent's constructors are internal
        private ObjectContent CreateObjectContent(object value, MediaTypeHeaderValue mthv)
        {
            if (value == null) return null;

            var ctor = typeof(ObjectContent).GetConstructors(BindingFlags.NonPublic | BindingFlags.Instance).FirstOrDefault(
                ci =>
                {
                    var parameters = ci.GetParameters();
                    if (parameters.Length != 3) return false;
                    if (parameters[0].ParameterType != typeof(Type)) return false;
                    if (parameters[1].ParameterType != typeof(object)) return false;
                    if (parameters[2].ParameterType != typeof(MediaTypeHeaderValue)) return false;
                    return true;
                });

            if (ctor == null) return null;

            return ctor.Invoke(new[] { value.GetType(), value, mthv }) as ObjectContent;
        }
    }

    public class ResultValue<T>
    {
        public int Count { get; set; }
        public T[] Results { get; set; }
    }
}
于 2012-05-13T23:10:46.303 回答
1

您可以包装 IQueryable 并拦截 GetEnumerator。示例如下:http: //blogs.msdn.com/b/alexj/archive/2010/03/01/tip-55-how-to-extend-an-iqueryable-by-wrapping-it .aspx。它做了一些不同的事情,但它应该给你这个想法。

此外 - 调用者可以使用 URL 中的 $inlinecount=allpages 使用 OData 协议来执行此操作。虽然我不确定 WebAPI 是否支持这个查询选项。

于 2012-05-11T08:51:00.653 回答