Serve both REST and GraphQL APIs from .NET Core application

Viewed 1608

I have a .NET Core REST API server that is already serving customers.

Can I configure the API server to also support GraphQL by adding the HotChocolate library and defining the queries? Is it OK to serve both GraphQL and REST APIs from my .NET Core server?

2 Answers

Yes, supporting both REST APIs (controllers) and GraphQL is totally OK.

If you take libraries out of the picture, handling a GraphQL request just means handling an incoming POST to /graphql.

You can write a typical ASP.NET Core controller that handles those POSTs, if you want. Frameworks like Hot Chocolate provide middleware like .UseGraphQl() that make it more convenient to configure, but conceptually you can think of .UseGraphQl() as adding a controller that just handles the /graphql route. All of your other controllers will continue to work just fine!

There is a way you can automate having both APIs up and running at the same time using hotchocolate and schema stitching.

Basically I followed this tutorial offered by Ian webbington. https://ian.bebbs.co.uk/posts/LessReSTMoreHotChocolate

Ian uses swagger schema from its API to create a graphql schema which saves us time if we think about it. It's easy to implement, however you still need to code to expose graphql endpoints.

This is what I implemented to connect all my graphql and rest APIs in just one API gateway. I'm sharing my custom implementation to have swagger schema (REST) running under hotchocolate (Graphql):

using System;
using HotChocolate;
using HotChocolate.AspNetCore;
using HotChocolate.AspNetCore.Playground;
using HotChocolate.AspNetCore.Voyager;
using HotChocolate.AspNetCore.Subscriptions;
using HotChocolate.Stitching;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.DependencyInjection;
using SmartGateway.Api.Filters;
using SmartGateway.Api.RestServices.SmartConfig;

namespace SmartGateway.Api.Extensions
{
    public static class GraphQlExtensions
    {
        public static IServiceCollection AddGraphQlApi(this IServiceCollection services)
        {
            services.AddHttpClient("smartauth", (sp, client) =>
            {
                sp.SetUpContext(client); //GRAPHQL API
                client.BaseAddress = new Uri(AppSettings.SmartServices.SmartAuth.Endpoint);
            });
            services.AddHttpClient("smartlog", (sp, client) =>
            {
                sp.SetUpContext(client); //GRAPHQL API
                client.BaseAddress = new Uri(AppSettings.SmartServices.SmartLog.Endpoint);
            });
            services.AddHttpClient("smartway", (sp, client) =>
            {
                sp.SetUpContext(client); //GRAPHQL API
                client.BaseAddress = new Uri(AppSettings.SmartServices.SmartWay.Endpoint);
            });

            services.AddHttpClient<ISmartConfigSession, SmartConfigSession>((sp, client) =>
                {
                    sp.SetUpContext(client); //REST API
                    client.BaseAddress = new Uri(AppSettings.SmartServices.SmartConfig.Endpoint);
                }
            );

            services.AddDataLoaderRegistry();

            services.AddGraphQLSubscriptions();

            services.AddStitchedSchema(builder => builder
                .AddSchemaFromHttp("smartauth")
                .AddSchemaFromHttp("smartlog")
                .AddSchemaFromHttp("smartway")
                .AddSchema(new NameString("smartconfig"), SmartConfigSchema.Build())
                .AddSchemaConfiguration(c =>
                {
                    c.RegisterExtendedScalarTypes();
                }));

            services.AddErrorFilter<GraphQLErrorFilter>();

            return services;
        }

        public static IApplicationBuilder UseGraphQlApi(this IApplicationBuilder app)
        {
            app.UseWebSockets();
            app.UseGraphQL("/graphql");
            app.UsePlayground(new PlaygroundOptions
            {
                Path = "/ui/playground",
                QueryPath = "/graphql"
            });
            app.UseVoyager(new PathString("/graphql"), new PathString("/ui/voyager"));

            return app;
        }
    }
}

Set up HttpContext extension:

using System;
using System.Net.Http;
using System.Net.Http.Headers;
using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.DependencyInjection;

namespace SmartGateway.Api.Extensions
{
    public static class HttpContextExtensions
    {
        public static void SetUpContext(this IServiceProvider servicesProvider, HttpClient httpClient)
        {
            HttpContext context = servicesProvider.GetRequiredService<IHttpContextAccessor>().HttpContext;

            if (context?.Request?.Headers?.ContainsKey("Authorization") ?? false)
            {
                httpClient.DefaultRequestHeaders.Authorization =
                    AuthenticationHeaderValue.Parse(context.Request.Headers["Authorization"].ToString());
            }
        }
    }
}

You need this to handle and pass the HTTPClient to your swagger Sdk.

using System.Net.Http;

namespace SmartGateway.Api.RestServices.SmartConfig
{
    public interface ISmartConfigSession
    {
        HttpClient GetHttpClient();
    }

    public class SmartConfigSession : ISmartConfigSession
    {
        private readonly HttpClient _httpClient;

        public SmartConfigSession(HttpClient httpClient)
        {
            _httpClient = httpClient;
        }

        public HttpClient GetHttpClient()
        {
            return _httpClient;
        }
    }
}

This is my graphql Schema:

namespace SmartGateway.Api.RestServices.SmartConfig
{
    public static class SmartConfigSchema
    {
        public static ISchema Build()
        {
            return SchemaBuilder.New()
                .AddQueryType<SmartConfigQueries>()
                .AddMutationType<SmartConfigMutations>()
                .ModifyOptions(o => o.RemoveUnreachableTypes = true)
                .Create();
        }
    }

    public class SmartConfigMutations
    {
        private readonly ISmartConfigClient _client;

        public SmartConfigMutations(ISmartConfigSession session)
        {
            _client = new SmartConfigClient(AppSettings.SmartServices.SmartConfig.Endpoint, session.GetHttpClient());
        }

        public UserConfigMutations UserConfig => new UserConfigMutations(_client);
    }
}

Finally, this is how you publish endpoints:

using System.Threading.Tasks;
using SmartConfig.Sdk;

namespace SmartGateway.Api.RestServices.SmartConfig.UserConfigOps
{
    public class UserConfigMutations
    {
        private readonly ISmartConfigClient _client;

        public UserConfigMutations(ISmartConfigClient session)
        {
            _client = session;
        }

        public async Task<UserConfig> CreateUserConfig(CreateUserConfigCommand createUserConfigInput)
        {
            var result = await _client.CreateUserConfigAsync(createUserConfigInput);
            return result.Response;
        }

        public async Task<UserConfig> UpdateUserConfig(UpdateUserConfigCommand updateUserConfigInput)
        {
            var result = await _client.UpdateUserConfigAsync(updateUserConfigInput);
            return result.Response;
        }
    }
}

More documentation about hotchocolate and schema stitching here: https://hotchocolate.io/docs/stitching

Related