Content Negotiation, Resources, and Input Body Edit on GitHub


Jasper tries to make it as easy as possible for you to code straight down to the ASP.Net Core metal as needed by just giving you access to HttpContext and its children at any point like this valid Jasper endpoint:


// Responds to "PUT: /something"
public Task put_something(HttpRequest request, HttpResponse response)
{
    // Read the HttpRequest
    // Write out some kind of response
}

The Jasper team thinks that's absolutely necessary for one off cases or any time you just need more power over the HTTP response. Most of the time however, we believe that you'll be much more productive using Jasper's support for the "one model in, one model out" approach where you mostly deal with strong typed .Net objects coming in and out, while letting Jasper deal with the repetitive muck of JSON serialization or whatever representation you're using.

For an HTTP endpoint action, Jasper formalizes the concept of an optional input type based on the signature of the endpoint action that will be read from the HttpRequest.Body data during an HTTP request -- most commonly by deserializing JSON into a .Net type. Likewise, the return value in the action signature is termed a "resource type." During the execution of an endpoint action with a resource type, the value returned would be written into the HttpResponse.Body -- most commonly by serializing the object into JSON.

Here's a concrete example showing both synchronous and asychronous methods:


// ResourceModel is the "resource" type
// Command is the "input" type
public ResourceModel post_resource(Command model)
{
    return new ResourceModel();
}

// ResourceModel is the "resource" type
// Command is the "input" type
public Task<ResourceModel> post_resource_async(Command model)
{
    return Task.FromResult(new ResourceModel());
}

Resources in Jasper are the model objects returned by endpoint actions. In the Jasper execution pipeline, resources are rendered into the HTTP response pipeline. Consider these two endpoint actions:


public Task<Invoice> get_invoice_async(string invoiceId, IQuerySession session)
{
    return session.LoadAsync<Invoice>(invoiceId);
}

public Task<Invoice> get_invoice_sync(string invoiceId, IQuerySession session)
{
    return session.LoadAsync<Invoice>(invoiceId);
}

In both methods, the resource type is the Invoice class.

In all cases, the usage of input types and resource types can be mixed with route arguments as shown below:


// ResourceModel is the resource type
// "id" is a route argument
public ResourceModel get_resource_id(string id)
{
    return lookupById(id);
}

Input types can be used without any kind of resource model if you are issuing some kind of command to the web service:


// Responds to "PUT: /invoice"
// Invoice is the input type
// There is no resource type
public void put_invoice(Invoice invoice)
{
    // process the new invoice
}

In the case above, Jasper (well, actually ASP.Net Core itself) will mark successful requests with HttpResponse.StatusCode = 200, but otherwise there's no other response. As shown in a section below, you can also take control over the status code by returning an int that Jasper will assume is the status code value:


// Responds to "POST: /invoice"
// Invoice is the input type
// There is no resource type
public int post_invoice(Invoice invoice)
{
    // process the new invoice

    // 201: Created
    return 201;
}

String Resources

If an action method returns a .Net string object or Task<string>, the results of the method will be written to the outgoing HTTP response with the content type header value text/plain. Consider this endpoint method:


public class StringEndpoint
{
    public string get_string()
    {
        return "some string";
    }
}

The behavior of that method is demonstrated in this test from the Jasper codebase:


[Fact]
public Task write_as_text()
{
    return scenario(_ =>
    {
        _.Get.Url("/string");
        _.ContentShouldBe("some string");
        _.ContentTypeShouldBe("text/plain");
        _.Header("content-length").SingleValueShouldEqual("11");
    });
}

Working with Json

Jasper's obvious default rendering strategy for any concrete resource type besides string or int is to write out the response by serializing the resource to JSON. Jasper uses Newtonsoft.Json as its default JSON serializer, but the JSON serialization can be customized.

For example, this endpoint would expect to read the request body as JSON by deserializing the request body to the SomeNumbers type, then serialize the outgoing SumValue type to the response:


public class NumbersEndpoint
{
    public static SumValue post_sum(SomeNumbers input)
    {
        return new SumValue {Sum = input.X + input.Y};
    }
}

To customize the JSON serialization with the built in Newtonsoft.Json serialization, register a custom JsonSerializationSettings object with the application's IoC registrations.

You can do that through the Startup class you're using to configure ASP.Net Core:


