initial commit
Some checks failed
Build, Test & Publish / Build and Publish Container Image (push) Has been cancelled
Build, Test & Publish / Deploy to Infrastructure (push) Has been cancelled
Build, Test & Publish / Build (push) Has been cancelled

This commit is contained in:
2024-09-05 13:54:08 +10:00
commit 8ad5845efc
57 changed files with 6046 additions and 0 deletions

View File

@ -0,0 +1,314 @@
# Blazor App with an inbuilt API
Using the new Blazor interactive auto mode we can combine the benifits of Blazor WASM with Blazor Server.
In order to create a unified experience the recommended way is to create an API that is exposed from the Server application.
The Server application will have a service class that will encapsulate all the actual logic, leaving the API controller to just call the service class.
The Blazor WASM application will then call the API controller to get the data or perform actions.
## Create the Blazor Application
The best way to create a new Blazor application is to use Visual Studio 2022.
1. Open Visual Studio 2022
2. Click on `Create a new project`
3. Search for `Blazor Web App` and select the `Blazor Web App` template
4. Ensure that the `Interactive Server Mode` is set to `Auto (Blazor Server and WebAssembly)`
5. Ensure that `Interactivity Mode` is set to `Per page/component`
6. Untick `Include sample pages`
## Create a model class in the client
Get started by creating a model class in the client application. This model class will be used to represent the data that is returned from the API.
```csharp [BlazorApp1.Client/Models/Movie.cs]
namespace BlazorApp1.Client.Models
{
public class Movie
{
public int Id { get; set; }
public string Title { get; set; } = string.Empty;
public string Genre { get; set; } = string.Empty;
public int Year { get; set; }
}
}
```
## Create a service interface in the client
Next we will create a service interface in the client application. This service interface will define the methods that will be used to interact with the API. For this guide only a GET method is defined, but more methods can be added as needed.
```csharp [BlazorApp1.Client/Services/IMovieService.cs]
using BlazorApp1.Client.Models;
namespace BlazorApp1.Client.Services
{
public interface IMovieService
{
public List<Movie> GetMovies();
}
}
```
## Create a client service
Now that we have our service contract we can create our client service. The service will look like the below:
```csharp [BlazorApp1.Client/Services/ClientMovieService.cs]
using BlazorApp1.Client.Models;
using System.Net.Http.Json;
namespace BlazorApp1.Client.Services
{
public class ClientMovieService(HttpClient httpClient) : IMovieService
{
private readonly HttpClient _httpClient = httpClient;
public async Task<List<Movie>> GetMoviesAsync()
{
return await _httpClient.GetFromJsonAsync<List<Movie>>("api/movie") ?? [];
}
}
}
```
The client service calls the API endpoint `movies` and deserializes the response into a list of `Movie` objects. We will also need to register this HTTP service and the Movie service in the `Program.cs` file.
```csharp [BlazorApp1/Client/Program.cs]
builder.Services.AddScoped<IMovieService, ClientMovieService>();
builder.Services.AddScoped(sp => new HttpClient { BaseAddress = new Uri(builder.Configuration["FrontendUrl"] ?? "https://localhost:5002") });
```
At this point also update the appsettings.json for both the client and server application to include the `FrontendUrl` key.
::: code-group
```json [BlazorApp1.Client/wwwroot/appsettings.json]
{
"FrontendUrl": "https://localhost:5002"
}
```
```json [BlazorApp1/appsettings.json]
{
"FrontendUrl": "https://localhost:5002"
}
```
:::
::: tip
For the client application the appsettings.json will need to be placed in the wwwroot folder, which can be created if it does not exist.
:::
## Create the CSR (Client Side Rendered) movie page
Now that we have our service we can create a page that will call the service and display the data.
```razor [BlazorApp1.Client/Pages/MoviesCSR.razor]
@page "/movies-csr";
/* NOTE: InteractiveAuto is used to specify the interactivity mode
for the page. This is set to InteractiveAuto so that the page
can be rendered on the client side.
Technically this is not doing anything at this point, as there is no
need for client interactivity, but if we add a button or some other
interactive element this will be useful.
*/
@rendermode InteractiveAuto
@using BlazorApp1.Client.Models
@using BlazorApp1.Client.Services
@inject IMovieService MovieService
<h3>MoviesCSR</h3>
@if (moviesList.Count == 0)
{
<h5>No movies found</h5>
} else
{
<table class="table">
<thead>
<tr>
<th>Id</th>
<th>Title</th>
<th>Genre</th>
<th>Year</th>
</tr>
</thead>
<tbody>
@foreach (Movie movie in moviesList)
{
<tr>
<td>@movie.Id</td>
<td>@movie.Title</td>
<td>@movie.Genre</td>
<td>@movie.Year</td>
</tr>
}
</tbody>
</table>
}
@code {
private List<Movie> moviesList = [];
protected override async Task OnInitializedAsync()
{
await GetMovies();
}
private async Task GetMovies()
{
moviesList = await MovieService.GetMoviesAsync();
}
}
```
Note the following:
* The `@page` directive specifies the route for the page.
* The `@inject` directive is used to inject the `IMovieService` into the page.
* The `@code` block contains the code for the page. In the block we have a `moviesList` variable that will hold the list of movies.
* The `OnInitializedAsync` method is called when the page is initialized. In this method we call the `GetMovies` method.
* The `GetMovies` method calls the `GetMoviesAsync` method on the `MovieService` and assigns the result to the `moviesList` variable.
## Create the server service
Now that the frontend part is done we can move on to the server part. Get started by creating a ServerMovieService class in the server application:
```csharp [BlazorApp1/Services/ServerMovieService.cs]
using BlazorApp1.Client.Models;
using BlazorApp1.Client.Services;
namespace BlazorApp1.Services
{
public class ServerMovieService : IMovieService
{
public Task<List<Movie>> GetMoviesAsync()
{
return Task.FromResult(new List<Movie>
{
new() { Title = "The Shawshank Redemption", Year = 1994 },
new() { Title = "The Godfather", Year = 1972 },
new() { Title = "The Dark Knight", Year = 2008 },
new() { Title = "Pulp Fiction", Year = 1994 },
new() { Title = "The Lord of the Rings: The Return of the King", Year = 2003 },
new() { Title = "Schindler's List", Year = 1993 }
});
}
}
}
```
## Create the API Controller
Now that our service is ready we can create the API controller. The controller will call the service and return the data.
```csharp [BlazorApp1/Controllers/MovieController.cs]
using BlazorApp1.Client.Services;
using Microsoft.AspNetCore.Mvc;
namespace BlazorApp1.Controllers
{
[ApiController]
[Route("api/[controller]")]
public class MovieController(IMovieService movieService) : ControllerBase
{
private readonly IMovieService _movieService = movieService;
public async Task<IActionResult> GetMoviesAsync()
{
var movies = await _movieService.GetMoviesAsync();
return Ok(movies);
}
}
}
```
This will require us to do two things in the `Program.cs` file. First we need to register the movie service in the file:
```csharp [BlazorApp1/Program.cs]
builder.Services.AddScoped<IMovieService, ServerMovieService>();
```
And second we also need to register the API controller in the file:
```csharp [BlazorApp1/Program.cs]
// Before builder.Build();
builder.Services.AddControllers();
// Before app.Run();
app.MapControllers();
```
## Add the server side movie page
Now that the API is ready we can create a server side page that will call the service directly and display the data.
```razor [BlazorApp1/Pages/MoviesSSR.razor]
@page "/movies-ssr";
@using BlazorApp1.Client.Models
@using BlazorApp1.Client.Services
@inject IMovieService MovieService
<h3>MoviesSSR</h3>
@if (moviesList.Count == 0)
{
<h5>No movies found</h5>
} else
{
<table class="table">
<thead>
<tr>
<th>Id</th>
<th>Title</th>
<th>Genre</th>
<th>Year</th>
</tr>
</thead>
<tbody>
@foreach (Movie movie in moviesList)
{
<tr>
<td>@movie.Id</td>
<td>@movie.Title</td>
<td>@movie.Genre</td>
<td>@movie.Year</td>
</tr>
}
</tbody>
</table>
}
@code {
private List<Movie> moviesList = [];
protected override async Task OnInitializedAsync()
{
await GetMovies();
}
private async Task GetMovies()
{
moviesList = await MovieService.GetMoviesAsync();
}
}
```
As you can see the code is almost identical to the CSR page. The only difference is that the data is fetched from the server side service.
## Run the application
At this point you can run the application and navigate to the `/movies-csr` and `/movies-ssr` pages to see the data being displayed.
### Conclusion
In this guide we have seen how to create a Blazor application that uses an inbuilt API. We have created a client service that calls the API and a server service that returns the data. We have also created a client side rendered page and a server side rendered page that display the data. This is a good way to create a unified experience for the user.
While simple in this example this can be extended so that the Blazor app stores data in a database and in this way we can build a full stack application.

