Configure CI/CD pipeline with Jenkins and Github using an asp.net core application

10 minute read

Overview

Jenkins is an open source automation server to facilitate CI/CD. It helps to automate software development jobs like building, testing, and deploying. In this article we are going to show you how to configure CI/CD pipeline with Jenkins and Github for an ASP.NET Core Web API application.

Tools and Technology used
The following tools and technologies are used to configure CI/CD

  • Visual Studio 2022
  • Visual C#
  • ASP.NET Core Web API
  • Jenkins
  • Github
  • IIS

Let’s configure Jenkins for an asp.net core web api applications.

Step 1: Download and install Java

Jenkin is developed by Java. So it needs Java runtime to run. Download and install latest version of Java SDK from here

Step 2: Download Jenkins

  • Download Jenkins from here
  • Click Download -> Download Jenkins 2.332.1 LTS for: -> Windows

Step 3: Install Jenkins on Windows

  • Double click on Jenkins.msi file and Click Next
  • Choose Logon Type (I have choosen Run Service as LocalSystem) and clik next.
  • Type a port number or keep default 8080 (I have choosen 8080). Click Test port and Next.
  • Once the installation is done. Navigate to the URL http://localhost:8080/ . You will get an screen mentioned Unlock Jenkins.
  • Go to the file mentioned in the screen and copy and pest the password on Andminstrator Password text box and click continue.
  • Now you will get the following screen and click on Install suggested plugins.

  • Wait untill all plugins are installed. If any plugins installation is failed, try again. Once installation is done, you will see the following screen.

  • Fill up the form, type user name and passwrod, and click Save & Continue.
  • You will get the another screen, clik Save & Finish.
  • Now our jenkins is ready. Click Start using Jenkins.

Step 4: To avoid a login each time do the following

  • Navigate Manage Jenkins -> Configure Global Security
  • Mark “Allow anonymous read access” as below

Step 5: Install some custom plugins

  • Navigate to “Manage Jenkins” -> “Manage Plugins”
  • Click “Available” tab
  • Install MSBuild, MSTest, MSTestRunner, PowerShell, VSTestRunner and Git plugin if those are not already installed.
  • You can use search field to find out the plugins.
  • Now click Install without restart.

Step 6: Create an asp.net core web api project

Step 6.1: Create an Web api project

  • Create a ASP.NET Core Web API Project Name HRM.API. Keep Solution name as HRM.

Step 6.2: Install the nuget packages

  • Install the following nuget packages on HRM.API project.
PM> Install-Package Microsoft.EntityFrameworkCore 6.0.3
PM> Install-Package Microsoft.EntityFrameworkCore.InMemory 6.0.3

Step 6.3: Create model class

  • Create a model class name - Employee in Models folder.

Employee.cs

namespace HRM.API.Models
{
    public class Employee
    {
        public int Id { get; set; }
        public string Name { get; set; }
        public string Designation { get; set; }
        public string FathersName { get; set; }
        public string MothersName { get; set; }
        public DateTime DateOfBirth { get; set; }
    }
}

Step 6.4: Create Context and Seed data generator class in Db folder

HRMContext.cs

using HRM.API.Models;
using Microsoft.EntityFrameworkCore;

namespace HRM.API.Db
{
    public class HRMContext : DbContext
    {
        public HRMContext(DbContextOptions<HRMContext> options) : base(options)
        {

        }
        public DbSet<Employee> Employees { get; set; }
    }
}

SeedDataGenerator.cs

using HRM.API.Models;
using Microsoft.EntityFrameworkCore;

namespace HRM.API.Db
{
    public class SeedDataGenerator
    {
        public static void Initialize(IServiceProvider serviceProvider)
        {
            using (var context = new HRMContext(
                serviceProvider.GetRequiredService<DbContextOptions<HRMContext>>()))
            {
                // Check any employee exists 
                if (context.Employees.Any())
                {
                    return; // Data already exists no need to generate
                }

                context.Employees.AddRange(
                    new Employee
                    {
                        Name = "Md. Mahedee Hasan",
                        Designation = "Head of Software Development",
                        FathersName = "Yeasin Bhuiyan",
                        MothersName = "Moriom Begum",
                        DateOfBirth = new DateTime(1984, 12, 19, 00, 00, 00)
                    },

                    new Employee
                    {
                        Name = "Khaleda Islam",
                        Designation = "Software Engineer",
                        FathersName = "Shahidul Islam",
                        MothersName = "Momtaz Begum",
                        DateOfBirth = new DateTime(1990, 10, 29, 00, 00, 00)
                    },

                    new Employee
                    {
                        Name = "Tahiya Hasan Arisha",
                        Designation = "Jr. Software Engineer",
                        FathersName = "Md. Mahedee Hasan",
                        MothersName = "Khaleda Islam",
                        DateOfBirth = new DateTime(2017, 09, 17, 00, 00, 00)
                    },

                    new Employee
                    {
                        Name = "Humaira Hasan",
                        Designation = "Jr. Software Engineer",
                        FathersName = "Md. Mahedee Hasan",
                        MothersName = "Khaleda Islam",
                        DateOfBirth = new DateTime(2021, 03, 17, 00, 00, 00)
                    }
                );
                context.SaveChanges();

            }
        }
    }
}

