Retrieving Trip Data With Azure Functions and Cosmos DB

In my previous post I setup a simple Azure function to update data in Cosmos DB. This post will review the simple code to retrieve trip entries by id using an Azure function. The next version of the project adds another Azure function for handling GET requests with the tripId on the end of the url.

Get Trigger/Binding

The GetTripRequest Azure function in the TripLoggerServices project supports retrieving a trip from Cosmos DB by the id passed on the url. The function uses the HttpTrigger attribute to trigger the function from an HTTP GET request that uses the proper route of ‘trips/{tripId}’. The CosmosDB attribute is used to look up the trip entry in Cosmos DB based on the tripId on the url.

public static class GetTripRequest
{
	[FunctionName("GetTripRequest")]
	public static async Task<IActionResult> Run(
		[HttpTrigger(AuthorizationLevel.Anonymous, "get", Route = "trips/{tripId}")] HttpRequest req,
		string tripId,
		[CosmosDB(
		databaseName: "TripLog",
		collectionName: "Trips",
		CreateIfNotExists = true,
		Id = "{tripId}",
		ConnectionStringSetting = "CosmosDBConnection")]TripEntry tripEntry,
		ILogger log)
	{
	}
}

HttpTrigger

The HttpTrigger attribute is configured to bind the HttpRequest to the req function parameter (line 5) and the route parameter “trips/{tripId}” to the string tripId function parameter (line 6). Note that the ‘req’ parameter isn’t used but it is still bound. The binding is configured to only respond to GET request by passing “get” to the methods attribute parameter.

Cosmos Input Binding

To retrieve the record from Cosmos DB by Id the Id=”{tripId}” parameter of the CosmosDB attribute is used. By setting the value to a template string the attribute knows to use the value for the tripId set by the HttpTrigger (line 11). If the trip entry is found in Cosmos DB the tripEntry parameter will contain the retrieved record. If the trip entry is not found the tripEntry parameter will be null.

Processing the Request

The processing for the GET request is very simple. If the entry isn’t found return a 404 otherwise return the entry.

if (tripEntry == null)
{
	return new NotFoundObjectResult($"Could not find trip entry with id: {tripId}");
}

return new OkObjectResult(tripEntry);

First the tripEntry is checked for null, if the parameter is null we know that the entity was not found in CosmosDB and return a 404 response (lines 1-4). Otherwise an 200 OK response is sent (line 6) with the tripEntry object for the result object.

With the addition of the GetTripRequest Azure function it is now possible to get previously logged trip by id in CosmosDB.

Updating Trip Data With Azure Functions and Cosmos DB

In my previous post I reviewed the simple code used to create new trip entries in Cosmos DB from an POST to an Azure Function. For this post I will be reviewing the code needed for a PUT request to an Azure Function that will update trip data in Cosmos DB. The next version of the project adds another Azure function for handling PUT requests.

Update Trigger/Binding

The UpdateTripRequest Azure function in the TripLoggerServices project supports updating existing trips stored in Cosmos DB. The function uses the HttpTrigger attribute to trigger the function from an HTTP PUT request. The CosmosDB attribute is used to look up the trip entry in Cosmos DB based on the submitted tripId.

public static class UpdateTripRequest
{
	[FunctionName("UpdateTripRequest")]
	public static async Task<IActionResult> Run(
		[HttpTrigger(AuthorizationLevel.Anonymous, "put", Route = "trips/{tripId}")] HttpRequest req,
		string tripId,
		[CosmosDB(
			databaseName: "TripLog",
			collectionName: "Trips",
			CreateIfNotExists = true,
			Id = "{tripId}",
			ConnectionStringSetting = "CosmosDBConnection")]TripEntry tripEntry,
		ILogger log)
	{
	}
}

HttpTrigger

The HttpTrigger attribute is configured to bind the HttpRequest to the req function parameter (line 5) and the route parameter “trips/{tripId}” to the string tripId function parameter (line 6). The binding is configured to only respond to PUT requests by passing “put” to the methods parameter.

Cosmos Input Binding

