Containerizing N-Tier MVC .Net Framework Application with Docker


Microservice Architecture and Containerization using docker are the latest buzzword in the software industry. But, Many people including me in the software industry developing big monolithic enterprise applications using .Net Framework for many years have very limited scope of applying these concepts into existing applications. Because, its not easy to break enterprise monolithic application into micro service architecture without redesigning the application. Also, .Net Core framework would be the de facto choice for micro service architecture because it supports cross platform so it can be hosted in linux container or windows container. As of today, Windows Docker container do not support GUI application such as winforms, wpf etc.. However, we can still consider modernizing .Net Framework monolithic application by packaging into docker image for automated end to end testing or security testing.

In this article, i will explain how to containerize a simple N-Tier CRUD MVC application using docker. We will create a separate app server and database server container images and deploy and run the simple N-Tier MVC application. If you are new to docker, i would recommend first to read Sahil Malik article about docker for developers and watch the awesome pluralsight course of Modernizing .NET Apps with Docker by Elton Stoneman.

How it works

I took the N-Tier Application on ASP.NET MVC - A Complete Solution from MSDN Code web site that runs on full .Net Framework. This sample application does the basic CRUD operation for maintaining Employees data using Model-View-Controller Patter with Repository Pattern and N-Tiers Deployment Architecture Pattern. We will modernize this application by containerizing into docker image. This application will have separate database and application server instances. The database server will be based Docker version of SQL Server Developer Edition and application server is based on microsoft/aspnet:latest docker image. Every time, when new container instance is created, new database will be created and all the data that were created in prior container instance will be destroyed when container instance is stopped which perfectly works for automated testing scenarios.

Steps

Now, i am going to explain about docker-compose file to orchestrate how to build and deploy .Net Framework application into docker container. Visual Studio provides the default container orchestration support for .Net Web Projects. You can add it by right click on the web project and select Container Orchestration Support as below.

However, i am not using the built-in container orchestration support feature for creating docker-compose file. I created it manually from scratch using visual studio code editor.

docker-compose

In the root folder of the project, create a new file called docker-compose.yml with the below code. I used visual studio code as my editor and it has great support for yaml file with intellisense.

version: '3'
services:
docker_ntierdemo_app:
image: jeevasubburaj/dockerntierdemo_app:v1
build:
context: ./NtierMvc/bin/Release/Publish
depends_on:
- docker_ntierdemo_db
hostname: ${APP_UUID}
container_name: ${APP_UUID}
networks:
docker_ntierdemo-net:
ipv4_address: 172.16.238.20
docker_ntierdemo_db:
image: jeevasubburaj/dockerntierdemo_db:v1
build:
context: ./Database
ports:
- "14333:1433"
env_file: db_dev.env
hostname: ${DB_UUID}
container_name: ${DB_UUID}
networks:
docker_ntierdemo-net:
ipv4_address: 172.16.238.21
networks:
docker_ntierdemo-net:
ipam:
driver: default
config:
- subnet: 172.16.238.0/24

Lets talk about each line in the above docker-compose file to understand what is going on. Before we take a deep dive into that, i would recommend you to read the official docker-compose guide from docker website.

version: '3'

This is the version of the docker-compose format that we use in this example.

services:
 docker_ntierdemo_app: 
 ....
 docker_ntierdemo_db:

Services definition contains configuration applied to each container started for that service. In our example, we will be creating application and db server services.

Before we go into services in detail, we will discuss about how to create environment variables in docker-compose using .env file and custom env file. we are going to create some custom environment variable such as hostname, sql server login password etc to access it from docker-compose file.

by default, you can set your environment variables using a .env file which docker-compose automatically looks for. if you want to create a custom environment file, you can also do that and reference that file inside the docker-compose file. In this example, i used both. In addition to that, you can also create the environment variable inside the docker-compose file without creating environment file.

.env file

APP_UUID=Demo_App_Server
DB_UUID=Demo_Db_Server

