Link Containerized DotNet Core Libraries During Runtime

A look at how to Package DotNet Core Libraries in a Docker Base Image to Link to Dynamically

Sami Islam
7 min readOct 17, 2020
Photo by S'well on Unsplash

In my quest to learn more about Docker and containerization I asked myself whether it was possible to package a dotnet core class library in a container and use it from projects in other containers.

This article takes a closer look into how that can be done by using:

  • A dotnet core class library that I want to containerize - mylib
  • A dotnet core console application that includes mylib as a reference assembly - myconsole
  • A dotnet core AspNet Api that includes mylib as a reference assembly - myapi

I use Visual Studio Code, Docker Desktop Community Edition 2.4.0.0 on Mac OS X 10.15.

MyLib - Jill

Mylib provides a class called ClassLib:

ClassLib in mylib.dll
ClassLib in mylib.dll

After building the library it is published in a lib folder under the root folder where my solution resides. To keep things simple, I have a single root folder containing mylib, myconsole and myapi. In reality, these could be separate solutions.

Build and publish mylib
Build and publish mylib

To package the library in a base image the following Dockerfile is used:

Dockerfile for the library
Dockerfile for the library

To create a base docker image that does not depend on any other docker images it starts FROM scratch. It then creates an /app directory in the image and sets it as the working directory. The COPY /lib . command copies all the files from the /lib folder where the mylib library was published into the working directory /app in the image.

The Docker Image build for the s/lib image is as follows:

Docker build for the library
Docker build for the library

The output of the mylib project is all that this image contains. It has no dependencies on any other images.

MyConsole - Jack

The first project to use mylib is myconsole:

MyConsole using mylib
MyConsole using mylib

It uses mylib.dll as a reference assembly from under the lib folder:

MyConsole project file referencing mylib
MyConsole project file referencing mylib

The HintPath tells it where to look for the mylib.dll during runtime. This is set to look for the reference assembly in the same folder where myconsole runs from. I have explicitly commented out the <Private>false</Private> tag since we need this to create the myconsole.deps.json dependency file properly. Setting <Private>true</Private> creates a dependency file without the mylib as a dependency which leads to a runtime failure even when the mylib.dll is present in the same folder and myconsole:

Deploying and starting myconsole with incorrect .deps.json file
Deploying and starting myconsole with incorrect .deps.json file

Another way to handle the issue would be to manually modify the myconsole.deps.json file to include the dependency which I choose not to in order to keep this article simpler.

Since the whole point of this article is to re-use a reference library from a docker container, during the creation of the console image we are going to explicitly only copy the myconsole* items into the image and ignore the mylib* items.

The myconsole items are published in the console folder:

Build and publish myconsole
Build and publish myconsole

The Docker Image file to create an s/console image is as follows:

Docker file for myconsole
Docker file for myconsole

Here we start by referencing the s/lib image as library. Since console is a dotnet core application, it requires the dotnet/core/runtime to run - which is the basis of this image. All the files published under /app/ folder of the base s/lib image are then copied into the working directly /app of this image. Only the files starting with myconsole* published by building myconsole are then copied into the /app directory. This allows myconsole to pick up the mylib.dll reference assembly (copied in the previous step) during runtime. To show that we are only copying the files starting with myconsole*, we copy the same batch under a /app/consoleonly/ directory. We are going to check this folder later in the article.

Let’s build the s/console image:

Docker build for the console
Docker build for the console

and run it:

Start a console container
Start a console container

My, my!! Jill and Jack are in love 💏 .

Let’s start the container again and go and have a peek inside:

Ensure that the correct files are copied for the console
Ensure that the correct files are copied for the console

and as expected, only the items starting with myconsole* has indeed been copied into the consoleonly directory. This indicates that the mylib* items in the app directory are from the base s/lib library-only image.

MyApi - Jane

The second project to use mylib is myapi:

Myapi using mylib
Myapi using mylib

It also uses mylib.dll as a reference assembly in the exact same way as myconsole. The myapi items are published in the api folder:

Build and publish myapi
Build and publish myapi

The Docker Image file to create s/api is almost exactly like the one to create s/console differing only in the inclusion of the dotnet/core/aspnet image:

Docker file for myapi
Docker file for myapi

Building and running the container:

Build api image and run container
Build api image and run container

The web api is published on port 5000 and navigating to it shows:

Output of running api
Output of running api

Well, well… Jill and Jane are in love 👩‍❤️‍💋‍👩 . Wait … What happened to Jack❗

What happened to Jack?

Let’s see what we did so far.

  1. We have created a dotnet core library and a base docker image that contains the library
  2. We have built two different kind of dotnet projects - console and webapi - that requires the dotnet library during the build process
  3. We have created a docker images for each of the dotnet projects that use the library but have explicitly made sure that the library being used is not the ones that these projects have built against. The library being used is the one that comes from the base docker image in step 1.

Step 3 feels very awkward. What is the point of using the library from the base image instead of keeping the dependency for each project during the image creation? Well, let’s see what the point really is.

Here is the list of Docker images that we have create so far:

List of first set of images
List of first set of images
  1. s/lib:latest — contains the class library
  2. s/console:latest — contains the class library + dotnet/core/runtime
  3. s/api:latest — contains the class library + dotnet/core/aspnet

Let us now go ahead and change the code in the library being shared and create a new version of the image s/lib:2.0:

Modify mylib, rebuild and recreate image 2.0
Modify mylib, rebuild and recreate image 2.0

Now, without rebuilding our console or api projects, let’s just create a new version of their corresponding images using s/lib:2.0 as the base image:

List of all images old and new
List of all images old and new

If we now run the console 2.0 container:

Run console 2.0
Run console 2.0

Well… Well… It looks like Jack has a new girlfriend named Amy ❤️ .

And running the api 2.0 container we see:

Run api 2.0
Run api 2.0

Alas! Our story does not end well for Jack and Jill 💔 . Will they find each other again?

Conclusion

I had a great time learning how to package a library in a docker image so that it can be used by other images. Of course, we have to keep in mind that only trusted docker images should be used — especially in this case since we are dynamically binding to the assembly. Moreover, I haven’t used any versioning in this article but in production, it is extremely important.

I hope you will have as much fun as I did learning about containers 😃. As always you will find all the code for this article in my GitHub Repo.

--

--