View File

@ -0,0 +1,488 @@
# Testing Controller
Testing controllers can be an important part of your application testing strategy. Controllers are the entry point for your application and are responsible for handling requests and responses. In this guide we will cover how to test controllers in a Web API application.
Testing controllers is a little more tricky but the best method is to actually run a test version of your app, and interact with it using HTTP requests. This style of testing is known as integration testing and is a great way to test the entire application stack, even though we will often use a in-memory database to avoid the need for a real database.
.NET provides the `WebApplicationFactory` class to help with this. This class allows you to create a test server that can be used to send HTTP requests to your application.
# Example Application
In this example we will create a simple Web API to maintain a user list. To get started create a new Web API project titled `UserApp` and enabled controller support.
## User Model and DB Context
Create a new `Data` folder and insert the following files:
::: code-group
```csharp [User.cs]
namespace UserApp.Data
{
public class User
{
public int Id { get; set; }
public string Name { get; set; } = string.Empty;
public string Email { get; set; } = string.Empty;
public string FirstName { get; set; } = string.Empty;
public string LastName { get; set; } = string.Empty;
}
}
```
:::
::: code-group
```csharp [UserAppContext.cs]
using Microsoft.EntityFrameworkCore;
namespace UserApp.Data
{
public class UserAppContext : DbContext
{
public UserAppContext(DbContextOptions<UserAppContext> options) : base(options) { }
public DbSet<User> Users { get; set; }
}
}
```
:::
::: code-group
```csharp [UserViewModel.cs]
namespace UserApp.Models
{
public class UserViewModel(string email, string firstName, string lastName)
{
public string Email { get; set; } = email;
public string FirstName { get; set; } = firstName;
public string LastName { get; set; } = lastName;
}
}
```
:::
As part of this ensure the following NuGet packages are installed:
* Microsoft.EntityFrameworkCore
## Service Interface and Implementation
First create a entity not found exception:
::: code-group
```csharp [EntityNotFoundException.cs]
namespace UserApp.Infrastructure.Exceptions
{
public class EntityNotFoundException(string message) : Exception(message) { }
}
```
:::
Then we can create the service interface and implementation:
::: code-group
```csharp [IUserService.cs]
using UserApp.Data;
namespace UserApp.Services
{
public interface IUserService
{
public Task<IEnumerable<User>> GetUsersAsync();
public Task<User> GetUserAsync(int id);
public Task<User> AddUserAsync(string email, string firstName, string lastName);
public Task<User> UpdateUserAsync(int id, string email, string firstName, string lastName);
public Task<User> DeleteUserAsync(int id);
}
}
```
```csharp [UserService.cs]
using Microsoft.EntityFrameworkCore;
using UserApp.Data;
using UserApp.Infrastructure.Exceptions;
namespace UserApp.Services
{
public class UserService(UserAppContext context) : IUserService
{
private readonly UserAppContext _context = context;
public async Task<User> AddUserAsync(string email, string firstName, string lastName)
{
User user = new()
{
Email = email,
FirstName = firstName,
LastName = lastName
};
await _context.Users.AddAsync(user);
await _context.SaveChangesAsync();
return user;
}
public async Task<User> DeleteUserAsync(int id)
{
User? user = await _context.Users.FindAsync(id) ?? throw new EntityNotFoundException("User not found");
_context.Users.Remove(user);
await _context.SaveChangesAsync();
return user;
}
public async Task<User> GetUserAsync(int id)
{
User user = await _context.Users.FindAsync(id) ?? throw new EntityNotFoundException("User not found");
return user;
}
public async Task<IEnumerable<User>> GetUsersAsync()
{
return await _context.Users.ToListAsync();
}
public async Task<User> UpdateUserAsync(int id, string email, string firstName, string lastName)
{
User user = await _context.Users.FindAsync(id) ?? throw new EntityNotFoundException("User not found");
user.Email = email;
user.FirstName = firstName;
user.LastName = lastName;
await _context.SaveChangesAsync();
return user;
}
}
}
```
:::
## Setup Dependency Injection
We now need to add our DI config to the `Program.cs` file. Add the following to the service section, you will also need to install the following NuGet packages:
* Microsoft.EntityFrameworkCore.Sqlite
* Microsoft.EntityFrameworkCore.Design
* Microsoft.EntityFrameworkCore.Tools
::: code-group
```csharp [Program.cs]
builder.Services.AddDbContext<UserAppContext>(options =>
{
options.UseSqlite("UserApp");
});
builder.Services.AddScoped<IUserService, UserService>();
```
:::
Once this is done we can generate the database migrations that we need:
::: code-group
```bash [dotnet console]
dotnet ef migrations add InitialCreate
```
```bash [package manager console]
Add-Migration InitialCreate
```
:::
## Controller
Finally we can create a controller to interact with the service:
::: code-group
```csharp [UsersController.cs]
using Microsoft.AspNetCore.Mvc;
using UserApp.Data;
using UserApp.Infrastructure.Exceptions;
using UserApp.Models;
using UserApp.Services;
namespace UserApp.Controllers
{
[Route("api/[controller]")]
public class UserController(IUserService userService, ILogger<UserController> logger) : ControllerBase
{
private readonly ILogger<UserController> _logger = logger;
private readonly IUserService _userService = userService;
[HttpGet]
public async Task<IActionResult> GetUsersAsync()
{
try
{
return Ok(await _userService.GetUsersAsync());
}
catch (Exception exception)
{
_logger.LogError(exception, "An error occurred while getting the users");
return StatusCode(500);
}
}
[HttpGet("{id}")]
public async Task<IActionResult> GetUserAsync(int id)
{
try
{
return Ok(await _userService.GetUserAsync(id));
}
catch (EntityNotFoundException exception)
{
_logger.LogError(exception, "User not found");
return NotFound();
}
catch (Exception exception)
{
_logger.LogError(exception, "An error occurred while getting the user");
return StatusCode(500);
}
}
[HttpPost]
public async Task<IActionResult> AddUserAsync([FromBody] UserViewModel user)
{
try
{
User createdUser = await _userService.AddUserAsync(user.Email, user.FirstName, user.LastName);
return CreatedAtAction("GetUser", new { id = createdUser.Id }, createdUser);
}
catch (Exception exception)
{
_logger.LogError(exception, "An error occurred while adding the user");
return StatusCode(500);
}
}
[HttpPut("{id}")]
public async Task<IActionResult> UpdateUserAsync(int id, [FromBody] UserViewModel user)
{
try
{
return Ok(await _userService.UpdateUserAsync(id, user.Email, user.FirstName, user.LastName));
}
catch (EntityNotFoundException exception)
{
_logger.LogError(exception, "User not found");
return NotFound();
}
catch (Exception exception)
{
_logger.LogError(exception, "An error occurred while updating the user");
return StatusCode(500);
}
}
[HttpDelete("{id}")]
public async Task<IActionResult> DeleteUserAsync(int id)
{
try
{
return Ok(await _userService.DeleteUserAsync(id));
}
catch (EntityNotFoundException exception)
{
_logger.LogError(exception, "User not found");
return NotFound();
}
catch (Exception exception)
{
_logger.LogError(exception, "An error occurred while deleting the user");
return StatusCode(500);
}
}
}
}
```
:::
# Controller Testing
To test the controller we will create a new MSTest Test project called `UserApp.Tests`. Add the following NuGet packages:
* Microsoft.EntityFrameworkCore.Sqlite
* Microsoft.AspNetCore.Mvc.Testing
* Microsoft.EntityFrameworkCore.Sqlite
## Test Setup
For this example we are going to be using a SQLite database for testing the application. This is not needed for this paticular example, as the code is already using an in-memory database, but it is a good example showing how to override the database connection for testing.
In an actual system you would want to use a real database for testing, such as one spun up in a docker container in the CI.
To setup the test project add the following to the `Program.cs` file:
::: code-group
```csharp [Program.cs]
// ...
public partial class Program { }
```
:::
Now we want to create a new factory to generate the test server:
::: code-group
```csharp [Factories/UserAppApplicationFactory.cs]
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Mvc.Testing;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.DependencyInjection;
using UserApp.Data;
namespace UserApp.Tests.Factories
{
public class UserAppApplicationFactory<TProgram>(object[] existingEntites = null!) : WebApplicationFactory<TProgram> where TProgram : class
{
private readonly object[] _existingEntites = existingEntites ?? [];
protected override void ConfigureWebHost(IWebHostBuilder builder)
{
builder.ConfigureServices(services =>
{
var descriptor = services.SingleOrDefault(d => d.ServiceType == typeof(DbContextOptions<UserAppContext>));
if (descriptor != null)
{
services.Remove(descriptor);
}
services.AddDbContext<UserAppContext>((container, options) =>
{
options.UseNpgsql("Server=localhost;Port=5432;Database=postgres;User Id=postgres;Password=postgres;");
});
// After the DbContext is registered, we need to create the database
using var scope = services.BuildServiceProvider().CreateScope();
var context = scope.ServiceProvider.GetRequiredService<UserAppContext>();
context.Database.ExecuteSqlRaw(DB_CLEAR);
context.Database.Migrate();
// Add any existing entities to the context
foreach (var entity in _existingEntites)
{
context.Add(entity);
}
if (_existingEntites.Length != 0)
{
context.SaveChanges();
}
});
}
public string DB_CLEAR = """
DROP TABLE IF EXISTS "Users";
DROP TABLE IF EXISTS "__EFMigrationsHistory";
""";
}
}
```
:::
::: danger
NOTE: The above code will delete any existing data in the database, so be careful when using this in a real application. Always make sure the connection string is pointing to a test database.
:::
This setup requires that a Postgres database is running, the best way to do this is to use a docker container (which is what will occur in the CI as well). A compose is below:
```yaml
services:
postgres:
image: postgres:16
environment:
POSTGRES_USER: postgres
POSTGRES_PASSWORD: postgres
POSTGRES_DB: postgres
ports:
- "5432:5432"
```
# Writing Controller Tests
Now we can write some tests for the controller. Create a new file called `UsersControllerTests.cs` and add the following:
::: code-group
```csharp [UsersControllerTests.cs]
using Newtonsoft.Json;
using UserApp.Data;
using UserApp.Tests.Factories;
namespace UserApp.Tests
{
[TestClass]
public class UserControllerTests
{
private HttpClient _client = null!;
private UserAppApplicationFactory<Program> _factory = null!;
[TestCleanup]
public void Cleanup()
{
_client.Dispose();
_factory.Dispose();
}
[TestMethod]
public async Task GetUsers_ForNoUsers_ReturnsSuccessAndNoUsers()
{
// Arrange
_factory = new UserAppApplicationFactory<Program>();
_client = _factory.CreateClient();
var request = new HttpRequestMessage(HttpMethod.Get, "/api/user");
// Act
var response = await _client.SendAsync(request);
// Assert
response.EnsureSuccessStatusCode();
var content = await response.Content.ReadAsStringAsync();
Assert.AreEqual("[]", content);
}
[TestMethod]
public async Task GetUsers_ForUsers_ReturnsSuccessAndUsers()
{
// Arrange
User user1 = new()
{
Id = 1,
FirstName = "John",
LastName = "Doe",
Email = "john.doe@test.com"
};
User user2 = new()
{
Id = 2,
FirstName = "Jane",
LastName = "Doe",
Email = "jane.doe@test.com"
};
_factory = new UserAppApplicationFactory<Program>([user1, user2]);
_client = _factory.CreateClient();
// Act
var response = await _client.GetAsync("/api/user");
// Assert
response.EnsureSuccessStatusCode();
var content = await response.Content.ReadAsStringAsync();
var users = JsonConvert.DeserializeObject<User[]>(content);
Assert.AreEqual(2, users!.Length);
Assert.AreEqual("john.doe@test.com", users[0].Email);
Assert.AreEqual("jane.doe@test.com", users[1].Email);
}
}
}
```
:::

