Create An Azure Devops Pipeline To Continuously Integrate And Deploy .NET 6 REST API To Azure Cloud

Introduction

In this article, we will learn the following things

  • Create a .NET 6 REST API using VS 2022
  • Create Unit Test
  • Create an Azure pipeline for CI/CD process
  • Deploy to Azure Cloud

Prerequisites 

  • Azure account - If you don't have it already you can create one for free by visiting Cloud Computing Services | Microsoft Azure
  • Github Account
  • Azure DevOps Account
  • Visual Studio 2022 
  • SQL Server Management Studio

Create a .NET 6 REST API

In this section, we will create a simple .NET 6 REST API that can perform CRUD operations on a SQL Server on Azure Cloud.

Create a New Project

1. Open VS 2022 and choose ASP .NET Core Web API project and click Next

Create a .NET 6 REST API

2. Give a meaningful name for your project and click Next

Create a .NET 6 REST API

3. Choose the framework as .NET 6 and click create

Create a .NET 6 REST API

4. This will create a new project for you with a default controller and a model class. Since we will be creating our own controller and Model classes let's start by deleting the default files. Open solution Explorer and delete the following files - WeatherForecastController.cs, and WeatherForecast.cs

Create a .NET 6 REST API

Add Model and DbContext class

1. In the Solution Explorer right-click on your project and add a new folder called Model, add two subfolders DAL and Entities inside Model and also add a new class called BookDBContext. Inside DAL add two subfolders called Contract and Implementation.

Create a .NET 6 REST API

2. In the Entities folder add a new class Book. This class will be used by EF-core to build the DB table using the code-first approach. Copy-paste the following code into Book.cs file

using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;

namespace BooksNET6API.Models.Entities
{
    public class Book
    {
        public Book()
        {
        }

        [Key]
        public int Id { get; set; }
        [Column(TypeName = "nvarchar(100)")]
        [Required]
        public string Name { get; set; }
        [Column(TypeName = "nvarchar(50)")]
        [Required]
        public string Genere { get; set; }
        [Column(TypeName = "nvarchar(50)")]
        [Required]
        public string PublisherName { get; set; }
    }
}

3. Install the following NuGet packages - Microsoft.EntityFrameworkCore, Microsoft.EntityFrameworkCore.SqlServer and Microsoft.EntityFrameworkCore.Tools

4. Copy-paste the following code into BookDBContext class

using BooksNET6API.Models.Entities;
using Microsoft.EntityFrameworkCore;

namespace BooksNET6API.Models
{
    public class BookDBContext: DbContext
    {
        public DbSet<Book> Books { get; set; }
        public BookDBContext(DbContextOptions<BookDBContext> options) : base(options)
        {

        }
    }
}

5. We have to add a connection string. For this article, we will be using Microsoft SQL Server local DB. Open the appsettings.json file and add the connection string

{
  "ConnectionStrings": {
    "DefaultConnection": "Data Source=(localdb)\\MSSQLLocalDB;Initial Catalog=BookDBRestAPI;Integrated Security=True"
  },
  "Logging": {
    "LogLevel": {
      "Default": "Information",
      "Microsoft.AspNetCore": "Warning"
    }
  },
  "AllowedHosts": "*"
}

6. Configure the connection string in Program.cs class. Unlike .NET 5 or .NET 3.1, we don't have a Startup class anymore. Both Startup and Program classes are merged into one. Open program.cs and copy-paste the following code. We have added line 6 and line 7 which pulls the connection string from the config file and registers the DbContext with services.


using BooksNET6API.Models;
using Microsoft.EntityFrameworkCore;

var builder = WebApplication.CreateBuilder(args);
string connString = builder.Configuration.GetConnectionString("DefaultConnection");
builder.Services.AddDbContext<BookDBContext>(options =>
{
    options.UseSqlServer(builder.Configuration.GetConnectionString("DefaultConnection"));

});
// Add services to the container.

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

var app = builder.Build();

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

app.UseHttpsRedirection();

app.UseAuthorization();

app.MapControllers();

app.Run();

Add Migrations

1. The next step is to add migrations and create the DB table using EF Core. Go to Tools-> NuGet Package Manager -> Package Manager Console and run the following commands

  • add-migration AddBooktoDB
  • update-database

2. Go to SQL Server and check if a database named BookDBRestAPI is created or not. Inside the database, there should be a table named Books.

Create a .NET 6 REST API

Add a new Controller

1. In solution explorer, right-click on Controllers folder -> Add -> Controller...

Create a .NET 6 REST API