public class Startup
{
    public void ConfigureServices(IServiceCollection services)
    {
        // Override the JSON serialization
        services.AddSingleton(new JsonSerializerSettings
        {
            DateParseHandling = DateParseHandling.DateTimeOffset,
            TypeNameHandling = TypeNameHandling.Objects
        });
    }
}

Or if you're using idiomatic Jasper style bootstrapping, you could instead use native Lamar service registrations like this:


public class MySpecialJsonUsingApp : JasperRegistry
{
    public MySpecialJsonUsingApp()
    {
        Services.For<JsonSerializerSettings>().Use(new JsonSerializerSettings
        {
            DateParseHandling = DateParseHandling.DateTime
        });
    }
}

Status Code

If an action method returns an int or Task<int>, the default behavior is to return an empty Http response body, but to set the HttpResponse.StatusCode property to the result of the method.

For example, the behavior of these endpoint methods below:


public class StatusCodeEndpoint
{
    public static int get_status1()
    {
        return 201;
    }

    public Task<int> get_status2()
    {
        return Task.FromResult(203);
    }
}

is demonstrated by this test:


[Fact]
public Task set_status_from_sync_action()
{
    return scenario(_ =>
    {
        _.Get.Url("/status1");
        _.StatusCodeShouldBe(201);
    });
}

Content Negotiation

Note! The reader/writer support and even the content negotiation is shared between the messaging and HTTP support within Jasper

Jasper also supports the concept of content negotiation. If you need to support multiple representations or formats of either the input or resource type for an endpoint action, you can register any number of custom readers and writers with Jasper, and Jasper will utilize content negotiation at runtime to choose the proper readers and writers based on the content-type and accepts headers in the HTTP request. If the request does not match a valid reader as specified in the request's content-type header, Jasper will abort the request and return a 415 status code for Unsupported Media Type. Likewise, if the accepts header in the request does not match any of the known writers for the endpoint, Jasper will abort the request with a 406 status code meaning Not Acceptable.

For complete examples of using content negotiation in Jasper, check out the acceptance tests in the codebase for conneg.

For custom resource representations, you need to implement Jasper's IMessageSerializer interface like so:


public interface IMessageSerializer
{
    Type DotNetType { get; }

    string ContentType { get; }
    byte[] Write(object model);

    Task WriteToStream(object model, HttpResponse response);
}

To help speed things along, there's a base class called Jasper.Conneg.MessageSerializerBase<T> that does some of the common grunt work with reading and writing header values.

A custom writer that writes an Xml representation of the Invoice resource type would look something like this below:


public class InvoiceXmlWriter : MessageSerializerBase<Invoice>
{
    public InvoiceXmlWriter() : base("application/xml")
    {
    }

    // We don't care in this case because this is only used inside the message bus
    // part of Jasper
    public override byte[] Write(Invoice model)
    {
        throw new NotSupportedException();
    }

    public override Task WriteToStream(Invoice model, HttpResponse response)
    {
        var serializer = new XmlSerializer(typeof(Invoice));
        serializer.Serialize(response.Body, model);

        return Task.CompletedTask;
    }
}

Likewise, for custom readers of the input type, use the IMessageDeserializer interface:


public interface IMessageDeserializer
{
    string MessageType { get; }
    Type DotNetType { get; }

    string ContentType { get; }
    object ReadFromData(byte[] data);
    Task<T> ReadFromRequest<T>(HttpRequest request);
}

And there is a Jasper.Conneg.MessageDeserializerBase<T> base class that does some of the repetitive work for you.

A custom reader that reads an Xml representation of the Invoice model as an input type would look something like this below:


public class InvoiceXmlReader : MessageDeserializerBase<Invoice>
{
    public InvoiceXmlReader() : base("application/xml")
    {
    }

    public override Invoice ReadData(byte[] data)
    {
        throw new NotSupportedException();
    }

    protected override Task<Invoice> ReadData(Stream stream)
    {
        var serializer = new XmlSerializer(typeof(Invoice));
        var model = (Invoice)serializer.Deserialize(stream);

        return Task.FromResult(model);
    }
}

Jasper will automatically find and register any IMessageSerializer or IMessageDeserializer types in your main application assembly. Otherwise, you can register these objects directly into your application's underlying IoC container like so:


public class AppWithCustomSerializers : JasperRegistry
{
    public AppWithCustomSerializers()
    {
        // Register a custom writer
        Services.AddSingleton<IMessageSerializer, InvoiceXmlWriter>();

        // Register a custom reader
        Services.AddSingleton<IMessageDeserializer, InvoiceXmlReader>();
    }
}