使用 $expand 时的 Web API OData 媒体类型格式化程序

Posted

技术标签:

【中文标题】使用 $expand 时的 Web API OData 媒体类型格式化程序【英文标题】:Web API OData media type formatter when using $expand 【发布时间】:2014-11-16 13:48:36 【问题描述】:

我正在尝试创建一个MediaTypeFormatter 来处理text/csv,但在OData 查询中使用$expand 时遇到了一些问题。

查询:

http://localhost/RestBlog/api/Blogs/121?$expand=Comments

控制器:

[EnableQuery]
public IQueryable<Blog> GetBlog(int id)

    return DbCtx.Blog.Where(x => x.blogID == id);

在我的媒体类型格式化程序中:

private static MethodInfo _createStreamWriter =
        typeof(CsvFormatter)
        .GetMethods(BindingFlags.Static | BindingFlags.NonPublic)
        .Single(m => m.Name == "StreamWriter");

internal static void StreamWriter<T, X>(T results)

    var queryableResult = results as IQueryable<X>;
    if (queryableResult != null)
    
        var actualResults = queryableResult.ToList<X>();
    


public override void WriteToStream(Type type, object value,
    Stream writeStream, HttpContent content)

    Type genericType = type.GetGenericArguments()[0];
    _createStreamWriter.MakeGenericMethod(
               new Type[]  value.GetType(), genericType )
                .Invoke(null, new object[]  value 
       );

注意value的类型是System.Data.Entity.Infrastructure.DbQuery&lt;System.Web.Http.OData.Query.Expressions.SelectExpandBinder.SelectAllAndExpand&lt;Rest.Blog&gt;&gt;,这意味着它不起作用。

value 的类型应该是 IQueryable,但在转换时它会返回 null

在没有$expand 的情况下进行查询时,事情会更加明智。我做错了什么?

我只是想在以 CSV 格式输出之前获取数据,因此我们将不胜感激。

【问题讨论】:

【参考方案1】:

如果您查看用于 OData Web API 的 source code,您会看到 SelectExpandBinder.SelectAllAndExpand 是泛型类 SelectExpandWrapper(TEntity) 的子类:

private class SelectAllAndExpand<TEntity> : SelectExpandWrapper<TEntity>


它本身是非泛型SelectExpandWrapper的子类:

internal class SelectExpandWrapper<TElement> : SelectExpandWrapper

    // Implementation...

这又实现了IEdmEntityObjectISelectExpandWrapper

internal abstract class SelectExpandWrapper : IEdmEntityObject, ISelectExpandWrapper

    // Implementation...

这意味着您可以访问ISelectExpandWrapper.ToDictionary 方法并且可以使用它来获取底层实体的属性:

public interface ISelectExpandWrapper

    IDictionary<string, object> ToDictionary();
    IDictionary<string, object> ToDictionary(Func<IEdmModel, IEdmStructuredType, IPropertyMapper> propertyMapperProvider);

这就是框架中实现 JSON 序列化的方式,从SelectExpandWrapperConverter 可以看出:

internal class SelectExpandWrapperConverter : JsonConverter

    public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer)
    
        ISelectExpandWrapper selectExpandWrapper = value as ISelectExpandWrapper;
        if (selectExpandWrapper != null)
        
            serializer.Serialize(writer, selectExpandWrapper.ToDictionary(_mapperProvider));
        
    

    // Other methods...

【讨论】:

从引用的页面添加适用的部分会很有用,因为 URL 不会永远存在(这两个就是这种情况)。 @Will 麻烦的是,这一切都没有记录,必须通过查看源代码来收集。我添加了源代码的摘录并修复了链接。希望现在的答案能更好地经受住时间的考验。【参考方案2】:

当我在我的任务中遇到这个问题时,我被谷歌搜索了。我从这个 thread 中得到了干净的实现

首先您需要以正确的方式验证 edm modelbuilder 以进行 expand 对象

你必须为博客和外键releations注册edm模型。然后才能成功

示例

  ODataModelBuilder builder = new ODataConventionModelBuilder();
        builder.EntitySet<Blog>("blog");
        builder.EntitySet<Profile>("profile");//ForeignKey releations of blog
        builder.EntitySet<user>("user");//ForeignKey releations of profile
        config.MapODataServiceRoute(
            routeName: "ODataRoute",
            routePrefix: null,
            model: builder.GetEdmModel());

那么你需要开发这个格式化程序..example源代码我们here

申请我的英语..

首先,我们需要创建一个派生自 MediaTypeFormatter 抽象类的类。这是带有构造函数的类:

public class CSVMediaTypeFormatter : MediaTypeFormatter 

    public CSVMediaTypeFormatter() 

        SupportedMediaTypes.Add(new MediaTypeHeaderValue("text/csv"));
    

    public CSVMediaTypeFormatter(
        MediaTypeMapping mediaTypeMapping) : this() 

        MediaTypeMappings.Add(mediaTypeMapping);
    

    public CSVMediaTypeFormatter(
        IEnumerable<MediaTypeMapping> mediaTypeMappings) : this() 

        foreach (var mediaTypeMapping in mediaTypeMappings) 
            MediaTypeMappings.Add(mediaTypeMapping);
        
    