2. Choose API-> API Controller - Empty and click Add

Create a .NET 6 REST API

3. Copy-paste the following code into BooksController class

#nullable disable
using BooksNET6API.Models;
using BooksNET6API.Models.DAL.Contract;
using BooksNET6API.Models.Entities;
using Microsoft.AspNetCore.Mvc;
using Microsoft.EntityFrameworkCore;

namespace BooksNET6API.Controllers
{
    [Route("api/[controller]")]
    [ApiController]
    public class BooksController : ControllerBase
    {
        private readonly IBookRepo bookRepo;

        public BooksController(IBookRepo _bookRepo)
        {
            bookRepo = _bookRepo;
        }

        [HttpGet]
        public async Task<ActionResult<IEnumerable<Book>>> GetBooks()
        {
            try
            {
                return await bookRepo.GetBooks();
            }
            catch
            {
                return StatusCode(500);
            }
        }

        [HttpGet("{id}")]
        public async Task<ActionResult<Book>> GetBook(int id)
        {
            try
            {
                var book = await bookRepo.GetBook(id);
                return book;
            }
            catch
            {
                return StatusCode(500);
            }
        }


        [HttpPut("{id}")]
        public async Task<IActionResult> PutBook(int id, Book book)
        {
            try
            {
                var result = await bookRepo.PutBook(id, book);
                return result;
            }
            catch(DbUpdateConcurrencyException ex)
            {
                return StatusCode(409);
            }
            catch (Exception ex)
            {
                return StatusCode(500);
            }
        }


        [HttpPost]
        public async Task<ActionResult<Book>> PostBook(Book book)
        {
            try
            {
                var result = await bookRepo.PostBook(book);
                return CreatedAtAction("GetBook", new { id = result.Value.Id }, book);
            }
            catch
            {
                return StatusCode(500);
            }
        }

        [HttpDelete("{id}")]
        public async Task<IActionResult> DeleteBook(int id)
        {
            try
            {
                var result = await bookRepo.DeleteBook(id);
                return result;
            }
            catch
            {
                return StatusCode(500);
            }
        }


    }
}

Create Repository Class

We will be following the Repository Design pattern. 

1. Create an interface IBookRepo in Models->DAL->Contract folder. Copy-paste the following code into IBookRepo

using BooksNET6API.Models.Entities;
using Microsoft.AspNetCore.Mvc;

namespace BooksNET6API.Models.DAL.Contract
{
    public interface IBookRepo
    {
        Task<ActionResult<IEnumerable<Book>>> GetBooks();
        Task<ActionResult<Book>> GetBook(int id);
        Task<IActionResult> PutBook(int id, Book book);
        Task<ActionResult<Book>> PostBook(Book book);
        Task<IActionResult> DeleteBook(int id);
    }
}

2. Create a class BookRepo in Models->DAL->Implementation folder. Copy-paste the following code

using BooksNET6API.Models.DAL.Contract;
using BooksNET6API.Models.Entities;
using Microsoft.AspNetCore.Mvc;
using Microsoft.EntityFrameworkCore;

namespace BooksNET6API.Models.DAL.Implementation
{
    public class BookRepo:IBookRepo
    {
        private readonly BookDBContext bookDBContext;
        public BookRepo(BookDBContext _bookDBContext)
        {
            bookDBContext = _bookDBContext;
        }

        public async Task<IActionResult> DeleteBook(int id)
        {
            var book = await bookDBContext.Books.FindAsync(id);
            if (book == null)
            {
                return new NotFoundResult();
            }

            bookDBContext.Books.Remove(book);
            await bookDBContext.SaveChangesAsync();

            return new NoContentResult();
        }

        public async Task<ActionResult<Book>> GetBook(int id)
        {
            var book = await bookDBContext.Books.FindAsync(id);

            if (book == null)
            {
                return new NotFoundResult();
            }

            return book;
        }

        public async Task<ActionResult<IEnumerable<Book>>> GetBooks()
        {
            return await bookDBContext.Books.ToListAsync();
        }

        public async Task<ActionResult<Book>> PostBook(Book book)
        {
            bookDBContext.Books.Add(book);
            await bookDBContext.SaveChangesAsync();

            return book;
        }

        public async Task<IActionResult> PutBook(int id, Book book)
        {
            if (id != book.Id)
            {
                return new BadRequestResult();
            }

            bookDBContext.Entry(book).State = EntityState.Modified;

            try
            {
                await bookDBContext.SaveChangesAsync();
            }
            catch (DbUpdateConcurrencyException)
            {
                if (!BookExists(id))
                {
                    return new NotFoundResult();
                }
                else
                {
                    throw;
                }
            }

            return new NoContentResult();
        }
        private bool BookExists(int id)
        {
            return bookDBContext.Books.Any(e => e.Id == id);
        }
    }
}