View File

@ -0,0 +1,58 @@
# Database Seeding in .NET
Often in development and testing, you need to seed your database with some data. This can be done manually, but it's a tedious process. In this article, we'll see how to seed a database in .NET.
The best way to seed the database in .NET is to first check that the application is running in development mode.
Most of the time such a check will already exist in your `Program.cs`:
```csharp [Program.cs]
// ...
if (builder.Environment.IsDevelopment()) {
// ...
}
// ...
```
To seed your database first add a SeedData file (personally I usually place this in a helpers folder):
```csharp [Helpers/SeedData.cs]
using Microsoft.EntityFrameworkCore;
using MySampleApp.Models;
namespace MySampleApp.Helpers;
public class SeedData
{
public static async Task InitializeAsync(IServiceProvider serviceProvider)
{
using var context = new AppContext(serviceProvider.GetRequiredService<DbContextOptions<AppContext>>());
context.Add(new Movie { Name = "Batman Begins", Genre = "Action" });
context.Add(new Movie { Name = "The Dark Knight", Genre = "Action" });
context.Add(new Movie { Name = "The Dark Knight Rises", Genre = "Action" });
await context.SaveChangesAsync();
}
}
```
In the `SeedData` class, we have a static method `InitializeAsync` that takes an `IServiceProvider` as a parameter. This method initializes the database with some sample data.
Next, we need to call this method in the `Program.cs` file:
```csharp [Program.cs]
// ...
if (builder.Environment.IsDevelopment()) {
// Seed the database
await using var scope = app.Services.CreateAsyncScope();
await SeedData.InitializeAsync(scope.ServiceProvider);
// ...
}
// ...
```

View File

