Demo Web API application for GitHub Copilot implementations focused on the .NET 8 framework.
- Create and configure a new dotnet api project.
- Configure database connection.
- Service abstraction.
- Configure CRUD methods and REST Controllers.
- Ask Copilot CLI for the command to generate a new .NET API project
gh copilot suggest "How to create a .NET webapi project"Copilot CLI Response:
Suggestion:
dotnet new webapi --use-controllers -n YourProjectName
? Select an option
> Copy command to clipboard
Explain command
Execute command
Revise command
Rate response
exit- Select the
Copy command to clipboardoption and change the project name to Marena.API
dotnet new webapi --use-controllers -n Marena.API- Generate a new solution file named Marena
dotnet new sln -n Marena- Use Copilot CLI to add the Marena.API project to the solution file created in the previous step.
gh copilot suggest "How can I add the Marena.API project to a solution file .sln"Copilot CLI Response:
dotnet sln Marena.sln add Marena.API/Marena.API.csproj- Open the solution file with Visual Studio.
Project structure obtained:
- Create a SQL Server docker container.
gh copilot suggest "How can I run Microsoft SQL Server in a docker container?"
docker run -e 'ACCEPT_EULA=Y' -e 'SA_PASSWORD=YourPassword123' -p 1433:1433 -d mcr.microsoft.com/mssql/server:2019-latest- If you don't have Docker installed on your PC, you can install it from here.
- If Docker is a limitation, you can install SQL Server Express from here.
- Validate connection with SQL Server.
You can test the connection using any database management tool like SQL Server Management Studio, Azure Data Studio, DBeaver, or Datagrip.
- Using Copilot CLI, generate the commands to use.
gh copilot suggest "How can I correctly install and configure Entity Framework Core?"dotnet add package Microsoft.EntityFrameworkCoredotnet add package Microsoft.EntityFrameworkCore.Toolsdotnet add package Microsoft.EntityFrameworkCore.Design
-
Using Copilot Chat, request to generate a class that inherits from DBContext.
Create a class named MarenaDBContext that inherits from DBContextusing Microsoft.EntityFrameworkCore; public class MarenaDBContext : DbContext { public MarenaDBContext(DbContextOptions<MarenaDBContext> options) : base(options) { } // Define DbSet properties for your entities here // public DbSet<YourEntity> YourEntities { get; set; } }
-
Create a folder named
Persistenceand add theMarenaDBContextclass suggested by Copilot inside that folder (Using the "Copy" or "Insert in new file" button). -
Register the MarenaDBContext class in the services loaded by the builder in the Program.cs file.
Register the MarenaDBContext in the `builder.services` method in the Program.cs file// Register MarenaDBContext with the dependency injection container builder.Services.AddDbContext<MarenaDBContext>(options => options.UseSqlServer(builder.Configuration.GetConnectionString("DefaultConnection")));
-
Configure the default connection string to the SQL Server database.
Add default connection string to appsettings.json{ "ConnectionStrings": { "DefaultConnection": "Server=your_server_name;Database=your_database_name;User Id=your_user_id;Password=your_password;" } }
It may happen that at this point, an error is generated when calling the UseSqlServer method. Using Copilot, we can investigate the cause of the error.
To solve it, we do the following:
- Select the method we just added.
- Right-click on the selection and select the "ask copilot" option.
- In the chat window, use the
/explaincommand. /explain Why the UseSqlServer is highlighted as an error?
Copilot mentions that we forgot to add the reference to the Microsoft.EntityFrameworkCore.SqlServer package. There are several ways to add this package to the project, for this occasion we will use the dotnet command line.
dotnet add package Microsoft.EntityFrameworkCore.SqlServer-
Add this value at the end of the connection string: TrustServerCertificate=True;
{ "ConnectionStrings": { "DefaultConnection": "Server=your_server_name;Database=your_database_name;User Id=your_user_id;Password=your_password;TrustServerCertificate=True;" } }
-
Using Copilot Chat, ask how to generate an entity named Movie, which contains the following attributes: Id, Name, Score, Genres, Year.
Generate an entity named Movie with the following attributes: Id, Name, Score, Genres, Year. Use Entity Framework Core Schema validationsusing System.ComponentModel.DataAnnotations; using System.ComponentModel.DataAnnotations.Schema; public class Movie { [Key] [DatabaseGenerated(DatabaseGeneratedOption.Identity)] public int Id { get; set; } [Required] [MaxLength(100)] public string Name { get; set; } [Range(0, 10)] public double Score { get; set; } [Required] [MaxLength(200)] public string Genres { get; set; } [Range(1888, 2100)] public int Year { get; set; } }
-
Manually add the model to the previously defined DBContext.
using Microsoft.EntityFrameworkCore; public class MarenaDBContext : DbContext { public MarenaDBContext(DbContextOptions<MarenaDBContext> options) : base(options) { } // Define DbSet properties for your entities here public DbSet<Movie> Movies { get; set; } }
-
With the help of Copilot Chat, ask how to generate migrations to create the database and the Movie table.
What is the command to create migrations with Entity Framework Core?Copilot will possibly suggest two response options:
If we run everything from the Nuget Package Manager Console in Visual Studio
Add-Migration Initial_MigrationUpdate-Database
If we run everything from a terminal using dotnet cli
dotnet ef migrations add Initial_Migrationdotnet ef database update
-
Using the Copilot Chat tool, generate a controller based on the
Moviesentity created earlier.Create a .NET API Controller for #Movie.Cs Entity
Note, here we use file references with the syntax "#[fileName]" in the chat.
```C#
using Microsoft.AspNetCore.Mvc;
using Microsoft.EntityFrameworkCore;
namespace Marena.API.Controllers
{
[Route("api/[controller]")]
[ApiController]
public class MoviesController : ControllerBase
{
private readonly MarenaDBContext _context;
public MoviesController(MarenaDBContext context)
{
_context = context;
}
// GET: api/Movies
[HttpGet]
public async Task<ActionResult<IEnumerable<Movie>>> GetMovies()
{
return await _context.Movies.ToListAsync();
}
// GET: api/Movies/5
[HttpGet("{id}")]
public async Task<ActionResult<Movie>> GetMovie(int id)
{
var movie = await _context.Movies.FindAsync(id);
if (movie == null)
{
return NotFound();
}
return movie;
}
// PUT: api/Movies/5
[HttpPut("{id}")]
public async Task<IActionResult> PutMovie(int id, Movie movie)
{
if (id != movie.Id)
{
return BadRequest();
}
_context.Entry(movie).State = EntityState.Modified;
try
{
await _context.SaveChangesAsync();
}
catch (DbUpdateConcurrencyException)
{
if (!MovieExists(id))
{
return NotFound();
}
else
{
throw;
}
}
return NoContent();
}
// POST: api/Movies
[HttpPost]
public async Task<ActionResult<Movie>> PostMovie(Movie movie)
{
_context.Movies.Add(movie);
await _context.SaveChangesAsync();
return CreatedAtAction(nameof(GetMovie), new { id = movie.Id }, movie);
}
// DELETE: api/Movies/5
[HttpDelete("{id}")]
public async Task<IActionResult> DeleteMovie(int id)
{
var movie = await _context.Movies.FindAsync(id);
if (movie == null)
{
return NotFound();
}
_context.Movies.Remove(movie);
await _context.SaveChangesAsync();
return NoContent();
}
private bool MovieExists(int id)
{
return _context.Movies.Any(e => e.Id == id);
}
}
}
```
- Add this file with the name `MoviesController.cs` under the `Controllers` folder.
- Run the application directly from Visual Studio.
- Test the API using a client like **Postman**, **Insomnia**, or **Swagger**.
A good development practice is to abstract the existing logic in Controllers to a service class, which allows implementing different business actions. Delegating to the controllers the exclusive responsibility of managing the input and output of API data.
-
Ask Copilot Chat to generate the structure of the service class.
Create a service class that contains all the logic for CRUD operations of the #Models.cs entity and implement it in #MoviesController.cs. -
Create a folder named
Servicesand within it two subfolders namedInterfacesandImplementationswhere we will place the interfaces and services respectively. -
Incorporate the code suggestions provided by Copilot Chat as appropriate.
-
Insert a new file with the content of the
IMovieServiceinterface in theInterfacesfolder.using Marena.API.Models; using System.Collections.Generic; using System.Threading.Tasks; public interface IMovieService { Task<IEnumerable<Movie>> GetMoviesAsync(); Task<Movie> GetMovieByIdAsync(int id); Task<Movie> AddMovieAsync(Movie movie); Task<Movie> UpdateMovieAsync(int id, Movie movie); Task<bool> DeleteMovieAsync(int id); }
-
Insert a new file with the content of the
MovieServiceimplementation in theImplementationsfolder.using Marena.API.Models; using Microsoft.EntityFrameworkCore; using System.Collections.Generic; using System.Threading.Tasks; public class MovieService : IMovieService { private readonly MarenaDBContext _context; public MovieService(MarenaDBContext context) { _context = context; } public async Task<IEnumerable<Movie>> GetMoviesAsync() { return await _context.Movies.ToListAsync(); } public async Task<Movie> GetMovieByIdAsync(int id) { return await _context.Movies.FindAsync(id); } public async Task<Movie> AddMovieAsync(Movie movie) { _context.Movies.Add(movie); await _context.SaveChangesAsync(); return movie; } public async Task<Movie> UpdateMovieAsync(int id, Movie movie) { if (id != movie.Id) { return null; } _context.Entry(movie).State = EntityState.Modified; try { await _context.SaveChangesAsync(); } catch (DbUpdateConcurrencyException) { if (!await MovieExistsAsync(id)) { return null; } else { throw; } } return movie; } public async Task<bool> DeleteMovieAsync(int id) { var movie = await _context.Movies.FindAsync(id); if (movie == null) { return false; } _context.Movies.Remove(movie); await _context.SaveChangesAsync(); return true; } private async Task<bool> MovieExistsAsync(int id) { return await _context.Movies.AnyAsync(e => e.Id == id); } }
-
Update the
MoviesController.cscode with the implementation of theMoviesServiceservice methods.using Marena.API.Models; using Microsoft.AspNetCore.Mvc; using System.Collections.Generic; using System.Threading.Tasks; namespace Marena.API.Controllers { [Route("api/[controller]")] [ApiController] public class MoviesController : ControllerBase { private readonly IMovieService _movieService; public MoviesController(IMovieService movieService) { _movieService = movieService; } // GET: api/Movies [HttpGet] public async Task<ActionResult<IEnumerable<Movie>>> GetMovies() { var movies = await _movieService.GetMoviesAsync(); return Ok(movies); } // GET: api/Movies/5 [HttpGet("{id}")] public async Task<ActionResult<Movie>> GetMovie(int id) { var movie = await _movieService.GetMovieByIdAsync(id); if (movie == null) { return NotFound(); } return Ok(movie); } // PUT: api/Movies/5 [HttpPut("{id}")] public async Task<IActionResult> PutMovie(int id, Movie movie) { var updatedMovie = await _movieService.UpdateMovieAsync(id, movie); if (updatedMovie == null) { return BadRequest(); } return NoContent(); } // POST: api/Movies [HttpPost] public async Task<ActionResult<Movie>> PostMovie(Movie movie) { var createdMovie = await _movieService.AddMovieAsync(movie); return CreatedAtAction(nameof(GetMovie), new { id = createdMovie.Id }, createdMovie); } // DELETE: api/Movies/5 [HttpDelete("{id}")] public async Task<IActionResult> DeleteMovie(int id) { var result = await _movieService.DeleteMovieAsync(id); if (!result) { return NotFound(); } return NoContent(); } } }
-
Register the dependency injection of the
MoviesServiceservice and theIMoviesServiceinterface in theBuilder.servicemethod of theProgram.csfile.// Register the MovieService builder.Services.AddScoped<IMovieService, MovieService>();
-
Run the API again and test the correct functioning of the CRUD methods.
To run unit tests, a new .NET project dedicated exclusively to tests will be created. Right-click on the solution file in the Visual Studio solution explorer, then click on Add and then on Project.
The next step is to create a new project of type xUnit Test Project, click on next and at this point name it Marena.Tests. The .NET version to be selected should be the same as the one used in the Web API project, in this case, select .NET 8. Finally, click on Create.
With the test project created, the next step is to use GitHub Copilot to build and develop the unit tests using the following prompt:
Create unit tests for the file #file:'MoviesService.cs' in the project #file:'Marena.Tests.csproj'
This will create a suggestion in the chat for a new MoviesServiceTest file. This file should be created and can be added to the root directory of the Tests project.
Once this is done, to run the tests follow these steps:
- Open the solution.
- Build the solution.
- Open the Test Explorer Test > Test Explorer.
- Run all tests (Run All Tests).
- Using the terminal, navigate to the path of the
Marena.Testsproject.
cd path/to/Marena.Tests- Run the test execution command.
dotnet testMost likely, Copilot will try to use the Moq library to mock the database context of the project. However, this can result in many problems and errors in running unit tests in this project.
It is suggested to use Entity Framework's in-memory database instead of Moq for these tests. Add the following package to the Tests project: Microsoft.EntityFrameworkCore.InMemory.
Using the following prompt:
I have noticed that using Moq in the test project to mock the project's DBContext will result in failures 100% of the time. Refactor the unit test code to use Entity Framework InMemory Database instead.
