• Scott J. Swindell

Setting up a Complete Framework for .NET 6 React Apps in Visual Studio 2022

Using Node.js, React.js, SASS, Jest, xUnit, Moq, NLog, Identity Framework, Entity Framework, and dependency injection


By now, I hope you've had a chance to check out Visual Studio 2022. I recently used it to create my first .NET 6 single-page application (SPA), using Node.js for hosting, React.js for a responsive frontend, Identity Framework for Authentication and Authorization, Entity Framework for data access, SASS for awesome styling, dependency injection, NLog for logging, and Jest, xUnit, plus Moq for automated testing the entire thing. This provides a robust framework, for building powerful, high-quality applications. In this tutorial, we’ll walk through the steps to set everything up.



Install Visual Studio 2022

Visual Studio 2022 Logo

First, if you haven’t done so, download and install Visual Studio (VS) 2022. The Community version is free and has all the features you need, for non-enterprise development.




Create the Project and Solution

Once you have Visual Studio 2022 installed, it’s time to create the project and solution. On the Visual Studio start screen, click on Create a new project.

Visual Studio 2022 Start Screen
Visual Studio 2022 Start Screen

This will bring up a wizard that lets you select the type of project to create. For our purposes, we’re going to select ASP.NET Core with React.js. Once you’ve selected it, click Next to enter the project name and location.

Create New Project Dialog with "ASP.NET Core with React.js" selected
Create a new project

This brings us to the Configure your new project screen. Here, you’re going to enter your Project name, select the Location of your repository, and set the Solution name, which by default is the same as the Project name. Once you’ve set the names and location, click Next. For this tutorial, I'll be using the name Net6ReactApp.

Configure your new project dialog screenshot
Configure your new project

That leads us to the Additional information screen. Here, we’re going to select .NET 6 (Long-term support) for the Framework. The Authentication type really depends on how you’re planning to authenticate your users. If anyone can use the app, then select None. For basic login functions, Individual Accounts sets up the fundamentals for user accounts. If you require Windows Authentication, then you'll have to go back a step and select the ASP.NET Core MVC application template and manually add React later.


We’re going to select Individual Accounts, because it uses Microsoft Identity Framework, which gives us an excellent platform, for authentication and authorization. Leave Configure for HTTPS configured, so the app’s connections to the users will be secure.

Additional information screenshot, with Framework .NET 6.0 and Individual Accounts Authentication, configured for HTTPS
Additional information

Once you have everything entered and selected, click Create, to have Visual Studio scaffold-out your new project. Once Visual Studio is finished creating your project and solution, it will take you to an overview of your app. Feel free to have a look around your solution, or explore some of the documentation, on the overview page. I’ll cover publishing your app to the cloud in a future article.

Project Overview screenshot
Project Overview

Run the App

As the project was scaffolded, Visual Studio created all the necessary files to run your React.js app using Node.js. When you’re done looking around, click on the Debug button on the toolbar to run the app. If you don't have the Debug button on your toolbar, right-click on the toolbar and select Debug from the list of options.


The first time you run the app, it has to restore all of the Node.js packages with Node Package Manager (NPM), which can take several minutes. Once NPM is finished, you’ll be prompted to trust the self-signed SSL certificate. This avoids getting a security warning when you view the app in a browser. Click Yes to continue.




This will lead to a more serious sounding prompt. Just click Yes again to trust the certificate.

Security Warning dialog
Accept the Security Warning

Upon trusting the certificate, the website should start to load in the browser.

Launching the SPA proxy screenshot
Launching the Website

While the site loads, you may be prompted, by Windows Defender Firewall, to allow access to Node.js. This is required for the site to operate correctly, so click Allow Access.

Windows Defender Firewall dialog
Allow Node.js through the firewall

Finally, after everything has loaded, Node.js will serve up your new single-page application (SPA). Take a look around the site to learn about your app.

Screenshot of the single-page application running
Single-page application running

Explore the Solution

After you’re finished looking around, let’s take a look at Solution Explorer. We have a Properties folder, which has important settings for the app. There’s also a ClientApp folder. This is where the React.js code for the project resides. Just like regular MVC projects, you’ll find Controllers and Models directories. In the Data folder, you’ll find the Entity Framework context and migrations. There’s also a Pages folder for the few pages that React.js won’t be handling, like an error page. Finally, we have appsettings.json and Program.cs for launching and initializing up the app.


Screenshot of Solution Explorer showing the files in the Solution.
Solution Explorer

Dependency Injection, Authentication, and Authorization