To retrieve a record from Cosmos DB by Id the Id = “{tripId}” parameter of the CosmosDB attribute is used. By setting the value to a template string the attribute knows to use the value for tripId set by the HttpTrigger (line 11). If the trip entry is found in Cosmos DB the tripEntry parameter will contain the retrieved record. If the trip entry is not found the tripEntry parameter will be null.

Processing the Request

Much of processing the PUT request is very similar to the POST request. The differences revolve around determining if the trip entry was found and updating the found trip entry.

if(tripEntry == null)
{
	return new NotFoundObjectResult($"Could not find trip entry with id: {tripId}");
}

// get raw request body
var requestBody = new StreamReader(req.Body).ReadToEnd();
var putRequest = JsonConvert.DeserializeObject<TripPut>(requestBody);

if (string.IsNullOrWhiteSpace(putRequest.TripFrom)) return new BadRequestObjectResult($"{nameof(putRequest.TripFrom)} missing");
if (string.IsNullOrWhiteSpace(putRequest.TripTo)) return new BadRequestObjectResult($"{nameof(putRequest.TripTo)} missing");
if (string.IsNullOrWhiteSpace(putRequest.Description)) return new BadRequestObjectResult($"{nameof(putRequest.Description)} missing");
if (putRequest.Distance == null) return new BadRequestObjectResult($"{nameof(putRequest.Distance)} missing");

tripEntry.Date = putRequest.Date;
tripEntry.Distance = putRequest.Distance;
tripEntry.TripFrom = putRequest.TripFrom;
tripEntry.TripTo = putRequest.TripTo;
tripEntry.Description = putRequest.Description;
tripEntry.ModifiedOn = DateTime.UtcNow;

// return results
return new OkObjectResult($"{tripId}");

First the tripEntry is checked for null, if the parameter is null we know that the entity was not found in CosmosDB and return a 404 response (lines 1-4). Next, like in the POST request, the update request is deserialized into a TripPut dto and some simple validation is performed (lines 7-13). Last, the updates are copied over from the TripPut dto to the tripEntry object and a OK(200) result is returned with the tripId as content (lines 15-20,23). The important thing to note here is that by virtue of the CosmosDB attribute any changes made to the tripEntry object is automatically updated in Cosmos DB when the function successfully finishes processing.

With the addition of the UpdateTripRequest Azure function it is now possible to update previously logged trips in Cosmos DB. More to come as the TripLoggerService evolves.

ASP.NET Core 3.1 API : Adding SCIM Media Type to JSON Formatter

On a recent project I worked on creating a SCIM (rfc 7642, 7643, 7644) provider using ASP.NET Core. The SCIM standard uses its own media type of application/scim+json by default. So I had to figure out how to add the scim media type to the existing json formatter. In the process I ran into several issues that I will highlight below.

Validate Accept Header

By default ASP.NET Core does not require content negotiation. If an Accept header is not present then ASP.NET Core will return JSON. To turn on validation of the content negotiation you have to set ReturnHttpNotAcceptable to true on the MvcOptions object when configuring your service. In addition you will want to set RespectBrowserAcceptHeader to true on the MvcOptions object to better support browsers calling your API. More details can be found here and here.

public void ConfigureServices(IServiceCollection services)
{
// ...

	services.AddControllers(config =>
	{​
		config.RespectBrowserAcceptHeader = true;​
		config.ReturnHttpNotAcceptable = true;​
	}).AddNewtonsoftJson();

// ...
}

Adding Media Type to JSON Formatter

By default the JSON formatter is registered with several media types that it supports (application/json, text/json, text/plain). For my project I needed to add application/scim+json as a supported media type. Unfortunately it was very hard to figure out how to accomplish adding a media type to a formatter that is already registered. I finally stumbled across this post on HATEOAS in ASP.NET Core Web API that helped me figure out the approach.

At first I tried something similar to this (will not work):

public void ConfigureServices(IServiceCollection services)
{
// ...

	services.AddControllers(config =>
	{​
		config.RespectBrowserAcceptHeader = true;​
		config.ReturnHttpNotAcceptable = true;​

		var jsonFormatter = c.OutputFormatters​
			.OfType<NewtonsoftJsonOutputFormatter>()?.FirstOrDefault();​
	​
		if (jsonFormatter != null)​
		{​
			jsonFormatter.SupportedMediaTypes.Add(MediaTypeHeaderValue.Parse("application/scim+json"));​
		}​
	}).AddNewtonsoftJson();

// ...
}

