.NET Web API Template - My Version

·

11 min read

Background

Have you ever found yourself searching online for general guidelines on how to start a project effectively? I've done that many times and wasn't sure why I hadn't documented or shared this before. So, here we go...

I'm sure others have different opinions on the best setup to start with. This is just my version for running on Windows.

Let’s start with the IDE/applications. Make sure you install them.

1. Create a new database and a table

From my personal experience, I usually work on projects with existing databases and tables. From an Entity Framework (EF) perspective, I find it more logical to use the database-first approach instead of code-first. I don't think there is a right or wrong approach here.

Using SSMS:

  1. Create a new database named StoreABC.

  2. Create a new table named Employees.

  3. Insert a record to Employees.

  4. You can also run below SQL code in new query window.

     CREATE DATABASE StoreABC;
     GO
     USE StoreABC;
     GO
     IF OBJECT_ID(N'Employees', N'U') IS NULL
     BEGIN
     CREATE TABLE Employees (
     Id INT NOT NULL IDENTITY(1,1),
     Name VARCHAR(255) NOT NULL,
     City VARCHAR(255) NOT NULL,
     Age INT NOT NULL,
     CONSTRAINT PK_Employees PRIMARY KEY (Id)
     ) END;
     GO
     INSERT INTO Employees(Name, City, Age)
     VALUES('John Smith', 'Denver', 40);
     GO
    

This should be good enough for setup.

💡
If you are unable to log in to SSMS using Windows Authentication, it may be due to a recent Microsoft update that set encryption to 'Mandatory' by default. You can change this setting to 'Optional' to resolve the issue

2. Create a new API project

Using VS Studio 2022:

  1. Select Create a new Project.

  2. Select ASP .NET Core Web API (No Razor or native AOT). Hit Next.

  3. Name the project StoreABC. Make sure you decide your location. Hit Next.

  4. Select .NET 8 Long Term Support. Hit Create.

  5. Try running it (Debug → Start Without Debugging) and play with the Weather API.

  6. Feel free to skip the next IMHO section, but at least skim through Planning section just so you have a general idea of my approach.

3. IMHO

