ASP.NET Core – Self-hosted service stub with a command line interface

When you’re integrating with a third-party API, you may want to send requests to a service stub instead of sending them to the real API. The purpose of a service stub is to simulate the third-party API by returning hardcoded responses. This is similar to how mocks are used in unit testing – it helps provide a reliable and predictable API to code and test against.

There are two main ways to implement a service stub:

  • Return responses based on values in the request (with rules in the code or a config file).
  • Return good default responses and let the user change what it should return (via a command line interface).

The second option is simpler, more explicit, and easier to maintain (since you don’t need to parse the request and have a bunch of conditions to determine the response). In this article, I’ll show how to implement this approach using a self-hosted ASP.NET Core web API. Since it’s self-hosted, it’s relatively simple to deploy and use.

1 – Configure self-hosting and start the command loop

ASP.NET Core uses Kestrel as the default web server, which means it’s self-hosted by default. You can call webBuilder.UseKestrel() if you want to make this more explicit. The command loop is a method running in another task (so it doesn’t block the web app), waiting for user input in a while loop.

To following code configures the web app to be self-hosted, enables console app support, and starts a command loop:

public static async Task Main(string[] args) { string url = "https://localhost:12345"; var commandLoopTask = Task.Run(() => CommandLoop(url)); var builder = Host.CreateDefaultBuilder(args) .ConfigureWebHostDefaults(webBuilder => { webBuilder.UseKestrel() .UseStartup<Startup>() .UseUrls(url) .ConfigureLogging(loggingBuilder => loggingBuilder.ClearProviders()); }); await Task.WhenAny(builder.RunConsoleAsync(), commandLoopTask); } private static void CommandLoop(string url) { Console.WriteLine("CommandLoop - Implement this in step 4."); while (true) { var input = Console.ReadLine(); } }
Code language: C# (cs)

When this is started, it’ll output this to the command line:

CommandLoop - Implement this in step 4.
Code language: plaintext (plaintext)

Notes:

  • builder.RunConsoleAsync() is used to enable console app support.
  • The reason to use Task.WhenAny(builder.RunConsoleAsync(), commandLoopTask) is to be able to stop the process if the command loop throws an exception.
  • loggingBuilder.ClearProvider() turns off startup logging messages.

2 – Optional – Remove the IISExpress profile from launchSettings.json

If you want to be able to run the service stub executable from Visual Studio, it’s best to update launchSettings.json so you don’t run into problems:

  • Remove the IISExpress profile, the IIS settings, and the applicationUrl property.
  • Set launchBrowser to false.

If you accidently launch this with the IISExpress profile, you’ll see the error: HTTP Error 500.30 – ANCM In-Process Start Failure. If you don’t set launchBrowser=false, when you close the console app you’ll see the error: No process is associated with this object.

If you’re using the default launchSettings.json, then remove all the highlighted lines:

{ "iisSettings": { "windowsAuthentication": false, "anonymousAuthentication": true, "iisExpress": { "applicationUrl": "http://localhost:30652/", "sslPort": 44367 } }, "profiles": { "IIS Express": { "commandName": "IISExpress", "launchBrowser": true, "environmentVariables": { "ASPNETCORE_ENVIRONMENT": "Development" } }, "ServiceStub": { "commandName": "Project", "launchBrowser": true, "environmentVariables": { "ASPNETCORE_ENVIRONMENT": "Development" }, "applicationUrl": "https://localhost:5001;http://localhost:5000" } } }
Code language: JSON / JSON with Comments (json)

And then set launchBrowser to false. In the end, launchSettings.json should look like this:

{ "profiles": { "ServiceStub": { "commandName": "Project", "launchBrowser": false, "environmentVariables": { "ASPNETCORE_ENVIRONMENT": "Development" } } } }
Code language: JSON / JSON with Comments (json)

3 – Create stubbed endpoint

Create controllers / endpoints that match the third-party API’s endpoints. Have the endpoints return good default values. To be able to change the return value from the command line interface, add a public static property (for each endpoint), and then return this property from the endpoint.

For example, let’s say you’re integrating with a third-party API that has a health status endpoint. It can return Healthy, Degraded, or Unhealthy. Here’s how to stub out this endpoint:

using Microsoft.AspNetCore.Mvc; using Microsoft.Extensions.Diagnostics.HealthChecks; using System; [ApiController] [Route("[controller]")] public class HealthStatusController : ControllerBase { public static HealthStatus Status { get; set; } = HealthStatus.Healthy; [HttpGet()] public string Get() { Console.WriteLine("Request received: GET /HealthStatus"); return Status.ToString(); } }
Code language: C# (cs)

4 – Implement the command loop

The purpose of the command loop is to allow you to change what the service stub returns. Continuing from the previous example, this allows you to change the value returned by the health status endpoint:

using ServiceStub.Controllers; using Microsoft.Extensions.Diagnostics.HealthChecks; private static void CommandLoop(string url) { Console.WriteLine($"Stubbed endpoint: GET {url}/status"); Console.WriteLine("Commands:"); Console.WriteLine("\tset-status <Healthy, Unhealthy, or Degraded> Example: set-status Healthy"); while (true) { Console.WriteLine($"Current status: {HealthStatusController.Status}"); var args = Console.ReadLine().Split(); if (args.Length < 2 || args[0] != "set-status") { Console.WriteLine("Invalid command"); continue; } if (!Enum.TryParse<HealthStatus>(args[1], ignoreCase: true, out HealthStatus status)) { Console.WriteLine("Invalid value for HealthStatus"); continue; } HealthStatusController.Status = status; } }
Code language: C# (cs)

This is a very simplified scenario that only accepts one command and a single simple value. Most likely you’ll be dealing with more complex data. In that case, you could hardcode the complex data and assign it a scenario name. Then the user could specify which scenario they want to use.

The main point is to keep this service stub app as simple as possible, so it’s easy to maintain.

5 – Run the app

You can run this app from Visual Studio (make sure you’re pointing to the project profile), or just double-click on the executable. Since this is a self-hosted web app, you don’t need to do anything special to deploy it.

Once this starts running, you’ll see the following in a console window:

Stubbed endpoint: GET https://localhost:12345/status Commands: set-status <Healthy, Unhealthy, or Degraded> Example: set-status Healthy Current status: Healthy
Code language: plaintext (plaintext)

Send a request to its endpoint (I’m using Postman):

GET https://localhost:12345/HealthStatus
Code language: plaintext (plaintext)

This returns back the following response:

Status: 200 Body: Healthy
Code language: plaintext (plaintext)

In the command line, change it to unhealthy:

set-status Unhealthy Current status: Unhealthy
Code language: plaintext (plaintext)

Send the request again:

GET https://localhost:12345/HealthStatus
Code language: plaintext (plaintext)

This time it will return:

Status: 200 Body: Unhealthy
Code language: plaintext (plaintext)

This shows how you can change the stubbed response from the command line interface.

Add support for automated tests

If you have automated tests and want to be able to change what the service stub returns programmatically, then you can add an endpoint for that.

[HttpPost("SetResponse/{status}")] public ActionResult SetResponse(HealthStatus status) { Console.WriteLine("Request received: POST /HealthStatus"); Status = status; return Ok($"Changed status to {status}"); }
Code language: C# (cs)

Send a request to this endpoint (I’m using Postman):

POST https://localhost:12345/HealthStatus/SetResponse/Unhealthy
Code language: plaintext (plaintext)

Here’s what it returns:

Status: OK Body: Changed status to Unhealthy
Code language: plaintext (plaintext)

Now send a GET request to verify the status was changed:

GET https://localhost:12345/HealthStatus
Code language: plaintext (plaintext)

This returns the following:

Status: OK Body: Unhealthy
Code language: plaintext (plaintext)

This shows that the response can be changed programmatically.

Code in GitHub

The full project source code used in this article can be found here: https://github.com/makolyte/aspdotnet-servicestub-withcli

Leave a Comment