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

C# 使用$expand时的Web API OData媒体类型格式化程序,c#,linq,entity-framework,odata,asp.net-web-api,C#,Linq,Entity Framework,Odata,Asp.net Web Api,我正在尝试创建一个MediaTypeFormatter来处理文本/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); }

我正在尝试创建一个
MediaTypeFormatter
来处理
文本/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);
}
[启用查询]
公共IQueryable GetBlog(int id)
{
返回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 }
       );
}
private static MethodInfo\u createStreamWriter=
类型(CsvFormatter)
.GetMethods(BindingFlags.Static | BindingFlags.NonPublic)
.Single(m=>m.Name==“StreamWriter”);
内部静态void StreamWriter(T结果)
{
var queryableResult=可查询的结果;
if(queryableResult!=null)
{
var actualResults=queryableResult.ToList();
}
}
public override void WriteToStream(类型、对象值、,
流写入流(HttpContent)
{
类型genericType=Type.GetGenericArguments()[0];
_createStreamWriter.MakeGenericMethod(
新类型[]{value.GetType(),genericType})
.Invoke(空,新对象[]{value}
);
}
请注意,
value
的类型是
System.Data.Entity.Infrastructure.DbQuery
,这意味着它不起作用

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

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


我只是想在将数据输出为CSV之前获取数据,因此非常感谢您的指导。

当我在任务中遇到这个问题时,我被谷歌搜索了一下。。我从中得到了清晰的实现

首先,您需要以适当的方式验证edm modelbuilder,以便展开 对象

你必须为博客和外键发布注册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());
这里要做的是检查对象是否实现了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());
}
在我的示例应用程序中,当您导航到/api/cars?format=csv时,将得到一个csv文件,但没有扩展名。继续添加csv扩展名。然后,用Excel打开它,您将看到类似于以下内容的内容:

如果您查看for OData Web API,您将看到
SelectExpandBinder.SelectAllAndExpand
是泛型类的一个子类:

私有类SelectAllAndExpand:SelectExpandWrapper
{
}
它本身是非泛型的一个子类:

内部类SelectExpandWrapper:SelectExpandWrapper
{
//实施。。。
}
依次实现
IEdmEntityObject
ISelectExpandWrapper

内部抽象类SelectExpandWrapper:IEdmEntityObject,ISelectExpandWrapper
{
//实施。。。
}
这意味着您可以访问
ISelectExpandWrapper.ToDictionary
方法,并可以使用它获取基础实体的属性:

公共接口被选中
{
IDictionary to dictionary();
IDictionary ToDictionary(Func Property映射提供程序);
}
事实上,这就是在框架中实现JSON序列化的方式,从以下几点可以看出:

内部类SelectExpandWrapperConverter:JsonConverter
{
公共重写void WriteJson(JsonWriter编写器、对象值、JsonSerializer序列化器)
{
ISelectExpandWrapper selectExpandWrapper=值为ISelectExpandWrapper;
如果(选择ExpandWrapper!=null)
{
serializer.Serialize(writer,selectExpandWrapper.ToDictionary(_mapperProvider));
}
}
//其他方法。。。
}

这更接近,但仍然不是我所需要的。如果要转换的对象与另一个实体具有外键关系,则调用val.Value.ToString()会将其全部转储到一个字符串中,作为“Property1=Value1 Property2=Value2”,这在csv中并不真正有用。如果在查询字符串中指定$expand,则自那以后情况更糟返回System.Web.OData.Query.Expressions.SelectExpandBinder.SelectAllAndExpand,因此它无法获得实际实体对象的正确属性。如果从引用的页面添加适用的部分,则会很有用,因为URL不会永远存在(这两种情况就是如此)@Will的问题是,这些都是未记录的,必须通过查看源代码来收集。我添加了源代码的摘录并修复了链接。希望答案现在能更好地经受住时间的考验。
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;
}
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());
}
GlobalConfiguration.Configuration.Formatters.Add(
    new CSVMediaTypeFormatter(
        new  QueryStringMapping("format", "csv", "text/csv")
    )
);