If you take a look at Program.cs, you’ll notice that we’re already set up for dependency injection. It initializes authentication and authorization, using Microsoft Identity Framework. This is because we selected Individual Accounts during scaffolding. It also sets up Entity Framework, URL routing, and HTTPS redirection.


using Microsoft.AspNetCore.Authentication;
using Microsoft.AspNetCore.Identity;
using Microsoft.AspNetCore.Identity.UI;
using Microsoft.EntityFrameworkCore;
using Net6ReactApp.Data;
using Net6ReactApp.Models;
using NLog;
using NLog.Web;

// Initialize NLog
var logger = NLog.LogManager.Setup().LoadConfigurationFromAppSettings().GetCurrentClassLogger();
logger.Debug("Initializing Program");

try
{
	var builder = WebApplication.CreateBuilder(args);

	// Add services to the container.
	var connectionString = builder.Configuration.GetConnectionString("DefaultConnection");
	builder.Services.AddDbContext<ApplicationDbContext>(options =>
		options.UseSqlServer(connectionString));
	builder.Services.AddDatabaseDeveloperPageExceptionFilter();

	builder.Services.AddDefaultIdentity<ApplicationUser>(options => options.SignIn.RequireConfirmedAccount = true)
		.AddEntityFrameworkStores<ApplicationDbContext>();

	builder.Services.AddIdentityServer()
		.AddApiAuthorization<ApplicationUser, ApplicationDbContext>();

	builder.Services.AddAuthentication()
		.AddIdentityServerJwt();

	builder.Services.AddControllersWithViews();
	builder.Services.AddRazorPages();

	// Setup NLog for Dependency injection
	builder.Logging.SetMinimumLevel(Microsoft.Extensions.Logging.LogLevel.Trace);
	builder.Host.UseNLog();

	var app = builder.Build();

	// Configure the HTTP request pipeline.
	if (app.Environment.IsDevelopment())
	{
		app.UseMigrationsEndPoint();
	}
	else
	{
		// The default HSTS value is 30 days. You may want to change this for production scenarios, see https://aka.ms/aspnetcore-hsts.
		app.UseHsts();
	}

	app.UseHttpsRedirection();
	app.UseStaticFiles();
	app.UseRouting();

	app.UseAuthentication();
	app.UseIdentityServer();
	app.UseAuthorization();

	app.MapControllerRoute(
		name: "default",
		pattern: "{controller}/{action=Index}/{id?}");
	app.MapRazorPages();

	app.MapFallbackToFile("index.html"); ;

	app.Run();
}
catch (Exception exception)
{
	logger.Error(exception, "Execution stoped, due to Exception");
	throw;
}
finally
{
	LogManager.Shutdown();
}

Program.cs


Add NLog for better logging

Every app needs logging, in order to gain insight into what the app is doing, at any given time. ASP.NET has some basic support for logging, out of the box. However, for more power and flexibility, we’re going to use a popular third-party logger named NLog. It’s highly configurable. You can log to a file or database, and you have complete control over everything that gets logged.


Add NLog.Web.AspNetCore NuGet Package

To get started, first install the NLog NuGet package. If you’re not familiar with how to get there, just right-click on the project, and select Manage NuGet packages... from the context menu.

Context menu in Solution Explorer, highlighting Manage NuGet Packages
Navigate to Manage NuGet Packages

Next, search for NLog.Web.AspNetCore, under the Browse tab. Once you locate it, select the latest stable version, then click Install. It should be pretty quick, and it will pop up a readme.txt file, when it’s complete.

Screenshot of Installing NLog.Web.AspNetCore using NuGet Package Manager
Install NLog.Web.AspNetCore using NuGet Package Manager

Configure NLog

After installing the NLog NuGet package, it’s time to configure it. The settings for NLog reside in nlog.config, which we’ll have to create. To do this, right-click on the project in Solution Explorer. Click Add on the context menu, then New Item...


Screenshot of context menu in Solution Explorer highlighting Add New Item.
Add New File

This will bring up a dialog box. Select Text File and enter nlog.config as the filename.

Screenshot of Add New Item highlighting Text File and the name nlog.config
Add NLog Configuration File

Once the file is created, copy and paste the following configuration XML into the file. For complete information on the configuration options, visit the NLog config section on their website.


Once you’ve copied the content into the nlog.config file, replace instances of Net6ReactApp with the name of your own app, and make any other changes you’d like.


