Cover Image for Build a Powerful Resume-Ready Web Application with .NET: A Step-By-Step Guide (Part 1)

Build a Powerful Resume-Ready Web Application with .NET: A Step-By-Step Guide (Part 1)

By Charlie Dalldorf on

My Motivation

I believe that one of the best ways to get your hands dirty with software development is to create a meaningful project. The issue that comes up with a lot of online guides and textbooks is the isolated nature of these projects. You end up learning a lot of “what” but not a lot of “why”.

When I was starting to learn software development, I spent a lot of time spinning my wheels on projects. I knew how to get Web and Console Applications working, but I wasn’t learning how to develop my “internal intuition”. I was in what was known as “Tutorial Hell”.

After the last couple of years of building real-world applications combined with mountains of reading, I think I have a good sense of what is needed to push past “Tutorial Hell”. This is the guide I wish existed when I started this journey years ago.

I am creating a multi-part series that tackles all of the major areas of software development that any engineer is bound to encounter on the job. Some of these areas include:

  • APIs
  • Web Applications
  • Object Relational Mapping / Databases
  • Source Control
  • Unit Testing
  • Logging
  • Linting
  • Continuous Integration / Continuous Development
  • Observability and Monitoring

The idea here is to keep the footprint of the application small and focused while tackling some of these bigger topics within the same project. Most importantly, we will answer the question of “why” we should be implementing these things.

I can not get through every major attribute of software development. There are entire books spent on each of those topics. For brevity, I have omitted some of these. That is not to say that they are not important, but they are slightly out-of-scope for the intended outcome of this series. When some of these topics arise (such as software architecture, cloud solutions, design patterns, etc), I will provide links for more reading material outside of this series.

The other note I wanted to let you know is that I will walk through and explain each major checkpoint in the project. If a new concept we haven’t seen before comes up, I will sit down and explain as much as I need to for that section. Even though this is considered a beginner guide, I want there to be plenty of meat on the bone for you to chew on.

If you are already well-versed in some of these topics, feel free to skip over these sections.

What Are We Building?

We will be building a Blazor Server Front-End with a Web API Back-End using .NET 7.0. SQL Server will be the Database where we will be storing data.

If you want to follow this guide with a different tech stack but apply the same principles, you are more than welcome to do so. Just be aware you may have to go off the beaten path to get the same tooling and implementation. In the future, I may tackle this same set of topics using a different programming language. I am using .NET because that is what I am most comfortable with.

At the end of the day, any modern programming language can complete this project. That includes Python, Golang, NodeJs, etc.

image 2

Table of Contents

  • .NET Web API Back-End with EFCore and SQL Server (Part 1 — You are here)
  • Blazor Server Front-End with Bootstrap (Part 2)
  • Git, GitHub, and Setting Up Our Repository (Part 3)
  • Unit Testing with xUnit, Linting, GitHub Actions, and Git Hooks (Part 4)
  • Writing out XML Documentation, Swagger, and Redoc (Part 5)
  • Health Checks and Logging with Serilog (Part 6)
  • Grafana, Prometheus, Tempo, and Loki Implementation (Part 7)

What You Will Need (Part 1)

There are going to be quite a few software installs and setups if you do not have this on your machine already. For now, let’s install these pieces of software:

What is an API?

API stands for Application Programming Interface. It allows developers to see a list of operations that are available to interact with. They are used for an endless array of implementations. For this project, our API will be used as an intermediary between the Web Application and the Database.

Speaking of which, we will be interacting with the database using CRUD methods (Create, Read, Update Delete). We will be using Entity Framework Core as our Object Relational Mapper instead of writing raw SQL queries. This will help standardize and simplify our interactions with the database.

You could say that I am adding an unneeded abstraction by creating an API to sit between the front end and the database. My counterpoint to that is understanding and utilizing APIs is becoming more and more important in the modern era of software development. Not only do you need to understand how to use other business’s APIs, but you need to be able to create your own.

This is why I am creating an entire section dedicated to documenting your API. You need to know how to use your own API and document for other people to use.

What is REST?

REST stands for Representational State Transfer. To summarize, it is a series of architectural decisions and constraints to create reliable services to interact with. It is similar to the idea of ACID in SQL.

Roy Fielding is the originator of coming up with this style of interaction between applications and services. You can read his original work here.

Creating our Back-End Web API