3. Add the following code to Program.cs

builder.Services.AddScoped<IBookRepo, BookRepo>();

Create Unit Test using xUnit and Moq

1. Right click on solution -> Add -> New Project

2. Choose xUnit Test project. Give it a proper name and choose the framework as .NET 6

Create Unit Test using xUnit and Moq

3. Install the NuGet package - Moq v 4.16.1, FluentAssertions v 6.4.0

4. Copy-paste the following code in your unit test class

using BooksNET6API.Controllers;
using BooksNET6API.Models;
using BooksNET6API.Models.DAL.Contract;
using BooksNET6API.Models.Entities;
using FluentAssertions;
using Microsoft.AspNetCore.Mvc;
using Microsoft.EntityFrameworkCore;
using Moq;
using System;
using System.Threading.Tasks;
using Xunit;

namespace BookRestAPIXUnitTest
{
    public class BookRestAPIUnitTest
    {
        private BooksController booksController;
        private int Id = 1;
        private readonly Mock<IBookRepo> bookStub = new Mock<IBookRepo>();
        Book sampleBook = new Book
        {
            Id = 1,
            Name = "State Patsy",
            Genere = "Action/Adventure",
            PublisherName = "Queens",
        };
        Book toBePostedBook = new Book
        {
            Name = "Federal Matters",
            Genere = "Suspense",
            PublisherName = "Harpers",
        };
        [Fact]
        public async Task GetBook_BasedOnId_WithNotExistingBook_ReturnNotFound()
        {
            //Arrange
            //use the mock to set up the test. we are basically telling here that whatever int id we pass to this method 
            //it will always return null
            booksController = new BooksController(bookStub.Object);
            bookStub.Setup(repo => repo.GetBook(It.IsAny<int>())).ReturnsAsync(new NotFoundResult());
            //Act
            var actionResult = await booksController.GetBook(1);
            //Assert
            Assert.IsType<NotFoundResult>(actionResult.Result);
        }
        [Fact]
        public async Task GetBook_BasedOnId_WithExistingBook_ReturnBook()
        {
            //Arrange
            //use the mock to set up the test. we are basically telling here that whatever int id we pass to this method 
            //it will always return a new Book object
            bookStub.Setup(service => service.GetBook(It.IsAny<int>())).ReturnsAsync(sampleBook);
            booksController = new BooksController(bookStub.Object);
            //Act
            var actionResult = await booksController.GetBook(1);
            //Assert
            Assert.IsType<Book>(actionResult.Value);
            var result = actionResult.Value;
            //Compare the result member by member
            sampleBook.Should().BeEquivalentTo(result,
                options => options.ComparingByMembers<Book>());
        }
        [Fact]
        public async Task PostVideoGame_WithNewVideogame_ReturnNewlyCreatedVideogame()
        {
            //Arrange
            bookStub.Setup(repo => repo.PostBook(It.IsAny<Book>())).ReturnsAsync(sampleBook);

            booksController = new BooksController(bookStub.Object);
            //Act
            var actionResult = await booksController.PostBook(toBePostedBook);
            //Assert
            Assert.Equal("201", ((CreatedAtActionResult)actionResult.Result).StatusCode.ToString());

        }
        [Fact]
        public async Task PostVideoGame_WithException_ReturnsInternalServerError()
        {
            //Arrange
            bookStub.Setup(service => service.PostBook(It.IsAny<Book>())).Throws(new Exception());
            booksController = new BooksController(bookStub.Object);
            //Act
            var actionResult = await booksController.PostBook(null);
            //Assert
            Assert.Equal("500", ((StatusCodeResult)actionResult.Result).StatusCode.ToString());
        }
        [Fact]
        public async Task PutVideoGame_WithException_ReturnsConcurrencyExecption()
        {
            //Arrange
            bookStub.Setup(service => service.PutBook(It.IsAny<int>(), It.IsAny<Book>())).Throws(new DbUpdateConcurrencyException());
            booksController = new BooksController(bookStub.Object);
            //Act
            var actionResult = await booksController.PutBook(Id, sampleBook);
            //Assert
            Assert.Equal("409", ((StatusCodeResult)actionResult).StatusCode.ToString());

        }
        [Fact]
        public async Task PutVideoGame_WithException_ReturnsExecption()
        {
            //Arrange
            bookStub.Setup(service => service.PutBook(It.IsAny<int>(), It.IsAny<Book>())).Throws(new Exception());
            booksController = new BooksController(bookStub.Object);
            //Act
            var actionResult = await booksController.PutBook(Id, sampleBook);
            //Assert
            Assert.Equal("500", ((StatusCodeResult)actionResult).StatusCode.ToString());
        }
        [Fact]
        public async Task PutVideoGame_WithExistingVideogame_BasedOnId_ReturnUpdatedVideogame()
        {
            //Arrange
            bookStub.Setup(service => service.PutBook(It.IsAny<int>(), It.IsAny<Book>())).ReturnsAsync(new NoContentResult());
            booksController = new BooksController(bookStub.Object);
            //Act
            var actionResult = await booksController.PutBook(Id, sampleBook);
            //Assert
            actionResult.Should().BeOfType<NoContentResult>();
        }
    }
}