以上,无论您使用哪个构造函数,我们总是添加 text/csv 媒体类型以支持此格式化程序。我们还允许注入自定义 MediaTypeMappings。

现在,我们需要重写两个方法:MediaTypeFormatter.CanWriteType 和 MediaTypeFormatter.OnWriteToStreamAsync。

首先,这里是 CanWriteType 方法的实现。这个方法需要做的是确定这个格式化程序是否支持对象的类型以便编写它。

protected override bool CanWriteType(Type type) 

    if (type == null)
        throw new ArgumentNullException("type");

    return isTypeOfIEnumerable(type);


private bool isTypeOfIEnumerable(Type type) 

    foreach (Type interfaceType in type.GetInterfaces()) 

        if (interfaceType == typeof(IEnumerable))
            return true;
    

    return false;

这里的作用是检查对象是否实现了 IEnumerable 接口。如果是这样,那么它很酷并且可以格式化对象。如果不是,它将返回 false 并且框架将忽略该特定请求的格式化程序。

最后,这是实际的实现。我们需要在这里做一些反射工作,以便从作为对象类型的 value 参数中获取属性名称和值:

protected override Task OnWriteToStreamAsync(
    Type type,
    object value,
    Stream stream,
    HttpContentHeaders contentHeaders,
    FormatterContext formatterContext,
    TransportContext transportContext) 

    writeStream(type, value, stream, contentHeaders);
    var tcs = new TaskCompletionSource<int>();
    tcs.SetResult(0);
    return tcs.Task;


private void writeStream(Type type, object value, Stream stream, HttpContentHeaders contentHeaders) 

    //NOTE: We have check the type inside CanWriteType method
    //If request comes this far, the type is IEnumerable. We are safe.

    Type itemType = type.GetGenericArguments()[0];

    StringWriter _stringWriter = new StringWriter();

    _stringWriter.WriteLine(
        string.Join<string>(
            ",", itemType.GetProperties().Select(x => x.Name )
        )
    );

    foreach (var obj in (IEnumerable<object>)value) 

        var vals = obj.GetType().GetProperties().Select(
            pi => new  
                Value = pi.GetValue(obj, null)
            
        );

        string _valueLine = string.Empty;

        foreach (var val in vals) 

            if (val.Value != null) 

                var _val = val.Value.ToString();

                //Check if the value contans a comma and place it in quotes if so
                if (_val.Contains(","))
                    _val = string.Concat("\"", _val, "\"");

                //Replace any \r or \n special characters from a new line with a space
                if (_val.Contains("\r"))
                    _val = _val.Replace("\r", " ");
                if (_val.Contains("\n"))
                    _val = _val.Replace("\n", " ");

                _valueLine = string.Concat(_valueLine, _val, ",");

             else 

                _valueLine = string.Concat(string.Empty, ",");
            
        

        _stringWriter.WriteLine(_valueLine.TrimEnd(','));
    

    var streamWriter = new StreamWriter(stream);
        streamWriter.Write(_stringWriter.ToString());

我们已经完成了部分工作。现在,我们需要利用它。我在 Global.asax Application_Start 方法中使用以下代码将此格式化程序注册到管道中:

GlobalConfiguration.Configuration.Formatters.Add(
    new CSVMediaTypeFormatter(
        new  QueryStringMapping("format", "csv", "text/csv")
    )
);

在我的示例应用程序中,当您导航到 /api/cars?format=csv 时,它会为您提供一个 CSV 文件,但没有扩展名。继续并添加 csv 扩展名。然后,用 Excel 打开它,你应该会看到类似下面的内容:

【讨论】:

这更接近,但仍然不是我需要的。如果正在转换的对象与另一个实体具有外键关系,则调用 val.Value.ToString() 会将其全部转储到一个字符串中,作为“Property1=Value1 Property2=Value2”,这在 csv 中并不是很有用。跨度> 如果你在查询字符串中指定 $expand ,它会更糟,因为 obj.GetType() 返回 System.Web.OData.Query.Expressions.SelectExpandBinder.SelectAllAndExpand 所以它不会获取实际实体对象的正确属性。

以上是关于使用 $expand 时的 Web API OData 媒体类型格式化程序的主要内容,如果未能解决你的问题,请参考以下文章

SharePoint REST API 的 Expand 方法

SharePoint REST API 的 Expand 方法

Sitefinity权限如何应用于oData Web服务?

值不能为空。参数名称:尝试使用 web api 获取数据时的源

在 Google Apps 脚本中使用 CloudKit Web 服务 API 查询 CloudKit 公共数据库时的 AUTHENTICATION_FAILED

构建 RESTful API 和 Web 应用程序时的最佳实践 [关闭]