When I start a new side project (like StoreABC), I focus on the minimum setup needed to begin developing business solutions or features. Side projects can grow quickly (at least, that's my hope). From my experience, spending time on design and then starting to code is one way to go. However, I prefer an iterative approach to design. As I develop, I get a better understanding of the challenges ahead.

I support the SOLID principles (look them up). No matter the size or idea of the project, sticking to SOLID from the start makes future redesigns or refactoring easier. That said, it's often easier said than done. My takeaway is to keep SOLID in mind, even if execution isn't perfect.

I also want to share more about my "hands-on pattern." By following this article, you might notice a pattern in my approach. When building a new service, I lean towards the Convention over Configuration paradigm. Like everything, there are always trade-offs. Software developers must decide on the best trade-offs, not the perfect solution. That's why the typical response is "It depends."

💡
I like to think of principles like SOLID as similar to the way people live and interact with others. Design patterns, on the other hand, are like cultures or trends. Just as trends from 20 years ago may no longer be relevant, some design patterns may have become outdated over time.

3. Planning

The image above is a rough sketch of what I think should be the minimal setup:

  • Configuration - While IOptions (or IOptionsSnapshot) isn't required for configuration, I find that decoupling makes maintenance easier. Of course, using the Options pattern has more benefits.

  • Entity Framework (EF) with Repository pattern - I prefer repository classes to manage all data aggregation. This approach helps create more loosely coupled classes.

  • Error handling - The goal here is to implement a catch-all error handling system instead of using many try-catch blocks in each controller action. Additionally, it's better not to expose our call stack to the caller.

  • Logging - Logging provides tracing, giving me better visibility of application flows. In relation to error handling, logging helps us understand the call stack internally.

  • Health check - Although it's not necessary for the initial setup, setting up basic infrastructure doesn't hurt. Sometimes, I need to check for invalid connections (e.g., database). A health check can save me some time.

My preference for project folders structures.

StoreABC.csproj

  1. Businesses

    1. Interfaces
  2. Controllers

  3. Models

  4. Options

  5. Repositories

    1. Interfaces
  6. ViewModels

StoreABCUnitTests.csproj

  1. BusinessesTests

  2. RepositoriesTests

3. Configuration setup

For the initial project, we will focus on setting up appsettings to accept our SQL Express connection string. Ideally, configuration, especially for production, should be separate from the actual project. Next time, I can probably share more about key vault or secret management.

How I set up appsettings.json and appsettings.Development.json:

  • appsettings.Development.json - You need to change the Data Source value to your SQL Express server. In Visual Studio 2022, you can connect to your SQL server using Server Explorer (View → Server Explorer). Once connected, you can copy and paste the connection string property.
  •   {
        "Logging": {
          "LogLevel": {
            "Default": "Information",
            "Microsoft.AspNetCore": "Warning"
          }
        },
        "Custom": {
          "ConnectionString": "Data Source=Hendra-PC\\SQLEXPRESS;Initial Catalog=StoreABC;Integrated Security=True;Encrypt=False"
        }
      }
    
  • appsettings.json - I intentionally use the value DEFAULT as a sample to later demonstrate how each appsettings file is recognized.

      {
        "Logging": {
          "LogLevel": {
            "Default": "Information",
            "Microsoft.AspNetCore": "Warning"
          }
        },
        "AllowedHosts": "*",
        "Custom": {
          "ConnectionString": "DEFAULT"
        }
      }
    
  • Create Options folder and CustomOption.cs class

  • CustomOption.cs class will only have connection string property

      namespace StoreABC.Options
      {
          public class CustomOption
          {
              public string ConnectionString { get; set; }
          }
      }
    
  • Update Program.cs code. Notice STOREABC UPDATE START/END comment

public static void Main(string[] args)
{
    var builder = WebApplication.CreateBuilder(args);

    // 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();

    //STOREABC UPDATE START
    var environment = Environment.GetEnvironmentVariable("ASPNETCORE_ENVIRONMENT");

    builder.Configuration.SetBasePath(Directory.GetCurrentDirectory())
        .AddJsonFile("appsettings.json", optional: false, reloadOnChange: true)
        .AddJsonFile($"appsettings.{environment}.json", optional: true, reloadOnChange: true)
        .AddEnvironmentVariables();

    builder.Services.AddOptions<CustomOption>()
       .Bind(builder.Configuration.GetSection("Custom"));
    //STOREABC UPDATE END

    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();
}
  • Let's go through the code:

    • The first part of the code handles configuration. Notice that ASPNETCORE_ENVIRONMENT tells the application which environment it's in. Typically, this variable is set in the environment variables.

    • Next, the builder sets up the configurations. The order is important here—notice that appsettings.{environment}.json is last. This is because we want to override the default settings with environment-specific ones.

    • The last part of the setup is for configuring options. For simplicity, let's assume any custom configuration will be under a custom object.

  • Let's add a controller for testing purposes. Right-click on the controllers folder → Add → Controller. Name the controller EmployeesController.cs.

  • Update EmployeesController to include custom options. Simply inject IOptions into the constructor, and it's ready to use.

  •       using Microsoft.AspNetCore.Mvc;
          using Microsoft.Extensions.Options;
          using StoreABC.Options;
    
          // For more information on enabling Web API for empty projects, visit https://go.microsoft.com/fwlink/?LinkID=397860
    
          namespace StoreABC.Controllers
          {
              [Route("api/[controller]")]
              [ApiController]
              public class EmployeesController : ControllerBase
              {
                  //STOREABC START
                  private CustomOption _customOptionsValue;        
    
                  public EmployeesController(IOptions<CustomOption> customOption)
                  {
                      _customOptionsValue = customOption.Value;
                  }
                  // GET: api/<EmployeesController>
                  [HttpGet]
                  public IEnumerable<string> Get()
                  {
                      string connString = _customOptionsValue.ConnectionString;
                      return new string[] { "value1", "value2", connString };
                  }
                  //STOREABC END
    
                  // GET api/<EmployeesController>/5
                  [HttpGet("{id}")]
                  public string Get(int id)
                  {
                      return "value";
                  }
    
          //.... more code
    
  • Try running the application. When you call localhost:7232/api/Employees, you should see the connection string value from appsettings.Development.json. Note: My port is 7232, yours might be different.

  • In launchSettings.json, try changing the ASPNETCORE_ENVIRONMENT value from Development to empty. Run the application again, and enter localhost:7232/api/Employees in your browser's URL. Notice that the connection string value is DEFAULT.

  • At this point, you have set up the basic app settings with environment separation. This prepares us for the next step in scalability. Right now, we are using an API controller for testing. Spoiler alert: this is why I believe having a health check is beneficial. As the application grows, we can rely more on health checks to ensure our infrastructure is set up correctly, rather than coding a test API.

4. EF Setup

Before we set up our EF, let's make sure we install all the necessary NuGet Packages. Right-click on the StoreABC project and select "Manage NuGet Packages." We need to install:

  • Mapster - this will be used to map models to view models

  • The rests are database connection setup:

    • Microsoft.Data.SqlClient

    • Microsoft.EntityFramework.Core

    • Microsoft.EntityFrameworCore.SqlServer

To reverse engineer the database and table we set up on SQLExpress, I will use EF Core Power Tools. In Visual Studio 2022, go to Extensions → Manage Extensions and install EF Core Power Tools. You might need to close all your Visual Studio instances for the installation to proceed.

At this point, we are ready to reverse engineer. Let's find out how complex this process is:

  • Right click on the StoreABC project → EF Core Power Tools → Reverse Engineer.

  • Since this is our first time, add a database connection.

  • I find the server search to be slow, so I just enter my server name directly. As mentioned, a recent Microsoft update sets the Encrypt value to Mandatory by default. You need to change it to Optional to view your database list.

  • Test Connection. If it is good, hit OK. You will be brought back to the original pop up screen. Hit OK again.

  • Now, it’s time to choose the table you want to reverse engineer. Hit OK.

  • I use the default values EF Core Power Tools provide for settings. Hit OK.

  • Notice that EF Core Power Tools creates two classes under Models folder.

  • Last, we need to add our newly created StoreABCContext on Program.cs. That’s it!

    public static void Main(string[] args)
    {
        var builder = WebApplication.CreateBuilder(args);

        // 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 environment = Environment.GetEnvironmentVariable("ASPNETCORE_ENVIRONMENT");

        builder.Configuration.SetBasePath(Directory.GetCurrentDirectory())
            .AddJsonFile("appsettings.json", optional: false, reloadOnChange: true)
            .AddJsonFile($"appsettings.{environment}.json", optional: true, reloadOnChange: true)
            .AddEnvironmentVariables();

        builder.Services.AddOptions<CustomOption>()
           .Bind(builder.Configuration.GetSection("Custom"));

        //STOREABC START
        builder.Services.AddDbContext<StoreABCContext>(options =>
        {
            options.UseSqlServer(builder.Configuration.GetSection("Custom")["ConnectionString"]);
        });
        //STOREABC END

        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();
    }
}

At this point, we have all the basic setup complete. Next, we need to add more business solutions to our application.

5. Health check

Here are the NuGet packages needed to have minimum health check:

  • Microsoft.Extensions.Diagnostics.HealthCheck

  • ASPNetCore.HealtChecks.UI.Client

First, create HealthChecks folder and then create SQLHealthCheck.cs class

Update SQLHealthCheck.cs with code below.

using Microsoft.Extensions.Diagnostics.HealthChecks;
using Microsoft.Extensions.Options;
using StoreABC.Options;
using System.Data.SqlClient;

namespace StoreABC.HealthChecks
{
    public class SQLHealthCheck : IHealthCheck
    {
        private readonly string _connString;
        public SQLHealthCheck(IOptions<CustomOption> customOption)
        {
            _connString = customOption.Value.ConnectionString;
        }
        public async Task<HealthCheckResult> CheckHealthAsync(HealthCheckContext context,
            CancellationToken cancellationToken = default)
        {
            try
            {
                using var sqlConnection = new SqlConnection(_connString);

                await sqlConnection.OpenAsync(cancellationToken);

                using var command = sqlConnection.CreateCommand();
                command.CommandText = "SELECT 1";

                await command.ExecuteScalarAsync(cancellationToken);

                return HealthCheckResult.Healthy();
            }
            catch (Exception ex)
            {
                return HealthCheckResult.Unhealthy(context.Registration.FailureStatus.ToString(), exception: ex);
            }
        }
    }
}

I credit codeman article for the SQLHealthCheck setup above. If you haven't noticed, there are many health check methods available online, including those with a nice UI. Personally, I'm fine with using a JSON result to check the application's health.

I'm sure you understand the basic idea of what the code does. Essentially, it performs a health check on our SQL Express.

In program.cs, you'll need to add AddHealthCheck and MapHealthChecks.

//....code before
builder.Services.AddSwaggerGen();
builder.Services.AddHealthChecks()
    .AddCheck<SQLHealthCheck>("SQL Express Database");

//... code in between

app.MapHealthChecks("/api/health", new HealthCheckOptions
{
    ResponseWriter = UIResponseWriter.WriteHealthCheckUIResponse
});

app.Run();

//... close curly braces

That's it. Test this by running the application and going to https://localhost:<port>/api/health. You should see the health status of your application. Try entering an incorrect connection string to see how it affects the health status.

6. Global error handling

In general, we want to avoid showing our stack trace to clients. We need to ensure that all exceptions are handled properly. However, it's easy to overlook or not anticipate every possible exception. Therefore, having global error handling is a good idea. I can't explain it better than Tim Corey on his YouTube Channel.

After watching his video, update your Program.cs by adding UseExceptionHandler.

//... code before
app.UseHttpsRedirection();
app.UseAuthorization();
app.MapControllers();

app.UseExceptionHandler(appError =>
{
    appError.Run(async context =>
    {
        context.Response.StatusCode = (int)HttpStatusCode.InternalServerError;
        context.Response.ContentType = "application/json";

        var contextFeature = context.Features.Get<IExceptionHandlerFeature>();
        if (contextFeature is not null)
        {
            await context.Response.WriteAsJsonAsync(new
            {
                StatusCode = context.Response.StatusCode,
                Message = "Internal Server Error"
            });
        }
    });
});

app.MapHealthChecks("/api/health", new HealthCheckOptions
{
    ResponseWriter = UIResponseWriter.WriteHealthCheckUIResponse
});
app.Run();

7. Code as document

This is partly because I'm lazy, and partly because I want to know if my code is readable to you. I do find documentation necessary in general. However, I also believe that as a new developer cloning someone’s repository, the code should be self-explanatory with minimal comments.

Please clone my code and provide feedback. If you find certain areas unclear, please mention them in the comments section.

Here's my thought process for grouping:

  • Businesses - I expect this folder to grow and possibly need more subfolders or separate folders (e.g., Helpers, Services). Initially, the core logic will be here. I tend to keep interfaces together in an Interfaces folder.

  • Repositories - This is where I group tasks that interact with the database.

  • Models - Most people might call these DTO (Data Transfer Object) classes. The idea is to group any data contracts between the application and the database.

  • ViewModels - I'm not sure if POCO (Plain Old C#/Class Object) is similar to this. Like Models, this will be the data contract between the application and the client (upstream).

  • Unit Tests - I find XUnit and Moq easy to understand, and there are plenty of resources available online to learn them.

8. Conclusion

I wrote this article mainly to share knowledge. I will also use it as a reference for myself in the future. I hope you find this setup helpful for starting a small side project you have in mind. As always, any feedback is appreciated. Feel free to comment.