Step 6.5:Create Repository and It’s Interface in Repository folder

IEmployeeRepository.cs

using HRM.API.Models;

namespace HRM.API.Repository
{
    public interface IEmployeeRepository
    {
        public Task<IEnumerable<Employee>> SelectAllEmployees();
        public Task<Employee> SelectEmployee(int id);
        public Task<string> UpdateEmployee(int id, Employee employee);
        public Task<string> SaveEmployee(Employee employee);
        public Task<string> DeleteEmployee(int id);
    }
}

EmployeeRepository.cs

using HRM.API.Db;
using HRM.API.Models;
using Microsoft.EntityFrameworkCore;

namespace HRM.API.Repository
{
    public class EmployeeRepository : IEmployeeRepository
    {
        private readonly HRMContext _context;

        public EmployeeRepository(HRMContext context)
        {
            _context = context;
        }

        public async Task<IEnumerable<Employee>> SelectAllEmployees()
        {
            try
            {
                var allemployess = _context.Employees.ToListAsync();
                return await allemployess;
            }
            catch (Exception exp)
            {
                throw (exp);
            }
        }

        public async Task<Employee> SelectEmployee(int id)
        {
            try
            {
                var employee = _context.Employees.FindAsync(id);
                return await employee;
            }
            catch (Exception exp)
            {
                throw (exp);
            }
        }

        public async Task<string> UpdateEmployee(int id, Employee employee)
        {
            if (id != employee.Id)
            {
                return "Cannot be updated!";
            }

            _context.Entry(employee).State = EntityState.Modified;

            try
            {
                await _context.SaveChangesAsync();
                return "Data updated successfully!";
            }
            catch (DbUpdateConcurrencyException exp)
            {
                if (!EmployeeExists(id))
                {
                    return "Data not found!";
                }
                else
                {
                    throw (exp);
                }
            }
        }

        public async Task<string> SaveEmployee(Employee employee)
        {
            _context.Employees.Add(employee);
            try
            {
                await _context.SaveChangesAsync();
                return "Data saved successfully!";
            }
            catch (Exception exp)
            {
                throw (exp);
            }
        }

        public async Task<string> DeleteEmployee(int id)
        {
            var employee = await _context.Employees.FindAsync(id);
            if (employee == null)
            {
                return "Data not found!";
            }

            _context.Employees.Remove(employee);
            await _context.SaveChangesAsync();

            return "Data deleted successfully!";
        }

        private bool EmployeeExists(int id)
        {
            return _context.Employees.Any(e => e.Id == id);
        }

    }
}

Step 6.6: Create Employee Service class and it’s interface in Services folder

IEmployeeService

using HRM.API.Models;

namespace HRM.API.Services
{
    public interface IEmployeeService
    {
        public Task<IEnumerable<Employee>> GetEmployees();
        public Task<Employee> GetEmployee(int id);
        public Task<string> EditEmployee(int id, Employee employee);
        public Task<string> AddEmployee(Employee employee);
        public Task<string> RemoveEmployee(int id);
    }
}

EmployeeService.cs

using HRM.API.Models;
using HRM.API.Repository;

namespace HRM.API.Services
{
    public class EmployeeService : IEmployeeService
    {
        IEmployeeRepository _employeeRepository;
        public EmployeeService(IEmployeeRepository repository)
        {
            _employeeRepository = repository;
        }

        public async Task<IEnumerable<Employee>> GetEmployees()
        {
            try
            {
                return await _employeeRepository.SelectAllEmployees();
            }
            catch (Exception exp)
            {
                throw (exp);
            }
        }

        public async Task<Employee> GetEmployee(int id)
        {
            try
            {
                return await _employeeRepository.SelectEmployee(id);
            }
            catch (Exception exp)
            {
                throw (exp);
            }
        }

