Tag: API
Parallel API calls in JavaScript

Parallel API calls in JavaScript

Although JavaScript is single-threaded, it is relatively simple to avoid blocking the thread by writing code in an asynchronous style through the use of functions with an async keyword that returns a promise and only awaiting when you need it.

For example, the following code contains an API call in an async function.

1 private async CallAPI() {
2 const response = await axios.get('https://fooapi.com/Films')
3
4 console.log(response.data)
5 }

If we wanted to call two APIs then we could do the following.

1 private async CallAPI() {
2 const response = await axios.get('https://fooapi.com/Films')
3 const response2 = await axios.get('https://fooapi.com/Music')
4
5 console.log(response.data)
6 console.log(response2.data)
7 }
8

However because both API calls are being immediately awaited, this will result in the execution of the function stopping while it waits for the result of the first API call before making the second.

As we don't need that response to make the second API call, we can improve the performance of the code by moving the await keywords to the point we actually need the responses. Both API calls will now be made in parallel rather than the second waiting for the first to finish.

1private async CallAPI() {
2 const response = axios.get('https://fooapi.com/Films')
3 const response2 = axios.get('https://fooapi.com/Music')
4
5 console.log(await response.data)
6 console.log(await response2.data)
7}

Call an API for each item in an array in parallel

Those initial examples were quite easy to understand, but what if we have a scenario where you get a list of films showing at the cinema on a particular day and display the show times for each? You have two APIs at your disposal; The first returns all the films and the second returns the showtimes for a given film.

To get all the data you need you first need to call the API to get the films and then loop through them to call the second API and get the show times.

1interface Film {
2 id: string
3 name: string
4 showtimes: ShowTime[]
5}
6
7interface ShowTime {
8 date: Date
9}
10
11class FilmService {
12 public async GetShowtimes(): Promise<Film[]> {
13 const filmResponse = axios.get('https://fooapi.com/Films')
14
15 const films: Film[] = []
16 for (let e of (await filmResponse).data) {
17 console.log("Get Showtimes")
18 const showTimes = await axios.get(
19 `https://fooapi.com/Films/${e.id}/ShowTimes`
20 )
21 console.log("Got Showtimes")
22
23 films.push({
24 id: e.id,
25 name: e.name,
26 showtimes: showTimes.data,
27 })
28 }
29
30 return films
31 }
32}
33
34// Output
35// Get Showtimes
36// Got Showtimes
37// Get Showtimes
38// Got Showtimes
39// Get Showtimes
40// Got Showtimes

The problem with this code though is that its execution length will be the films API call response time + (showtime API response time * the number of films).

As each call to the second API have no dependencies on each other, it doesn't make any sense for them to have to be performed sequentially in the loop.

Instead, we can refactor for loop to be a map calling an async function which returns the new array and make use of Promise.all() to ensure they have all completed before returning the results.

1interface Film {
2 id: string
3 name: string
4}
5
6interface FilmWithShowtime extends Film {
7 showtimes: ShowTime[]
8}
9
10interface ShowTime {
11 date: Date
12}
13
14class FilmService {
15 public async GetShowtimes(): Promise<FilmWithShowtime[]> {
16 const filmResponse = await axios.get('https://fooapi.com/Films')
17
18 const films: FilmWithShowtime[] = await Promise.all(
19 filmResponse.data.map(async (e: Film) => {
20 return await this.getFilmWithShowtimes(e)
21 })
22 )
23
24 return films
25 }
26
27 private async getFilmWithShowtimes(film: Film): Promise<FilmWithShowtime> {
28 console.log("Get Showtimes")
29 const showTimes = await axios.get(
30 `https://fooapi.com/Films/${film.id}/ShowTimes`
31 )
32
33 console.log("Got Showtimes")
34
35 return {
36 id: film.id,
37 name: film.name,
38 showtimes: showTimes.data,
39 }
40 }
41}
42
43// Output
44// Get Showtimes
45// Get Showtimes
46// Get Showtimes
47// Got Showtimes
48// Got Showtimes
49// Got Showtimes
50

The API calls for show times will now app be made in parallel reducing the overall execution time of the code.

How to create a Graph QL API on Azure Functions

How to create a Graph QL API on Azure Functions

REST APIs are great, but they can result in either your application making an excessive number of requests to multiple endpoints, then only using a small percentage of the data returned. Or you end up making a large number of endpoints for specific purposes and now have maintenance hell to deal with.

