Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Web API OData - ODataMediaTypeFormatter MediaTypeResolver no longer exists

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?

like image 842
Michael C. Gates Avatar asked Jan 10 '19 21:01

Michael C. Gates


2 Answers

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:

  1. Implement your container builder based on your DI framework.
  2. Register the required services from both ODataLib and your application.
  3. 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 and ODataSimplifiedOptions. 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, and ODataUriParser.

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 in IContainerProvider, 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 and ODataSimplifiedOptions can be overridden and will affect the behavior of URI parsers.

like image 65
MasLoo Avatar answered Nov 07 '22 02:11

MasLoo


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);
}
like image 2
Sreeram Nair Avatar answered Nov 07 '22 03:11

Sreeram Nair