        public async Task<string> EditEmployee(int id, Employee employee)
        {
            try
            {
                return await _employeeRepository.UpdateEmployee(id, employee);
            }
            catch (Exception exp)
            {
                throw (exp);
            }
        }

        public async Task<string> AddEmployee(Employee employee)
        {
            try
            {
                return await _employeeRepository.SaveEmployee(employee);
            }
            catch (Exception exp)
            {
                throw (exp);
            }
        }

        public async Task<string> RemoveEmployee(int id)
        {
            try
            {
                return await _employeeRepository.DeleteEmployee(id);
            }
            catch (Exception exp)
            {
                throw (exp);
            }
        }

    }
}

Step 6.7: Modify Program.cs file

  • Modify Program.cs file for InMemory database and Enable Swagger for both dev and release.

Program.cs

using HRM.API.Db;
using HRM.API.Repository;
using HRM.API.Services;
using Microsoft.EntityFrameworkCore;

var builder = WebApplication.CreateBuilder(args);

// Add services to the container.

// Configure in memory database
builder.Services.AddDbContext<HRMContext>(opt => opt.UseInMemoryDatabase("HRMDB"));

builder.Services.AddControllers();
// Learn more about configuring Swagger/OpenAPI at https://aka.ms/aspnetcore/swashbuckle
builder.Services.AddEndpointsApiExplorer();
builder.Services.AddSwaggerGen();


//Register DI
builder.Services.AddScoped<IEmployeeRepository, EmployeeRepository>();
builder.Services.AddScoped<IEmployeeService, EmployeeService>();


var app = builder.Build();


// 2. Find the service within the scope to use
using (var scope = app.Services.CreateScope())
{
    // 3. Get the instance of HRMContext in our service layer
    var services = scope.ServiceProvider;
    var context = services.GetRequiredService<HRMContext>();

    // 4. Call the SeedDataGenerator to generate seed data
    SeedDataGenerator.Initialize(services);
}



// Configure the HTTP request pipeline.
if (app.Environment.IsDevelopment())
{
    //app.UseSwagger();
    //app.UseSwaggerUI();
}

// To show both development and deployment
app.UseSwagger();
app.UseSwaggerUI();

app.UseHttpsRedirection();

app.UseAuthorization();

app.MapControllers();

app.Run();

Step 7: Create a publish profile on our application

  • Click right button on the HRM.API Project -> Click Publish.
  • Click ‘+’ sign to create a profile.
  • Select Folder and click Next.
  • Release location is like - bin\Release\net6.0\publish\
  • Click Finish
  • From “more action” rename the profile as - JenkinsProfile
  • You will see two folders are created in Properties -> PublishProfiles folder. Their name is - JenkinsProfile.pubxml and JenkinsProfile.pubxml.user

  • Modify the pubxml file in Properties/PublishProfiles/JnekinsProfile.pubxml
  • Change WebPublishMethod to Package from FileSystem as like below.
<?xml version="1.0" encoding="utf-8"?>
<!--
https://go.microsoft.com/fwlink/?LinkID=208121. 
-->
<Project ToolsVersion="4.0" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
  <PropertyGroup>
    <DeleteExistingFiles>False</DeleteExistingFiles>
    <ExcludeApp_Data>False</ExcludeApp_Data>
    <LaunchSiteAfterPublish>True</LaunchSiteAfterPublish>
    <LastUsedBuildConfiguration>Release</LastUsedBuildConfiguration>
    <LastUsedPlatform>Any CPU</LastUsedPlatform>
    <PublishProvider>FileSystem</PublishProvider>
    <PublishUrl>bin\Release\net6.0\publish\</PublishUrl>
    <WebPublishMethod>Package</WebPublishMethod>
  </PropertyGroup>
</Project>
  • Commit and push this changes on the github.

Step 8: Create a test project name HRM.Test on the same solution

  • Add necessary test cases for HRM.API

Step 9: Build and Run Unit Test cases localy

  • To build the application locally. Go to the directory of the HRM.API applycation where solution file exists.
  • Run the following command on terminal to build the application.
PS D:\Projects\Github\code-sample02\jenkins-demo\HRM> dotnet build .\HRM.sln
  • To Run unit test locally go the directory where test project file exists and run the following command.
PS D:\Projects\Github\code-sample02\jenkins-demo\HRM\HRM.Test> dotnet test .\HRM.Test.csproj

Step 10: Install Microsoft Web Deploy

  • Download and install latest version of Microsoft web deploy, if it is not already installed. In my case, it is already installed with visual studio 2022.