@ -0,0 +1,35 @@
# Dockerising a Blazor Web App
Dockerising a blazor web app is a simple process. The first step is to create a Dockerfile in the root of the project. The Dockerfile should contain the following:
```Dockerfile
FROM mcr.microsoft.com/dotnet/aspnet:8.0 AS base
USER app
WORKDIR /app
EXPOSE 8080
FROM mcr.microsoft.com/dotnet/sdk:8.0 AS build
ARG BUILD_CONFIGURATION=Release
WORKDIR /src
COPY ["BlazorApp1/BlazorApp1.csproj", "BlazorApp1/"]
COPY ["BlazorApp1.Client/BlazorApp1.Client.csproj", "BlazorApp1.Client/"]
RUN dotnet restore "./BlazorApp1/BlazorApp1.csproj"
COPY . .
WORKDIR "/src/BlazorApp1"
RUN dotnet build "./BlazorApp1.csproj" -c $BUILD_CONFIGURATION -o /app/build
FROM build AS publish
ARG BUILD_CONFIGURATION=Release
RUN dotnet publish "./BlazorApp1.csproj" -c $BUILD_CONFIGURATION -o /app/publish /p:UseAppHost=false
FROM base AS final
WORKDIR /app
COPY --from=publish /app/publish .
ENTRYPOINT ["dotnet", "BlazorApp1.dll"]
```
::: tip
The dockerfile has the HTTPS port disabled, so you can run the app on HTTP. If you want to enable HTTPS add `EXPOSE 8081`, however a reverse proxy like Nginx is recommended for production.
:::

View File