First thing first, let’s create our back end. Open up Visual Studio Community 2022 and select ASP.NET Core Web API as your new project.

image 2

Give your Project a name that you feel comfortable with. For this project, I will be using TodoApplication as the project name.

image 3

On the Additional Information page, keep everything as is.

image 4

image 5

Everything should be running smoothly so far. You should see your IDE look like this:

image 6

Installing our NuGet Packages

NuGet is Microsoft’s repository where packages are hosted and installed from. This is similar to Javascript’s NPM. The good thing about NuGet is that it is integrated right inside of Visual Studio. To install NuGet packages, right-click on your project (TodoApplication) and select Manage NuGet Packages…

image 7

Go ahead and install:

  • Microsoft.EntityFrameworkCore — Version 7.0.15
  • Microsoft.EntityFrameworkCore.Design — Version 7.0.15
  • Microsoft.EntityFrameworkCore.SqlServer — Version 7.0.15
  • Microsoft.Extensions.DependencyInjection — Version 7.0.0

Setting Up Our Database

Open up SQL Server Management Studio and connect to localhost or what the name of your SQL Server is:

image 8

Next, open up a SQL Query and type in:

CREATE DATABASE TodoApplication

Or another name for the SQL database we will be storing our data into. If it runs successfully, we should have our database ready to interact with our project.

image 9

Folder Structure

Let’s create some new folders and files, and some of the default files in our project.

  • Create a “Data” Folder, create ./Data/DataContext.cs
  • Create a “Models” Folder, create ./Models/Todos.cs
  • Create a “Repositories” Folder, create ./Repositories/TodoRepository.cs
  • Create ./Controllers/TodoController.cs
  • Delete WeatherForecastController.cs and WeatherForecast.cs

Your project should now look like this:

image 10

Repository Pattern

If this is your first time seeing this folder structure, then great! This is called the Repository Pattern. It is a pattern where we structure an abstraction layer between the endpoint (controller), business logic (service), and data access (repository) layers. The idea here is we do not want to mix concerns in the same class. By splitting everything apart into three layers, it becomes easier to debug, test, and make enhancements.

As an aside, I have heard some people say that Entity Framework Core is a repository, and it is redundant to have a repository folder and class structure. I believe that is a fair argument. If your application is simple and lacks complex business logic, then it is fair game if you want to collapse the repository abstraction into the services.

Once your application becomes more complex and has more areas it needs to access, it does make things a bit cleaner to have that separated.

For this guide, we will keep things together in a single class.

EFCore DataContext, Our First Model and Migrating It

First, let’s build out our DataContext class. For right now, this will be pretty simple. We want to inherit DbContext from Microsoft.EntityFrameworkCore and then we will add a constructor for DbContextOptions . Next, we want to add a DbSet<Todos> property where this will be a getter and setter. Your DataContext class should look like this:

using Microsoft.EntityFrameworkCore;
using TodoApplication.Models;

namespace TodoApplication.Data
{
    public class DataContext : DbContext
    {
        public DataContext(DbContextOptions<DataContext> options) : base(options)
        {
        }

        public DbSet<Todos> Todos { get; set; }
    }
}

If we want to add extra functionality to our object relational mapper, this will be the spot where we will do so.

Next, let’s fill out our Todos Model. We will also keep this simple:

namespace TodoApplication.Models
{
    public class Todos
    {
        public int Id { get; set; }
        public string Task { get; set; }
        public string Category { get; set; }
        public string Status { get; set; }
        public DateTime CreatedDate { get; set; }
    }
}

Then, we need to register DataContext. So let’s go to Program.cs and add a new service:

using Microsoft.EntityFrameworkCore;
using TodoApplication.Data;

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddDbContext<DataContext>(options =>
    options.UseSqlServer(builder.Configuration.GetConnectionString("Localhost")));

// Don't write this - Other Program.cs defaults....

Go to appsettings.json and add a Connection String called localhost:

{
  "Logging": {
    "LogLevel": {
      "Default": "Information",
      "Microsoft.AspNetCore": "Warning"
    }
  },
  "ConnectionStrings": {
    "Localhost": "Server=localhost;Database=TodoApplication;Trusted_Connection=True;Encrypt=False"
  },
  "AllowedHosts": "*"
}

I will get to why we have to use Encrypt = False in a second.

