initial commit
This commit is contained in:
314
docs/dotnet/blazor-with-api.md
Normal file
314
docs/dotnet/blazor-with-api.md
Normal 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.
|
488
docs/dotnet/controller-testing.md
Normal file
488
docs/dotnet/controller-testing.md
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
:::
|
58
docs/dotnet/database-seed.md
Normal file
58
docs/dotnet/database-seed.md
Normal 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);
|
||||
|
||||
// ...
|
||||
}
|
||||
|
||||
// ...
|
||||
```
|
35
docs/dotnet/dockerising-blazor.md
Normal file
35
docs/dotnet/dockerising-blazor.md
Normal 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.
|
||||
|
||||
:::
|
230
docs/dotnet/google-sign-in-without-identity.md
Normal file
230
docs/dotnet/google-sign-in-without-identity.md
Normal 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
13
docs/dotnet/index.md
Normal 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)
|
125
docs/dotnet/jwt-authentication-cookie.md
Normal file
125
docs/dotnet/jwt-authentication-cookie.md
Normal 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.
|
187
docs/dotnet/jwt-authentication.md
Normal file
187
docs/dotnet/jwt-authentication.md
Normal 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.
|
29
docs/dotnet/owin-logging.md
Normal file
29
docs/dotnet/owin-logging.md
Normal 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>
|
||||
```
|
489
docs/dotnet/service-testing.md
Normal file
489
docs/dotnet/service-testing.md
Normal 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.
|
21
docs/dotnet/system-net-logging.md
Normal file
21
docs/dotnet/system-net-logging.md
Normal 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>
|
||||
```
|
177
docs/dotnet/unit-of-work-template.md
Normal file
177
docs/dotnet/unit-of-work-template.md
Normal 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();
|
||||
}
|
||||
```
|
Reference in New Issue
Block a user