ASP.NET Core und Custom Formatter - Norbert Eder

Der Datenaustausch kann in ASP.NET Core MVC per JSON, XML bzw. reinem Text geschehen. Dies ist allerdings nicht für alle Fälle ausreichend. Daher kann man selbst eingreifen und für zusätzliche Unterstützung sorgen. Dieser Artikel zeigt, wie man das iCalendar-Format unterstützt.

Grundlagen

Formatter werden verwendet, um Inhaltsanfragen (siehe Accept-Header – Stichwort Content Negotiation) zu verarbeiten. Das Standard-Format in ASP.NET Core MVC ist JSON (application/json). Sendet der Client einen Accept-Header mit einem unbekannten Inhaltstyp, erfolgt eine Umleitung auf den Standard-Typ. Das ist (sofern keine Änderung erfolgte), JSON. Die Verwendung von XML muss explizit konfiguriert werden (siehe Startup.cs):

services.AddMvc()
    .AddXmlSerializerFormatters();

Formatter können für beide Richtungen, also Input und Output, implementiert werden. Ebenfalls gibt es die Möglichkeit per Text (siehe TextInputFormatter und TextOutputFormatter), sowie per Streams (siehe StreamInputFormatter und StreamOutputFormatter) zu arbeiten.

Konkrete Implementierung

Im Beispiel soll es folgende Möglichkeiten geben:

  1. Rückgabe eines Termins als JSON
  2. Rückgabe eines Termins im iCalendar-Format
  3. Direktaufruf Link ohne eigenen Accept-Header für Download

Da nur Daten zurückgegeben, aber nicht angenommen werden, ist nur ein Output-Formatter zu implementieren. Der nachfolgende Code zeigt die Details. Im Konstruktor werden die unterstützten MediaTypes angegeben.

public class IcsOutputFormatter : TextOutputFormatter
{
    public IcsOutputFormatter()
    {
        SupportedEncodings.Add(Encoding.UTF8);
        SupportedEncodings.Add(Encoding.Unicode);

        SupportedMediaTypes.Add(MediaTypeHeaderValue.Parse("application/x-ical"));
        SupportedMediaTypes.Add(MediaTypeHeaderValue.Parse("text/ical"));
    }

    protected override bool CanWriteType(Type type)
    {
        // this should only be available for appointments
        if (typeof(Appointment).IsAssignableFrom(type) 
            || typeof(IEnumerable<Appointment>).IsAssignableFrom(type))
        {
            return base.CanWriteType(type);
        }

        return false;
    }

    public override Task WriteResponseBodyAsync(    
        OutputFormatterWriteContext context, 
        Encoding selectedEncoding
    )
    {
        IServiceProvider serviceProvider = context.HttpContext.RequestServices;
        var logger = serviceProvider.GetService(typeof(ILogger<IcsOutputFormatter>)) as ILogger;

        var response = context.HttpContext.Response;

        var buffer = new StringBuilder();
        if (context.Object is IEnumerable<Appointment>)
        {
            foreach (Appointment appointment in context.Object as IEnumerable<Appointment>)
            {
                FormatIcs(buffer, appointment, logger);
            }
        }
        else
        {
            var appointment = context.Object as Appointment;
            FormatIcs(buffer, appointment, logger);
        }
        return response.WriteAsync(buffer.ToString());
    }

    // more code
}

In der Methode CanWriteType wird auf einen konkreten Typ eingeschränkt. Zugelassen wird werden alle Objekte, die ein Appointment oder eine Liste davon darstellen.

Sollen auch Daten im iCalendar-Format empfangen und verarbeitet werden können, müsste ein Input-Formatter implementiert werden. Dazu wäre eine Ableitung von TextInputFormatter vorzunehmen.

Konfiguration

In der Methode ConfigureServices wird der neue IcsOutputFormatter gesetzt. Ebenfalls wird ein MediaType-Mapping von ics auf application/x-ical gesetzt. Darüber wird die Extension ics in der URL ermöglicht. Wird diese gesetzt, erfolgt eine Behandlung, für den dazu gemappten MediaType, also unserem Formatter.

services.AddMvc(options =>
{
    options.OutputFormatters.Insert(0, new IcsOutputFormatter());
    // for accessing via URL
    options.FormatterMappings.SetMediaTypeMappingForFormat("ics", "application/x-ical");
});

Damit das MediaType-Mapping funktioniert, müssen die dafür in Frage kommenden Methoden mit zusätzlichen Attributen versehen werden:

[Route("api/1/appointment"), EnableCors("AllowAllOrigins")]
public class AppointmentController : Controller
{
    [FormatFilter]
    [HttpGet("{id}"), HttpGet("{id}.{format}")]
    public Appointment Get(int id)
    {
        var organizer = new User()
        {
            FirstName = "Norbert",
            LastName = "Eder",
            Organization = "NE",
            Email = "thisismyemail@herewego"
        };
        return new Appointment()
        {
            IsPublic = true,
            Organizer = organizer,
            Description = "Wall of text",
            Title = "So important",
            Location = "Vienna",
            Start = DateTime.Now,
            End = DateTime.Now.AddHours(2)
        };
    }
}

Das FormatFilterAttribute definiert, dass ein format-Wert der Route verwendet wird. So wird im Beispiel die Route mit{id}.{format} definiert. d.h. mit /api/1/appointment/1.ics werden die Daten im iCalendar-Format zurückgeliefert.

Das FormatFilterAttribute erzwingt die format-Angabe. Damit dies auch ohne möglich wird, kann die Route zweimal konfiguriert werden:

[HttpGet("{id}"), HttpGet("{id}.{format}")]

Nun sind beide Varianten möglich. Ohne Format wird die Default-Verarbeitung angeworfen. In der Regel wird also JSON zurückgegeben.

Demo-Anwendung

Das gezeigte Beispiel steht via GitHub zur Verfügung und zeigt den gezeigten IcsOutputFormatter in Aktion.

Viel Spaß beim Entwickeln!

Über den Autor

Norbert Eder

Ich bin ein leidenschaftlicher Softwareentwickler und Fotograf. Mein Wissen und meine Gedanken teile ich nicht nur hier im Blog, sondern auch in Fachartikeln und Büchern.