Hosting ASP.NET Core Razor Pages and Web APIs in a Single Project

This blog post explains one way that you can host ASP.NET Core Web APIs and Razor Pages in a single project. Performance is best if the client has a fast network connection to the server. The fastest connection is probably from a host to itself. This is not the most scalable configuration for production (so some of this logic should run only in development environments and possibly at developer preference), but hosting the Web API and the razor pages in a single project provides some developer conveniences, such as launching and debugging both Web API and razor pages together.

For context, I recently implemented my first Web API as described in the following blog post.

I was able to get the Web API and razor pages to work by having the razor pages project reference the assembly that contains the Web API controller, which the routing system registers automatically. The Web API controller route (/pathapi) must appear first in the routing system to override the catchall route defined in the razor page view that I apply to all pages. There were some other minor configuration requirements, and to support re-separation of client and server, it is important to remember not to write code in the razor pages solution that depends on server components in the Web API project.

The relevant bits of the ConfigureServices() method in the Startup.cs of the solution that hosts both Web APIs and razor pages are as follows.

// cms client
ContentstackDeliveryClientOptions options = new ContentstackDeliveryClientOptions(
    Configuration.GetSection("ContentstackOptions"));
services.AddSingleton<ContentstackDeliveryClientOptions>(options);
services.AddHttpClient<IDeliveryClient, ContentstackDeliveryClient>();

// web API client configuration 
services.AddSingleton<PathApiConfig>(new PathApiConfig(Configuration.GetSection("PathApi")));
services.AddTransient<PathApiClient, PathApiClient>();

// web API server configuration
services.AddSingleton<PathApiCache>(serviceProvider => { return new PathApiCache(
    serviceProvider.GetRequiredService<IDeliveryClient>()); });
services.AddMvc();
services.AddControllers(options => options.EnableEndpointRouting = false);

// web API UI
services.AddSwaggerGen(c =>
{
    c.SwaggerDoc("v1", new OpenApiInfo { 
        Title = "HeadlessArchitect.DeliveryApi", Version = "v1" });
});

services.AddRazorPages();

The end of the Configure() method in Startup.cs of the same solution is as follows.

app.UseMvcWithDefaultRoute();
app.UseRouting();
app.UseEndpoints(endpoints =>
{
    endpoints.MapRazorPages();
});

I also updated appsettings.json to use the same address for the Web API that I use for the website. I use HTTP rather than HTTPS, as encryption seems unnecessary and would reduce performance.

"PathApi": {
  "BaseAddress":  "http://localhost:5000"
},

Here is the entire Startup.cs after some refactoring to apply server configuration only in development environments and narrowed to fit in the blog format.

using System;
using System.Reflection;

using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using Microsoft.OpenApi.Models;

using Newtonsoft.Json;

using Deliverystack.StackContent.Models;
using Deliverystack.StackContent.DeliveryClient;
using Deliverystack.DeliveryClient;
using Deliverystack.DeliveryApi.Models;

public class Startup
{
    public Startup(IConfiguration configuration, IHostEnvironment env)
    {
        Configuration = configuration;
        IsDevelopment = env.IsDevelopment();
    }

    public IConfiguration Configuration { get; }
    public bool IsDevelopment { get; }

    public void ConfigureServices(IServiceCollection services)
    {
        JsonConvert.DefaultSettings = () =>
        {
            JsonSerializerSettings settings = new JsonSerializerSettings();

            foreach (Type blockType in ModularBlockBase.Implementations)
            {
                MethodInfo methodInfo = blockType.GetRuntimeMethod(
                    "GetJsonConverter", new Type[0]);
                object obj = Activator.CreateInstance(blockType);
                JsonConverter jsonConverter = methodInfo.Invoke(
                    obj, new object[0]) as JsonConverter;
                settings.Converters.Add(jsonConverter);
            }

            return settings;
        };

        // cms client
        ContentstackDeliveryClientOptions options = 
            new ContentstackDeliveryClientOptions(
                Configuration.GetSection("ContentstackOptions"));
        services.AddSingleton<ContentstackDeliveryClientOptions>(options);
        services.AddHttpClient<IDeliveryClient, ContentstackDeliveryClient>();

        if (IsDevelopment)
        {
            // server cache
            services.AddSingleton<PathApiCache>(serviceProvider =>
            {
                return new PathApiCache(
                    serviceProvider.GetRequiredService<IDeliveryClient>());
            });
        }

        // web API client configuration 
        services.AddSingleton<PathApiConfig>(
            new PathApiConfig(Configuration.GetSection("PathApi")));
        services.AddTransient<PathApiClient, PathApiClient>();

        if (IsDevelopment)
        {
            // web API server configuration
            services.AddMvc();
            services.AddControllers(
                options => options.EnableEndpointRouting = false);

            // web API UI - //TODO: does not work? /swagger/index.html
            services.AddSwaggerGen(c =>
            {
                c.SwaggerDoc("v1",
                    new OpenApiInfo
                    {
                        Title = "HeadlessArchitect.DeliveryApi",
                        Version = "v1"
                    });
            });
        }

        services.AddRazorPages();
    }

    public void Configure(IApplicationBuilder app)
    {
        if (IsDevelopment)
        {
            app.UseDeveloperExceptionPage();
        }
        else
        {
            app.UseExceptionHandler("/Error");
            // TODO: https://aka.ms/aspnetcore-hsts
            app.UseHsts();
        }

        app.UseHttpsRedirection();
        app.UseStaticFiles();

        if (IsDevelopment)
        {
            app.UseMvcWithDefaultRoute();
        }

        app.UseRouting();
        app.UseAuthorization();
        app.UseEndpoints(endpoints =>
        {
            endpoints.MapRazorPages();
        });
    }
}

Unfortunately, I could not get swagger to work, but I can use a browser, curl, or Postman if I want to call the Web API manually.

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out /  Change )

Google photo

You are commenting using your Google account. Log Out /  Change )

Twitter picture

You are commenting using your Twitter account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

Connecting to %s

%d bloggers like this: