Home .Net [Tutorial] Debugging Multi-Project .Net Core Solutions in Docker

[Tutorial] Debugging Multi-Project .Net Core Solutions in Docker

by Trent
0 comments

Dotnet Docker Compose debugging in Visual Studio lets us simultaneously set breakpoints in multiple running apps. The developer experience is super simple, but behind the scenes, Visual Studio manages a lot of docker compose dotnet complexity for us. This tutorial will show you how to debug Docker dotnet apps with Docker Compose and how Microsoft makes it possible for us.

This article is a continuation of Debugging .Net Core Applications in Docker.

One is Never Enough

While the prior tutorial was essential in covering how Visual Studio generates and consumes Docker files for local .Net Core debugging, the context was contrived. It is not often that we work with solutions that contain just a single executable application with no solution-defined DLLs or infrastructure dependencies.

This tutorial will expand on what we have already learned by building and debugging multiple applications within a single Visual Studio solution. Each project will leverage functionality from a shared DLL that is also defined within the solution and depend on a local instance of Redis for reading or writing data.

Our Base Solution Setup

If following along with another solution setup, feel free to skip to here. Otherwise let’s take a look at the structure we’ll be working with.

dotnet Docker solution

This solution contains the following projects:

  1. RedisWriter (exe) is a simple BackgroundService which writes a random integer value to a shared string key in a local Redis instance at a set interval
  2. RedisReader (exe) is a simple BackgroundService which reads from a local Redis instance at a set interval. The value is read from the shared string key that the RedisWriter writes to and reflects the updates that it performs on the console
  3. SharedGoodness(dll) is a shared project which defines:
    1. An abstraction over the Redis cache, facilitating standardised logging
    2. The shared string key that both services use to interact with Redis

Let’s take a closer look at the setup of each of these projects.

RedisWriter

The BackgroundRedisWriter is the brains of this executable:

  • It will continue to loop indefinitely until the cancellation token is signaled
  • Each iteration it write a new random number to the same Redis key, overwriting the last value that was persisted and log the value
  • It will wait for 2 seconds before writing the next value
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;
using SharedGoodness.Redis;
using SharedGoodness.SharedUtilities;

namespace RedisWriter
{
    public class BackgroundRedisWriter : BackgroundService
    {
        ILogger<BackgroundRedisWriter> _logger;
        IRedisRepository _redisRepository;

        public BackgroundRedisWriter(ILogger<BackgroundRedisWriter> logger, IRedisRepository redisRepository)
        {
            _logger = logger ?? throw new ArgumentNullException(nameof(logger));
            _redisRepository = redisRepository ?? throw new ArgumentNullException(nameof(redisRepository));
        }

        protected override async Task ExecuteAsync(CancellationToken stoppingToken)
        {
            var random = new Random();
            while (!stoppingToken.IsCancellationRequested)
            {
                _logger.LogInformation($"Executing again at {DateTime.Now.PrettifyDate()}");

                var valueToWrite = random.Next(0, 1000000);

                await _redisRepository.WriteToRedis(RedisConstants.RedisKey, valueToWrite.ToString());

                _logger.LogInformation($"Wrote {valueToWrite}");

                await Task.Delay(TimeSpan.FromSeconds(2));
            }
        }
    }
}

Program.cs is very simple:

  • It registers the BackgroundRedisWriter as a HostedService
  • Calls a shared function to register the dependencies of Redis
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using RedisWriter;
using SharedGoodness.DependencyInjection;

var host = Host.CreateDefaultBuilder(args)
    .ConfigureServices((hostContext, serviceCollection) =>
    {
        serviceCollection.AddHostedService<BackgroundRedisWriter>();
    })
    .AddRedisDependencies()
    .Build();

host.Run();

RedisReader

The BackgroundRedisReader is the brains of this executable. It shares many similarities with the writer:

  • It will wait 1 second before starting, to allow the writer a chance to set its first value
  • It will continue to loop indefinitely until the cancellation token is signaled
  • Each iteration it read from the Redis key that the writer has written to and log the value
  • It will wait for 2 seconds before reading the next value
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;
using SharedGoodness.Redis;
using SharedGoodness.SharedUtilities;

namespace RedisReader
{
    public class BackgroundRedisReader : BackgroundService
    {
        ILogger<BackgroundRedisReader> _logger;
        IRedisRepository _redisRepository;

        public BackgroundRedisReader(ILogger<BackgroundRedisReader> logger, IRedisRepository redisRepository)
        {
            _logger = logger ?? throw new ArgumentNullException(nameof(logger));
            _redisRepository = redisRepository ?? throw new ArgumentNullException(nameof(redisRepository));
        }

        protected override async Task ExecuteAsync(CancellationToken stoppingToken)
        {
            var random = new Random();

            await Task.Delay(TimeSpan.FromSeconds(1));

            while (!stoppingToken.IsCancellationRequested)
            {
                _logger.LogInformation($"Executing again at {DateTime.Now.PrettifyDate()}");

                var currentValue = await _redisRepository.ReadFromRedis(RedisConstants.RedisKey);

                _logger.LogInformation($"Fetched {currentValue}");

                await Task.Delay(TimeSpan.FromSeconds(2));
            }
        }
    }
}

Program.cs is basically identical to the writer’s:

using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using RedisReader;
using SharedGoodness.DependencyInjection;

var host = Host.CreateDefaultBuilder(args)
    .ConfigureServices((hostContext, serviceCollection) =>
    {
        serviceCollection.AddHostedService<BackgroundRedisReader>();
    })
    .AddRedisDependencies()
    .Build();

host.Run();

SharedGoodness Project

Docker dotnet shared project

The SharedGoodness project contains just that: shared goodness that is consumed by the Reader and Writer executables.

Dotnet Docker Dependency Injection

HostBuilderExtensions.cs contains the DI function that is used in the Reader and Writer projects. It:

  • Binds the shared IRedisRepository to its concrete implementation
  • Registers the Microsoft Redis abstraction, passing it some hard coded configuration
    • It is important to call out that if you were debugging with Redis locally on your Windows computer, you’d specify localhost:6379 however, because we are working with container to container networking, you must specify the name of the container that is running Redis. In this case, the container itself is called redis. This container name is specified in the Compose file below
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using SharedGoodness.Redis;

namespace SharedGoodness.DependencyInjection
{
    public static class HostBuilderExtensions
    {
        public static IHostBuilder AddRedisDependencies(this IHostBuilder builder)
        {
            builder.ConfigureServices((hostContext, serviceCollection) =>
            {
                // Define the distributed cache
                serviceCollection.AddStackExchangeRedisCache(options =>
                {
                    options.Configuration = "redis:6379";
                    options.InstanceName = "RedisInstance";
                });

                // Bind our Redis abstraction
                serviceCollection.AddTransient<IRedisRepository, RedisRepository>();
            });

            return builder;
        }
    }
}

Redis

The IRedisRepository is an abstraction over Redis, which standardises logging for its consumers. It exposes one method for writing to the cache and another to reading from it.

namespace SharedGoodness.Redis
{
    public interface IRedisRepository
    {
        Task WriteToRedis(string key, string value);
        Task<string> ReadFromRedis(string key);
    }
}

The concrete RedisRepository implements the above interface, by consuming the Microsoft Redis abstraction IDistributedCache

using Microsoft.Extensions.Caching.Distributed;
using Microsoft.Extensions.Logging;

namespace SharedGoodness.Redis
{
    public class RedisRepository : IRedisRepository
    {
        ILogger<RedisRepository> _logger;
        IDistributedCache _cache;

        public RedisRepository(ILogger<RedisRepository> logger, IDistributedCache cache)
        {
            _logger = logger ?? throw new ArgumentNullException(nameof(logger));
            _cache = cache ?? throw new ArgumentNullException(nameof(cache));
        }

        public async Task<string> ReadFromRedis(string key)
        {
            var value = await _cache.GetStringAsync(key);
            _logger.LogInformation($"Key {key} has value {value} at time {DateTime.Now:yyyyMMdd hh:mm:ss}");
            return value;
        }

        public async Task WriteToRedis(string key, string value)
        {
            _logger.LogInformation($"Giving key {key} value {value} at time {DateTime.Now:yyyyMMdd hh:mm:ss}");
            await _cache.SetStringAsync(key, value);
        }
    }
}

SharedUtilities

StringFormatting.cs contains a simple function to standardise how dates are written to the console.

namespace SharedGoodness.SharedUtilities
{
    public static class StringFormatting
    {
        public static string PrettifyDate(this DateTime date)
        {
            return $"{date:yyyyMMdd hh:mm:ss}";
        }
    }
}

Adding Docker Support for Multiple Projects

Now that we’ve covered the landscape of our applications, let’s take a quick look at the “moving parts” of our solution that we might want to orchestrate with Docker:

  • The Redis Writer .exe
  • The Redis Reader .exe
  • The local instance of Redis itself

Let’s get started with Visual Studio’s Docker orchestration and extend the auto-generated foundation to support Redis.

Enabling Docker compose dotnet support

For each executable project in the solution:

  • Right click it
  • Select Add
  • Select Container Orchestration Support…
  • Choose Docker Compose
  • Choose Linux
  • Click OK

This is not required for Class Libraries (DLLs) as they are not executable.

dotnet docker compose

Once finished, you’ll have a Dockerfile generated for each project, which will be visible within the solution. The contents of this file will look very similar to that which we covered in the previous article.

Each Dockerfile will capture the current dependencies, by explicitly copying them during the dotnet restore stage. We’ll cover a more extensible alternative to this in another article.

Alongside your existing projects you’ll also have a docker-compose project, with a docker-compose.yml and a .dockerignore file

Finally, in order to support running Redis alongside our applications (a critical piece of infrastructure without which our applications would not run) we need to extend the docker-compose.yml file, adding in a new service called redis. We’ll point it to the redis image in Docker Hub which will implicitly pull the latest version:

version: '3.4'

services:
  redisreader:
    image: ${DOCKER_REGISTRY-}redisreader
    build:
      context: .
      dockerfile: RedisReader/Dockerfile

  rediswriter:
    image: ${DOCKER_REGISTRY-}rediswriter
    build:
      context: .
      dockerfile: RedisWriter/Dockerfile

  redis:
    image: redis

Now, ensure that the docker-compose project is set as the default startup project by:

  • Right clicking the docker-compose project
  • Selecting Set as startup project

At this point rebuild your solution, which will cause Visual Studio to do some magic.

A Similar Story of Abstracted Simplicity

Doesn’t our Dockerfile look nice and neat? Don’t let this simplicity deceive you, though. Much like the process for debugging a single .Net Core application in Docker, Visual Studio is wonderfully hiding a large amount of complexity behind the scenes.

Let’s take a closer look at the output of a re-build.

Rebuild started...
1>------ Rebuild All started: Project: SharedGoodness, Configuration: Debug Any CPU ------
Restored C:\_dev\CodeSloth\multi project docker debugging\DebuggingMultipleProjectsInDocker\SharedGoodness\SharedGoodness.csproj (in 28 ms).
Restored C:\_dev\CodeSloth\multi project docker debugging\DebuggingMultipleProjectsInDocker\RedisReader\RedisReader.csproj (in 32 ms).
Restored C:\_dev\CodeSloth\multi project docker debugging\DebuggingMultipleProjectsInDocker\RedisWriter\RedisWriter.csproj (in 32 ms).
1>SharedGoodness -> C:\_dev\CodeSloth\multi project docker debugging\DebuggingMultipleProjectsInDocker\SharedGoodness\bin\Debug\net6.0\SharedGoodness.dll
2>------ Rebuild All started: Project: RedisWriter, Configuration: Debug Any CPU ------
3>------ Rebuild All started: Project: RedisReader, Configuration: Debug Any CPU ------
2>RedisWriter -> C:\_dev\CodeSloth\multi project docker debugging\DebuggingMultipleProjectsInDocker\RedisWriter\bin\Debug\net6.0\RedisWriter.dll
3>RedisReader -> C:\_dev\CodeSloth\multi project docker debugging\DebuggingMultipleProjectsInDocker\RedisReader\bin\Debug\net6.0\RedisReader.dll
4>------ Rebuild All started: Project: docker-compose, Configuration: Debug Any CPU ------
4>docker-compose  -f "C:\_dev\CodeSloth\multi project docker debugging\DebuggingMultipleProjectsInDocker\docker-compose.yml" -f "C:\_dev\CodeSloth\multi project docker debugging\DebuggingMultipleProjectsInDocker\docker-compose.override.yml" -p dockercompose8155732768790040920 --ansi never --profile "*" config
4>name: dockercompose8155732768790040920
4>services:
4>  redis:
4>    image: redis
4>    networks:
4>      default: null
4>  redisreader:
4>    build:
4>      context: C:\_dev\CodeSloth\multi project docker debugging\DebuggingMultipleProjectsInDocker
4>      dockerfile: RedisReader/Dockerfile
4>    image: redisreader
4>    networks:
4>      default: null
4>  rediswriter:
4>    build:
4>      context: C:\_dev\CodeSloth\multi project docker debugging\DebuggingMultipleProjectsInDocker
4>      dockerfile: RedisWriter/Dockerfile
4>    image: rediswriter
4>    networks:
4>      default: null
4>networks:
4>  default:
4>    name: dockercompose8155732768790040920_default
4>docker-compose  -f "C:\_dev\CodeSloth\multi project docker debugging\DebuggingMultipleProjectsInDocker\docker-compose.yml" -f "C:\_dev\CodeSloth\multi project docker debugging\DebuggingMultipleProjectsInDocker\docker-compose.override.yml" -f "C:\_dev\CodeSloth\multi project docker debugging\DebuggingMultipleProjectsInDocker\obj\Docker\docker-compose.vs.release.partial.g.yml" -p dockercompose8155732768790040920 --ansi never kill
4>no container to kill
4>docker-compose  -f "C:\_dev\CodeSloth\multi project docker debugging\DebuggingMultipleProjectsInDocker\docker-compose.yml" -f "C:\_dev\CodeSloth\multi project docker debugging\DebuggingMultipleProjectsInDocker\docker-compose.override.yml" -f "C:\_dev\CodeSloth\multi project docker debugging\DebuggingMultipleProjectsInDocker\obj\Docker\docker-compose.vs.release.partial.g.yml" -p dockercompose8155732768790040920 --ansi never down --rmi local --remove-orphans
4>Warning: No resource found to remove for project "dockercompose8155732768790040920".
4>docker-compose  -f "C:\_dev\CodeSloth\multi project docker debugging\DebuggingMultipleProjectsInDocker\docker-compose.yml" -f "C:\_dev\CodeSloth\multi project docker debugging\DebuggingMultipleProjectsInDocker\docker-compose.override.yml" -f "C:\_dev\CodeSloth\multi project docker debugging\DebuggingMultipleProjectsInDocker\obj\Docker\docker-compose.vs.debug.partial.g.yml" -p dockercompose8155732768790040920 --ansi never kill
4>no container to kill
4>docker-compose  -f "C:\_dev\CodeSloth\multi project docker debugging\DebuggingMultipleProjectsInDocker\docker-compose.yml" -f "C:\_dev\CodeSloth\multi project docker debugging\DebuggingMultipleProjectsInDocker\docker-compose.override.yml" -f "C:\_dev\CodeSloth\multi project docker debugging\DebuggingMultipleProjectsInDocker\obj\Docker\docker-compose.vs.debug.partial.g.yml" -p dockercompose8155732768790040920 --ansi never down --rmi local --remove-orphans
4>Warning: No resource found to remove for project "dockercompose8155732768790040920".
4>docker images --filter dangling=true --format {{.ID}}
========== Rebuild All: 4 succeeded, 0 failed, 0 skipped ==========

Here we go again. In summary we see:

  1. Packages being restored
  2. Projects being rebuilt
  3. A series of docker-compose commands being run, geared at validating the compose file and tearing down any existing containers that may be running

The first of these commands has been decomposed over multiple lines below:

docker-compose  
-f "C:\_dev\CodeSloth\multi project docker debugging\DebuggingMultipleProjectsInDocker\docker-compose.yml" 
-f "C:\_dev\CodeSloth\multi project docker debugging\DebuggingMultipleProjectsInDocker\docker-compose.override.yml" 
-p dockercompose8155732768790040920 
--ansi never 
--profile "*" 
config

Let’s break this down:

  • docker-compose is being invoked using the old compose CLI syntax. As discussed here the new version of compose allows you to specify the command without the hyphen
  • -f specifies alternate compose files by their name:
    • The compose file itself
    • The override file
  • -p specifies the project name. This name appears as the parent name of the services displayed in the Docker Dashboard
  • --ansi never suppresses logging of ANSI control characters
  • --profile "*" represents a compose profile. At time of writing it isn’t clear why this is specified, given that the default compose launcher runs all services, and explicit service names are specified for conditional launching of services
  • config represents the actual command, which validates and outputs the compose file represented by overrides

After this step, the existing containers are torn down through a series of kill and down commands

docker-compose  
-f "C:\_dev\CodeSloth\multi project docker debugging\DebuggingMultipleProjectsInDocker\docker-compose.yml" 
-f "C:\_dev\CodeSloth\multi project docker debugging\DebuggingMultipleProjectsInDocker\docker-compose.override.yml" 
-f "C:\_dev\CodeSloth\multi project docker debugging\DebuggingMultipleProjectsInDocker\obj\Docker\docker-compose.vs.release.partial.g.yml" 
-p dockercompose8155732768790040920 
--ansi never 
kill

Let’s break this command down:

  • Similarly to the prior command, we see files provided, a project name and specification of ANSI control character suppression
  • The kill command itself, to kill any existing containers of the project

We’ll touch on the additional partial file below. Moving onto the next command:

docker-compose  
-f "C:\_dev\CodeSloth\multi project docker debugging\DebuggingMultipleProjectsInDocker\docker-compose.yml" 
-f "C:\_dev\CodeSloth\multi project docker debugging\DebuggingMultipleProjectsInDocker\docker-compose.override.yml" 
-f "C:\_dev\CodeSloth\multi project docker debugging\DebuggingMultipleProjectsInDocker\obj\Docker\docker-compose.vs.release.partial.g.yml" 
-p dockercompose8155732768790040920 
--ansi never 
down 
--rmi local 
--remove-orphans

Let’s break down this command:

  • The parameters of this command match the prior
  • This action is down which will stop and remove the containers and networks of the compose file
  • --remove-orphans will remove containers for services now defined in the Dockerfile

The exact same steps are then repeated for debug builds:

docker-compose  
-f "C:\_dev\CodeSloth\multi project docker debugging\DebuggingMultipleProjectsInDocker\docker-compose.yml" 
-f "C:\_dev\CodeSloth\multi project docker debugging\DebuggingMultipleProjectsInDocker\docker-compose.override.yml" 
-f "C:\_dev\CodeSloth\multi project docker debugging\DebuggingMultipleProjectsInDocker\obj\Docker\docker-compose.vs.debug.partial.g.yml" 
-p dockercompose8155732768790040920 
--ansi never 
kill

docker-compose  
-f "C:\_dev\CodeSloth\multi project docker debugging\DebuggingMultipleProjectsInDocker\docker-compose.yml" 
-f "C:\_dev\CodeSloth\multi project docker debugging\DebuggingMultipleProjectsInDocker\docker-compose.override.yml" 
-f "C:\_dev\CodeSloth\multi project docker debugging\DebuggingMultipleProjectsInDocker\obj\Docker\docker-compose.vs.debug.partial.g.yml" 
-p dockercompose8155732768790040920 
--ansi never 
down 
--rmi local 
--remove-orphans

This is our first time seeing docker-compose.vs.release.partial.g.yml and docker-compose.vs.debug.partial.g.yml. Time for a side-quest!

Sloth Side Quest

Let’s look into docker-compose.debug.partial.g.yml specifically, as we are understanding how to debug our multi project appications. The partial release file is a subset of this:

version: '3.4'

services:
  redisreader:
    image: redisreader:dev
    container_name: RedisReader
    build:
      target: base
      labels:
        com.microsoft.created-by: "visual-studio"
        com.microsoft.visual-studio.project-name: "RedisReader"
    environment:
      - NUGET_FALLBACK_PACKAGES=/root/.nuget/fallbackpackages
    volumes:
      - C:\_dev\CodeSloth\multi project docker debugging\DebuggingMultipleProjectsInDocker\RedisReader:/app
      - C:\_dev\CodeSloth\multi project docker debugging\DebuggingMultipleProjectsInDocker:/src
      - C:\Users\TJ\vsdbg\vs2017u5:/remote_debugger:rw
      - C:\Users\TJ\.nuget\packages\:/root/.nuget/packages:ro
      - C:\Program Files\dotnet\sdk\NuGetFallbackFolder:/root/.nuget/fallbackpackages:ro
    entrypoint: tail -f /dev/null
    labels:
      com.microsoft.visualstudio.debuggee.program: "dotnet"
      com.microsoft.visualstudio.debuggee.arguments: " --additionalProbingPath /root/.nuget/packages --additionalProbingPath /root/.nuget/fallbackpackages  \"/app/bin/Debug/net6.0/RedisReader.dll\""
      com.microsoft.visualstudio.debuggee.workingdirectory: "/app"
      com.microsoft.visualstudio.debuggee.killprogram: "/bin/sh -c \"if PID=$$(pidof dotnet); then kill $$PID; fi\""
    tty: true
  rediswriter:
    image: rediswriter:dev
    container_name: RedisWriter
    build:
      target: base
      labels:
        com.microsoft.created-by: "visual-studio"
        com.microsoft.visual-studio.project-name: "RedisWriter"
    environment:
      - NUGET_FALLBACK_PACKAGES=/root/.nuget/fallbackpackages
    volumes:
      - C:\_dev\CodeSloth\multi project docker debugging\DebuggingMultipleProjectsInDocker\RedisWriter:/app
      - C:\_dev\CodeSloth\multi project docker debugging\DebuggingMultipleProjectsInDocker:/src
      - C:\Users\TJ\vsdbg\vs2017u5:/remote_debugger:rw
      - C:\Users\TJ\.nuget\packages\:/root/.nuget/packages:ro
      - C:\Program Files\dotnet\sdk\NuGetFallbackFolder:/root/.nuget/fallbackpackages:ro
    entrypoint: tail -f /dev/null
    labels:
      com.microsoft.visualstudio.debuggee.program: "dotnet"
      com.microsoft.visualstudio.debuggee.arguments: " --additionalProbingPath /root/.nuget/packages --additionalProbingPath /root/.nuget/fallbackpackages  \"/app/bin/Debug/net6.0/RedisWriter.dll\""
      com.microsoft.visualstudio.debuggee.workingdirectory: "/app"
      com.microsoft.visualstudio.debuggee.killprogram: "/bin/sh -c \"if PID=$$(pidof dotnet); then kill $$PID; fi\""
    tty: true

The contents of each service in this file are reminiscent of the Dockerfile in our last article. For our two exectuable applications we see:

  • A dev image name being built
  • The compose file targeting only the base stage of the respective Docker file
  • Environment variable being set for NuGet packages that had previously been restored to the local disk
  • Volumes being mounted for:
    • The compiled app
    • The apps source code (for remote debugging)
    • The remote debugging tools
    • The primary NuGet packages
    • The fallback NuGet packages
  • An entry point being set to the tail with the null device to keep the container running
  • A teletype being allocated

Running the Thing!

Now we understand that compose is behaving in a very similar way to debugging a single Dockerfile, we can run our newly created compose file.

docker-compose  
-f "C:\_dev\CodeSloth\multi project docker debugging\DebuggingMultipleProjectsInDocker\docker-compose.yml" 
-f "C:\_dev\CodeSloth\multi project docker debugging\DebuggingMultipleProjectsInDocker\docker-compose.override.yml" 
-f "C:\_dev\CodeSloth\multi project docker debugging\DebuggingMultipleProjectsInDocker\obj\Docker\docker-compose.vs.debug.g.yml" 
-p dockercompose8155732768790040920 
--ansi never 
up -d --build --remove-orphans

If you swap to the build output window you’ll see that Visual Studio, on running our application(s), issues a docker compose up command. Let’s take a quick look at the additional up specific parameters of this command:

  • -d run the containers in detached mode
  • --build builds images before running
  • --remove-orphans once again removes orphaned containers that are no longer specified in the compose file

Note: the contents of the debug.g.yml and partial file are the same.

Now, for the moment(s) of truth!

Firstly, if we open the Docker Dashboard, we’ll see our new composed services running happily:

dotnet docker containers running

Here we can see each of our three required services running under the parent project name:

  • Both of the applications that we have authored
  • The Redis cluster that they depend on

Now, let’s take a look at the apps themselves! But wait… no console windows have popped up?!

Viewing Application Logs for Running Containers in Visual Studio

The first way is to view the logs of a single running container. In order to achieve this, open the Containers window: View -> Other windows -> Containers. Alternatively you can use the CTRL + K, CTRL + O key chord.

dotnet docker containers running in visual studio

In the Containers window you’ll be able to select the running contain that you’d like to inspect, followed by clicking the Logs tab on the right hand window. Each message that is written to the console will appear here.

This strategy is great when laser focusing on debugging a specific application. However, sometimes it is helpful to view all of our logs alongside each other for context. In this case, it would be great to visually confirm that the RedisWriter is writing the same value that the RedisReader is actually fetching.

In order to achieve this, open the Output window: View -> Output, or use CTRL + Alt + O.

multi container dotnet docker logs

In the Output window, choose show output from debug. In this screenshot we can see a green dot next to each of our applications logs. This confirms to use that the RedisReader is fetching the same value set by the RedisWriter!

At this point you are able to set breakpoints in your code and continue enjoying your local Windows development experience through Visual Studio. However, you’re now doing it from inside multiple Docker containers!

Now, stop debugging your solution. If you observe the Docker Dashboard at this point, the compose file and its services will still be running. Return to Visual Studio and this time rebuild the solution. Once this has completed you’ll notice that Docker Dashboard no longer has the compose file and services,

This demonstrates that Visual Studio is creating long running container that do not terminate when our application ends, and that the explicit “tear down” steps of the rebuild process are required to clean these resources up.

Infrastructure Management and Conditional Service Debugging

The case that we have covered above is great when we want to run all of our services at once. However, there may be times when we only want to run a single service in our solution. This is where Visual Studio continues to shine as our Integrated Development Environment. Through the configuration of different launch profiles, we can tell Visual Studio which parts of our Compose file we would actually like to orchestrate!

Click the caret next to the Docker Compose play button and select Manage Docker Compose Launch Settings, as below:

dotnet docker launch profiles for compose

This will load the default launch profile:

dotnet docker launch profiles for compose default profile

As we can see here, the default Docker Compose launch profile runs both of our applications with the debugger attached, and runs Redis without the debugger.

Click the New button, and give the profile a name.

dotnet docker launch profiles redis only

The example of a Redis Only profile will only run Redis. This will will allow us to run our infrastructure separate from the app and keep the Redis container running across multiple debug sessions of our apps.

A second profile is required to launch just the reader and writer applications, which can be created after clicking Save.

Each new set of launch settings that you create will then appear in the play button dropdown.

dotnet docker launch profiles list

With this setup we would first select Redis Only and run it. Then we would select Apps Only and start only our apps, in debug mode.

When running our apps this time, the Docker Compose command looks slightly different:

docker-compose  
-f "C:\_dev\CodeSloth\multi project docker debugging\DebuggingMultipleProjectsInDocker\docker-compose.yml" 
-f "C:\_dev\CodeSloth\multi project docker debugging\DebuggingMultipleProjectsInDocker\docker-compose.override.yml" 
-f "C:\_dev\CodeSloth\multi project docker debugging\DebuggingMultipleProjectsInDocker\obj\Docker\docker-compose.vs.debug.g.yml" 
-p dockercompose8155732768790040920 
--ansi never 
up 
-d 
--no-build  
redisreader rediswriter

When orchestrating explicit parts of the Docker file you’ll notice the specific service names specified in the build command.

Sloth Summary

There we have it! Debugging multiple projects in Docker with Visual Studio, while also ensuring we have supporting infrastructure running!

In summary:

  • Right click your projects and select the option to add orchestration support
  • Define launch settings to ensure that you are able to spin up only the services that you require
  • Ensure that you remember that we are working with container to container networking and to refer to the respective container’s name instead of localhost

You may also like