<?xml version="1.0" encoding="utf-8" ?>
<nlog xmlns="http://www.nlog-project.org/schemas/NLog.xsd"
	  xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
	  autoReload="true"
	  internalLogLevel="Info"
	  internalLogFile=".\logs\internal-nlog-Net6ReactApp.txt">
 
	<!-- ASP.NET Core Renderer -->
	<extensions>
		<add assembly="NLog.Web.AspNetCore"/>
	</extensions>
 
	<!-- Logging Targets -->
	<targets>
		<!-- File Target for general logging  -->
		<target xsi:type="File" name="allfile" fileName=".\logs\Net6ReactApp-${shortdate}.log"
				layout="${longdate} | ${event-properties:item=EventId_Id:whenEmpty=0} | ${level:uppercase=true} | ${logger} | ${message} ${exception:format=tostring}" />
 
		<!-- File Target for own logging -->
		<target xsi:type="File" name="ownFile-web" fileName=".\logs\Net6ReactApp-own-${shortdate}.log"
				layout="${longdate} | ${event-properties:item=EventId_Id:whenEmpty=0} | ${level:uppercase=true} | ${logger} | ${message} ${exception:format=tostring} | url: ${aspnet-request-url} | action: ${aspnet-mvc-action} | ${callsite}" />
 
		<!-- Console Target to improve startup detection -->
		<target xsi:type="Console" name="lifetimeConsole" layout="${MicrosoftConsoleLayout}" />
	</targets>
 
	<!-- Logging Rules -->
	<rules>
		<!-- All logs -->
		<logger name="*" minlevel="Trace" writeTo="allfile" />
 
		<!-- Output hosting lifetime messages to console target for faster startup detection -->
		<logger name="Microsoft.Hosting.Lifetime" minlevel="Info" writeTo="lifetimeConsole, ownFile-web" final="true" />
 
		<!-- Skip non-critical Microsoft logs and so log only own logs (BlackHole) -->
		<logger name="Microsoft.*" maxlevel="Info" final="true" />
		<logger name="System.Net.Http.*" maxlevel="Info" final="true" />
 
		<logger name="*" minlevel="Trace" writeTo="ownFile-web" />
	</rules>
</nlog>

nlog.config


Once the nlog.config file is finished, we need to update appsettings.json. Locate the Logging section and set the LogLevel.Default to Trace. This will give us more verbose logging. If you’d rather less detailed logging, skip this step.

...
"Logging": {
	"LogLevel": {
		"Default": "Trace",
		"Microsoft": "Warning",
		"Microsoft.Hosting.Lifetime": "Information"
	}
}
...

appsettings.json


Initialize NLog in Program.cs

Next, we need to update Program.cs to initialize NLog and set up dependency injection. Add the following statements at the top of Program.cs.

using NLog;
using NLog.Web;

// Initialize NLog
var logger = NLog.LogManager.Setup().LoadConfigurationFromAppSettings().GetCurrentClassLogger();
logger.Debug("Initializing Program");

After adding the initialization code, wrap the remaining code in the file with a try block, with the following catch and finally blocks:

catch (Exception exception)
{
	logger.Error(exception, "Execution stoped, due to Exception");
	throw;
}
finally
{
	LogManager.Shutdown();
}


Finally, we need to set up NLog for dependency injection. Insert the following code right above the var app = builder.Build(); statement.

//  Setup NLog for dependency injection
builder.Logging.SetMinimumLevel(Microsoft.Extensions.Logging.LogLevel.Trace);
builder.Host.UseNLog();

Using NLog

In order to use NLog in your app, simply inject a new logger in the constructor, as seen below, and call the appropriate Log method.

private readonly NLog.ILogger _logger;

public WeatherForecastController(NLog.ILogger logger)
{
    _logger = logger;
    _logger.Log(NLog.LogLevel.Trace, "WeatherForecastController Initializing");
}

Once you have it all set up, run the app to see if you get a log file, where you’re expecting it.


Add SASS for better styling

Syntactically Awesome Style Sheets (SASS) has become the standard technology for improving cascading style sheets (CSS). It extends CSS, adding a bunch of new, useful functionality. We’re going to be adding SASS to our application, using the steps below.


There is already a CSS file called custom.css under ClientApp\src. We’re going to rename that to custom.scss to make it a SASS file.


Screenshot showing the renaming of custom.css to custom.scss
Rename custom.css to custom.scss

Next, we need to replace the reference to custom.css in App.js with a reference to bundle.css. This tells it to use the bundle.css file, which will be created, from the SCSS files.

Screenshot of reference to bundle.css in App.js
Reference bundle.css in App.js

Now it’s time to add some Node packages, using the following command:

npm i --save-dev autoprefixer clean-webpack-plugin css-loader cssnano             
    file-loader mini-css-extract-plugin node-sass  postcss-loader sass- 
    loader style-loader webpack webpack-cli ajv acorn bootstrap jquery