Last, we need to migrate our object model Todos and create a table in SQL Server. EFCore has built-in functionality to do this. We can open up our Terminal and change our directory to our root solution location. Then we can execute these two commands:

dotnet ef migrations add InitialCreate
dotnet ef database update

You should see some SQL DDL commands at the bottom of the migration:

image 11

When we open up SQL Server Management Studio, we can see that our table is magically available:

image 11p2

And we double check our table definition:

image 12

Entity Framework Core - Object Relational Mapper

Before we move on, we should touch on what exactly Entity Framework Core is. EFCore is an Object Relational Mapper (ORM) that creates a bridge between backend software and databases. It allows you to read and write changes to the database without having to write raw SQL in your backend code. It also makes the process of mapping objects from the database and into memory much easier.

Even if you are strong in writing SQL and understanding relational databases, I still recommend using an ORM in the backend. Your code becomes cleaner when you are interacting with the database. Also, the way that EFCore explicitly forces developers to set each model that the application interacts with makes it easier to know what exactly you are interacting with. Raw SQL and viewing SQL objects in SQL Server Management Studio tend to "hide" things from developers.

Why Are We Not Encrypted?

Previously with Microsoft.EntityFrameworkCore, it allowed unencrypted connections from applications to the database. With version 7.0 onwards, EFCore forces encrypted connections because of an update to Microsoft.Data.SqlClient. To bypass this, there are two options available:

  • Acquire and apply an SSL Certificate to your SQL Server.
  • Set Encrypt = False in the connection string.

Getting an SSL Certificate and applying it to our database is outside the scope of our project. So we will do the latter option instead. Be aware that this is NOT RECOMMENDED in a production environment. In our local environment at home, this is okay.

Please consult your Manager(s), Database Administrators, and/or System Engineers when applying SSL certificates on a production SQL Server. This may have unforeseen side effects depending on how your environment is set up and other applications connecting to it.

You can read about this in full at Breaking changes in EF Core 7.0 (EF7)

For Our Todo Model, What is a Primary Key and DatabaseGenerated?

A Primary Key is a unique identifier for a data row in a SQL database. This is required if our models have relationships with each other (Foreign Keys). The attribute above int id tells EFCore to let SQL Server generate an incrementing integer every time a new value gets inserted.

Note: If you are creating a web application that is client-facing, I would recommend against doing an incrementing integer for your primary key. Tech-savvy customers can guess and open up places in your application if your authorization/authentication is not air-tight. As an alternative, consider using a generated GUID or some other string. This will make your joins slower since your primary key is a VARCHAR instead of an INT, but it will be more secure.

Building Our TodoRepository Class

First, let’s build out our IGenericRepository. This will be the basis of all of our interactions with the database.

namespace TodoApplication.Repositories
{
    public interface IGenericRepository<T> where T : class
    {
        public Task<IEnumerable<T>> GetAll();
        public Task<T> GetById(int id);
        public Task<T> Add(T entity);
        public Task Update(T entity);
        public Task Remove(T entity);
    }
}

Then we will construct the concrete implementation of that interface in GenericRepository:

using Microsoft.EntityFrameworkCore;
using TodoApplication.Data;

namespace TodoApplication.Repositories
{
    public class GenericRepository<T> : IGenericRepository<T> where T : class
    {
        protected readonly DataContext DataContext;
        public GenericRepository(DataContext DataContext) => this.DataContext = DataContext;

        public async Task<IEnumerable<T>> GetAll()
        {
            return await this.DataContext.Set<T>().ToListAsync();
        }

        public async Task<T> GetById(int id)
        {
            return await this.DataContext.Set<T>().FindAsync(id);
        }

        public async Task<T> Add(T entity)
        {
            await this.DataContext.Set<T>().AddAsync(entity);
            await this.DataContext.SaveChangesAsync();
            return entity;
        }

        public async Task Update(T entity)
        {
            this.DataContext.Set<T>().Update(entity);
            await this.DataContext.SaveChangesAsync();
        }

        public async Task Remove(T entity)
        {
            this.DataContext.Set<T>().Remove(entity);
            await this.DataContext.SaveChangesAsync();
        }
    }
}

This allows the DataContext dependency inside GenericRepository to work properly.

T’s, Interfaces, and Wheres oh my!

If you have never seen this type of abstraction before, it can be overwhelming. Let’s walk through it step by step.