Create Build pipeline on Azure Devops

1. Go to https://dev.azure.com/ and login with your credentials

2. On the home page click on New project button

Create Build pipeline on Azure Devops

3. Give a proper project name and choose the visibility as Public. The visibility selected here should match the visibility of the Github repo

Create Build pipeline on Azure Devops

4. Hover your mouse over to pipeline and click on pipeline in the popup menu

Create Build pipeline on Azure Devops

5. Click on Create Pipeline button

Create Build pipeline on Azure Devops

6. Choose the appropriate repository where your code is located. For me its github so I will be selecting that

Create Build pipeline on Azure Devops

7. Select your code repository

Create Build pipeline on Azure Devops

8. It may ask you for permission. Say yes to it.

9. It will ask you to configure your pipeline. Choose ASP .NET Core

10. If everything goes well it will create a .yml file for you. Click on Save and run

Create Build pipeline on Azure Devops

11. In the popup window you can change the settings if you want or leave them to default and click on save and run

Create Build pipeline on Azure Devops

12. If everything goes well it will create a pipeline for you.

Create Build pipeline on Azure Devops

 

13. Click on pipeline, then click on 3 dots and click edit

Create Build pipeline on Azure Devops

14. Click on 3 dots at top and click Trigger

Create Build pipeline on Azure Devops

15. Continuous Integration is enabled so this means whenever we do a commit to git repository it will automatically trigger a build

Create Build pipeline on Azure Devops

16. Next step is to update the .yml file so that it can run the unit tests when a build is triggered. Replace the content of the .yml file.

We have added lines 8 - 12 that instructs the pipeline to run unit tests.

trigger:
  - master
pool:
  vmImage: ubuntu-latest
variables:
  buildConfiguration: Release
steps:
  - task: DotNetCoreCLI@2
    inputs:
      command: test
      projects: '**/*Test/*.csproj'
      arguments: '--configuration $(buildConfiguration)'
  - script: dotnet build --configuration $(buildConfiguration)
    displayName: dotnet build $(buildConfiguration)

17. Commit the file changes to Github and it should start a new build. Once the build is succeeded open the job and you can see your unit tests ran successfully. This means from now on whenever  a code commit will happen it will start the build process automatically and run the unit tests before building the project

Create Build pipeline on Azure Devops

18. Next step is to package and publish our project. So for that, we will have to edit our .yml file again to include the steps. Replace the .yml file with the below-mentioned code.

Notice we have added lines 15 - 24 that will zip and publish the output.

One important thing to note is since we are building and publishing a REST API we need to explicitly set publishWebProjects to false.

trigger:
  - master
pool:
  vmImage: ubuntu-latest
variables:
  buildConfiguration: Release
steps:
  - task: DotNetCoreCLI@2
    inputs:
      command: test
      projects: '**/*Test/*.csproj'
      arguments: '--configuration $(buildConfiguration)'
  - script: dotnet build --configuration $(buildConfiguration)
    displayName: dotnet build $(buildConfiguration)
  - task: DotNetCoreCLI@2
    displayName: 'dotnet publish --configuration $(buildConfiguration) --output $(Build.ArtifactStagingDirectory)'
    inputs:
      command: publish
      publishWebProjects: false
      projects: 'BooksNET6API/BooksNET6API.csproj'
      arguments: '--configuration $(BuildConfiguration) --output $(Build.ArtifactStagingDirectory)'
      zipAfterPublish: true
  - task: PublishBuildArtifacts@1
    displayName: 'publish artifacts'

Publish SQL Server DB to Azure Cloud

