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
2. Give a meaningful name for your project and click Next
3. Choose the framework as .NET 6 and click create
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
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.
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.
Add a new Controller
1. In solution explorer, right-click on Controllers folder -> Add -> Controller...
2. Choose API-> API Controller - Empty and click Add
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
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
3. Give a proper project name and choose the visibility as Public. The visibility selected here should match the visibility of the Github repo
4. Hover your mouse over to pipeline and click on pipeline in the popup menu
5. Click on Create Pipeline button
6. Choose the appropriate repository where your code is located. For me its github so I will be selecting that
7. Select your code repository
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
11. In the popup window you can change the settings if you want or leave them to default and click on save and run
12. If everything goes well it will create a pipeline for you.
13. Click on pipeline, then click on 3 dots and click edit
14. Click on 3 dots at top and click Trigger
15. Continuous Integration is enabled so this means whenever we do a commit to git repository it will automatically trigger a build
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
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
2. Click on Create button to create a new resource group. Give it a proper name and click on Review+Create button.
3. Open the resource group and click on Create button
4. Search for sql server in the search box
5. Select Azure SQL from the list of Marketplace suggestions and click Create
6. In the Select SQL Deployment option, choose the SQL Databases and Resource type as Database Server and click Create button
7. Enter a server name and user name, and password for SQL Authentication. Click on the Review + create button.
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
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
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.
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
2. Choose the appropriate settings and create the resource
3. Open the resource and copy the URL, we will need it to set up our release pipeline
4. Go to Azure DevOps and open your project. Click on Pipelines -> Releases
5. Click on New Pipeline
6. Choose Azure App Service deployment
7. Give an appropriate stage name and close the popup
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
9. Click on Pipeline again and click the Add button to add an artifact. Choose appropriate values in the dropdown and click Add
10. Click on the bolt icon and make sure the Continuous deployment trigger is enabled.
11. Click on save and give a comment and click Ok
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
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