Форматировщик медиафайлов 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, поэтому руководство будет с большой благодарностью.
Ответы
Ответ 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));
}
Эта статья в которой четко указано, что параметр должен быть вызван ключом!