@ -0,0 +1,230 @@
# Google Sign-In Without Identity
This guide will show you how to implement Google Sign-In without using the Identity framework. While Identity is a great framework, it can be overkill for some applications. Often we just want to use a third-party provider to authenticate a user and use default cookie or JWT authentication for the rest of the application.
## Prerequisites
* A Google account
* .NET 8
* Visual Studio 2022
Note: This article assumes you are using a Windows development environment. If you are using a Mac or Linux, you will need to use the appropriate tools for your environment.
## Implementation
### Create a new .NET Core MVC Application
Open Visual Studio and create a new .NET Core MVC application. You can use the default template. I have called my project `GoogleAuthentication`.
::: warning
Ensure that when you create the project the 'None' option is selected for authentication. (Otherwise, you will need to remove the Identity framework later.)
:::
### Create a new Account Controller
Create a new blank MVC controller called `AccountController.cs`. This controller will be used to handle the Google Sign-In process.
Inject the configuration service into the controller so that we can access the Google Client ID from the `appsettings.json` file later.
After creating the controller and injecting the service the code should look like this:
```csharp
using Microsoft.AspNetCore.Mvc;
namespace GoogleAuthentication.Controllers
{
public class AccountController(IConfiguration configuration) : Controller
{
private readonly IConfiguration _configuration = configuration;
public IActionResult Index()
{
return View();
}
}
}
```
### Create a Login View
We will now add a `Login` method to the `AccountController`. This method will be used to redirect the user to the Google Sign-In page.
Start by creating a `LoginViewModel` in the `Models` folder. This view model will be used to pass the Google Client ID to the view.
```csharp
public class LoginViewModel
{
public string GoogleClientId { get; set; } = null!;
}
```
Then the following code to the `AccountController` class:
```csharp
public IActionResult Login()
{
LoginViewModel model = new()
{
GoogleClientId = _configuration["GoogleClientId"]!
};
return View(model);
}
```
This code will create a new `LoginViewModel` and pass the Google Client ID from the configuration to the view. At this state we can also remove the default `Index` method from the controller as we will not be using it.
Add a new view called `Login.cshtml` to the `Views/Account` folder. This view will be used to display the Google Sign-In button.
```razor
@model LoginViewModel
@{
ViewData["Title"] = "Login";
}
<div class="row">
<div class="column text-center">
<h3>Please sign in with your Google Account</h3>
<hr />
<div id="buttonDiv" class="has-text-centered"></div>
</div>
</div>
<form id="login-form" asp-asp-controller="Account" asp-action="Callback" method="post">
<input type="hidden" name="jwt" />
</form>
@section Scripts {
<script src="https://accounts.google.com/gsi/client" async defer></script>
<script>
function handleCredentialResponse(response) {
if (response.credential) {
let form = document.getElementById('login-form');
let jwt = document.getElementsByName('jwt')[0];
jwt.value = response.credential;
form.submit();
}
}
window.onload = function () {
google.accounts.id.initialize({
client_id: "@Model.GoogleClientId",
callback: handleCredentialResponse
});
google.accounts.id.renderButton(
document.getElementById("buttonDiv"),
{ theme: "outline", size: "large" } // customization attributes
);
google.accounts.id.prompt(); // also display the One Tap dialog
}
</script>
}
```
The above code will render the Google Sign-In button and handle the response from Google. When the user signs into google, the `handleCredentialResponse` function will be called and the authentication response from Google will be submitted as the data. This function will then submit the form with the JWT token (called credential in the response) as a hidden field.
### Create a Callback Method
Start by creating a model to represent the submitted form data.
```csharp
public class LoginRequestViewModel
{
public string Jwt { get; set; } = string.Empty;
}
```
We will now create a `Callback` method in the `AccountController`. This method will be used to handle the response from Google and then perform the authentication step that our appliation requires.
Add the following code to the `AccountController` class:
```csharp{19-20}
[HttpPost]
public async Task<IActionResult> Callback(LoginRequestViewModel login)
{
// If the jwt token is empty then user is not authorized
if (string.IsNullOrEmpty(login.Jwt))
{
throw new Exception("The jwt token is empty.");
}
// Otherwise we can verify the token with google and get the user's email address
Payload payload = await GoogleJsonWebSignature.ValidateAsync(login.Jwt);
// If the payload is not null and is valid then get the user by email address
if (payload != null)
{
string userEmail = payload.Email!;
// Perform necessary logic to sign in the user here
// e.g. create a cookie, or a JWT token, etc.
// Return the user to the home page
return RedirectToAction("Index", "Home");
}
else
{
// If the payload is null then the user is not authorized
throw new Exception("The payload is null.");
}
}
```
The above code will retrive the JWT token and then validate it with Google, using the GoogleJsonWebSignature class. This class is provided as part of the nuget package `Google.Apis.Auth`. If the token is valid, we can then perform any necessary logic to sign the user in. In this example we will just redirect the user to the home page, however in your own applications should could create a sign-in cookie, create a JWT or start a session.
In this above example if the sign in fails, an exception will be thrown. This will cause the user to be redirected to the error page. In a production application you would want to handle this error more gracefully.
### Add the Google Client ID to the Configuration
We will now add the Google Client ID to the configuration. This will allow us to access the value from the `appsettings.json` file.
Add the following code to the `appsettings.json` file:
```json
{
"GoogleClientId": "YOUR_GOOGLE_CLIENT_ID"
}
```
To generate a Google Client ID, follow the steps below:
1. Go to the [Google Cloud Console](https://console.cloud.google.com/)
2. Create a new project
3. Go to the [Credentials Page](https://console.cloud.google.com/apis/credentials)
4. Click the `Create Credentials` button and select `OAuth client ID`
5. Select `Web application` as the application type
6. Enter a name for the application
7. Add the following URL to the `Authorized JavaScript origins` section: `https://localhost:5001` (replace with your own URL)
8. Add the following URL to the `Authorized redirect URIs` section: `https://localhost:5001/account/callback` (replace with your own URL)
9. Click the `Create` button
10. Copy the Client ID and paste it into the `appsettings.json` file
You will also need to setup the consent screen for your application. To do this, follow the steps below:
1. Go to the [OAuth consent screen](https://console.cloud.google.com/apis/credentials/consent)
2. Select `External` as the user type
3. Click the `Create` button
4. Enter a name for the application
5. Add the following URL to the `Authorized domains` section: `localhost`
6. Click the `Save and continue` button
7. Click the `Add or remove scopes` button
8. Select the `email` and `profile` scopes
9. Click the `Update` button
10. Click the `Save and continue` button
### Test the Authentication
We will now test the authentication using the Google Sign-In button.
Start the application and then manually update the url to go to the `/Account/Login` route. This will redirect you to the Google Sign-In page. Either click the Google Sign-In button or use the one tap feature to sign in with your Google account. You should then be redirected to the home page.
## Conclusion
We have now learnt how to implement Google Sign-In without using the Identity framework. This is a great way to add authentication to your application without the overhead of the Identity framework.
This method works well when combined with cookie or JWT authentication. You can use the Google Sign-In to authenticate the user and then use the cookie or JWT to authenticate the user for future requests.

13
docs/dotnet/index.md Normal file
View File

@ -0,0 +1,13 @@
# .NET Snippets and Musings
#### [Blazor with an inbuilt API](./blazor-with-api.md)
#### [Database Seeding](./database-seed.md)
#### [Dockerising a Blazor Web App](./dockerising-blazor.md)
#### [OWIN Logging](./owin-logging.md)
#### [System.NET Logging](./system-net-logging.md)
#### [Unit of Work Template](./unit-of-work-template.md)
#### [JWT Authentication](./jwt-authentication.md)
#### [JWT Authentication with Cookie](./jwt-authentication-cookie.md)
#### [Google Sign in Without Identity](./google-sign-in-without-identity.md)
#### [Service Testing](./service-testing.md)
#### [Controller Testing](./controller-testing.md)

View File

@ -0,0 +1,125 @@
# JWT Authentication Stored in a Cookies
## Overview
Best practice for storing JWT tokens is to store them in a cookie. This is because cookies are automatically sent with every request to the server. This means that we do not need to manually add the token to the request header for every request. It also means that (assuming the cookie is set to HttpOnly) the token cannot be accessed by JavaScript. This is important as it prevents malicious JavaScript from accessing the token and sending it to a third party.
## Prerequisites
* Previous article completed: [JWT Authentication](./jwt-authentication.md)
Note: This article assumes you are using a Windows development environment. If you are using a Mac or Linux, you will need to use the appropriate tools for your environment.
## Implementation
### Open the JWT Authentication Project
We will start by opening the project we created in the previous article. This JWT Authentication project will be modified to store the JWT token in a cookie.
### Remove Swagger Bearer Authentication
We will start by removing the Swagger Bearer authentication. This is because we will be using cookies for authentication instead of a Bearer token and thus we no longer need to manually paste our token into the Swagger UI.
Simple open the `Program.cs` file and replace `builder.Services.AddSwaggerGen` section with the below:
```csharp
builder.Services.AddSwaggerGen();
```
This will remove the Bearer authentication from Swagger UI (i.e. the default setup).
### Update JWT Authentication to check cookies for JWT
We now need to update our JWT authentication to check for the JWT token in the cookies. We will do this by modifying the `AddJwtBearer` call in the `Program.cs` file. See changes below:
```csharp{15-26}
builder.Services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
.AddJwtBearer(o =>
{
o.TokenValidationParameters = new TokenValidationParameters
{
ValidIssuer = builder.Configuration["Jwt:Issuer"],
ValidAudience = builder.Configuration["Jwt:Audience"],
IssuerSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(builder.Configuration["Jwt:Key"]!)),
ValidateIssuer = true,
ValidateAudience = true,
ValidateLifetime = true,
ValidateIssuerSigningKey = true
};
o.Events = new JwtBearerEvents
{
OnMessageReceived = context =>
{
if (context.Request.Cookies.TryGetValue(WeatherForecastController.ACCESS_TOKEN_NAME, out var accessToken))
{
context.Token = accessToken;
}
return Task.CompletedTask;
}
};
});
```
This will configure JWT authentication to check for the JWT token in the `WeatherForecastController.ACCESS_TOKEN_NAME` cookie. If the cookie is found, it will be used as the JWT token, otherwise no token will be used, and the request will be rejected as per normal.
### Add Cookie Authentication to the Controller
Open the `WeatherForecastController.cs` file and add the following code to the top of the class:
```csharp
public const string ACCESS_TOKEN_NAME = "X-Access-Token";
```
This will create a constant that we can use to reference the name of the cookie that will store our JWT token.
In the same file, replace the `Ok(token)` line with the following:
```csharp
Response.Cookies.Append(ACCESS_TOKEN_NAME, token, new CookieOptions
{
HttpOnly = true,
Secure = true,
SameSite = SameSiteMode.Strict,
Expires = DateTime.UtcNow.AddMinutes(expireMinutes)
});
return Ok();
```
Here we are adding a HTTP only cookie to the response. This cookie will be used to store our JWT token. We are also setting the cookie to expire after 20 minutes (as per our configuration). This way when the JWT token expires, the cookie will also expire and the user will need to login again.
### Logout Method
We will now add a logout method to the controller. This method will be used to remove the JWT token cookie from the response. This will effectively log the user out of the application.
Add the following method to the `WeatherForecastController` class:
```csharp
[AllowAnonymous]
[HttpGet("logout")]
public IActionResult Logout()
{
Response.Cookies.Delete(ACCESS_TOKEN_NAME);
return Ok();
}
```
### Test the Authentication
We will now test the authentication using Swagger UI.
Start the application and then make the following requests:
* GET /weatherforecast/auth - This should return a 401 Unauthorized response.
* GET /weatherforecast - This should return a 200 OK response.
* POST /weatherforecast/login - Try this with both valid and invalid credentials, you should get a 200 OK response with when using valid credentials.
* GET /weatherforecast/auth - This should return a 200 OK response.
* GET /weatherforecast/logout - This should return a 200 OK response.
* GET /weatherforecast/auth - This should return a 401 Unauthorized response.
## Conclusion
We have now learnt how to update our JWT authentication to store the JWT token in a cookie. This is a more secure way of using JWTs with single page applications and also simplifies the process of authenticating requests as we no longer need to manually add the JWT token to the request header.

View File

@ -0,0 +1,187 @@
# JWT Authentication
## Overview
JWT authentication is a common authentication mechanism for web applications. It generally works well when using with APIs and SPAs. This article will cover how to implement JWT authentication in a .NET Core web application.
## Prerequisites
* .NET Core 8
* Visual Studio 2022
Note: This article assumes you are using a Windows development environment. If you are using a Mac or Linux, you will need to use the appropriate tools for your environment.
## Implementation
### Create a new .NET Core Web API Application
Open Visual Studio and create a new .NET Core Web API application. You can use the default template.
### Create an Authenticated Route
We will get started by creating a new authenticated route that will return the default weather forecast data. This will be the only route that requires authentication. We will be able to use this route to ensure that our authentication is working properly.
Simply add the following code to the `WeatherForecastController` class. You will need to add the `using using Microsoft.AspNetCore.Authorization;` namespace.
```csharp
[Authorize]
[HttpGet("auth")]
public IEnumerable<WeatherForecast> GetAuthenicated()
{
return Enumerable.Range(1, 5).Select(index => new WeatherForecast
{
Date = DateOnly.FromDateTime(DateTime.Now.AddDays(index)),
TemperatureC = Random.Shared.Next(-20, 55),
Summary = Summaries[Random.Shared.Next(Summaries.Length)]
})
.ToArray();
}
```
### Add JWT Authentication
We will now add JWT authentication to our application. We will be using the `Microsoft.AspNetCore.Authentication.JwtBearer` package to handle the authentication. This package will need to be installed via NuGet.
Open the Program.cs file and add the following code before the `builder.Build()` call.
```csharp
builder.Services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
.AddJwtBearer(o =>
{
o.TokenValidationParameters = new TokenValidationParameters
{
ValidIssuer = builder.Configuration["Jwt:Issuer"],
ValidAudience = builder.Configuration["Jwt:Audience"],
IssuerSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(builder.Configuration["Jwt:Key"]!)),
ValidateIssuer = true,
ValidateAudience = true,
ValidateLifetime = true,
ValidateIssuerSigningKey = true
};
});
```
In the above we are adding a default JWT authentication scheme and configuring it to use the values pulled from our configuration sources. In this case we will add them to the appsettings.json, however in a production application you would want to use a more secure method of storing these values.
::: tip
You can use the [Secret Manager](https://docs.microsoft.com/en-us/aspnet/core/security/app-secrets?view=aspnetcore-6.0&tabs=windows) to store your secrets locally.
:::
```json
{
"Jwt": {
"Key": "ThisIsMySuperSecretKeyForTheDevEnvironment",
"Issuer": "https://localhost:5001",
"Audience": "https://localhost:5001",
"ExpireMinutes": 20
}
}
```
### Create a Login Method
We will now create a login method that will return a JWT token. This token will be used to authenticate the user for future requests.
Add the following code to the `WeatherForecastController` class.
First we will need to inject an IConfiguration instance into the controller. This will allow us to access the configuration values we added earlier.
```csharp
private readonly IConfiguration _configuration;
public WeatherForecastController(ILogger<WeatherForecastController> logger, IConfiguration configuration)
{
_logger = logger;
_configuration = configuration;
}
```
We can then fill out the login method as below:
::: warning
This is a very basic implementation of a login method. In a production application you would want to use a more secure method of storing and retrieving user credentials.
:::
```csharp
[AllowAnonymous]
[HttpPost("login")]
public IActionResult Login([FromBody] string username, [FromBody] string password)
{
if (username == "username" && password == "password")
{
var issuer = _configuration["Jwt:Issuer"];
var audience = _configuration["Jwt:Audience"];
var expireMinutes = Convert.ToInt32(_configuration["Jwt:ExpireMinutes"]);
var key = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(_configuration["Jwt:Key"]!));
var tokenDescriptor = new SecurityTokenDescriptor
{
Subject = new ClaimsIdentity(new Claim[]
{
new(ClaimTypes.Name, username)
}),
Expires = DateTime.UtcNow.AddMinutes(expireMinutes),
Issuer = issuer,
Audience = audience,
SigningCredentials = new SigningCredentials(key, SecurityAlgorithms.HmacSha256Signature)
};
var tokenHandler = new JwtSecurityTokenHandler();
var createdToken = tokenHandler.CreateToken(tokenDescriptor);
var token = tokenHandler.WriteToken(createdToken);
return Ok(token);
}
return Unauthorized();
}
```
At this stage we can do some testing to ensure out methods are working as expected. Start the API project and then use the default Swagger UI to test the following:
* GET /weatherforecast/auth - This should return a 401 Unauthorized response.
* GET /weatherforecast - This should return a 200 OK response.
* POST /weatherforecast/login - Try this with both valid and invalid credentials, you should get a 200 OK response with a token when using valid credentials.
### Setting up Swagger UI to allow authentication
Finally for us to test if our authentication is working we will need to configure Swagger UI to allow us to pass the token in the request header. Simple reaplace the `builder.Services.AddSwaggerGen()` call in the Program.cs file with the following:
```csharp
builder.Services.AddSwaggerGen(options =>
{
options.AddSecurityDefinition("Bearer", new OpenApiSecurityScheme
{
Description = "JWT Authorization header using the Bearer scheme.",
Type = SecuritySchemeType.Http,
Scheme = "bearer"
});
options.AddSecurityRequirement(new OpenApiSecurityRequirement
{
{
new OpenApiSecurityScheme
{
Reference = new OpenApiReference
{
Id = "Bearer",
Type = ReferenceType.SecurityScheme
}
},
Array.Empty<string>()
}
});
});
```
To test this out, start the API project and then use the login method to get a token. You can then click the Authorize button in the Swagger UI and enter the token.
You should now be able to access the authenticated route. If you try to access the route without the token you should get a 401 Unauthorized response.
## Conclusion
In this article we have covered how to implement JWT authentication in a .NET Core web application. We have also covered how to test the authentication using Swagger UI.

View File

@ -0,0 +1,29 @@
# Enable OWIN Logging in .NET Framework
The following code snippet shows how to enable Microsoft.OWIN logging in ASP.NET. OWIN logging is mostly useful for debugging issues relating to user sign in / flow, especially when using external identity providers such as Google, Facebook, etc.
To enable OWIN logging, add the following to your `web.config` file inside the `<configuration>` element:
```xml
<system.diagnostics>
<switches>
<add name="Microsoft.Owin" value="Verbose" />
</switches>
<trace autoflush="true" />
<sources>
<source name="Microsoft.Owin">
<listeners>
<add name="console" />
</listeners>
</source>
<source name="Microsoft.Owin">
<listeners>
<add name="file"
type="System.Diagnostics.TextWriterTraceListener"
initializeData="traces-Owin.log"
/>
</listeners>
</source>
</sources>
</system.diagnostics>
```

View File

@ -0,0 +1,489 @@
# Testing Services
Testing services is a important part of .NET devolopment as it helps to ensure that the services are working as expected and also helps to catch bugs early in the development process. It's most likely for a service test to be a unit test, and it's important to test the service in isolation. This means that the service should be tested without any dependencies on external services or databases.
For this reason it's important to use dependency injection to inject mock services into the service being tested. This allows the service to be tested in isolation and ensures that the test is repeatable and reliable. This will also require that interfaces are used for the services so that the mock services can be injected into the service being tested.
# Example Application
In this example we will create a simple Web API to maintain a user list. To get started create a new Web API project titled `UserApp` and enabled controller support.
## User Model and DB Context
Create a new `Data` folder and insert the following files:
::: code-group
```csharp [User.cs]
namespace UserApp.Data
{
public class User
{
public int Id { get; set; }
public string Name { get; set; } = string.Empty;
public string Email { get; set; } = string.Empty;
public string FirstName { get; set; } = string.Empty;
public string LastName { get; set; } = string.Empty;
}
}
```
:::
::: code-group
```csharp [UserAppContext.cs]
using Microsoft.EntityFrameworkCore;
namespace UserApp.Data
{
public class UserAppContext : DbContext
{
public UserAppContext(DbContextOptions<UserAppContext> options) : base(options) { }
public DbSet<User> Users { get; set; }
}
}
```
:::
::: code-group
```csharp [UserViewModel.cs]
namespace UserApp.Models
{
public class UserViewModel(string email, string firstName, string lastName)
{
public string Email { get; set; } = email;
public string FirstName { get; set; } = firstName;
public string LastName { get; set; } = lastName;
}
}
```
:::
As part of this ensure the following NuGet packages are installed:
* Microsoft.EntityFrameworkCore
## Service Interface and Implementation
First create a entity not found exception:
::: code-group
```csharp [EntityNotFoundException.cs]
namespace UserApp.Infrastructure.Exceptions
{
public class EntityNotFoundException(string message) : Exception(message) { }
}
```
:::
Then we can create the service interface and implementation:
::: code-group
```csharp [IUserService.cs]
using UserApp.Data;
namespace UserApp.Services
{
public interface IUserService
{
public Task<IEnumerable<User>> GetUsersAsync();
public Task<User> GetUserAsync(int id);
public Task<User> AddUserAsync(string email, string firstName, string lastName);
public Task<User> UpdateUserAsync(int id, string email, string firstName, string lastName);
public Task<User> DeleteUserAsync(int id);
}
}
```
```csharp [UserService.cs]
using Microsoft.EntityFrameworkCore;
using UserApp.Data;
using UserApp.Infrastructure.Exceptions;
namespace UserApp.Services
{
public class UserService(UserAppContext context) : IUserService
{
private readonly UserAppContext _context = context;
public async Task<User> AddUserAsync(string email, string firstName, string lastName)
{
User user = new()
{
Email = email,
FirstName = firstName,
LastName = lastName
};
await _context.Users.AddAsync(user);
await _context.SaveChangesAsync();
return user;
}
public async Task<User> DeleteUserAsync(int id)
{
User? user = await _context.Users.FindAsync(id) ?? throw new EntityNotFoundException("User not found");
_context.Users.Remove(user);
await _context.SaveChangesAsync();
return user;
}
public async Task<User> GetUserAsync(int id)
{
User user = await _context.Users.FindAsync(id) ?? throw new EntityNotFoundException("User not found");
return user;
}
public async Task<IEnumerable<User>> GetUsersAsync()
{
return await _context.Users.ToListAsync();
}
public async Task<User> UpdateUserAsync(int id, string email, string firstName, string lastName)
{
User user = await _context.Users.FindAsync(id) ?? throw new EntityNotFoundException("User not found");
user.Email = email;
user.FirstName = firstName;
user.LastName = lastName;
await _context.SaveChangesAsync();
return user;
}
}
}
```
:::
## Setup Dependency Injection
We now need to add our DI config to the `Program.cs` file. Add the following to the service section, you will also need to install the following NuGet packages:
* Microsoft.EntityFrameworkCore.InMemory
::: code-group
```csharp [Program.cs]
builder.Services.AddDbContext<UserAppContext>(options =>
{
options.UseInMemoryDatabase("UserApp");
});
builder.Services.AddScoped<IUserService, UserService>();
```
:::
## Controller
Finally we can create a controller to interact with the service:
::: code-group
```csharp [UsersController.cs]
using Microsoft.AspNetCore.Mvc;
using UserApp.Data;
using UserApp.Infrastructure.Exceptions;
using UserApp.Models;
using UserApp.Services;
namespace UserApp.Controllers
{
[Route("api/[controller]")]
public class UserController(IUserService userService, ILogger<UserController> logger) : ControllerBase
{
private readonly ILogger<UserController> _logger = logger;
private readonly IUserService _userService = userService;
[HttpGet]
public async Task<IActionResult> GetUsersAsync()
{
try
{
return Ok(await _userService.GetUsersAsync());
}
catch (Exception exception)
{
_logger.LogError(exception, "An error occurred while getting the users");
return StatusCode(500);
}
}
[HttpGet("{id}")]
public async Task<IActionResult> GetUserAsync(int id)
{
try
{
return Ok(await _userService.GetUserAsync(id));
}
catch (EntityNotFoundException exception)
{
_logger.LogError(exception, "User not found");
return NotFound();
}
catch (Exception exception)
{
_logger.LogError(exception, "An error occurred while getting the user");
return StatusCode(500);
}
}
[HttpPost]
public async Task<IActionResult> AddUserAsync([FromBody] UserViewModel user)
{
try
{
User createdUser = await _userService.AddUserAsync(user.Email, user.FirstName, user.LastName);
return CreatedAtAction("GetUser", new { id = createdUser.Id }, createdUser);
}
catch (Exception exception)
{
_logger.LogError(exception, "An error occurred while adding the user");
return StatusCode(500);
}
}
[HttpPut("{id}")]
public async Task<IActionResult> UpdateUserAsync(int id, [FromBody] UserViewModel user)
{
try
{
return Ok(await _userService.UpdateUserAsync(id, user.Email, user.FirstName, user.LastName));
}
catch (EntityNotFoundException exception)
{
_logger.LogError(exception, "User not found");
return NotFound();
}
catch (Exception exception)
{
_logger.LogError(exception, "An error occurred while updating the user");
return StatusCode(500);
}
}
[HttpDelete("{id}")]
public async Task<IActionResult> DeleteUserAsync(int id)
{
try
{
return Ok(await _userService.DeleteUserAsync(id));
}
catch (EntityNotFoundException exception)
{
_logger.LogError(exception, "User not found");
return NotFound();
}
catch (Exception exception)
{
_logger.LogError(exception, "An error occurred while deleting the user");
return StatusCode(500);
}
}
}
}
```
:::
# Service Testing
Now that we have our service and controller setup we can start testing the service. We will be using MSTest and Moq to test the service.
Create a new MSTest project titled `UserApp.Service.Test` and then add a new file titled `UserServiceTests.cs`. We will first get started with two tests, to ensure our `GetUsersAsync` method is working as expected.
::: code-group
```csharp [UserServiceTests.cs]
using Microsoft.EntityFrameworkCore;
using UserApp.Data;
using UserApp.Services;
namespace UserApp.Service.Test
{
[TestClass]
public class UserServiceTests
{
private UserAppContext _context = null!;
[TestInitialize]
public void Initialize()
{
DbContextOptions<UserAppContext> options = new DbContextOptionsBuilder<UserAppContext>()
.UseInMemoryDatabase(Guid.NewGuid().ToString())
.Options;
_context = new UserAppContext(options);
}
[TestCleanup]
public void Cleanup()
{
_context.Dispose();
_context = null!;
}
[TestMethod]
public async Task GetUsersAsync_NoUsers_ReturnsNoUsers()
{
// Arrange
UserService service = new(_context);
// Act
IEnumerable<User> users = await service.GetUsersAsync();
// Assert
Assert.AreEqual(0, users.Count());
}
[TestMethod]
public async Task GetUsersAsync_OneUser_ReturnsOneUser()
{
// Arrange
_context.Users.Add(new User() { Email = "john@test.com", FirstName = "John", LastName = "Smith" });
_context.SaveChanges();
UserService service = new(_context);
// Act
IEnumerable<User> users = await service.GetUsersAsync();
// Assert
Assert.AreEqual(1, users.Count());
Assert.AreEqual("john@test.com", users.First().Email);
}
}
}
```
Run these two tests and confirm that they pass. This will confirm that the `GetUsersAsync` method is working as expected. You can now continue to write tests for the other methods in the `UserService` class.
:::
```csharp [UserServiceTests]
// ...
[TestMethod]
public async Task GetUserAsync_ForExistingUser_ReturnsUser()
{
// Arrange
User user1 = new() { Email = "john@test.com", FirstName = "John", LastName = "Smith" };
User user2 = new() { Email = "jane@test.com", FirstName = "Jane", LastName = "Doe" };
await _context.Users.AddRangeAsync(user1, user2);
await _context.SaveChangesAsync();
UserService service = new(_context);
// Act
User user = await service.GetUserAsync(1);
// Assert
Assert.AreEqual("john@test.com", user.Email);
}
[TestMethod]
public async Task GetUserAsync_ForNonExistingUserAndNoUsers_ThrowsEntityNotFoundException()
{
// Arrange
UserService service = new(_context);
// Act and Assert
await Assert.ThrowsExceptionAsync<EntityNotFoundException>(() => service.GetUserAsync(1));
}
[TestMethod]
public async Task GetUserAsync_ForNonExistingUserAndUsers_ThrowsEntityNotFoundException()
{
// Arrange
User user1 = new() { Email = "john@test.com", FirstName = "John", LastName = "Smith" };
User user2 = new() { Email = "jane@test.com", FirstName = "Jane", LastName = "Doe" };
await _context.Users.AddRangeAsync(user1, user2);
await _context.SaveChangesAsync();
UserService service = new(_context);
// Act and Assert
await Assert.ThrowsExceptionAsync<EntityNotFoundException>(() => service.GetUserAsync(3));
}
[TestMethod]
public async Task AddUserAsync_AddsUser()
{
// Arrange
UserService service = new(_context);
// Act
int oldCount = _context.Users.Count();
User user = await service.AddUserAsync("john@test.com", "John", "Smith");
int newCount = _context.Users.Count();
// Assert
Assert.AreEqual(0, oldCount);
Assert.AreEqual(1, newCount);
Assert.AreEqual("john@test.com", _context.Users.First().Email);
}
[TestMethod]
public async Task UpdateUserAsync_CanUpdateExistingUser_UserUpdated()
{
// Arrange
User user1 = new() { Email = "john@test.com", FirstName = "John", LastName = "Smith" };
User user2 = new() { Email = "jane@test.com", FirstName = "Jane", LastName = "Doe" };
await _context.Users.AddRangeAsync(user1, user2);
await _context.SaveChangesAsync();
UserService service = new(_context);
// Act
await service.UpdateUserAsync(1, "johnnew@test.com", "JohnNew", "SmithNew");
// Assert
User updatedUser = (await _context.Users.FindAsync(1))!;
Assert.AreEqual("johnnew@test.com", updatedUser.Email);
Assert.AreEqual("JohnNew", updatedUser.FirstName);
Assert.AreEqual("SmithNew", updatedUser.LastName);
}
public async Task UpdateUserAsync_ForNonExistingUser_ThrowsEntityNotFoundException()
{
// Arrange
User user1 = new() { Email = "john@test.com", FirstName = "John", LastName = "Smith" };
User user2 = new() { Email = "jane@test.com", FirstName = "Jane", LastName = "Doe" };
await _context.Users.AddRangeAsync(user1, user2);
await _context.SaveChangesAsync();
UserService service = new(_context);
// Act and Assert
await Assert.ThrowsExceptionAsync<EntityNotFoundException>(() => service.UpdateUserAsync(3, "", "", ""));
}
[TestMethod]
public async Task DeleteUserAsync_ForExistingUser_RemovesUser()
{
// Arrange
User user1 = new() { Email = "john@test.com", FirstName = "John", LastName = "Smith" };
await _context.Users.AddAsync(user1);
await _context.SaveChangesAsync();
UserService service = new(_context);
// Act
int oldCount = _context.Users.Count();
User user = await service.DeleteUserAsync(1);
int newCount = _context.Users.Count();
// Assert
Assert.AreEqual(1, oldCount);
Assert.AreEqual(0, newCount);
Assert.AreEqual("john@test.com", user.Email);
}
[TestMethod]
public async Task DeleteUserAsync_ForNonExistingUser_ThrowsEntityNotFoundException()
{
// Arrange
UserService service = new(_context);
// Act and Assert
await Assert.ThrowsExceptionAsync<EntityNotFoundException>(() => service.DeleteUserAsync(1));
}
// ...
```
# Conclusion
This this example we have created a simple Web API, and tested the service using MSTest. This is a good starting point for testing services in .NET and can be expanded upon to test more complex services.

View File

@ -0,0 +1,21 @@
# Enable System.NET Logging in .NET Framework
The following code snippet shows how to enable System.NET logging in ASP.NET. System.NET logging is mostly useful for debugging issues relating to HTTP requests and responses. Often this is useful when proxies are involved, or when you need to see the raw HTTP request and response.
To enable System.NET logging, add the following to your `web.config` file inside the `<configuration>` element:
```xml
<system.diagnostics>
<trace autoflush="true" />
<sharedListeners>
<add name="file" initializeData="D:\\network.log" type="System.Diagnostics.TextWriterTraceListener" />
</sharedListeners>
<sources>
<source name="System.Net" switchValue="Verbose">
<listeners>
<add name="file" />
</listeners>
</source>
</sources>
</system.diagnostics>
```

View File

@ -0,0 +1,177 @@
# Unit of Work Pattern
The unit of work pattern is a way to manage the state of multiple objects in a single transaction. This pattern is useful when you need to update multiple objects in a single transaction, and you need to ensure that all of the objects are updated successfully or none of them are updated.
A common way of implementing the unit of work pattern is to use a repository class that manages the state of multiple objects. The repository class is responsible for managing the state of the objects and for committing the changes to the database. The repository class is also responsible for ensuring that all of the objects are updated successfully or none of them are updated.
A generic repository class can be used in most cases, and where not possible it can be extended into a more specific repository class. The following example shows a generic repository class that can be used to manage the state of multiple objects.
```csharp title="GenericRepository.cs"
using Microsoft.EntityFrameworkCore;
using System.Linq.Expressions;
public class GenericRepository < TEntity > where TEntity: class
{
internal CMSContext _context;
internal DbSet < TEntity > _dbSet;
public GenericRepository(CMSContext context)
{
_context = context;
_dbSet = context.Set < TEntity > ();
}
public virtual async Task < IEnumerable < TEntity >> GetAsync(
Expression < Func < TEntity, bool >> ? filter = null,
Func < IQueryable < TEntity > , IOrderedQueryable < TEntity >> ? orderBy = null,
string includeProperties = "")
{
IQueryable < TEntity > query = _dbSet;
if (filter != null)
{
query = query.Where(filter);
}
foreach(var includeProperty in includeProperties.Split(new char[]
{
','
}, StringSplitOptions.RemoveEmptyEntries)) {
query = query.Include(includeProperty);
}
if (orderBy != null)
{
return await orderBy(query).ToListAsync();
} else
{
return await query.ToListAsync();
}
}
public virtual async Task < TEntity ? > GetByIdAsync(object id)
{
return await _dbSet.FindAsync(id);
}
public virtual async Task InsertAsync(TEntity entity)
{
await _dbSet.AddAsync(entity);
}
public virtual async Task < bool > DeleteAsync(object id)
{
TEntity ? entityToDelete = await _dbSet.FindAsync(id);
if (entityToDelete != null)
{
Delete(entityToDelete);
return true;
}
return false;
}
public virtual void DeleteRange(ICollection < TEntity > entitiesToDelete)
{
foreach(TEntity entity in entitiesToDelete)
{
Delete(entity);
}
}
public virtual void Delete(TEntity entityToDelete) {
if (_context.Entry(entityToDelete).State == EntityState.Detached)
{
_dbSet.Attach(entityToDelete);
}
_dbSet.Remove(entityToDelete);
}
public virtual void Update(TEntity entityToUpdate)
{
_dbSet.Attach(entityToUpdate);
_context.Entry(entityToUpdate).State = EntityState.Modified;
}
public virtual async Task < bool > AnyAsync(
Expression < Func < TEntity, bool >> ? filter = null
)
{
IQueryable < TEntity > query = _dbSet;
if (filter != null)
{
query = query.Where(filter);
}
return await query.AnyAsync();
}
}
```
The following example shows a Unit of Work class that uses the generic repository class to manage the state of multiple objects.
```csharp title="UnitOfWork.cs"
public class UnitOfWork: IDisposable, IUnitOfWork
{
private bool disposedValue;
private readonly CMSContext _context;
private GenericRepository < User > ? _userRepository;
public UnitOfWork(CMSContext context)
{
_context = context;
}
public GenericRepository < User > UserRepository
{
get
{
_userRepository ??= new GenericRepository < User > (_context);
return _userRepository;
}
}
public async Task SaveChangesAsync()
{
await _context.SaveChangesAsync();
}
protected virtual void Dispose(bool disposing)
{
if (!disposedValue)
{
if (disposing)
{
_context.Dispose();
}
disposedValue = true;
}
}
public void Dispose()
{
// Do not change this code. Put cleanup code in 'Dispose(bool disposing)' method
Dispose(disposing: true);
GC.SuppressFinalize(this);
}
}
```
::: tip
Add extra repositories for each model you have in your source code. For example, if you have a `Item` model, add a `ItemRepository` property to the `UnitOfWork` class.
:::
The following example shows the interface for the Unit of Work class, this should be used when setting up dependency injection.
```csharp title="IUnitOfWork.cs"
public interface IUnitOfWork : IDisposable
{
GenericRepository<User> UserRepository { get; }
Task SaveChangesAsync();
}
```