Skip to content

Commit 1992cf4

Browse files
authored
Added a Person example (#15)
* Add a User object example. * Update .net sdk to latest 8.0.412 LT
1 parent 649ab65 commit 1992cf4

File tree

13 files changed

+297
-66
lines changed

13 files changed

+297
-66
lines changed
Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
namespace BestWeatherForecast.Api._2023_06_06.Controllers;
2+
3+
using Asp.Versioning;
4+
using BestWeatherForecast.Api._2023_06_06.Models;
5+
using BestWeatherForecast.Domain;
6+
using Microsoft.AspNetCore.Mvc;
7+
8+
9+
/// <summary>
10+
/// User management controller.
11+
/// </summary>
12+
[ApiController]
13+
[ApiVersion("2023-06-06")]
14+
[Consumes("application/json")]
15+
[Produces("application/json")]
16+
[Route("api/[controller]")]
17+
public class UsersController : ControllerBase
18+
{
19+
/// <summary>
20+
/// Register a new user.
21+
/// </summary>
22+
/// <param name="request"></param>
23+
/// <returns></returns>
24+
[HttpPost]
25+
public ActionResult<User> RegisterUser([FromBody] RegisterUserRequest request) =>
26+
FirstName.TryCreate(request.FirstName)
27+
.Combine(LastName.TryCreate(request.LastName))
28+
.Combine(EmailAddress.TryCreate(request.Email))
29+
.Bind((firstName, lastName, email) => Domain.User.TryCreate(firstName, lastName, email, request.Password))
30+
.ToActionResult(this);
31+
32+
/// <summary>
33+
/// Get a greeting for the specified user.
34+
/// </summary>
35+
/// <param name="name"></param>
36+
/// <returns></returns>
37+
[HttpGet("{name}")]
38+
public ActionResult<string> Get(string name) => Ok($"Hello {name}!");
39+
40+
/// <summary>
41+
/// Delete a user by ID.
42+
/// </summary>
43+
/// <param name="id"></param>
44+
/// <returns></returns>
45+
[HttpDelete("{id}")]
46+
public ActionResult<Unit> Delete(string id) =>
47+
UserId.TryCreate(id).Finally(
48+
ok => NoContent(),
49+
err => err.ToActionResult<Unit>(this));
50+
}
Lines changed: 63 additions & 64 deletions
Original file line numberDiff line numberDiff line change
@@ -1,73 +1,72 @@
1-
namespace BestWeatherForecast.Api._2023_06_06.Controllers
1+
namespace BestWeatherForecast.Api._2023_06_06.Controllers;
2+
3+
using Asp.Versioning;
4+
using BestWeatherForecast.Application.WeatherForcast;
5+
using BestWeatherForecast.Domain;
6+
using Mapster;
7+
using Mediator;
8+
using Microsoft.AspNetCore.Mvc;
9+
using ServiceLevelIndicators;
10+
11+
/// <summary>
12+
/// Weather forecast controller.
13+
/// </summary>
14+
[ApiController]
15+
[ApiVersion("2023-06-06")]
16+
[Consumes("application/json")]
17+
[Produces("application/json")]
18+
[Route("api/[controller]")]
19+
public class WeatherForecastController : ControllerBase
220
{
3-
using Asp.Versioning;
4-
using BestWeatherForecast.Application.WeatherForcast;
5-
using BestWeatherForecast.Domain;
6-
using Mapster;
7-
using Mediator;
8-
using Microsoft.AspNetCore.Mvc;
9-
using ServiceLevelIndicators;
21+
private readonly ISender _sender;
1022

1123
/// <summary>
12-
/// Weather forecast controller.
24+
/// Constructor
1325
/// </summary>
14-
[ApiController]
15-
[ApiVersion("2023-06-06")]
16-
[Consumes("application/json")]
17-
[Produces("application/json")]
18-
[Route("api/[controller]")]
19-
public class WeatherForecastController : ControllerBase
20-
{
21-
private readonly ISender _sender;
26+
/// <param name="sender"></param>
27+
public WeatherForecastController(ISender sender) => _sender = sender;
2228

23-
/// <summary>
24-
/// Constructor
25-
/// </summary>
26-
/// <param name="sender"></param>
27-
public WeatherForecastController(ISender sender) => _sender = sender;
28-
29-
/// <summary>
30-
/// Get the weather forecast for Redmond,WA.
31-
/// </summary>
32-
/// <param name="cancellationToken">Cancellation Token.</param>
33-
/// <returns></returns>
34-
[HttpGet]
35-
[ProducesResponseType(typeof(Models.WeatherForecast), StatusCodes.Status200OK)]
36-
[ProducesResponseType(StatusCodes.Status400BadRequest)]
37-
[ProducesResponseType(StatusCodes.Status404NotFound)]
38-
public async ValueTask<ActionResult<Models.WeatherForecast>> GetRedmond(CancellationToken cancellationToken)
39-
=> await ZipCode.TryCreate("98052")
40-
.Bind(static zipCode => WeatherForecastQuery.TryCreate(zipCode))
41-
.BindAsync(q => _sender.Send(q, cancellationToken))
42-
.MapAsync(r => r.Adapt<Models.WeatherForecast>())
43-
.ToActionResultAsync(this);
29+
/// <summary>
30+
/// Get the weather forecast for Redmond,WA.
31+
/// </summary>
32+
/// <param name="cancellationToken">Cancellation Token.</param>
33+
/// <returns></returns>
34+
[HttpGet]
35+
[ProducesResponseType(typeof(Models.WeatherForecast), StatusCodes.Status200OK)]
36+
[ProducesResponseType(StatusCodes.Status400BadRequest)]
37+
[ProducesResponseType(StatusCodes.Status404NotFound)]
38+
public async ValueTask<ActionResult<Models.WeatherForecast>> GetRedmond(CancellationToken cancellationToken)
39+
=> await ZipCode.TryCreate("98052")
40+
.Bind(static zipCode => WeatherForecastQuery.TryCreate(zipCode))
41+
.BindAsync(q => _sender.Send(q, cancellationToken))
42+
.MapAsync(r => r.Adapt<Models.WeatherForecast>())
43+
.ToActionResultAsync(this);
4444

45-
/// <summary>
46-
/// Get the weather forecast for the given zip code.
47-
/// </summary>
48-
/// <param name="zipCode">Zip code.</param>
49-
/// <param name="cancellationToken">Cancellation Token.</param>
50-
/// <returns></returns>
51-
[HttpGet("{zipCode}")]
52-
[ProducesResponseType(typeof(Models.WeatherForecast), StatusCodes.Status200OK)]
53-
[ProducesResponseType(StatusCodes.Status400BadRequest)]
54-
[ProducesResponseType(StatusCodes.Status404NotFound)]
55-
public async ValueTask<ActionResult<Models.WeatherForecast>> Get([CustomerResourceId] string zipCode, CancellationToken cancellationToken)
56-
=> await ZipCode.TryCreate(zipCode)
57-
.Bind(static zipCode => WeatherForecastQuery.TryCreate(zipCode))
58-
.BindAsync(q => _sender.Send(q, cancellationToken))
59-
.MapAsync(r => r.Adapt<Models.WeatherForecast>())
60-
.ToActionResultAsync(this);
45+
/// <summary>
46+
/// Get the weather forecast for the given zip code.
47+
/// </summary>
48+
/// <param name="zipCode">Zip code.</param>
49+
/// <param name="cancellationToken">Cancellation Token.</param>
50+
/// <returns></returns>
51+
[HttpGet("{zipCode}")]
52+
[ProducesResponseType(typeof(Models.WeatherForecast), StatusCodes.Status200OK)]
53+
[ProducesResponseType(StatusCodes.Status400BadRequest)]
54+
[ProducesResponseType(StatusCodes.Status404NotFound)]
55+
public async ValueTask<ActionResult<Models.WeatherForecast>> Get([CustomerResourceId] string zipCode, CancellationToken cancellationToken)
56+
=> await ZipCode.TryCreate(zipCode)
57+
.Bind(static zipCode => WeatherForecastQuery.TryCreate(zipCode))
58+
.BindAsync(q => _sender.Send(q, cancellationToken))
59+
.MapAsync(r => r.Adapt<Models.WeatherForecast>())
60+
.ToActionResultAsync(this);
6161

62-
/// <summary>
63-
/// This method throws to show the error handling middleware handles it.
64-
/// </summary>
65-
/// <returns></returns>
66-
/// <exception cref="NotImplementedException"></exception>
67-
[HttpGet("Throw")]
68-
public string Throw()
69-
{
70-
throw new NotImplementedException("Catch me middleware.");
71-
}
62+
/// <summary>
63+
/// This method throws to show the error handling middleware handles it.
64+
/// </summary>
65+
/// <returns></returns>
66+
/// <exception cref="NotImplementedException"></exception>
67+
[HttpGet("Throw")]
68+
public string Throw()
69+
{
70+
throw new NotImplementedException("Catch me middleware.");
7271
}
7372
}
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
namespace BestWeatherForecast.Api._2023_06_06.Models;
2+
3+
/// <summary>
4+
/// Request model for registering a user.
5+
/// </summary>
6+
/// <param name="FirstName"></param>
7+
/// <param name="LastName"></param>
8+
/// <param name="Email"></param>
9+
/// <param name="Password"></param>
10+
public record RegisterUserRequest(
11+
string FirstName,
12+
string LastName,
13+
string Email,
14+
string Password
15+
);

Api/src/api.http

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
// Okay
2+
GET {{HostAddress}}/api/Users/xavier?api-version=2023-06-06
3+
Accept: application/json
4+
5+
###
6+
// Create User
7+
// Okay
8+
POST {{HostAddress}}/api/Users?api-version=2023-06-06
9+
Content-Type: application/json
10+
11+
{
12+
"firstName": "Xavier",
13+
"lastName": "John",
14+
"email": "[email protected]",
15+
"password": "Super!Complex6"
16+
}
17+
18+
###
19+
// Bad Request email
20+
POST {{HostAddress}}/api/Users?api-version=2023-06-06
21+
Content-Type: application/json
22+
23+
{
24+
"firstName": "Xavier",
25+
"lastName": "John",
26+
"email": "xavier",
27+
"password": "Super!Complex6"
28+
}
29+
30+
###
31+
// Bad Request password
32+
POST {{HostAddress}}/api/Users?api-version=2023-06-06
33+
Content-Type: application/json
34+
35+
{
36+
"firstName": "Xavier",
37+
"lastName": "John",
38+
"email": "[email protected]",
39+
"password": "SimplePassword"
40+
}
41+
42+
###
43+
// Bad Request multiple fields
44+
POST {{HostAddress}}/api/Users?api-version=2023-06-06
45+
Content-Type: application/json
46+
47+
{
48+
"firstName": "",
49+
"lastName": "",
50+
"email": "xavier",
51+
"password": "Super!Complex6"
52+
}

Api/src/http-client.env.json

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
{
2+
"development": {
3+
"HostAddress": "https://localhost:7011"
4+
}
5+
}

Api/src/person.ps1

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
2+
# Demo error handling in PowerShell
3+
$base = "https://localhost:7011/"
4+
5+
$body = @"
6+
{
7+
"firstName": "Xavier",
8+
"lastName": "John",
9+
"email": "[email protected]",
10+
"password": "SimplePassword"
11+
}
12+
"@
13+
14+
$uri = $base + 'api/users?api-version=2023-06-06'
15+
16+
try {
17+
18+
$content = Invoke-WebRequest -Method Post -Uri $uri -Body $body -ContentType 'application/json'
19+
Write-Host "✅ Response received:"
20+
Write-Output $content
21+
}
22+
catch {
23+
$webResponse = $_.Exception.Response
24+
if ($webResponse -ne $null) {
25+
$statusCode = [int]$webResponse.StatusCode
26+
$statusDescription = $webResponse.StatusDescription
27+
28+
Write-Host "❌ HTTP Error ($statusCode): ($statusDescription)"
29+
30+
$stream = $webResponse.GetResponseStream()
31+
$reader = New-Object System.IO.StreamReader($stream)
32+
$errorResponse = $reader.ReadToEnd()
33+
try {
34+
# Try to parse and pretty-print the JSON error response
35+
$parsedJson = $errorResponse | ConvertFrom-Json
36+
$formattedJson = $parsedJson | ConvertTo-Json -Depth 10
37+
Write-Output $formattedJson
38+
}
39+
catch {
40+
# If response is not valid JSON, just print raw text
41+
Write-Host "(Unformatted response)"
42+
Write-Output $errorResponse
43+
}
44+
}
45+
else {
46+
Write-Host "An unexpected error occurred:"
47+
Write-Host $_.Exception.Message
48+
}
49+
}

Directory.Packages.props

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
<Project>
22
<PropertyGroup>
3-
<FunctionalDddVersion>2.0.1</FunctionalDddVersion>
3+
<FunctionalDddVersion>2.1.1</FunctionalDddVersion>
44
</PropertyGroup>
55
<!-- Runtime -->
66
<ItemGroup>

Domain/src/Aggregates/User.cs

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
namespace BestWeatherForecast.Domain;
2+
using FluentValidation;
3+
4+
public class User : Aggregate<UserId>
5+
{
6+
public FirstName FirstName { get; }
7+
public LastName LastName { get; }
8+
public EmailAddress Email { get; }
9+
public string Password { get; }
10+
11+
public static Result<User> TryCreate(FirstName firstName, LastName lastName, EmailAddress email, string password) // password shown as string to demo validation but you should have a Password Type.
12+
{
13+
var user = new User(firstName, lastName, email, password);
14+
var validator = new UserValidator();
15+
return validator.ValidateToResult(user);
16+
}
17+
18+
private User(FirstName firstName, LastName lastName, EmailAddress email, string password)
19+
: base(UserId.NewUnique())
20+
{
21+
FirstName = firstName;
22+
LastName = lastName;
23+
Email = email;
24+
Password = password;
25+
}
26+
27+
public class UserValidator : AbstractValidator<User>
28+
{
29+
public UserValidator()
30+
{
31+
RuleFor(user => user.FirstName).NotNull();
32+
RuleFor(user => user.LastName).NotNull();
33+
RuleFor(user => user.Email).NotNull();
34+
RuleFor(user => user.Password)
35+
.NotEmpty().WithMessage("Password must not be empty.")
36+
.MinimumLength(8).WithMessage("Password must be at least 8 characters long.")
37+
.Matches("[A-Z]").WithMessage("Password must contain at least one uppercase letter.")
38+
.Matches("[a-z]").WithMessage("Password must contain at least one lowercase letter.")
39+
.Matches("[0-9]").WithMessage("Password must contain at least one number.")
40+
.Matches("[^a-zA-Z0-9]").WithMessage("Password must contain at least one special character.");
41+
}
42+
}
43+
44+
}

Domain/src/Domain.csproj

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
<Project Sdk="Microsoft.NET.Sdk">
22
<ItemGroup>
3+
<PackageReference Include="FunctionalDdd.CommonValueObjectGenerator" />
4+
<PackageReference Include="FunctionalDdd.CommonValueObjects" />
35
<PackageReference Include="FunctionalDDD.DomainDrivenDesign" />
46
<PackageReference Include="FunctionalDDD.FluentValidation" />
57
<PackageReference Include="FunctionalDDD.RailwayOrientedProgramming" />
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
namespace BestWeatherForecast.Domain;
2+
3+
public partial class FirstName : RequiredString
4+
{
5+
}

0 commit comments

Comments
 (0)