Right now we are using a local SQL server for testing purposes. In this step, we will publish the SQL server to cloud

1. Login to Azure Portal and click on Resource Groups

Publish SQL Server DB to Azure Cloud

2. Click on Create button to create a new resource group. Give it a proper name and click on Review+Create button.

Publish SQL Server DB to Azure Cloud

3. Open the resource group and click on Create button

Publish SQL Server DB to Azure Cloud

4. Search for sql server in the search box

Publish SQL Server DB to Azure Cloud

5. Select Azure SQL from the list of Marketplace suggestions and click Create

Publish SQL Server DB to Azure Cloud

6. In the Select SQL Deployment option, choose the SQL Databases and Resource type as Database Server and click Create button

Publish SQL Server DB to Azure Cloud

7. Enter a server name and user name, and password for SQL Authentication. Click on the Review + create button.

Publish SQL Server DB to Azure Cloud

8. If everything goes well SQL server will be created. Copy the server name we will need it to publish our SQL DB from local SQL Server to Azure SQL Server

Publish SQL Server DB to Azure Cloud

9. Open SQL server management studio and in Object Explorer right-click on local DB you want to deploy to Azure and select Task -> Deploy Database to Microsoft Azure SQL Database

Publish SQL Server DB to Azure Cloud

10. In the window that opens click Next to go to the Deployment Settings window. Click on connect button and in the popup window that opens give the server name as the server name of SQL Server on Azure from step 8 and login with your credentials. If everything goes alright your DB would be deployed on the Azure SQL Server.

Publish SQL Server DB to Azure Cloud

11. Open your REST API project in VS 2022 and change the connection string to point to Azure SQL Server now in the appsettings.json file.

"DefaultConnection": "<Server name>;Initial Catalog=BookDBRestAPI;User Id=<User ID>;Password=<Password>"

Deploy the app to Azure Cloud using the Azure Release pipeline

1. Go to your resource group on the Azure portal and create a new API App

Deploy the app to Azure Cloud using the Azure Release pipeline

2. Choose the appropriate settings and create the resource

Deploy the app to Azure Cloud using the Azure Release pipeline

3. Open the resource and copy the URL, we will need it to set up our release pipeline

Deploy the app to Azure Cloud using the Azure Release pipeline

4. Go to Azure DevOps and open your project. Click on Pipelines -> Releases

Deploy the app to Azure Cloud using the Azure Release pipeline

5. Click on New Pipeline

Deploy the app to Azure Cloud using the Azure Release pipeline

6. Choose Azure App Service deployment

Deploy the app to Azure Cloud using the Azure Release pipeline

7. Give an appropriate stage name and close the popup

Deploy the app to Azure Cloud using the Azure Release pipeline

8. Click on Task. Choose your subscription, App type as API App, and App Service name as the name of your API App on Azure and click Save. After that a popup will be displayed click Ok on the popup

Deploy the app to Azure Cloud using the Azure Release pipeline

Deploy the app to Azure Cloud using the Azure Release pipeline

9. Click on Pipeline again and click the Add button to add an artifact. Choose appropriate values in the dropdown and click Add

Deploy the app to Azure Cloud using the Azure Release pipeline

10. Click on the bolt icon and make sure the Continuous deployment trigger is enabled.

Deploy the app to Azure Cloud using the Azure Release pipeline

11. Click on save and give a comment and click Ok

Deploy the app to Azure Cloud using the Azure Release pipeline

Testing

1. To test the pipeline we will make some changes to our app on VS 2022 and commit the code to Github. If everything is set up correctly it will trigger a new build automatically. Once the build is successful it will start the release process automatically

Deploy the app to Azure Cloud using the Azure Release pipeline

Once the release is complete, enter the API app URL in the browser to make sure everything works perfectly.

2. You will not be able to access swagger from the Azure URL because in the program.cs file swagger is applicable only for development by default

Remove if condition if you want to use swagger in release mode.

3. If you have troubles connecting to SQL server from deployed app make sure the IP address of the API app is added to SQL Server list of allowed IPs. You can get the IP address of API app from Networking Page -> Inbound Address. It has to be added in SQL Server Firewall and Virtual Network window.

Summary

In this article, we learned how to develop REST API using .NET 6 and SQL Server with EF Core. We also learned how to make a build and release pipeline on Azure Devops to deploy the app continuously on Microsoft Azure. I hope you liked the article. Please let me know if you have any feedback in the comments section.

Thank you for reading and I wish you "Happy Coding"

Source Code: https://github.com/tanujgyan/BooksNET6API


Similar Articles