i have created the custom host name for both app and db server and i will be using these variables inside the docker-compose file. The same value is also configured in web.config so that app server will be connecting to db server.

db_dev.env

SA_PASSWORD=P@ssw0rd
ACCEPT_EULA=Y

In this custom environment file, i have defined the default sa account password and accept EULA flag for the sql server to start inside the container.

Database Server Services

image: jeevasubburaj/dockerntierdemo_db:v1   
    build:      
      context: ./Database 
    ports: 
      - "14333:1433"
    env_file: db_dev.env   
    hostname: ${DB_UUID}
    container_name: ${DB_UUID}

In the first line, i defined the name of the image with version number.

Before we jump into build section, let us look at other references in that section. I mapped the default sql port 1433 from container into 14333 on host port using ports configuration so that you can connect the database from your host server with servername as localhost,14333. This step is optional only.

we have also defined the hostname and container_name using environment variable. This will be needed to configure the database server name in our web.config, before we deploy the application in to the container.

Build configurations are applied at docker build time. The context configuration defines the path to a directory containing the DockerFile. I created a new folder Database and placed the DockerFile and Database_Setup.sql file and pointing the context to that folder. When we build the docker image using docker-compose, it runs the DockerFile inside the Database Folder and build the database image. By Default, it will look for the file with name of DockerFile. if you want to create a custom DockerFile Name, you have to add dockerfile configuration to specify the custom docker file name.

DockerFile

FROM microsoft/mssql-server-windows-developer:latest
COPY ./Database_Setup.sql .
RUN sqlcmd -i Database_Setup.sql

This dockerfile gets the base image from sql server developer edition and copy the Database_Setup.sql into the image and execute the sql using sqlcmd command which will create the database and the tables defined in the sql file.

Database_Setup.sql

USE [master] 
GO

CREATE DATABASE [NtierMvcDB]
GO

USE [NtierMvcDB]
GO

CREATE SCHEMA [HR]
GO

CREATE TABLE [HR].[Employees]
(
[Id] [int] NOT NULL,
[Name] [nvarchar](50) NOT NULL,
[Age] [int] NOT NULL,
[HiringDate] [datetime] NULL,
[GrossSalary] [decimal](10, 2) NOT NULL,
[ModifiedDate] [datetime] NOT NULL,
CONSTRAINT [PK_Employees] PRIMARY KEY CLUSTERED ([Id] ASC) ON [PRIMARY]
) ON [PRIMARY]
GO

ALTER TABLE [HR].[Employees] ADD CONSTRAINT [DF_Employees_ModifiedDate] DEFAULT (GETDATE()) FOR [ModifiedDate]
GO

Networks

networks:
docker_ntierdemo-net:
ipam:
driver: default
config:
- subnet: 172.16.238.0/24

In the networks configuration section, we can define any custom network properties that are needed. if we don’t define any networks configuration, docker will create a default network with bridge mode enabled. In the above example, i created custom network with default subnet range so that i can configure the custom ip address for my app and db server. This will be useful for scenarios like when you have some enterprise application with licensing tool installed based on certain device parameter such as mac address, ip address so that you will have the container instances created with same ip address, mac address every time it created with out installing the license for every instance.

App Server services

docker_ntierdemo_app:    
image: jeevasubburaj/dockerntierdemo_app:v1
build:
context: ./NtierMvc/bin/Release/Publish
depends_on:
- docker_ntierdemo_db
hostname: ${APP_UUID}
container_name: ${APP_UUID}
networks:
docker_ntierdemo-net:
ipv4_address: 172.16.238.20

In the App Server Services Configuration, we define the name of the image and in the build context, configure the published folder output path. We will create a publish profile from visual studio to deploy the build output in the above mentioned folder along with the DockerFile. The DockerFile must be added in the project and set the build action as content so that it will also get deployed to publish folder.

dockerfile

FROM microsoft/aspnet:latest
COPY . /inetpub/wwwroot/