What is happening in IGenericRepository is we are declaring several methods that will be standard across all classes that implement it. Instead of creating an interface specific TodoRepository that has a contract for all of these methods, we create a generic interface and give it an object model to which those methods will be applied.

You may have to read that a couple of times for it to sink in.

Dependency Injection and Inversion of Control

This is another concept that is being utilized in GenericRepository. Dependency Injection is when one class is using the functionality of another class.

DataContext is being passed into the constructor so we can use it throughout the GenericRepository class. This means GenericRepository depends on DataContext for its methods.

For GenericRepository to receive DataContext something must be providing that dependency, right? That is what builder.Services is doing in Program.cs. It registers a service and allows it to be used wherever in the application. It keeps all of our boilerplate code nice a neat.

Apply Generic Interfaces to TodoRepository

Let’s add IGenericRepository<T> to ITodoRepository:

using TodoApplication.Models;

namespace TodoApplication.Repositories
{
    public interface ITodoRepository : IGenericRepository<Todos>
    {
    }
}

And add GenericRepository<T> and ITodoRepository to TodoRepository:

using TodoApplication.Data;
using TodoApplication.Models;

namespace TodoApplication.Repositories
{
    public class TodoRepository : GenericRepository<Todos>, ITodoRepository
    {
        public TodoRepository(DataContext dataContext) : base(dataContext)
        {
        }
    }
}

So it looks like there is not a whole lot going on here. What is happening is TodoRepository is implementing the methods defined in IGenericRepository by using the Todos object model. Task<IEnumerable<T>> GetAll() becomes Task<IEnumerable<Todos>> GetAll().

In future portions of this series, we will define more methods specific to TodoRepository. For now, just the generics are good.

Lastly, we need to add these interfaces and their concrete implementations to our services in Program.cs:

using Microsoft.EntityFrameworkCore;
using TodoApplication.Data;
using TodoApplication.Repositories;

var builder = WebApplication.CreateBuilder(args);

// From earlier
builder.Services.AddDbContext<DataContext>(options =>
    options.UseSqlServer(builder.Configuration.GetConnectionString("Localhost")));

builder.Services.AddScoped(typeof(IGenericRepository<>), typeof(GenericRepository<>));
builder.Services.AddScoped<ITodoRepository, TodoRepository>();

// ... Other services below here

What is AddScoped?

Taken from this StackOverflow Post:

Transient objects are always different; a new instance is provided to every controller and every service.

Scoped objects are the same within a request, but different across different requests.

Singleton objects are the same for every object and every request.

Taken from TekTutorialsHub, this is a pretty good image representing the difference between transient and scoped:

image 13

Essentially, we want our Repository classes to be scoped because we could have multiple interactions with the database in a single request. We want the lifetime for that object to be the same per request, and then dispose of it after we are finished interacting with the database.

Language Integrated Query (LINQ)

This is the one area that gets neglected the most when talking about beginner projects and .NET Web Applications. Language Intergrated Query (LINQ) is a set of commands that allow software developers to interact with objects similarly to SQL. The advantage here is we do not need to write raw SQL to interact with the database. We use EFCore to pull data and map the objects. Then we use LINQ to manipulate the data further in-memory.

There are times when I break from this standard. The first instance is when your business logic is starting to become complex. When you have to join multiple models together, aggregate data, or both, I start to consider utilizing Stored Procedures to handle these operations. LINQ, which we will get to in a bit, likes to get messy when things get complicated.

The other instance is complicated update patterns. If you are performing an UPSERT or MERGE in the database, this would be better handled with a Stored Procedure. It’s difficult to get the proper inserts, updates, and deletes all in one operation with just LINQ.

Making Our TodoController Endpoints

The last thing we will do is implement all of our controller methods. We will inject ITodoRepository into TodoController and create all of the endpoints inherited from IGenericRepository.

using Microsoft.AspNetCore.Mvc;
using TodoApplication.Models;
using TodoApplication.Repositories;

namespace TodoApplication.Controller
{
    [ApiController]
    [Route("api/[controller]")]
    [Produces("application/json")]
    public class TodoController : ControllerBase
    {
        private readonly ITodoRepository TodoRepository;
        public TodoController(ITodoRepository TodoRepository)
        {
            this.TodoRepository = TodoRepository;
        }

        [HttpGet]
        public async Task<IActionResult> GetAllTodos()
        {
            return Ok(await this.TodoRepository.GetAll());
        }

