Web API OData v7. I'm writing a custom formatter for CSV, Excel, etc. I have a disconnect of how I point my custom formatter (ODataMediaTypeFormatter
) to my custom classes where I modify the output.
CustomFormatter : ODataMediaTypeFormatter - had a MessageWriterSettings.MediaTypeResolver which no longer exists in v. 7
When I debug, I get to the GetPerRequestFormatterInstance
, and after that it dies with A supported MIME type could not be found that matches the content type of the response.
I can't figure out the flow--how to tie it to my custom (ODataWriter
) writer (csv, or whatever I wish to create).
For instance, from the example on git:
public class CustomFormatter : ODataMediaTypeFormatter
{
private readonly string csvMime = ;
public CustomFormatter(params ODataPayloadKind[] kinds)
: base(kinds) {
//----no longer exists in 7
//MessageWriterSettings.MediaTypeResolver = new MixResolver();
SupportedEncodings.Add(Encoding.UTF8);
SupportedEncodings.Add(new UTF8Encoding(encoderShouldEmitUTF8Identifier: false));
SupportedMediaTypes.Add(new MediaTypeHeaderValue("text/csv"));
}
}
public class MixResolver : ODataMediaTypeResolver
{
public override IEnumerable<ODataMediaTypeFormat> GetMediaTypeFormats(ODataPayloadKind payloadKind)
{
if (payloadKind == ODataPayloadKind.Resource || payloadKind == ODataPayloadKind.ResourceSet)
{
return CsvMediaTypeResolver.Instance.GetMediaTypeFormats(payloadKind);
}
return base.GetMediaTypeFormats(payloadKind);
}
}
public class CsvMediaTypeResolver : ODataMediaTypeResolver
{
private static readonly CsvMediaTypeResolver instance = new CsvMediaTypeResolver();
private readonly ODataMediaTypeFormat[] mediaTypeFormats =
{
new ODataMediaTypeFormat(new ODataMediaType("text", "csv"), new CsvFormat())
};
public class CsvMediaTypeResolver : ODataMediaTypeResolver
{
private static readonly CsvMediaTypeResolver instance = new CsvMediaTypeResolver();
private readonly ODataMediaTypeFormat[] mediaTypeFormats = { new ODataMediaTypeFormat(new ODataMediaType("text", "csv"), new CsvFormat())};
private CsvMediaTypeResolver() { }
public static CsvMediaTypeResolver Instance { get { return instance; } }
public override IEnumerable<ODataMediaTypeFormat> GetMediaTypeFormats(ODataPayloadKind payloadKind)
{
if (payloadKind == ODataPayloadKind.Resource || payloadKind == ODataPayloadKind.ResourceSet)
{
return mediaTypeFormats.Concat(base.GetMediaTypeFormats(payloadKind));
}
return base.GetMediaTypeFormats(payloadKind);
}
}
public class CsvWriter : ODataWriter
{
// Etc..
}
The disconnect is with ODataMediaTypeFormatter
and CsvMediaTypeResolver
. How do I link the ODataMediaTypeFormatter
to my resolver?
As described in this document:
In ODataLib v7.0, Dependency Injection (or "DI" in short) support is introduced to simplify the API and implementation of ODataLib by eliminating redundant function parameters and class properties.
To make DI work properly with ODataLib, basically there are several things you have to do within your application:
- Implement your container builder based on your DI framework.
- Register the required services from both ODataLib and your application.
- Build and use the container (to retrieve the services) in ODataLib.
Microsoft uses IServiceProvider
interface as the abstraction of container. Whereas container is read only you have to implement IContainerBuilder
interface. Then inject your container. After that, register the required services into the container. You can use extension methods defined in ContainerBuilderExtensions
class to register services as ease.
You have to be cautious before using these methods:
For
AddServicePrototype
, we currently only support the following service types:ODataMessageReaderSettings
,ODataMessageWriterSettings
andODataSimplifiedOptions
. This design follows the Prototype Pattern where you can register a globally singleton instance (as the prototype) for each service type then you will get an individual clone per scope/request. Modifying that clone will not affect the singleton instance as well as the subsequent clones. That is to say now you don't need to clone a writer setting before editing it with the request-related information just feel safe to modify it for any specific request.The
AddDefaultODataServices
method registers a set of service types with default implementations that come from ODataLib. Typically you MUST call this method first on your container builder before registering any custom service. Please note that the order of registration matters! ODataLib will always use the last service implementation registered for a specific service type.
There is a list of services in the mentioned document that you can override; ODataMediaTypeResolver
is one of them. Consider to the list, before any service registeration.
Now you can build a container by calling BuildContainer
on your builder. That gives you a container instance that implements IServiceProvider
.
In order to use registered services in ODataLib, you must pass the container into ODataLib through some entry point.
Currently entry points in ODataLib are
ODataMessageReader
,ODataMessageWriter
, andODataUriParser
.
1. Serialization and Deserialization:
You could pass container into ODataMessageReader
or ODataMessageWriter
through request and response message. To do so you should create a class that implements IODataRequestMessage
and IODataResponseMessage
, and IContainerProvider
like below:
class ODataMessageWrapper : IODataRequestMessage, IODataResponseMessage, IContainerProvider, ...
{
public IServiceProvider Container { get; set; }
// rest of the implementation here
}
And then you can use the ODataMessageWrapper
class to pass the container into ODataLib as below:
ODataMessageWrapper responseMessage = new ODataMessageWrapper();
responseMessage.Container = Request.GetRequestContainer();
ODataMessageWriter writer = new ODataMessageWriter(responseMessage);
In the above example GetRequestContainer
is an extension of HttpRequestMessage
implemented in Microsoft.AspNet.OData.HttpRequestMessageExtensions.cs.
Now container is stored in the Container
properties of ODataMessageInfo
, ODataInputContext
, and ODataOutputContext
and their subclasses. In order of implementing custom media types, you can access the container through those properties.
If you fail to set the
Container
inIContainerProvider
, it will remain null. In this case, ODataLib will not fail internally but all services will have their default implementations and there would be NO way to replace them with custom ones. That said, if you want extensibility, please use DI :-)
2. URI Parsing:
To pass a container into URI parser you should use the constructor overloads of ODataUriParser
. If you use other constructors the DI support in URI parsers will be disabled. This way container will be saved in ODataUriParserConfiguratino
and used in URI parser.
public sealed class ODataUriParser
{
public ODataUriParser(IEdmModel model, Uri serviceRoot, Uri uri, IServiceProvider container);
public ODataUriParser(IEdmModel model, Uri relativeUri, IServiceProvider container);
}
Currently
ODataUriResolver
,UriPathParser
andODataSimplifiedOptions
can be overridden and will affect the behavior of URI parsers.
I have solved this by the CsvOutputContext
and CsvWriterDemo
explained in the examples in Microsoft.OData.Core
Example Code Updated
public CsvOutputContext(
ODataFormat format,
ODataMessageWriterSettings settings,
ODataMessageInfo messageInfo,
bool synchronous)
: base(format, settings, messageInfo.IsResponse, synchronous,
messageInfo.Model, messageInfo.UrlResolver)
{
this.stream = messageInfo.GetMessageStream();
this.Writer = new StreamWriter(this.stream);
}
}
private static void CsvWriterDemo()
{
EdmEntityType customer = new EdmEntityType("ns", "customer");
var key = customer.AddStructuralProperty("Id", EdmPrimitiveTypeKind.Int32);
customer.AddKeys(key);
customer.AddStructuralProperty("Name", EdmPrimitiveTypeKind.String);
ODataEntry entry1 = new ODataEntry()
{
Properties = new[]
{
new ODataProperty(){Name = "Id", Value = 51},
new ODataProperty(){Name = "Name", Value = "Name_A"},
}
};
ODataEntry entry2 = new ODataEntry()
{
Properties = new[]
{
new ODataProperty(){Name = "Id", Value = 52},
new ODataProperty(){Name = "Name", Value = "Name_B"},
}
};
var stream = new MemoryStream();
var message = new Message { Stream = stream };
// Set Content-Type header value
message.SetHeader("Content-Type", "text/csv");
var settings = new ODataMessageWriterSettings
{
// Set our resolver here.
MediaTypeResolver = CsvMediaTypeResolver.Instance,
DisableMessageStreamDisposal = true,
};
using (var messageWriter = new ODataMessageWriter(message, settings))
{
var writer = messageWriter.CreateODataFeedWriter(null, customer);
writer.WriteStart(new ODataFeed());
writer.WriteStart(entry1);
writer.WriteEnd();
writer.WriteStart(entry2);
writer.WriteEnd();
writer.WriteEnd();
writer.Flush();
}
stream.Seek(0, SeekOrigin.Begin);
string msg;
using (var sr = new StreamReader(stream)) { msg = sr.ReadToEnd(); }
Console.WriteLine(msg);
}
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With