This article is the first part of the Netssential series. View project on GitHub repo
Project organization and coding patterns will always differ for each individual's taste. Many developers do not have the chance to stamp their mark on a project as it is usually the work of a 'team lead' or 'architect' but these tips I've used over the years can be useful.
Here are my top 5 tips when organizing an ASP.Net Core Web API project from scratch.
1. Setup Logging
ASP.Net Core projects can frustrate you (just like any other) when logs are not setup. This can cost you precious time, mostly when moving the app within new environments. Luckily ASP.Net Core is built to handle startup errors easily when set to do so from the Program.cs file. My personal log library preference is NLog but similar configurations should exist for other libraries.
public class Program
{
public static void Main(string[] args)
{
var logger = NLog.Web.NLogBuilder.ConfigureNLog("nlog.config").GetCurrentClassLogger();
try
{
logger.Debug("Initializing application..");
CreateHostBuilder(args).Build().Run();
}
catch (Exception ex)
{
logger.Error(ex, "An error occured while initializing application.. shutting down!");
throw;
}
finally
{
NLog.LogManager.Shutdown();
}
}
public static IHostBuilder CreateHostBuilder(string[] args) =>
Host.CreateDefaultBuilder(args)
.ConfigureWebHostDefaults(webBuilder =>
{
webBuilder
.UseStartup<Startup>()
.UseIISIntegration()
.UseNLog(); // enable NLog
});
}
2. Tier the Startup.cs file
This file can grow so big in a short period since almost every config goes here, this makes it harder to locate block of codes. Like many developers who do not like staring at a single 3k lines of code file, this should work. I usually separate the file according to content thereby leaving me basically with:
Startup.cs - basic API setup and configuration
Startup.DI.cs - dependency injection setups
Startup.Identity.cs - JWT and other identity setup
All files should have public partial class Startup
as seen here for Startup.DI.cs
public partial class Startup
{
public void ConfigureDI(IServiceCollection services)
{
services.AddTransient<DbContext, SqlServerDbContext>();
}
}
These are then called from the Startup.cs file:
public void ConfigureServices(IServiceCollection services)
{
ConfigureLogging(services);
ConfigureDI(services);
ConfigureIdentity(services);
// more section ...
}
This leave you with a well tiered and neat configuration setup.
3. Dynamic Configuration Options
This is a technique I hope to enforce on front-end applications real soon. The ability to turn a feature ON and OFF for the user without the need of killing a micro-service from the server. The below illustrate how you can use choose which config or services to enable on production environment, all from the appsettings.json file.
First, create a config option class to hold all toggleable features and other config options. I call this Settings.cs
public class Settings
{
public bool EnableHttps { get; set; }
public bool EnableSwagger { get; set; }
public bool EnableHealthCheck { get; set; }
public bool EnableMiniProfiler { get; set; }
public string Datastore { get; set; }
}
Add a Settings node with all options to the appsettings.json as seen below.
{
"ConnectionStrings": { ... },
"Settings": {
"Datastore": "SQLSERVER", //SQLSERVER, MYSQL, COSMOS
"EnableHttps": false,
"EnableSwagger": true,
"EnableHealthCheck": true,
"UseMiniProfiler": true
}
}
All services are now being registered based on the config option
public partial class Startup
{
public Settings _settings { get; set; } = new Settings();
public void ConfigureServices(IServiceCollection services)
{
// load config from Settings.json
Configuration.Bind(nameof(Settings), _settings);
services.AddSingleton(_settings);
if (_settings.EnableMiniProfiler)
ConfigureMiniProfiler(services);
if (_settings.EnableHealthCheck)
{
services.AddHealthChecks()
.AddDbContextCheck<SqlServerDbContext>("Microsoft SQL Server Database")
.AddCheck<ThirdPartyServiceHealthCheck>("ThirdParty Services");
}
if (_settings.EnableHttps)
app.UseHttpsRedirection();
if (_settings.EnableSwagger)
{
app.UseSwagger();
app.UseSwaggerUI(options => options.SwaggerEndpoint("v1/swagger.json", "Netssentials"));
}
if (_settings.EnableMiniProfiler)
{
app.UseMiniProfiler();
}
}
}
4. Error Handling
Errors must surely surface, and surely handled, not too sure about this for every dev. This can be done using a Middleware or a BaseController method. Users of an application should never see error details (type, source, stack trace, inner ex bla bla), but on development environment you surely want to see the error details (on front-end toasts or API calls) without having to check logs.
My BaseController.cs technique.
[Route("api/[controller]/[action]")]
public class BaseController : ControllerBase
{
public readonly ILogger Logger;
private readonly IWebHostEnvironment _env;
public BaseController(ILogger logger, IWebHostEnvironment env)
{
Logger = logger;
_env = env;
}
protected IActionResult HandleError(Exception ex, string customErrorMessage = null)
{
ApiResponse rsp = new ApiResponse
{
Code = "500"
};
Logger.LogError(ex, customErrorMessage);
if (_env.IsDevelopment())
{
rsp.Message = $"{(ex?.InnerException?.Message ?? ex.Message)} --> {ex?.StackTrace}";
return StatusCode(StatusCodes.Status500InternalServerError, rsp);
}
else
{
rsp.Message = customErrorMessage ?? "An error occurred while processing your request!";
return StatusCode(StatusCodes.Status500InternalServerError, rsp);
}
}
}
Usage
public class UserController : BaseController
{
[HttpGet]
public async Task<IActionResult> GetUserInfo(string uniquId)
{
try
{
return Ok(apiResult);
}
catch (Exception ex)
{
return HandleError(ex);
}
}
}
5. Easy Debugging
Whenever I step close to a developer debugging ASP.Net Core APIs from Visual Studio and they always have to click 'Start' or 'IIS Express' I really do pity them. This mean that for every single change you have to launch the app on a new console or browser tab.
But here is a short step to allow you just build the code with 'Ctrl + B' (project) or 'Ctrl + Shift + B (solution) and continue from the client end (Postman or Web). Just do this:
Project properties > Debug > Hosting Model: 'Out of Process'.
To prevent the app ending debug mode when you close the associated browser tab, do this:
VS Options > Web Projects > Stop debugger when browser window is closed: 'false'.
There are various easy hack around project structure and patterns that could be shared but these, for now, are my top 5.
Top comments (1)
Wow! I never thought that Startup.cs could be tiered up!
I use a Mac and I have both Visual Studio for Mac and Jetbrains Rider, however, I use Jetbrains Rider for development, I don't know if I can apply tip no. 5 😭
Thanks for sharing!