Deploy Rest API to Azure API App with using DevOps CI/CD pipelines
In this post, I am going to Create a Rest API and connect to SQL DB, then create a CI Pipeline in Azure DevOps, Publish SQL DB to Azure and create API API to deploy Rest API to this App in following order:
- create a REST API with .NET 6 in Visual Studio 2022 and connect it to SQL Local Database
- Create Unit Test for this REST API
- Create Azure DevOps CI Pipelines for this REST API
- Publish SQL Database to Azure SQL Server
- Create an API App in Azure Portal
- Create Azure Release Pipeline in Azure DevOps to deploy Rest API to Azure API App.
Prerequisites
- Visual Studio 2022
- Github Account
- SQL Server Management Studio
- Azure account – If you don’t have, you can create one for free here
- Azure DevOps Account
Create Rest API in .NET 6
I am creating a simple .NET 6 REST API that can perform CRUD operations on a SQL Server on Azure Cloud.
1. Open VS 2022, select new project, and choose ASP .NET Core Web API project as following figure:
Press to Next button and give a name and location for project as following figure:
Press to the Next and select .NET 6 and select the check boxes to configure as following figure:
Press to Create button to create the Project template as follow:
Now we have created Project BooksRestAPI which is a template, with Controller for a default WeatherForecast.cs
I want to develop this project. First I delete the WeatherForecastController under Controller and WeatherForecast.cs. Then I need Model and DbContext class to add this project.
Add Model and DbContext class
1. In the Solution Explorer right-click on your project and add a new folder called Models, add two subfolders DataAccess and Entities inside Models and also add a new class called BookDBContext. Inside DataAccess add two subfolders called Contract and Implementation as shown in the following:
2. Create a new class Book In the Entities folder. This class will be used by EF-core to build the Database table Book, using the code-first approach. Copy-paste the following code into Book.cs file
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;
namespace BooksRestAPI.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. We need to install the three following NuGet packages via VS :Tools: NuGet Package Manager, Microsoft.EntityFrameworkCore, Microsoft.EntityFrameworkCore.SqlServer and Microsoft.EntityFrameworkCore.Tools. Then after copy-paste the following code into BookDBContext class:
using BooksRestAPI.Models.Entities;
using BooksRestAPI.Models.Entities;
using Microsoft.EntityFrameworkCore;
namespace BooksRestAPI.Models
{
public class BookDBContext : DbContext
{
public DbSet<Book> Books { get; set; }
public BookDBContext(DbContextOptions<BookDBContext> options) : base(options)
{
}
}
}
4. We need to add a connection string to appsettings.json file . We are using Microsoft SQL Server local DB (in Visual studio). Open the appsettings.json file and add the following connection string in the beginning of file:
{
"ConnectionStrings": {
"DefaultConnection": "Data Source=(localdb)\\MSSQLLocalDB;Initial Catalog=BookDBRestAPI;Integrated Security=True"
}
Note: If you want connect to the SQL Server installed in your machine then you should use connection string with login information to your SQL Server as:
"DefaultConnection": "Data Source=Servername; Initial Catalog=BookDBRestAPI; Integrated Security=true; Encrypt=False;"
So that after adding the appsettings.json file shall be as follow:
{
"ConnectionStrings": {
"DefaultConnection": "Data Source=(localdb)\\MSSQLLocalDB;Initial Catalog=BookDBRestAPI;Integrated Security=True"
},
"Logging": {
"LogLevel": {
"Default": "Information",
"Microsoft.AspNetCore": "Warning"
}
},
"AllowedHosts": "*"
}
5. Add the connection string in Program.cs class. There is no Startup.cs class in the .NET 6, Unlike .NET 5 or .NET 3.1. Both Startup and Program classes are merged into one. Open program.cs and add following code:
string connString = builder.Configuration.GetConnectionString("DefaultConnection");
builder.Services.AddDbContext<BookDBContext>(options =>
{
options.UseSqlServer(builder.Configuration.GetConnectionString("DefaultConnection"));
});
So that after adding the above code the progam.cs fils shall be as following. As you see We have added line 6 to 11,which pulls the connection string from the appsettings.json file and registers the DbContext with services.
using BooksRestAPI.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();
Create Database by Adding Migrations
Next step is to add migrations and create the Database and table using EF Core. Go to Tools-> NuGet Package Manager -> Package Manager Console and run the the following commands:
- add-migration AddBooktoDB
- update-database
After the first command, if it is succeed then you see the following in the terminal:
PM> add-migration AddBooktoDB
Build started...
Build succeeded.
To undo this action, use Remove-Migration.
PM>
In the Visual Studio you see folder Migrations is created with two files: 20230216112830_AddBooktoDB, and BookDBContextModelSnapshot.
After executing the second command, Go to Local SQL Server in the Visual Studio under Connected Services right click and press to the SQL Server Database.. and select the Open SQL Server Object Explorer and expand it then you see that the database BookDBRestAPI and inside it the table Books. as following figure:
Create Repository
We need to create Repository Interface and Repository class as following:
1. Create an interface IBookRepo in Models->DataAccess->Contract folder. Copy-paste the following code into IBookRepo
using BooksRestAPI.Models.Entities;
using BooksRestAPI.Models.Entities;
using Microsoft.AspNetCore.Mvc;
namespace BooksRestAPI.Models.DataAccess.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 Repository class BookRepo in Models->DataAccess->Implementation folder. Copy-paste the following code:
using BooksRestAPI.Models.DataAccess.Contract;
using BooksRestAPI.Models.Entities;
using Microsoft.AspNetCore.Mvc;
using Microsoft.EntityFrameworkCore;
namespace BooksRestAPI.Models.DataAccess.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);
}
}
}
Register Repository BookRepo in Program class
so that the repository’s dependency is resolved at a run time when needed run time
by adding the following line to program.cs, so that the repository’s dependency is resolved at a run time.
builder.Services.AddTransient<IBookRepo, BookRepo>()
line 19 in the program.cs is the now add code in the following code:
using BooksRestAPI.Models;
using BooksRestAPI.Models.DataAccess.Contract;
using BooksRestAPI.Models.DataAccess.Implementation;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.OpenApi.Models;
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.AddTransient<IBookRepo, BookRepo>();
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();
Create a new Controller class
We need create BooksController.cs class as following:
- In solution explorer, right-click on Controllers folder -> Add -> Controller… and choose API-> API Controller – Empty and click Add
Give BooksController.cs as name.
2. Copy-paste the following code into BooksController class:
using BooksRestAPI.Models; using BooksRestAPI.Models.DataAccess.Contract; using BooksRestAPI.Models.Entities; using Microsoft.AspNetCore.Mvc; using Microsoft.EntityFrameworkCore; namespace BooksRestAPI.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); } } } }
Add XML documentation to API actions
In the BooksController.cs add xml documentation for each actions: Post, Get, Put, delete according to the new code in BooksController.cs in the following:
using BooksRestAPI.Models; using BooksRestAPI.Models.DataAccess.Contract; using BooksRestAPI.Models.Entities; using Microsoft.AspNetCore.Mvc; using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.DependencyInjection; namespace BooksRestAPI.Controllers { [Route("api/[controller]")] [ApiController] public class BooksController : ControllerBase { private readonly IBookRepo bookRepo; public BooksController(IBookRepo _bookRepo) { bookRepo = _bookRepo; } /// <summary> /// Action to retrieve all Books. /// </summary> /// <returns>Returns a list of all Books or an empty list, if no Book is exist</returns> /// <response code="200">Returned if the list of Books was retrieved</response> /// <response code="400">Returned if the Books could not be retrieved</response> [ProducesResponseType(StatusCodes.Status200OK)] [ProducesResponseType(StatusCodes.Status400BadRequest)] [HttpGet] public async Task<ActionResult<IEnumerable<Book>>> GetBooks() { try { return await bookRepo.GetBooks(); } catch { return StatusCode(500); } } /// <summary> /// Action Get a Book. /// </summary> /// <param name="Id">The Id is a Book Id which should be retaind from DB </param> /// <returns>Returns is OK </returns> /// <response code="200">Returned if the Book has been found and retained </response> /// <response code="400">Returned if the Book could not be found to retaind with BookId</response> [ProducesResponseType(StatusCodes.Status200OK)] [ProducesResponseType(StatusCodes.Status400BadRequest)] [HttpGet("{Id}", Name = "GetBook")] public async Task<ActionResult<Book>> GetBook(int Id) { try { var book = await bookRepo.GetBook(Id); return book; } catch { return StatusCode(500); } } /// <summary> /// Action: Put to update a book in the Database /// </summary> /// <param name="id" book>The Book is a book which should be updated in DB </param> /// <returns>Returns is OK </returns> /// <response code="200">Returned if the Book was updated </response> /// <response code="400">Returned if the book could not be found with book.Id</response> [ProducesResponseType(StatusCodes.Status200OK)] [ProducesResponseType(StatusCodes.Status400BadRequest)] [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); } } /// <summary> /// Action: Post to create a new product in the database. /// </summary> /// <param name="book">Model to create a new book</param> /// <returns>Returns the created book</returns> /// <response code="200">Returned if the book was created</response> /// <response code="400">Returned if the model couldn't be parsed or the book couldn't be saved</response> /// <response code="422">Returned when the validation failed</response> [ProducesResponseType(StatusCodes.Status200OK)] [ProducesResponseType(StatusCodes.Status400BadRequest)] [ProducesResponseType(StatusCodes.Status422UnprocessableEntity)] [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); } } /// <summary> /// Action Delete: to delete a book on the Database. /// </summary> /// <param name="id">The bookId is a book id which should be deleted from DB </param> /// <returns>Returns is OK </returns> /// <response code="200">Returned if the book id has been found and deleted </response> /// <response code="400">Returned if the book could not be found for deletion with bookId</response> [ProducesResponseType(StatusCodes.Status200OK)] [ProducesResponseType(StatusCodes.Status400BadRequest)] [HttpDelete("{id}")] public async Task<IActionResult> DeleteBook(int id) { try { var result = await bookRepo.DeleteBook(id); return result; } catch { return StatusCode(500); } } } }
Generate xml file (BooksRestAPI.xml)
- Add the following code to the program.cs file:
builder.Services.AddSwaggerGen(c =>
{
c.SwaggerDoc("v1", new OpenApiInfo
{
Version = "v1",
Title = "Book Api",
Description = "A simple API to create a book",
Contact = new OpenApiContact
{
Name = "Mehrdad Zandi",
Email = "mehrdad.zandi@softsolutionsahand.com",
Url = new Uri("http://www.softsolutionsahand.com/")
}
});
var xmlFile = $"{Assembly.GetExecutingAssembly().GetName().Name}.xml";
var xmlPath = Path.Combine(AppContext.BaseDirectory, xmlFile);
c.IncludeXmlComments(xmlPath);
});
The above code needed to generate an BookRestAPI.xml file for documentation.
The new code for program.cs is as follow:
using BooksRestAPI.Models;
using BooksRestAPI.Models.DataAccess.Contract;
using BooksRestAPI.Models.DataAccess.Implementation;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.OpenApi.Models;
using System.Reflection;
// program file in .NET 6, include all needed for Startup.cs in the .NET 5
// This a a program for books, get, post etc.
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.AddOptions();
builder.Services.AddTransient<IBookRepo, BookRepo>();
builder.Services.AddScoped<IBookRepo, BookRepo>();
builder.Services.AddControllers();
// Learn more about configuring Swagger/OpenAPI at https://aka.ms/aspnetcore/swashbuckle
builder.Services.AddEndpointsApiExplorer();
builder.Services.AddSwaggerGen(c =>
{
c.SwaggerDoc("v1", new OpenApiInfo
{
Version = "v1",
Title = "Book Api",
Description = "A simple API to create a book",
Contact = new OpenApiContact
{
Name = "Mehrdad Zandi",
Email = "mehrdad.zandi@softsolutionsahand.com",
Url = new Uri("http://www.softsolutionsahand.com/")
}
});
var xmlFile = $"{Assembly.GetExecutingAssembly().GetName().Name}.xml";
var xmlPath = Path.Combine(AppContext.BaseDirectory, xmlFile);
c.IncludeXmlComments(xmlPath);
});
var app = builder.Build();
// Configure the HTTP request pipeline.
if (app.Environment.IsDevelopment())
{
app.UseDeveloperExceptionPage();
}
// move these out of if statment to upload Swagger UI in Production too
app.UseSwagger();
app.UseSwaggerUI();
app.UseHttpsRedirection();
app.UseAuthorization();
app.MapControllers();
app.Run();
2. add the following code the project file (BooksRestAPI.csproj):
Right-click the project in Solution Explorer and select Edit <project_name>.csproj. Manually add following code to the .csproj file:
<PropertyGroup>
<GenerateDocumentationFile>true</GenerateDocumentationFile>
<NoWarn>$(NoWarn);1591</NoWarn>
</PropertyGroup>
Note: without above code xml file can’t be created and you can get error with running of project.
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 (BooksRestAPITest) and choose the framework as .NET 6
Press Next and give the name BooksRestAPITest then project is created with clas: UnitTest1.cs:
3. Install the NuGet packages: Moq, FluentAssertions
4- Add Project Reference: BooksRestAPI to this Test Project
5. Change the name of class from UnitTest1.cs to BookRestAPIUnitTest.cs and copy-paste the following code in this class:
using BooksRestAPI.Controllers;
using BooksRestAPI.Models;
using BooksRestAPI.Models.DataAccess.Contract;
using BooksRestAPI.Models.Entities;
using FluentAssertions;
using Microsoft.AspNetCore.Mvc;
using Microsoft.EntityFrameworkCore;
using Moq;
using System;
using System.Threading.Tasks;
using Xunit;
namespace BooksRestAPITest
{
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>();
}
}
}
Testing Rest API
Test Project: BooksRestAPI from the Visual Studio by pressing to the IIS Explorer or just press to the F5 on keyboard, then Swagger UI is uploaded as following figure:
Test to access to database:
In the above UI press to Post field and the Try out and give the following book :
{
"id": 0,
"name": "All the Light We Cannot See:",
"genere": "Science fiction",
"publisherName": "SAGE Publications Ltd"
}
As shown in the following figure:
Press to the Execute button then book information is sent to the database and it is displayed the saved book information as following:
AS we see Post (adding book to DB) is succedded.
If we now Press to GET then it retuns all the books from database which is only one book:
[
{
"id": 2,
"name": "All the Light We Cannot See:",
"genere": "Science fiction",
"publisherName": "SAGE Publications Ltd"
}
]
Test the UnitTest Project:
From Visual Studio menu, Press to Test and Run All Tests, then the result shall be displayed as following:
That was all. We have created the Rest API project in ASP.NET Core, connected to the Local DB and Created a Unit test project and test all is fine.
Add the Source code to Github:
The Source code can be found from my Github
Next step is to create CI Pipeline in Azure DeveOps.
Create ci 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, if you don’t know how to create a new project in Azure DevOps look to my post: How to create a DevOps Organization and DevOps project
I have create a new project in my Azure DevOps and called it BooksRestAPI as seen in the following figure:
3. Create a CI Pipeline, if you don’t know what is a Pipeline and how to create a Pipleine look to my post: Azure Pipelines.
In the above UI, in the left side menu press to Pipelines and then Create your first Pipeline then the following UI is displayed:
This asks you where is your code? Select Github then the following UI is dispalyed ad asking you Select a repository (from your list of repositories), I have selected mehzan07/BooksRestAPI (which is in my Github)
With selecting of BooksRestAPI repository the following UI is displayed and asks you to login to Github:
Put in your Github password and press to Confirm, then it takes you to your Githup, scroll down and find the following UI:
Press to Approve and Install, then asks you to login to Azure DevOps, login and select organization and project in Azure DevOps (if your project is not in the list of Projects create it again. Then you are coming back to the Azure DevOps and select again the same Repository again then following UI:
Select ASP.net Core as in the above figure then a default yaml Pipeline is creates as following:
A default yml Pipeline template is created The name of pipeline is azure-pipline.yml, I press to rename and change it to Pipelines/azure-pipelines-CI-restapi.yml (creating it in the Pipelines folder)
If you press to Save and Run, it takes a while and the result shall be failed, by showing job failed.
I am editing the Pipeline by pressing to the Pipelines and selecting the new created Pipeline mehzan07.BooksRestAPI and then click on 3 dots and click edit, past the following YAML code to the Pipeline:
name : NetCore-BooksRestAPI-CI
trigger:
branches:
include:
- master
paths:
include:
- BooksRestAPI/*
pool:
vmImage: 'ubuntu-latest'
variables:
buildConfiguration: 'Release'
steps:
- task: DotNetCoreCLI@2
inputs:
command: 'restore'
projects: '**/BooksRestAPI*.csproj'
displayName: 'Restore Nuget Packages'
- task: DotNetCoreCLI@2
inputs:
command: 'build'
projects: '**/BooksRestAPI*.csproj'
arguments: '--no-restore'
displayName: 'Build projects'
- task: DotNetCoreCLI@2
inputs:
command: 'test'
projects: '**/BooksRestAPITest.csproj'
arguments: '--no-restore --no-build'
displayName: 'Run Tests'
- task: DotNetCoreCLI@2
inputs:
command: 'publish'
publishWebProjects: false
projects: '**/BooksRestAPI.csproj'
arguments: '--configuration $(buildConfiguration) --output $(Build.ArtifactStagingDirectory)'
zipAfterPublish: True
modifyOutputPath: false
displayName: 'Publish BooksRestAPI'
condition: and(succeeded(), ne(variables['Build.Reason'],'PullRequest'))
Description of Pipleline code:
I wan to run the project: BooksRestAPI, and Run BooksRestAPITest, and also want to publish the API, and create an zipArtifact. I have also add and condition that build not started with Pull Request.
Run the Pipeline after updating then job is succeed and shows the Summary for API and Tests by default the Summary is displayed., and shows job success. Press to the job then shows details as following:
If you select Test then you see that all the Tests are succeed as follow:
Check Continuous Integration
- Click on pipeline, then click on 3 dots and click edit then opens the Pipline and shows Variables and Run in the upper right
- Click on 3 dots at top and click Trigger then shows Continuous Integration is enabled so this means whenever we do a commit to git repository it will automatically trigger a build shown as following figure:
Do a comment to test CI (Continuous Integration) in Visual Studio for a file (e.g. progam.cs) commit this to Git and then push to Github repository, back to the Azure DevOps and click on Pipelines: shows the BooksRestAPI begins to build the pipeline, by showing just running in the right side of Pipe line, when it finished press to the Runs in the menu then shows the newly run Pipeline as shown in the following figure:
Publish Artifact
Edit the yml Pipline by adding the following in the end of yml file:
- task: PublishBuildArtifacts@1
inputs:
pathToPublish: '$(Build.ArtifactStagingDirectory)'
artifactName: BooksRestAPIArtfact
displayName: 'PublishBuildArtifacts for BooksRestAPI'
- task: DownloadBuildArtifacts@0
inputs:
buildType: 'current'
downloadType: 'single'
artifactName: 'BooksRestAPIArtfact'
downloadPath: '$(System.ArtifactDirectory)'
displayName: 'DownloadBuildArtifacts for BooksRestAPiI'
The new yml file shall be as following:
name : NetCore-BooksRestAPI-CI
trigger:
branches:
include:
- master
paths:
include:
- BooksRestAPI/*
pool:
vmImage: 'ubuntu-latest'
variables:
buildConfiguration: 'Release'
steps:
- task: DotNetCoreCLI@2
inputs:
command: 'restore'
projects: '**/BooksRestAPI*.csproj'
displayName: 'Restore Nuget Packages'
- task: DotNetCoreCLI@2
inputs:
command: 'build'
projects: '**/BooksRestAPI*.csproj'
arguments: '--no-restore'
displayName: 'Build projects'
- task: DotNetCoreCLI@2
inputs:
command: 'test'
projects: '**/BooksRestAPITest.csproj'
arguments: '--no-restore --no-build'
displayName: 'Run Tests'
- task: DotNetCoreCLI@2
inputs:
command: 'publish'
publishWebProjects: false
projects: '**/BooksRestAPI.csproj'
arguments: '--configuration $(buildConfiguration) --output $(Build.ArtifactStagingDirectory)'
zipAfterPublish: True
modifyOutputPath: false
displayName: 'Publish BooksRestAPI'
condition: and(succeeded(), ne(variables['Build.Reason'],'PullRequest'))
- task: PublishBuildArtifacts@1
inputs:
pathToPublish: '$(Build.ArtifactStagingDirectory)'
artifactName: BooksRestAPIArtfact
displayName: 'PublishBuildArtifacts for BooksRestAPI'
- task: DownloadBuildArtifacts@0
inputs:
buildType: 'current'
downloadType: 'single'
artifactName: 'BooksRestAPIArtfact'
downloadPath: '$(System.ArtifactDirectory)'
displayName: 'DownloadBuildArtifacts for BooksRestAPiI'
Run the Pipe line then you see that it is success, click on the job you see the following:
As we see 1 Artifact is produced. By clicking on the link of ‘1 artifact produced’ shows the following:
Press to the BooksRestAPIArtifact to download it. A file with name a.zip is downlaoded, take it to somewhere in your local machine, expand and run it with the following command:
>dotnet BooksResultAPI.dll
with run of this command on command line the following is displayed:
If we start browser with URL: http://localhost:5000, then shows page can’t be loaded.
The reason is that in the Program.cs we have the following configuration:
if (app.Environment.IsDevelopment())
{
app.UseSwagger();
app.UseSwaggerUI();
}
This means that Swagger UI can be loaded in the development Environment not in the Production.
We move the two line out of if stament and add the two lines for add.UseSwagger() as following:
if (app.Environment.IsDevelopment())
{ }
app.UseSwagger();
app.UseSwaggerUI(c =>
{
c.SwaggerEndpoint("/swagger/v1/swagger.json", "Book API V1");
c.RoutePrefix = string.Empty; });
Commit this changing and push to Github. DevOps Pipeline begins to run the changes and creates a new artifact BooksRestAPIArtifact again and run as before, in this time the Swagger UI is loaded as follow:
press to press to dropdown of GET and press to Try it out then execute then you can get all the books from Database as follow:
That was all for creating a Azure DevOps CI/CD Pipelines in this post.
The yml file: Pipelines/azure-pipelines-CI-restapi.yml can be found in my Github
Next step is to publish our Local DB to Azure Cloud.
Publish SQL Local DB to Azure SQL Server
Until now we are using a local SQL server for testing purposes. In this part, we will publish the SQL Local DB to Azure.
1. Login to Azure Portal and click on Resource Groups
2. Click on Create button as shown in the figure above to create a new resource group. Give it a proper name(e.g. BookRestRG) and click on Review+Create button as shown in the bellow figure:
Then press to the Create Button, now we have the Resouce Group: BookRestRG is created.
3. Press to the Resource group BookRestRG and click on Create button and in the search field search Sql Server and select Azure SQL from the Marketplace as follow figure:
4. Select Azure SQL and then press to the Create button
5. In the Select SQL Deployment option, choose the SQL Databases and Resource type as Database Server:
6. Click Create button and then enter a server name select SQL Authentication, and user name, and password for SQL Authentication and save user name and password for using later.
7. Click on the Review + create button and then after press to Create, it takes some minutes, when it finish, press to Go to Resource, then it shows SQL Server bookrest is created as follow:
8. Copy the server name (bookrest) we will need it to publish our SQL DB from local SQL Server to Azure SQL Server in next step.
9. Open SQL server management Studio (SSMS) 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
When I open SSMS: Object Explorer then I see that there is no BookDBRestAPI database to deploy, because for me the database has been created in the Visual Studio Local DB (under Connected Services). I should create this Database in my SQL Server, for this I should change the connection string from:
"DefaultConnection": "Data Source=(localdb)\\MSSQLLocalDB;Initial Catalog=BookDBRestAPI;Integrated Security=True"
To the following Connection string:
"DefaultConnection": "Data Source=CND7496N83\\SQLExpress; Catalog=BookDBRestAPI; Integrated Security=true;"
I have just have changed from (localdb)\\MSSQLLocalDB to CND7496N83\\SQLEXPress which is my Server name in my local machine.
Now I run the following command from NuGet Package Manager in Visual studio:
PM> add-migration InitialCreate
PM>update-database
In command line 2, I have got the following error:
Error Number:-2146893019,State:0,Class:20
A connection was successfully established with the server, but then an error occurred during the login process. (provider: SSL Provider, error: 0 – The certificate chain was issued by an authority that is not trusted.)
I have solved this problem by adding Encrypt=False in the end of ConnectionString as following:
"DefaultConnection": "Data Source=CND7496N83\\SQLEXPress; Initial Catalog=BookDBRestAPI; Integrated Security=true; Encrypt=False;"
Then run the commands:
PM>add-migration InitialCreate
PM>update-database
This time the Database BooksDBRestAPI is created in My SQL Server as following:
Now we can test this database and add some books to it before deploying to Azure SQL Server.
I have added to books via Swagger UI and database has the following books:
Now we can deploy deploy this DB to Azure SQL Server.
9. Open SQL server management Studio (SSMS) and in Object Explorer right-click on Database: BookDBRestAPI you want to deploy to Azure and select Task -> Deploy Database to Microsoft Azure SQL Database:
10. In the new window that opens click Next to go to the Deployment Settings window. Click on connect button and in the popup window that opens, select Authentication: SQL Server Authentication and give the server name as the server name of SQL Server on Azure from step 8 (bookrest.database.windows.net) and login with your credentials.
Then click on the Connect button:
press to Sign in to login in to your Azure account. After login and verification following shall be displayed to as you create a new Firewall Rule in Azure to access to Azure SQL Server from your local machine.
Press to Ok (you shall do this later)
Click on Finish button:
After process everything is fine:
Press to Close and go to the Azure Portal and under SQL Server (bookrest) select SQL Database the you see that BookDBRestAPI is deployed to Azure SQL Server as following:
Test this DB from Visual Studio by a new Connection string to Azure SQL Server as follow:
"DefaultConnection": "<Server name>;Initial Catalog=BookDBRestAPI;User Id=<User ID>;Password=<Password>"
Where Server name is Azure SQL Server (bookrest.database.windows.net) and User Id and Password is Azure SQL Server credential.
That was all to deploy SQL Database to Azure SQL Server and now we have a Database in Azure.
Next step is to create an API App in Azure.
Create API App In Azure Portal
We need an API App in cloud to deploy our Rest API (BooksRestAPI) to this App.
- Go to Azure portal select your Resource group you have created before when you want deploy your database (BookRestRG) and then press to Create button, Search for api app and select API App from the Marketplace as follow:
2. Select API App (as in the figure above) and press to the Create button then fill the required fields as follow:
I have give name: BooksRestAPIApp, Region: North Europe ( as before) , Run Time stack: .NET 6, Operating System: Windows, Publish: Code, Windows plan: BooksRestAPIPlan, Pricing plan: Standard S1.
3. press to Review + Create and then press to Create button then creating the API App takes progress and after about 2 minutes ready, Press to the Go to Resource then you have the following API App in Azure:
As you se the API app: BookResAPIApp is created with URL : https://booksrestapiapp.azurewebsites.net.
4. Start your web browser with this URL.
5. Copy the URL ( https://bookrestapiapp.azurewebsites.net), we will need it to set up our release pipeline in next step.
That was all creating of API App In Azure.
The next step is to create Release Pipe line in Azure DevOps
Create Azure Release pipeline in Azure DevOps
Go to Azure DevOps and open your the Project you have created when you have created Pipeline, in my case: BooksRestAPI. Click on Pipelines -> Releases:
2. Click on New Pipeline:
3. In the list of templates on the right side, choose Azure App Service deployment:
4. Give an appropriate stage name( Book Rest API Azure deployment) and close the popup for stage by pressing to the cross (X).
5. Click on Task. Choose your subscription, App type as API App, and App Service name as the name of your API App on Azure.
6. Click Save. After that a popup will be displayed
7. click Ok on the popup
8. Click on Pipeline on the menu (as shown in the bellow) and click the Add button to add an artifact. Choose appropriate values, for project, Source, version, etc. in the dropdowns.
9.Press to Add button:
10. Click on the bolt icon and enable the Continuous deployment trigger:
11. Enable the Continuous deployment trigger
12. Click on Save button in the upper right and give a comment and press to OK button.
Saving the Release Pipeline.
That was all for creating Release Pipeline in Azure DevOps.
Next step is testing this Release Pipeline.
Testing the Release Pipeline
- Open Visual Studio for BooksRestAPI and do a comment and then commit the changes and push to Github.
- Check the DevOps and look to the Pipeline if everything is set up correctly it will trigger a new build automatically.
- By checking our Pipeline which we have created in step 3, find that it is triggered and a new build has been done.
- Press to Pipelines and Releases then you see that Release process has been done automatically:
5. Enter the API app URL (https://booksrestapiapp.azurewebsites.net) in the browser to make sure everything works perfectly:
6. press to the GET operation then you shall get an 500 code Internal error.
To solve this problem you need add IP address of API App to the SQL Server Firewall Rule list as following:
- In Azure portal, top search bar, search for your SQL Server( bookrest) which you have created earlier and select it from the results.
- On the left navigation, select Networking.
- On the Public access tab, select Add your client IPv4 address (xx.xx.xx.xx) to add a firewall rule that will allow you to access the database shown as bellow:
Try test again GET Operation in Swagger UI, to get all books in the Azure SQL DB.
Source code can be found in my Github
Cleanup all resources
When you are finished, delete all created resources in your Azure account, by deleting the Resource groups: BookRestRG and one extra Resource group is created.
Conclusion
In this post I have created Rest API with .NET 6 in Visual Studio 2022 and connected to SQL DB via EF, After that have build a CI Pipeline in Azure DevOps and tested the Rest API. Then deployed SQL local DB to Azure SQL, Created API App in Azure Portal and in the end have created Release Pipeline in Azure to deploy Rest API to Azure API App automatically when code is changed in VS2022 and pushed to Github.
This post was an CI/CD process for Rest API which is hosted on the Azure cloud. Every thing should be done automatically from implementation test to deployment for end users.
This post is part of Azure DevOps step by step.