This code attempts to find the NewtonsoftJsonOutputFormatter in the OutputFormatters collection and add the scim media type to the SupportedMediaTypes collection.

Working Around Newtonsoft Json Issues

Since Microsoft has moved to use their own Json library instead of Newtonsoft Json you have to explicitly add Newtonsoft Json if you want the original behavior. Thus the call to AddNewtonsoftJson. The problem with the above approach is that the AddController configuration lambda code executes before the Newtonsoft Json libraries are swapped in for the in-the-box JSON formatter. As a result you have to register a separate Configure lambda after the AddNewtonsoftJson executes.

public void ConfigureServices(IServiceCollection services)
{
// ...

	services.AddControllers(config =>
	{​
		config.RespectBrowserAcceptHeader = true;​
		config.ReturnHttpNotAcceptable = true;​
	}).AddNewtonsoftJson();

	services.Configure<MvcOptions>(c =>​
	{​
		var jsonFormatter = c.OutputFormatters​
			.OfType<NewtonsoftJsonOutputFormatter>()?.FirstOrDefault();​
​
		if (jsonFormatter != null)​
		{​
			jsonFormatter.SupportedMediaTypes.Add(MediaTypeHeaderValue.Parse("application/scim+json"));​
		}​
	});

// ...
}

Using the default Json Formatter

If you are not using Newtonsoft then you can use the same approach, just change the formatter from NewtonsoftJsonOutputFormatter to SystemTextJsonOutputFormatter.

ProducesAttribute in the Way

Once I added the scim media type to the JSON formatter I tested and the scim media type still didn’t work for me. I searched the code for application/json and found the ProducesAttribute set on the controller I was using to testing. The ProducesAttribute effectively overrides any settings defined on the configured formatters. So a ProducesAttribute of [Produces(“application/json”)] was blocking the scim media type from working. Details on the ProducesAttribute are here. There are two options that worked for dealing with this issue.

  1. Remove the ProducesAtrribute. This will enable content negotiation to work properly.
  2. Change the ProducesAttribute to [Produces(“application/scim+json”, “application/json”)]. This will allow both application/json and application/scim+json to work as Accept headers while returning application/scim+json as the Content-Type.

Creating Trip Data With Azure Functions and Cosmos DB

In my previous post I reviewed the bindings that are setup for an Azure function that handles POST requests for the trip logger service. For this post I will be reviewing the simple code that translates the posted data to a trip entry to be saved in Cosmos DB. The initial setup only contains one Azure Function that supports POSTing data to create trips in Cosmos DB.

Deserializing the Data

Following the template Azure Function you can get the string request body from the passed in HttpRequest object (line 1).

var requestBody = new StreamReader(req.Body).ReadToEnd();
var postRequest = JsonConvert.DeserializeObject<TripPost>(requestBody);

if (string.IsNullOrWhiteSpace(postRequest.TripFrom)) return new BadRequestObjectResult($"{nameof(postRequest.TripFrom)} missing");
if (string.IsNullOrWhiteSpace(postRequest.TripTo)) return new BadRequestObjectResult($"{nameof(postRequest.TripTo)} missing");
if (string.IsNullOrWhiteSpace(postRequest.Description)) return new BadRequestObjectResult($"{nameof(postRequest.Description)} missing");
if (postRequest.Distance == null) return new BadRequestObjectResult($"{nameof(postRequest.Distance)} missing");

But instead of deserializing the JSON to a dynamic object I have created a POST model and I deserialize the JSON to the TripPost model (line 2).

Once I have the deserialized TripPost I perform some simple validation (lines 4-7). I will have to explore ways to perform more advanced validation later.

Creating the Trip Entry

Once I have the deserialized TripPost data and the data is validated the next step is to translate the post data to a new TripEntry object. I first generate a new Guid (line 2) to assign to the Id of the TripEntry. Then I create a new TripEntry object and copy over the post data using the object initializer syntax (lines 5-13).

