Search code examples
c#vb.netfuncmediatypeformatter

Custom mediatypeformatter not working on inherited classes


I have this line:

GlobalConfiguration.Configuration.Formatters.Add(New ExcelMediaTypeFormatter(Of Classification)(Function(t) New ExcelRow(ExcelCell.Map(t.ChemicalAbstractService), ExcelCell.Map(t.Substance), ExcelCell.Map(t.Columns("Classifidcation")), ExcelCell.Map(t.Columns("Classification"))), Function(format) "excel"))

It works fine and creates a excelfile from my web api.

I have several subclasses that inherits this Classification class and I want to make a mediaformatter for each subclass for getting specific columns in the excelformatter. The problems is that if I do like this:

GlobalConfiguration.Configuration.Formatters.Add(New ExcelMediaTypeFormatter(Of CustomClassification)(Function(t) New ExcelRow(ExcelCell.Map(t.ChemicalAbstractService), ExcelCell.Map(t.Substance), ExcelCell.Map(t.Columns("Classifidcation")), ExcelCell.Map(t.Columns("Classification"))), Function(format) "excel"))

Then it doesn't work at all. It just generates xml from the standard formatter instead. How can I make it react to a subclass, when the web api returns a

IQueryable(Of Classification)

The formatter:

public class ExcelMediaTypeFormatter<T> : BufferedMediaTypeFormatter 
    {
    private const string ContentType = "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet";
    private readonly Func<T, ExcelRow> builder;

    public ExcelMediaTypeFormatter(Func<T, ExcelRow> value)
    {
        builder = value;
        SupportedMediaTypes.Add(new MediaTypeHeaderValue(ContentType));
    }

    public ExcelMediaTypeFormatter(Func<T, ExcelRow> value, params Func<object, string>[] type)
        : this(value)
    {
        foreach (var mediaTypeMapping in type) {
            this.MediaTypeMappings.Add(Map(mediaTypeMapping));
        }
    }

    public override bool CanWriteType(Type type)
    {
        return type == typeof(IQueryable<T>) || type == typeof(T);
    }

    public override bool CanReadType(Type type)
    {
        return false;
    }
public override void WriteToStream(Type type, object value, Stream writeStream, HttpContent content)
    {            
        using (Stream ms = new MemoryStream()) {
            using (var book = new ClosedXML.Excel.XLWorkbook()) {
                var sheet = book.Worksheets.Add("sample");

                ICollection<T> rows = type == typeof(IQueryable<T>) ? ((IQueryable<T>)value).ToList() : new List<T>() { (T)value };

                for (var r = 0; r < rows.Count; r++) {
                    var result = builder((T)rows.ElementAt(r));

                    for (var c = 0; c < result.Count(); c++) {
                        if (result.ElementAt(c) != null)
                            sheet.Cell(r + 2, c + 1).Value = result.ElementAt(c).Value.ToString();
                    }
                }

                sheet.Columns().AdjustToContents();

                book.SaveAs(ms);

                byte[] buffer = new byte[ms.Length];

                ms.Position = 0;
                ms.Read(buffer, 0, buffer.Length);

                writeStream.Write(buffer, 0, buffer.Length);
            }
        }
    }

Solution

  • CanWriteType will return false, because T is CustomClassification and type is Classification. Your formatter will not be used if it returns false.

    Because a Classification is not necessarily a CustomClassification this can't work.

    To achieve what you want, you need to change your implementation a bit.

    Your ExcelMediaTypeFormatter will no longer be generic. And it will not get passed one Func but a list of IRowsBuilder instances. These will be used in the WriteToStream method to select the correct builder:

    public interface IRowsBuilder
    {
        bool CanBuildFor(Type type);
        IEnumerable<Type> SupportedTypes { get; }
        ExcelRow BuildRow(object value);
    }
    
    public class RowsBuilder<T> : IRowsBuilder where T : Classification
    {
        Func<T, ExcelRow> _builder;
    
        public RowsBuilder(Func<T, ExcelRow> builder)
        {
            if(builder == null) throw new ArgumentNullException("builder");
    
            _builder = builder;
        }
    
        public bool CanBuildFor(Type type)
        {
            return type.IsSubclassOf(typeof(T));
        }
    
        public IEnumerable<Type> SupportedTypes
        {
            get { yield return typeof(T); }
        }
    
        public ExcelRow BuildRow(object value)
        {
            if(!CanBuildFor(value.GetType()))
                throw new ArgumentException();
    
            return _builder((T)value);
        }
    }
    
    public class ExcelMediaTypeFormatter : BufferedMediaTypeFormatter 
    {
        private readonly ILookup<Type, IRowsBuilder> _builder;
    
        public ExcelMediaTypeFormatter(IEnumerable<IRowsBuilder> builder)
        {
            _builder = builder.SelectMany(x => builder.SupportedTypes
                                                      .Select(y => new
                                                              {
                                                                  Type = y,
                                                                  Builder = x
                                                              }))
                              .ToLookup(x => x.Type, x => x.Builder); 
        }
    
        public override bool CanWriteType(Type type)
        {
            return type == typeof(IQueryable<Classification>) ||
                   type == typeof(Classification);
        }
    
        // ...
    
        public override void WriteToStream(Type type, object value,
                                           Stream writeStream, HttpContent content)
        {
    
            // ...
    
            List<Classification> rows;
            if(type == typeof(IQueryable<Classification>))
                rows = ((IQueryable<Classification>)value).ToList();
            else
                rows = new List<Classification> { (Classification)value };
    
            for (var r = 0; r < rows.Count; r++)
            {
                var value = rows.ElementAt(r);
                var builder = _builder[value.GetType()];
                var result = builder(value);
                // ...
            }
        }
    }
    

    You would now only register one ExcelMediaTypeFormatter with all the builders:

    var customBuilder = new RowsBuilder<CustomClassification>(
                            t => new ExcelRow(ExcelCell.Map(t.ChemicalAbstractService),
                                              ExcelCell.Map(t.Substance), 
                                              ExcelCell.Map(t.Columns("Classifidcation")),
                                              ExcelCell.Map(t.Columns("Classification"))));
    var builders = new List<IRowsBuilder>();
    builder.Add(customBuilder);
    builder.Add(someOtherBuilder);
    var excelFormatter = new ExcelMediaTypeFormatter(builders, format => "excel");
    GlobalConfiguration.Configuration
                       .Formatters
                       .Add(excelFormatter);