If that's your situation then one option is to look at replacing some of that functionality with a Graph QL API. I'm not going to dig into what Graph QL APIs are (that's been covered by many people before me), but what I will do is show you how to make one in an Azure Function.

Your starting point is to use Hot Chocolate by Chilli Cream, not only does it have a fun meaningless name, but it also offers some great simple-to-use functionality. However, despite stating it works with Azure Functions, the documentation is all for ASP.NET Core, which is not the same thing.

Another issue I have with the documentation is that it doesn't explain particularly well how you configure it to work with a data access layer. Examples either have methods that return a dataset containing all related data, or they use Entity Framework, which as you generally wouldn't use your DB schema as an API schema feels like cheating.

So here is my guide from file new project to a working Graph QL API in an Azure Function.

File New Project

Starting right at the beginning, open Visual Studio and create a new Azure Function. For this demo, I'm using .NET 6 as that's the latest at the time of writing, and am going to create an HTTP trigger.

Create new Azure Function screen

For a data source, I've created a hard-coded repository containing Schools, Classes, and Students. Schools contain multiple classes and classes contain multiple students. Each repository contains functions to get all, get by id or get by the thing it's related to. e.g. Get Students by Class. Here's my code for it.

1using AzureFunctionWithGraphApi.Models;
2using System.Collections.Generic;
3using System.Linq;
4
5namespace AzureFunctionWithGraphApi.DataAccess
6{
7 public interface ISchoolRepository
8 {
9 List<School> All();
10 School GetById(int id);
11 }
12
13 public interface IClassRepository
14 {
15 List<Class> All();
16 Class GetById(int id);
17 List<Class> GetBySchool(int schoolId);
18 }
19
20 public interface IStudentRepository {
21 List<Student> All();
22 Student GetById(int id);
23 List<Student> GetByClass(int classId);
24 }
25
26 public static class DemoData
27 {
28 public static List<School> Schools = new List<School>()
29 {
30 new School() {Id = 1, Name = "Foo School"},
31 new School() {Id = 2 , Name = "Boo School"},
32 };
33
34 public static List<Class> ClassList = new List<Class>()
35 {
36 new Class() {Id = 3, SchoolId = 1, Name = "Red Class", YearGroup = 1},
37 new Class() {Id = 4, SchoolId = 1, Name = "Blue Class", YearGroup = 2},
38 new Class() {Id =5, SchoolId = 2, Name = "Yellow Class", YearGroup = 1},
39 new Class(){Id = 6, SchoolId = 2, Name = "Green Class", YearGroup = 2}
40 };
41
42 public static List<Student> Students = new List<Student>()
43 {
44 new Student() {Id = 1, ClassId = 3, FirstName = "John", Surname = "Smith"},
45 new Student() {Id = 2, ClassId = 3, FirstName = "Sam", Surname = "Smith"},
46 new Student() {Id = 3, ClassId = 4, FirstName = "Eric", Surname = "Smith"},
47 new Student() {Id = 4, ClassId = 4, FirstName = "Rachel", Surname = "Smith"},
48 new Student() {Id = 5, ClassId = 5, FirstName = "Tom", Surname = "Smith"},
49 new Student() {Id = 6, ClassId = 5, FirstName = "Sally", Surname = "Smith"},
50 new Student() {Id = 7, ClassId = 6, FirstName = "Sharon", Surname = "Smith"},
51 new Student() {Id = 8, ClassId = 6, FirstName = "Kate", Surname = "Smith"}
52 };
53 }
54
55 public class SchoolRepository : ISchoolRepository
56 {
57 public List<School> All()
58 {
59 return DemoData.Schools;
60 }
61
62 public School GetById(int id)
63 {
64 return DemoData.Schools.Where(x => x.Id == id).FirstOrDefault();
65 }
66 }
67
68 public class ClassRepository : IClassRepository
69 {
70 public List<Class> All()
71 {
72 return DemoData.ClassList;
73 }
74
75 public Class GetById(int id)
76 {
77 return DemoData.ClassList.Where(x => x.Id == id).FirstOrDefault();
78 }
79
80 public List<Class> GetBySchool(int schoolId)
81 {
82 return DemoData.ClassList.Where((x) => x.SchoolId == schoolId).ToList();
83 }
84 }
85
86 public class StudentRepository : IStudentRepository
87 {
88 public List<Student> All()
89 {
90 return DemoData.Students;
91 }
92
93 public List<Student> GetByClass(int classId)
94 {
95 return DemoData.Students.Where((x) => x.ClassId == classId).ToList();
96 }
97
98 public Student GetById(int id)
99 {
100 return DemoData.Students.Where(x => x.Id == id).FirstOrDefault();
101 }
102 }
103}
104

If you want to use it, you'll also need the related models.

1public class School
2 {
3 public int Id { get; set; }
4 public string Name { get; set; }
5 }
6
7public class Class
8 {
9 public int Id { get; set; }
10 public int SchoolId { get; set; }
11 public int YearGroup { get; set; }
12 public string Name { get; set; }
13 }
14
15public class Student
16 {
17 public int Id { get; set; }
18 public int ClassId { get; set; }
19 public string FirstName { get; set; }
20 public string Surname { get; set; }
21 }

Create a Graph QL API

With our project and data access layer created, lets get on with how to create a Graph QL in a .NET Azure Function.

Hot chocolate will provide all the functionality and can be added to your solution via Nuget. Just search for Hot Chocolate and make sure you pick the Azure Function version.

Hot Chocolate NuGet package

The HTTP Endpoint we created when creating the function needs updating to provide the route for the graph API.

1using System.Threading.Tasks;
2using Microsoft.AspNetCore.Mvc;
3using Microsoft.Azure.WebJobs;
4using Microsoft.Azure.WebJobs.Extensions.Http;
5using Microsoft.AspNetCore.Http;
6using Microsoft.Extensions.Logging;
7using HotChocolate.AzureFunctions;
8
9namespace AzureFunctionWithGraphApi
10{
11 public class GraphQlApi
12 {
13 [FunctionName("HttpExample")]
14 public async Task<IActionResult> Run(
15 [HttpTrigger(AuthorizationLevel.Anonymous, "get", "post", Route = "graphql/{**slug}")] HttpRequest req,
16 [GraphQL] IGraphQLRequestExecutor executor,
17 ILogger log)
18 {
19 log.LogInformation("C# HTTP trigger function processed a request.");
20
21 return await executor.ExecuteAsync(req);
22 }
23 }
24}
25

Next we need to configure what queries can be performed on the graph. For my example, I'm replicating the Get All and Get By Id methods from my data access layer.

One thing to note here is although I'm using dependency injection for my repositories they are using resolver injection on the methods rather than constructor injection. You can read more about why this is on the Chilli Cream site here, but essentially constructor injector won't work.

1using AzureFunctionWithGraphApi.DataAccess;
2using AzureFunctionWithGraphApi.Models;
3using HotChocolate;
4using System.Collections.Generic;
5
6namespace AzureFunctionWithGraphApi
7{
8 public class Query
9 {
10 public List<School> GetSchools([Service] ISchoolRepository schoolRepository)
11 {
12 return schoolRepository.All();
13 }
14 public School GetSchoolById([Service] ISchoolRepository schoolRepository, int schoolId)
15 {
16 return schoolRepository.GetById(schoolId);
17 }
18
19 public List<Class> GetClasses([Service] IClassRepository classRepository)
20 {
21 return classRepository.All();
22 }
23 public Class GetClassById([Service] IClassRepository classRepository, int classId)
24 {
25 return classRepository.GetById(classId);
26 }
27
28 public List<Class> GetClassesBySchoolId([Service] IClassRepository classRepository, int schoolId)
29 {
30 return classRepository.GetBySchool(schoolId);
31 }
32
33 public List<Student> GetStudents([Service] IStudentRepository studentRepository)
34 {
35 return studentRepository.All();
36 }
37 public Student GetStudentById([Service] IStudentRepository studentRepository, int studentId)
38 {
39 return studentRepository.GetById(studentId);
40 }
41
42 public List<Student> GetStudentsBySchoolId([Service] IStudentRepository studentRepository, int classId)
43 {
44 return studentRepository.GetByClass(classId);
45 }
46 }
47}
48

At this point (apart from the fact we haven't configured the startup file with our DI) you will now have a Graph QL API but it won't be able to load any related items. You will however be able to pick which fields you want from the datasets.

To add the related data we need to create extension methods for our models. These inject the instance of the item using Hot Chocolates Parent attribute, and the repository we're going to use to get the data.

1using AzureFunctionWithGraphApi.DataAccess;
2using AzureFunctionWithGraphApi.Models;
3using HotChocolate;
4using HotChocolate.Types;
5using System.Collections.Generic;
6
7namespace AzureFunctionWithGraphApi
8{
9 [ExtendObjectType(typeof(School))]
10 public class SchoolExtensions
11 {
12 public List<Class> GetClasses([Parent] School school, [Service] IClassRepository classRepository)
13 {
14 return classRepository.GetBySchool(school.Id);
15 }
16 }
17
18 [ExtendObjectType(typeof(Class))]
19 public class ClassExtensions
20 {
21 public School GetSchool([Parent] Class schoolClass, [Service] ISchoolRepository schoolRepository)
22 {
23 return schoolRepository.GetById(schoolClass.SchoolId);
24 }
25 public List<Student> GetStudents([Parent] Class schoolClass, [Service] IStudentRepository studentRepository)
26 {
27 return studentRepository.GetByClass(schoolClass.Id);
28 }
29 }
30
31 [ExtendObjectType(typeof(Student))]
32 public class StudentExtensions
33 {
34 public Class GetClass([Parent] Student student, [Service] IClassRepository classRepository)
35 {
36 return classRepository.GetById(student.ClassId);
37 }
38 }
39}
40

Now all that's left is to configure our startup file. This file no longer gets created when you create the Azure Function so you'll need to add it yourself.

Here's mine. As you can see I'm registering the dependency injection for my repositories, and also configuring the GraphQL. This needs to include the query class we made and any extension classes.

1using AzureFunctionWithGraphApi.DataAccess;
2using Microsoft.Azure.Functions.Extensions.DependencyInjection;
3using Microsoft.Extensions.DependencyInjection;
4
5[assembly: FunctionsStartup(typeof(AzureFunctionWithGraphApi.Startup))]
6namespace AzureFunctionWithGraphApi
7{
8 public class Startup : FunctionsStartup
9 {
10 public override void Configure(IFunctionsHostBuilder builder)
11 {
12 builder.Services.AddScoped<ISchoolRepository, SchoolRepository>();
13 builder.Services.AddScoped<IClassRepository, ClassRepository>();
14 builder.Services.AddScoped<IStudentRepository, StudentRepository>();
15
16 builder.AddGraphQLFunction()
17 .AddQueryType<Query>()
18 .AddTypeExtension<SchoolExtensions>()
19 .AddTypeExtension<ClassExtensions>()
20 .AddTypeExtension<StudentExtensions>();
21 }
22 }
23}
24

Run the Application and navigate in a browser to it's one route and you should get see the Banana Cake Pop UI to be able to view your schema.

Banana Cake Pop UI showing Scheme Reference

You can also test out queries selecting just the data you want even on related items.

Banana Cake Pop UI showing Query

We could even start with selecting a specific student and pull in their related class and school info.

Banana Cake Pop Graph QL Query

The Bad News

All of this is great and in fact, even more, functionality is available to be added, but there is some bad news. Not all of Hot Chocolates functionality actually works in an Azure Function, specifically authentication.

You can read about Hot Chocolates implementation of Authentication and Authorization here however it uses ASP.NET Core authentication middleware and Authorize attributes which do not work in Azure Functions. So unless you want your Graph QL API to be fully public you may be out of luck with this being a solution.

Code for this Demo

Now for the good news, if you want to try this without typing all the code, you can get a copy of it from my GitHub here.

https://github.com/timgriff84/AzureFunctionWithGraphApi

API Routes stopped working in Sitecore 9

API Routes stopped working in Sitecore 9

We recently undertook a Sitecore 9 upgrade and one issue we encountered was controllers we had set up for API routes stopped working on content delivery servers. Content management and single server setups were unaffected.

Our routes had been set up by the controllers inheriting from SitecoreController and then using the default route which Sitecore would create. e.g. /api/sitecore/foo/getFoo

1public class FooController : SitecoreController
2{
3 ...
4 [System.Web.Http.HttpPost]
5 public ActionResult GetFoo(FooModel queryModel)
6 {
7 ...
8 }
9}

MVC.LegalRoutes

After a bit of investigation we found a new setting in Sitecore 9 called MVC.LegalRoutes. From the documentation:

MVC: Pipe separated list of route URL's that are not valid for use with Sitecore.Mvc.
For instance, the default ASP.NET route ({controller}/{action}/{id}) catches most requests that are actually meant to be handled by the default Sitecore route.
Default: "{controller}/{action}/{id}"

If we added our route to the list everything started to work ok.

1<setting name="Mvc.LegalRoutes" value="|Sitecore.Mvc:sitecore/shell/api/sitecore/{controller}/{action}|Sitecore.Mvc.Api:/api/sitecore/{controller}/{action}|" />

A different approach

Not wanting to just change random settings we stumble across we contacted Sitecore support to see what they thought.

The route 'api/sitecore/{controller}/{action}' is a pre-defined Sitecore SPEAK route for Sitecore Client usage. So when trying to access on a content delivery server where the Sitecore Client is disabled, it no longer works.

So to get around the issue we can start registering the route on content delivery servers through the Global.asax file.

1public override void Application_Start(object sender, EventArgs args)
2{
3 base.Application_Start(sender, args);
4
5 if (System.Configuration.ConfigurationManager.AppSettings["role:define"] == "ContentDelivery")
6 {
7 System.Web.Routing.RouteTable.Routes.MapRoute("Sitecore.Speak.Commands", "api/sitecore/{controller}/{action}");
8 }
9}