Step 11: Create Credentials Id

  • Navigate to “Manage Jenkins” -> Manage Credentials -> Click Jenkins -> Click Global Credentials (Unrestricted) -> Click Adding some credentials.

  • In this case, I have given my Github user name and password.
  • Fill the form and click OK
  • Id will be generated automatically.
  • Back to Global credentials (unrestricted) and click Update. You will get ID here.

Step 12: Create Pipeline using Jenkins

  • Click “Create a job” on the Jenkins home page.

  • Select Pipeline of the next screen, give a name of the pipeline, I am giving the name of pipeline is “HRMPipelines” and Click Ok.

  • Now add follwoing script on Pipleline section as mentioned below.

pipeline {  
 agent any  
 environment {  
  dotnet = 'C:\\Program Files\\dotnet\\dotnet.exe'  
 }  
 stages {  
  stage('Checkout') {  
   steps {
       git credentialsId: 'aee7d65a-33a7-4616-ab5e-4a1598289b1b', url: 'https://github.com/mahedee/code-sample02.git', branch: 'main'
   }  
  }  
 stage('Build') {  
   steps {  
    bat 'dotnet build %WORKSPACE%\\jenkins-demo\\HRM\\HRM.sln --configuration Release' 
    //bat 'dotnet build C:\\ProgramData\\Jenkins\\.jenkins\\workspace\\HRMPipelines\\jenkins-demo\\HRM\\HRM.sln --configuration Release'  
   }  
  }  
  stage('Test') {  
   steps {  
    bat 'dotnet test %WORKSPACE%\\jenkins-demo\\HRM\\HRM.Test\\HRM.Test.csproj --logger:trx'  
   }  
  }
  
  stage("Release"){
      steps {
      bat 'dotnet build  %WORKSPACE%\\jenkins-demo\\HRM\\HRM.sln /p:PublishProfile=" %WORKSPACE%\\jenkins-demo\\HRM\\HRM.API\\Properties\\PublishProfiles\\JenkinsProfile.pubxml" /p:Platform="Any CPU" /p:DeployOnBuild=true /m'
    }
  }
  
  stage('Deploy') {
    steps {
    // Stop IIS
    bat 'net stop "w3svc"'
    
    // Deploy package to IIS
    bat '"C:\\Program Files (x86)\\IIS\\Microsoft Web Deploy V3\\msdeploy.exe" -verb:sync -source:package="%WORKSPACE%\\jenkins-demo\\HRM\\HRM.API\\bin\\Debug\\net6.0\\HRM.API.zip" -dest:auto -setParam:"IIS Web Application Name"="HRM.Web" -skip:objectName=filePath,absolutePath=".\\\\PackageTmp\\\\Web.config$" -enableRule:DoNotDelete -allowUntrusted=true'
    
    // Start IIS again
    bat 'net start "w3svc"'
    }
 }

 }  
} 

This is the sequential script for the pipeline to execute the stages one by one. Here we used 5 steps - Checkout, Build, Test, Release, Deploy

Stage 1 - Checkout: In this stage, we provide the URL and Git repository, branch as main and git credentials id. Which I have created on step 10. I pull the source code to workspace. Default workspace is - C:\ProgramData\Jenkins.jenkins\workspace

Stage 2 - Build: In this stage, I have build the specific project. Here HRM.API. Keep in mind you have to direct the location where sln file exists.

Stage 3 - Test : In this stage, I have run test project providing test project link.

Stage 4 - Release: In this stage, A release package HRM.zip is created in the mentioned location which is provided on JenkinsProfile.pubxml. You may check each steps output using console after executing pipeline.

Stage 5 - Deploy: In this tage, first stop IIS. Then deploy package to IIS and then Start IIS again. So, I have used three bat command here.

Step 13: Configure Build Triggers

  • Navigate to HRMPipelines -> Build Triggers
  • Mark GitHub hook trigger for GITScm polling and Pool SCM
  • Type schedule as * * * * * which means, keep checking the Git repository and as soon as commit/check-in is done, trigger the build process. Note: there is a space after each star.

  • You can use */15 * * * * if you want to run pipeline after 15 minutes of push on main.

  • Click Save

Step 14: Run pipeline manually

  • Go to the dashboard
  • Click HRMPipelines
  • Click Build Now and you will see the following screen. It means, build and deployment completed sucessfully.

Step 15: Change application and see output

  • Change a little bit on your application
  • Commit and push it to the Github
  • Wait a bit and you will see the build and deployed successfully.

Now test your application using the following URL:

http://localhost:8012/swagger/index.html

  • Here I have hosted the application on 8012 port on IIS.

Source Code