// create id
var tripId = Guid.NewGuid();

// create trip cosmos db model
tripEntry = new TripEntry
{
	Id = tripId,
	Date = postRequest.Date,
	Distance = postRequest.Distance,
	TripFrom = postRequest.TripFrom,
	TripTo = postRequest.TripTo,
	Description = postRequest.Description,
};

// return results
return new OkObjectResult($"{tripId}");

The important thing to note is that the out tripEntry parameter is assigned the new TripEntry object (line 5). The CosmosDB attribute handles saving the tripEntry object to Cosmos DB for you automatically. The last step on the happy path is to return the OkObjectResult with the generated tripId as the content of the response.

Exploring Azure Functions

Awhile ago I was asked to write some demo service code. Two of the suggested technologies were Azure Functions and Azure Cosmos DB. The experience prompted me to finally setup a blog and commit to exploring technologies that interest me. So my plan is to start exploring Azure Functions using a demo project that I have started in my github account.

The Project

The project I will be working on is a trip logger service. It is named generically but it is intended for logging bicycling trips. I am an avid cyclist and track the miles I bike for commuting and fun. I have been logging the data just in Excel but I have several years worth of data that I can eventually use as the project expands.

The initial setup uses the Azure Functions v3 library and is written in C#. I have only created a single function that triggers on a http POST request and saves the submitted request to a Cosmos DB collection. There is minimal validation and no security at this point. There will be a lot of future post that I will be able to write to explore validation, security, and I’m sure a lot more.

Create Trigger/Binding

The first Azure function in the TripLoggerServices project is for creating new ‘trips’ within Cosmos DB. The function takes advantage of the trigger/binding attributes for input via a HttpTrigger attribute and persistence via a CosmosDB attribute.

public static class LogTripRequest
{
	[FunctionName("LogTripRequest")]
	public static IActionResult Run(
		[HttpTrigger(AuthorizationLevel.Anonymous, "post", Route = "trips")] HttpRequest req,
		[CosmosDB(
			databaseName: "TripLog",
			collectionName: "Trips",
			CreateIfNotExists = true,
			ConnectionStringSetting = "CosmosDBConnection")]out TripEntry tripEntry,
		ILogger log)
	{
	}
}

In the code sample above the FunctionName attribute marks the method as an Azure function entry point.

HttpTrigger

The first parameter to the http trigger sets the authorization level. For now the function is set to AuthorizationLevel. Anonymous to simplify development. Eventually I will explore security with functions.

Since this function is for creating trips the http trigger is configured to respond to http POST requests using the second parameter.

The Route parameter sets the relative url path for the function when it is hosted.
Note: The host.json file is setup so there is no prefix for the url.

{
  "version": "2.0",
  "extensions": {
    "http": {
      "routePrefix": ""
    }
  }
}

Using this trigger setup POST requests will trigger the function and the http request details will be contained in the req parameter.

Cosmos Binding

To persist data for a valid request the CosmosDB binding attribute is used. The databaseName and collectionName help identify the database and collection within the database where you want to persist your entities.

The CreateIfNotExists is set to true for development but in production you should create/configure your Cosmos DB database/collection outside the function.

The ConnectionStringSetting is used to specify the configuration variable that contains the connection string to the Cosmos DB subscription. The local.settings.json contains the default connection string to the Cosmos DB emulator for local development.

{
  "IsEncrypted": false,
  "Values": {
    "AzureWebJobsStorage": "UseDevelopmentStorage=true",
    "FUNCTIONS_WORKER_RUNTIME": "dotnet",
    "CosmosDBConnection": "AccountEndpoint=https://localhost:8081/;AccountKey=C2y6yDjf5/R+ob0N8A7Cgv30VRDJIWEHLM+4QDU5DE2nQ9nDuVTqobD4b8mGGyPMbIZnqyMsEcaGQy67XIw/Jw=="
  }
}

The tripEntry parameter is an out parameter since it is only being used to create new entries in Cosmos DB if the function execution is successful. The CosmosDB binding knows how to save the object instance assigned to the tripEntry parameter.