After installing all the packages required by SASS, we need to edit the package.json file and add the following two lines to the “scripts” section:

	"webpack-dev": "webpack --progress --profile --mode development",
	"webpack-prod": "webpack --progress --profile --mode production"	

Screenshot of adding Webpack scripts to package.json
Add Webpack scripts to package.json

Next, we have to configure Webpack, to bundle our SCSS file. Create a webpack.config.js file in your ClientApp folder. Next, populate it with the following contents. You can add more SCSS and JavaScript files by adding the filenames to the entry value array.

const { CleanWebpackPlugin } = require('clean-webpack-plugin');
const MiniCssExtractPlugin = require("mini-css-extract-plugin");
const path = require('path');
 
const directoryName = './public';
const fileName = 'bundle';
 
module.exports = (env, argv) => {
	return {
		mode: argv.mode === "production" ? "production" : "development",
		entry: ['./src/custom.scss'],
		output: {
			filename: fileName + '.js',
			path: path.resolve(__dirname, directoryName)
		},
		module: {
			rules: [
				{
					test: /\.s[c|a]ss$/,
					use:
						[
							'style-loader',
							MiniCssExtractPlugin.loader,
							'css-loader',
							{
								loader: 'postcss-loader', // Run postcss actions
								options: {
									postcssOptions: {
										plugins: function () { // postcss plugins, can be exported to postcss.config.js
 
											let plugins = [require('autoprefixer')];
 
											if (argv.mode === "production") {
 
												plugins.push(require('cssnano'));
											}
 
											return plugins;
										}
									}
								}
							},
							'sass-loader'
						]
				}
			]
		},
		plugins: [
			new CleanWebpackPlugin(),
			new MiniCssExtractPlugin({
				filename: fileName + '.css'
			})
		]
	};
};

webpack.config.js


In order to test everything out, enter the following command in your Node.js window. This will bundle the src/custom.scss file into public/bundle.css.

npm run webpack-dev

If all goes well, you should see bundle.css in ClientApp/public

Screenshot showing bundle.css in Solution Explorer
bundle.css in Solution Explorer

It also creates a bundle.js file, which needs to be included in the project. If you want to automate this process, I suggest you add a pre-build event to the project, which executes the npm run webpack-dev command.



Add Automated Testing for more reliable code

When it comes to ensuring code quality and program correctness, nothing beats automated testing. For our app, we need to test both the frontend JavaScript code and the backend C# code. We’re going to be using xUnit for backend testing, because it’s fast, powerful, and it plays nice with .NET 6. It requires a little bit of setup, which we’ll cover.


For frontend testing, we’re going to use Jest, because it’s created by the same folks that created React, and it comes set-up, right out of the box. Below, I’ll explain the steps necessary to do both backend and frontend automated testing.


Frontend Testing

React was actually designed with unit testing in mind, making it easy to test. In fact, when we scaffolded our project, it automatically set up Jest for running tests. It also created a sample test, App.test.js.

Screenshot of App.test.js in Solution Explorer
App.test.js in Solution Explorer

import React from 'react';
import { createRoot } from 'react-dom/client';
import { MemoryRouter } from 'react-router-dom';
import App from './App';

it('renders without crashing', async () => {
  const div = document.createElement('div');
  const root = createRoot(div);
  root.render(
    <MemoryRouter>
      <App />
    </MemoryRouter>);
  await new Promise(resolve => setTimeout(resolve, 1000));
});

App.test.js



The scaffolding process also created a test script in package.json.

Screenshot of Package.json showing test script
Package.json showing test script
Using Jest

To run the tests, all we need to do is open a Node.js command prompt and navigate to the ClientApp folder. From there, run the following command, and if all goes well, you should see something similar to the output below..

npm run test

Screenshot showing results of running rront-end tests
Running Front-end Tests

That’s all the setup that’s required for front-end unit testing, using React and Jest. From here, you’re free to add more tests, as you develop your code.



Backend Testing

As mentioned, for testing our C# code, we’re going to be using xUnit. When .NET Core first came out, it was the recommended testing framework. Now there is support for others, but xUnit is still a favorite by many, because it is fast and easy to use.


Install xUnit

To install xUnit, open Package Manager Console in Visual Studio, under the Tools menu.

Screenshot showing how to Navigate to Package Manager Console
Navigate to Package Manager Console

This will bring up the Package Manager Console window.


In the console, change to the directory with your project name and enter the following command.

dotnet add package Microsoft.NET.Test.Sdk

That will install the required MS testing SDK. Next, we need to install xUnit itself, using the following command.