Подтвердить что ты не робот

Форматировщик медиафайлов Web API OData при использовании $expand

Я пытаюсь создать MediaTypeFormatter для обработки text/csv, но при запуске нескольких проблем при использовании $expand в запросе OData.

Query:

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<System.Web.Http.OData.Query.Expressions.SelectExpandBinder.SelectAllAndExpand<Rest.Blog>>, что означает, что он не работает.

Тип value должен быть IQueryable, но после кастования он возвращает null.

При выполнении запроса без $expand все работает намного разумнее. Что я делаю неправильно?

Я просто пытаюсь получить данные перед тем, как вывести их как CSV, поэтому руководство будет с большой благодарностью.

4b9b3361

Ответ 1

SelectExpandBinder.SelectAllAndExpand является подклассом SelectExpandWrapper, который реализует IEdmEntityObject и ISelectExpandWrapper. Используя метод ISelectExpandWrapper.ToDictionary, вы можете получить свойства базового объекта. Таким образом, объект сериализуется в JSON, как видно из SelectExpandWrapperConverter.

Ответ 2

Я был googled, когда сталкивался с этой проблемой в своей задаче. У меня есть чистая реализация из этого thread

сначала вам нужно проверить правильность edm modelbuilder для expand объекты

вам нужно зарегистрировать модель 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());

Затем вам нужно разработать этот форматтер.. пример исходного кода < здесь

агитации для моего английского языка.

Прежде всего нам нужно создать класс, который будет получен из абстрактного класса 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);
        }
    }
}

Выше, независимо от того, какой конструктор вы используете, мы всегда добавляем тип медиатекста /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, и framework будет игнорировать этот форматтер для этого конкретного запроса.

И, наконец, вот фактическая реализация. Нам нужно сделать некоторую работу с отражением здесь, чтобы получить имена свойств и значения из параметра значения, который является типом объекта:

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, и вы увидите что-то похожее ниже:

Ответ 3

Ваше действие GetBlog неправильно присвоило имя параметра. Имя параметра должно быть "ключ", а не id. Вместо этого попробуйте сделать это,

[Queryable]
public SingleResult<Blog> GetBlog([FromODataUri]int key)
{ 
    return SingleResult.Create(DbCtx.Blog.Where(x => x.blogID == key));
}

Эта статья в которой четко указано, что параметр должен быть вызван ключом!