In this dockerfile, we are taking the base image of microsoft aspnet docker image and copy the build output directly into wwwroot folder inside the container image. we can also put the build output into different folder and create IIS web site using powershell command.

depends_on configuration defines the dependency between services. In this example, app server is dependent on database server so when we run the service , docker will start the database service first and then it will start the app service based on the order we defined.

Demo

We are now done with the orchestration configuration of deploying our application into docker container using docker compose, we can now build the image and bring up the container instances to test it. Before we start, we must create the publish profile to deploy the build output into publish folder. Make sure dockerfile in the web project has build action as content.

Also, change the database server name matching with db server name defined in env file in web.config file.

Launch the powershell window from the root folder and run the docker images command to show the list of images. I have already downloaded aspnet and sql server images from docker hub.

Lets build the docker image using docker-compose build command. This will first create the database image using base sql server developer edison and create the database and tables based on the SQL we provided and then it will create app server based on aspnet framework docker image and copy the build output from publish folder and put it into wwwroot folder inside the container image.

Now, that we have successfully created the docker images, we can verify that by running docker images command.

Let us now bring up new container instance from our image using docker-compose up command. This command will create a database container instance first and then app server instance and attach it with the database server. Once the container instances are we can verify the instance by testing our application from the browser.

Verify the application by launching the browser and put the ip address of app server container instance.

Now, home page is up and running, let try adding new employee into our table.

Lets also verify the data in sql server by connecting with localhost:14333 port from host.

Great. If we stop the container now, all the data that we created will be gone and it will start from clean slate for next instance. Let us test that by running docker-compose down command. you can also verify if all the running instances are down by running docker ps command.

If we create a new instance now, it will start from clean slate and the employee record that we created should not be exists.
Let us run docker-compose up command to bring up the new instance.

We have successfully deployed the complete .N-Tier CRUD MVC application into docker container. As i mentioned earlier, we can use the containerization for automated end to end or security testing for monolithic application. We can also integrate with CI / CD pipeline to run all the test scenarios before merging the pull request from the feature branch.

Additional Notes

In the above example, we did not store the state changes as part of the container instances. All the changes are gone when the container instance is stopped. However, if we want to store the state of the application and database changes, docker provides the functionality of creating volumes which will mount the folder from host to docker container so that all the state changes will be persisted. This will be useful in the scenario like automated testing to store the results.

In order to create volume in docker, we should use volumes configuration section in docker-compose file. In the example below, i created the directory called DB on my host server and put the MDF and LDF database file inside the folder and then mount that folder to container.

volumes: 
    - ./DB/:c:\db

Next step is to attach the database instead of creating database by adding attach_dbs command in env file. This will create a database called NtierMvcDB and attach the existing MDF and LDF file into that every time when the container instance is created. Also, this will store all the DB state changes even after the container is stopped. When we initiate the new container instance, it will show the data from the previous instance as well.

SA_PASSWORD=P@ssw0rd
ACCEPT_EULA=Y
attach_dbs=[{'dbName':'NtierMvcDB','dbFiles':['C:\\\\DB\\\\NtierMvcDB.mdf','C:\\\\DB\\\\NtierMvcDB.ldf']}]

Some of monolithic core application engine may run on windows service. The good thing with docker on windows, it supports windows service since there is no GUI involved. If you want to install your application engine windows service as part of docker image build and run the windows service, use the below powershell commands in DockerFile.

RUN powershell new-service -Name "AppEngineService" -StartupType Automatic -BinaryPathName "C:\app\bin\AppEngineService.exe"

RUN powershell start-service -Name "AppEngineService"

Conclusion

I hope this article helps you understand how to containerize .net framework monolithic application. Docker containerization is not just only for breaking monolithic application into micro service architecture. It can also be considered to modernize monolithic application packaging into docker image and ship it very frequently for various scenarios like automated end to end testing, security testing.

I have uploaded the entire source code in my github repository.

Happy Coding!!