        [HttpGet("{id}")]
        public async Task<IActionResult> GetAllTodos(int id)
        {
            return Ok(await this.TodoRepository.GetById(id));
        }

        [HttpPost]
        public async Task<IActionResult> AddTodo([FromBody] Todos todo)
        {
            var entity = await this.TodoRepository.Add(todo);
            return Created(nameof(todo), entity);
        }
         
        [HttpPut]
        public async Task<IActionResult> UpdateTodo([FromBody] Todos todo)
        {
            await this.TodoRepository.Update(todo);
            return NoContent();
        }
        
        [HttpDelete]
        public async Task<IActionResult> RemoveTodo([FromBody] Todos todo)
        {
            await this.TodoRepository.Remove(todo);
            return NoContent();
        }
    }
}

What Are These HTTP Attributes?

These attributes tell the API what type of HTTP call to make. Mozilla Developer will have more in-depth information on these methods.

The four main methods an API will have are GET, POST, PUT, and DELETE. These line up with the CRUD application we are creating where we read, create, update, and delete records to the database. They both share similarities, but there are some differences. Going through those differences is out-of-scope for this series.

Running our Web API Application and Testing with Postman

When running your application, you should see your primary web browser open up with an index page that looks like this:

image 14

If you can see that, then your application is working properly. We will be returning to this portion of the application in Part 5 of this series.

Open up Postman and Create a new collection called TodoApplication. In that collection, add our five controller methods in there with the proper HTTP calls.

image 15

Next, let’s fill out the GetAll HTTP Call. Put in the URI https://localhost:<Your SSLPort>/api/Todo where SSLPort is the defaulted value in launchSettings.json

image 16

image 17

Now we will click theSend button. If everything works correctly, we should get a HTTP 200 Status Code and an empty JSON payload.

image 18

What we don’t want is a 404 (Not Found) or 500 (Internal Server Error) and a blank JSON payload. If you see this, then something is wrong in your application and you need to retrace some steps.

image 19

Anyhow, it is not that exciting looking at nothing. So let’s add some values in there.

Go to AddTodo and copy and paste the URI for GetAll into AddTodo URI. Then go to the Body tab, select raw for the dropdown, and JSON right next to it. In the body of our HTTP POST Request, create a payload that looks similar to this:

{
    "Task": "<Insert Task Here>",
    "Category": "<Insert Task Here>",
    "Status": "<Insert Task Here>",
    "CreatedDate": "<Insert Task Here>"
}

image 20

Send this POST Request and if everything goes well, we should get an HTTP 201 Created status code and an auto-incremented Id 1!

image 21

Feel free to send a couple more POST Requests through. Then let’s go back to our GetAll HTTP Get Request and pull those records back.

image 22

Go to the GetById HTTP GET Request and use the same URI but add a /1 at the end of the URI. If we send a request, we should get back our first record:

image 23

Now let’s go to UpdateTodo. I realized I still have some bad habits I need to break because I typed too many capitalized words for that task description. So let’s update it! Send a new JSON payload through that has our updates in it and then check the output using GetById.

image 24

In the end, I decided that todo is a bit on the nose. So I am going to delete it. Grab the last full body of the record you want to remove and paste it in the JSON payload and send it in. Double check that it got deleted in GetAll.

image 25

image 26

What is With These Status Codes Anyway?

Mozilla Developer is a great resource to read through on these HTTP response status codes. Status codes give you information on what to expect on a successful or unsuccessful HTTP response. It is unlikely you will be using all of the status codes available on any one project. The most common ones you will see are 200, 201, 204, 404 and 500.

And yes, the numbers are not made up and they do matter.

Take Aways

I hope this was a helpful guide for you. This is a good stopping place after creating our Web API and demonstrating that it is working properly.

Coming up next is setting up our Blazor Server Web Application and interacting with the API we have created here.

Stay tuned and see you next time!

If you want to see the finished code for this article, it can be found here in this GitHub Repository

Additional Documentation and Reading Material

Microsoft Documentation — Infrastructure Persistence Layer — Repository Pattern

Microsoft Documentation — Entity Framework Core

Microsoft Documentation — Language Intergrated Query

Roy Fielding’s Chapter on Representational State Transfer (REST)

Mozilla Developer Documentation — REST

Mozilla Developer Documentation — HTTP Methods

Mozilla Developer Documentation — HTTP Response Status Codes