diff --git a/.vscode/settings.json b/.vscode/settings.json
index d7770cd..ed0b12c 100644
--- a/.vscode/settings.json
+++ b/.vscode/settings.json
@@ -1,13 +1,8 @@
{
"rest-client.environmentVariables": {
-
- "$shared": {},
- "dev" : {
+ "$shared": {
"host":"https://localhost:7059"
},
- "prod" : {
- "host":"https://localhost:7059"
- }
},
"cSpell.words": [
"Xunit"
diff --git a/BuberDinner.Api/src/2022-12-21/DinnersController.cs b/BuberDinner.Api/src/2022-12-21/DinnersController.cs
index 4a58035..2343c0a 100644
--- a/BuberDinner.Api/src/2022-12-21/DinnersController.cs
+++ b/BuberDinner.Api/src/2022-12-21/DinnersController.cs
@@ -1,10 +1,20 @@
-namespace BuberDinner.Api.Controllers;
+namespace BuberDinner._2022_12_21.Controllers;
-using FunctionalDDD.BuberDinner.Api;
+using Asp.Versioning;
+using BuberDinner.Api;
using Microsoft.AspNetCore.Mvc;
+///
+/// CRUD for dinner.
+///
+[ApiVersion("2022-10-01")]
public class DinnersController : ApiControllerBase
{
+ ///
+ /// Get all the dinners.
+ ///
+ ///
+ [HttpGet]
public IActionResult ListDinners()
{
return Ok(Array.Empty());
diff --git a/BuberDinner.Api/src/ApiControllerBase.cs b/BuberDinner.Api/src/ApiControllerBase.cs
index e0477fe..036c4f8 100644
--- a/BuberDinner.Api/src/ApiControllerBase.cs
+++ b/BuberDinner.Api/src/ApiControllerBase.cs
@@ -4,8 +4,10 @@
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
+///
+/// API Base Controller.
+///
[Route("[controller]")]
-[ApiController]
[Authorize]
public class ApiControllerBase : FunctionalDDDBase
{
diff --git a/BuberDinner.Api/src/BuberDinner.Api.csproj b/BuberDinner.Api/src/BuberDinner.Api.csproj
index 82d463d..b5a0a13 100644
--- a/BuberDinner.Api/src/BuberDinner.Api.csproj
+++ b/BuberDinner.Api/src/BuberDinner.Api.csproj
@@ -2,12 +2,15 @@
56db5788-9ea3-4265-858d-0a347e105439
+ true
+
+
@@ -17,6 +20,6 @@
+
-
diff --git a/BuberDinner.Api/src/ConfigureSwaggerOptions.cs b/BuberDinner.Api/src/ConfigureSwaggerOptions.cs
new file mode 100644
index 0000000..9dc35b6
--- /dev/null
+++ b/BuberDinner.Api/src/ConfigureSwaggerOptions.cs
@@ -0,0 +1,89 @@
+namespace BuberDinner.Api;
+
+using Asp.Versioning;
+using Asp.Versioning.ApiExplorer;
+using Microsoft.Extensions.DependencyInjection;
+using Microsoft.Extensions.Options;
+using Microsoft.OpenApi.Models;
+using Swashbuckle.AspNetCore.SwaggerGen;
+using System.Text;
+
+///
+/// Configures the Swagger generation options.
+///
+/// This allows API versioning to define a Swagger document per API version after the
+/// service has been resolved from the service container.
+public class ConfigureSwaggerOptions : IConfigureOptions
+{
+ private readonly IApiVersionDescriptionProvider provider;
+
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ /// The provider used to generate Swagger documents.
+ public ConfigureSwaggerOptions(IApiVersionDescriptionProvider provider) => this.provider = provider;
+
+ ///
+ public void Configure(SwaggerGenOptions options)
+ {
+ // add a swagger document for each discovered API version
+ // note: you might choose to skip or document deprecated API versions differently
+ foreach (var description in provider.ApiVersionDescriptions)
+ {
+ options.SwaggerDoc(description.GroupName, CreateInfoForApiVersion(description));
+ }
+ }
+
+ private static OpenApiInfo CreateInfoForApiVersion(ApiVersionDescription description)
+ {
+ var text = new StringBuilder("Buber Dinner the AirBnB for dinner.");
+ var info = new OpenApiInfo()
+ {
+ Title = "Buber Dinner",
+ Version = description.ApiVersion.ToString(),
+ Contact = new OpenApiContact() { Name = "Xavier John", Email = "xavier@somewhere.com" },
+ License = new OpenApiLicense() { Name = "MIT", Url = new Uri("https://opensource.org/licenses/MIT") }
+ };
+
+ if (description.IsDeprecated)
+ {
+ text.Append(" This API version has been deprecated.");
+ }
+
+ if (description.SunsetPolicy is SunsetPolicy policy)
+ {
+ if (policy.Date is DateTimeOffset when)
+ {
+ text.Append(" The API will be sunset on ")
+ .Append(when.Date.ToShortDateString())
+ .Append('.');
+ }
+
+ if (policy.HasLinks)
+ {
+ text.AppendLine();
+
+ for (var i = 0; i < policy.Links.Count; i++)
+ {
+ var link = policy.Links[i];
+
+ if (link.Type == "text/html")
+ {
+ text.AppendLine();
+
+ if (link.Title.HasValue)
+ {
+ text.Append(link.Title.Value).Append(": ");
+ }
+
+ text.Append(link.LinkTarget.OriginalString);
+ }
+ }
+ }
+ }
+
+ info.Description = text.ToString();
+
+ return info;
+ }
+}
diff --git a/BuberDinner.Api/src/DependencyInjection.cs b/BuberDinner.Api/src/DependencyInjection.cs
index ac09327..25c8d06 100644
--- a/BuberDinner.Api/src/DependencyInjection.cs
+++ b/BuberDinner.Api/src/DependencyInjection.cs
@@ -4,13 +4,37 @@
using Mapster;
using MapsterMapper;
using Microsoft.Extensions.DependencyInjection;
+using Microsoft.Extensions.Options;
+using Swashbuckle.AspNetCore.SwaggerGen;
-public static class DependencyInjection
+internal static class DependencyInjection
{
public static IServiceCollection AddPresentation(this IServiceCollection services)
{
services.AddControllers();
services.AddMappings();
+ services.AddApiVersioning(
+ options =>
+ {
+ options.ReportApiVersions = true;
+ })
+ .AddMvc()
+ .AddApiExplorer();
+
+ // Learn more about configuring Swagger/OpenAPI at https://aka.ms/aspnetcore/swashbuckle
+ services.AddTransient, ConfigureSwaggerOptions>();
+ services.AddSwaggerGen(
+ options =>
+ {
+ // add a custom operation filter which sets default values
+ options.OperationFilter();
+
+ var fileName = typeof(Program).Assembly.GetName().Name + ".xml";
+ var filePath = Path.Combine(AppContext.BaseDirectory, fileName);
+
+ // integrate XML comments
+ options.IncludeXmlComments(filePath);
+ });
return services;
}
diff --git a/BuberDinner.Api/src/Netural/Controllers/AuthenticationController.cs b/BuberDinner.Api/src/Netural/Controllers/AuthenticationController.cs
index 6925a7a..f43c5da 100644
--- a/BuberDinner.Api/src/Netural/Controllers/AuthenticationController.cs
+++ b/BuberDinner.Api/src/Netural/Controllers/AuthenticationController.cs
@@ -1,34 +1,54 @@
namespace BuberDinner.Api.Netural.Controllers;
+using Asp.Versioning;
using BuberDinner.Api.Netural.Models.Authentication;
using MapsterMapper;
using Mediator;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
+///
+/// Authentication Controller
+///
[AllowAnonymous]
+[ApiVersionNeutral]
public class AuthenticationController : ApiControllerBase
{
private readonly ISender _sender;
private readonly IMapper _mapper;
+ ///
+ /// Constructor.
+ ///
+ ///
+ ///
public AuthenticationController(ISender sender, IMapper mapper)
{
_sender = sender;
_mapper = mapper;
}
+ ///
+ /// Register a new user.
+ ///
+ ///
+ ///
[HttpPost("register")]
public async Task> Register(RegisterRequest request) =>
await request.ToRegisterCommand()
.BindAsync(command => _sender.Send(command))
- .MapAsync(authResult => _mapper.Map(authResult))
+ .MapAsync(_mapper.Map)
.FinallyAsync(result => MapToActionResult(result));
+ ///
+ /// Login for existing user.
+ ///
+ ///
+ ///
[HttpPost("login")]
public async Task> Login(LoginRequest request) =>
await request.ToLoginQuery()
.BindAsync(command => _sender.Send(command))
- .MapAsync(authResult => _mapper.Map(authResult))
+ .MapAsync(_mapper.Map)
.FinallyAsync(result => MapToActionResult(result));
}
diff --git a/BuberDinner.Api/src/Netural/Controllers/ErrorController.cs b/BuberDinner.Api/src/Netural/Controllers/ErrorController.cs
index 3a7f95c..db2c9e1 100644
--- a/BuberDinner.Api/src/Netural/Controllers/ErrorController.cs
+++ b/BuberDinner.Api/src/Netural/Controllers/ErrorController.cs
@@ -1,14 +1,23 @@
-namespace FunctionalDDD.BuberDinner.Api.Netural.Controllers
-{
- using Microsoft.AspNetCore.Mvc;
+namespace BuberDinner.Api.Netural.Controllers;
+using Microsoft.AspNetCore.Mvc;
+using Asp.Versioning;
- [ApiController]
- public class ErrorController : ControllerBase
+///
+/// Unhandled error controller.
+///
+[ApiVersionNeutral]
+[ApiExplorerSettings(IgnoreApi = true)]
+public class ErrorController : ControllerBase
+{
+ ///
+ /// Show error
+ ///
+ ///
+ [HttpGet]
+ [Route("/error")]
+ public IActionResult Error()
{
- [Route("/error")]
- public IActionResult Error()
- {
- return Problem();
- }
+ return Problem();
}
}
+
diff --git a/BuberDinner.Api/src/Netural/Models/Authentication/AuthenticationMappingConfig.cs b/BuberDinner.Api/src/Netural/Models/Authentication/AuthenticationMappingConfig.cs
index 248c427..986e247 100644
--- a/BuberDinner.Api/src/Netural/Models/Authentication/AuthenticationMappingConfig.cs
+++ b/BuberDinner.Api/src/Netural/Models/Authentication/AuthenticationMappingConfig.cs
@@ -3,12 +3,13 @@
using BuberDinner.Application.Services.Authentication.Common;
using Mapster;
-public class AuthenticationMappingConfig : IRegister
+internal class AuthenticationMappingConfig : IRegister
{
public void Register(TypeAdapterConfig config)
{
config.NewConfig()
.Map(dest => dest.Token, src => src.Token)
+ .Map(dest => dest.UserId, src => src.User.Id)
.Map(dest => dest, src => src.User);
}
}
diff --git a/BuberDinner.Api/src/Netural/Models/Authentication/AuthenticationResponse.cs b/BuberDinner.Api/src/Netural/Models/Authentication/AuthenticationResponse.cs
index 3419db6..e347b53 100644
--- a/BuberDinner.Api/src/Netural/Models/Authentication/AuthenticationResponse.cs
+++ b/BuberDinner.Api/src/Netural/Models/Authentication/AuthenticationResponse.cs
@@ -1,10 +1,33 @@
namespace BuberDinner.Api.Netural.Models.Authentication;
+///
+/// Authentication Response
+///
public class AuthenticationResponse
{
- //public Guid Id { get; set; }
+ ///
+ /// User Id
+ ///
+ public string? UserId { get; set; }
+
+
+ ///
+ /// First Name
+ ///
public string? FirstName { get; set; }
+
+ ///
+ /// Last Name
+ ///
public string? LastName { get; set; }
+
+ ///
+ /// Email address
+ ///
public string? Email { get; set; }
+
+ ///
+ /// Token
+ ///
public string? Token { get; set; }
}
diff --git a/BuberDinner.Api/src/Netural/Models/Authentication/LoginRequest.cs b/BuberDinner.Api/src/Netural/Models/Authentication/LoginRequest.cs
index 9c1a6ff..25dfe12 100644
--- a/BuberDinner.Api/src/Netural/Models/Authentication/LoginRequest.cs
+++ b/BuberDinner.Api/src/Netural/Models/Authentication/LoginRequest.cs
@@ -3,12 +3,17 @@
using BuberDinner.Application.Services.Authentication.Queries;
using BuberDinner.Domain.User.ValueObjects;
+///
+/// Login request model.
+///
+/// Email address
+/// Password
public record LoginRequest(
string email,
string password
)
{
- public Result ToLoginQuery() =>
+ internal Result ToLoginQuery() =>
EmailAddress.Create(email)
.Combine(Password.Create(password))
.Bind((email, pwd) => LoginQuery.Create(email, pwd));
diff --git a/BuberDinner.Api/src/Netural/Models/Authentication/RegisterRequest.cs b/BuberDinner.Api/src/Netural/Models/Authentication/RegisterRequest.cs
index 80ac560..3ca23af 100644
--- a/BuberDinner.Api/src/Netural/Models/Authentication/RegisterRequest.cs
+++ b/BuberDinner.Api/src/Netural/Models/Authentication/RegisterRequest.cs
@@ -3,6 +3,13 @@
using BuberDinner.Application.Services.Authentication.Commands;
using BuberDinner.Domain.User.ValueObjects;
+///
+/// Register request model.
+///
+/// First Name
+/// Last Name
+/// Email address
+/// Password
public record RegisterRequest(
string firstName,
string lastName,
@@ -11,7 +18,7 @@ string password
)
{
- public Result ToRegisterCommand() =>
+ internal Result ToRegisterCommand() =>
FirstName.Create(firstName)
.Combine(LastName.Create(lastName))
.Combine(EmailAddress.Create(email))
diff --git a/BuberDinner.Api/src/Program.cs b/BuberDinner.Api/src/Program.cs
index fc45c85..7107d94 100644
--- a/BuberDinner.Api/src/Program.cs
+++ b/BuberDinner.Api/src/Program.cs
@@ -12,6 +12,21 @@
var app = builder.Build();
{
+ app.UseSwagger();
+ app.UseSwaggerUI(
+ options =>
+ {
+ options.RoutePrefix = string.Empty;
+ var descriptions = app.DescribeApiVersions();
+
+ // build a swagger endpoint for each discovered API version
+ foreach (var description in descriptions)
+ {
+ var url = $"/swagger/{description.GroupName}/swagger.json";
+ var name = description.GroupName.ToUpperInvariant();
+ options.SwaggerEndpoint(url, name);
+ }
+ });
app.UseExceptionHandler("/error");
app.UseHttpsRedirection();
app.UseAuthentication();
@@ -20,4 +35,7 @@
app.Run();
}
+///
+/// Main program
+///
public partial class Program { }
diff --git a/BuberDinner.Api/src/Properties/launchSettings.json b/BuberDinner.Api/src/Properties/launchSettings.json
index 0aaf69f..c87607c 100644
--- a/BuberDinner.Api/src/Properties/launchSettings.json
+++ b/BuberDinner.Api/src/Properties/launchSettings.json
@@ -1,41 +1,14 @@
{
"$schema": "https://json.schemastore.org/launchsettings.json",
- "iisSettings": {
- "windowsAuthentication": false,
- "anonymousAuthentication": true,
- "iisExpress": {
- "applicationUrl": "http://localhost:32364",
- "sslPort": 44338
- }
- },
"profiles": {
- "http": {
- "commandName": "Project",
- "dotnetRunMessages": true,
- "launchBrowser": true,
- "launchUrl": "swagger",
- "applicationUrl": "http://localhost:5246",
- "environmentVariables": {
- "ASPNETCORE_ENVIRONMENT": "Development"
- }
- },
"https": {
"commandName": "Project",
"dotnetRunMessages": true,
"launchBrowser": true,
- "launchUrl": "swagger",
"applicationUrl": "https://localhost:7059;http://localhost:5246",
"environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development"
}
- },
- "IIS Express": {
- "commandName": "IISExpress",
- "launchBrowser": true,
- "launchUrl": "swagger",
- "environmentVariables": {
- "ASPNETCORE_ENVIRONMENT": "Development"
- }
}
}
}
diff --git a/BuberDinner.Api/src/SwaggerDefaultValues.cs b/BuberDinner.Api/src/SwaggerDefaultValues.cs
new file mode 100644
index 0000000..b908352
--- /dev/null
+++ b/BuberDinner.Api/src/SwaggerDefaultValues.cs
@@ -0,0 +1,68 @@
+namespace BuberDinner.Api;
+
+using Microsoft.AspNetCore.Mvc.ApiExplorer;
+using Microsoft.AspNetCore.Mvc.ModelBinding;
+using Microsoft.OpenApi.Models;
+using Swashbuckle.AspNetCore.SwaggerGen;
+using System.Text.Json;
+
+///
+/// Represents the OpenAPI/Swashbuckle operation filter used to document information provided, but not used.
+///
+/// This is only required due to bugs in the .
+/// Once they are fixed and published, this class can be removed.
+public class SwaggerDefaultValues : IOperationFilter
+{
+ ///
+ public void Apply(OpenApiOperation operation, OperationFilterContext context)
+ {
+ var apiDescription = context.ApiDescription;
+
+ operation.Deprecated |= apiDescription.IsDeprecated();
+
+ // REF: https://github.com/domaindrivendev/Swashbuckle.AspNetCore/issues/1752#issue-663991077
+ foreach (var responseType in context.ApiDescription.SupportedResponseTypes)
+ {
+ // REF: https://github.com/domaindrivendev/Swashbuckle.AspNetCore/blob/b7cf75e7905050305b115dd96640ddd6e74c7ac9/src/Swashbuckle.AspNetCore.SwaggerGen/SwaggerGenerator/SwaggerGenerator.cs#L383-L387
+ var responseKey = responseType.IsDefaultResponse ? "default" : responseType.StatusCode.ToString();
+ var response = operation.Responses[responseKey];
+
+ foreach (var contentType in response.Content.Keys)
+ {
+ if (!responseType.ApiResponseFormats.Any(x => x.MediaType == contentType))
+ {
+ response.Content.Remove(contentType);
+ }
+ }
+ }
+
+ if (operation.Parameters == null)
+ {
+ return;
+ }
+
+ // REF: https://github.com/domaindrivendev/Swashbuckle.AspNetCore/issues/412
+ // REF: https://github.com/domaindrivendev/Swashbuckle.AspNetCore/pull/413
+ foreach (var parameter in operation.Parameters)
+ {
+ var description = apiDescription.ParameterDescriptions.First(p => p.Name == parameter.Name);
+
+ if (parameter.Description == null)
+ {
+ parameter.Description = description.ModelMetadata?.Description;
+ }
+
+ if (parameter.Schema.Default == null &&
+ description.DefaultValue != null &&
+ description.DefaultValue is not DBNull &&
+ description.ModelMetadata is ModelMetadata modelMetadata)
+ {
+ // REF: https://github.com/Microsoft/aspnet-api-versioning/issues/429#issuecomment-605402330
+ var json = JsonSerializer.Serialize(description.DefaultValue, modelMetadata.ModelType);
+ parameter.Schema.Default = OpenApiAnyFactory.CreateFromJson(json);
+ }
+
+ parameter.Required |= description.IsRequired;
+ }
+ }
+}
diff --git a/BuberDinner.Api/tests/Netural/AuthenticationControllerTests.cs b/BuberDinner.Api/tests/Netural/AuthenticationControllerTests.cs
index b50746b..330e214 100644
--- a/BuberDinner.Api/tests/Netural/AuthenticationControllerTests.cs
+++ b/BuberDinner.Api/tests/Netural/AuthenticationControllerTests.cs
@@ -58,17 +58,7 @@ public async Task Register_user()
var response = await client.PostAsync(url, new StringContent(json, Encoding.UTF8, "application/json"));
// Assert
- response.EnsureSuccessStatusCode();
-#pragma warning disable CS8602 // Dereference of a possibly null reference.
- response.Content.Headers.ContentType.ToString().Should().Be("application/json; charset=utf-8");
-#pragma warning restore CS8602 // Dereference of a possibly null reference.
- var registeredUser = await response.Content.ReadAsExample(new { firstName = default(string), lastName = default(string), email = default(string) });
- registeredUser.Should().BeEquivalentTo(new
- {
- firstName = "Xavier",
- lastName = "John",
- email = "someone@somewhere.com"
- });
+ await ValidateAuthenticationResponse(response);
}
[Fact, Priority(3)]
@@ -88,16 +78,25 @@ public async Task Login_with_registered_user()
var response = await client.PostAsync(url, new StringContent(json, Encoding.UTF8, "application/json"));
// Assert
- response.StatusCode.Should().Be(HttpStatusCode.OK);
+ await ValidateAuthenticationResponse(response);
+ }
+
+ private static async Task ValidateAuthenticationResponse(HttpResponseMessage response)
+ {
+ response.EnsureSuccessStatusCode();
#pragma warning disable CS8602 // Dereference of a possibly null reference.
response.Content.Headers.ContentType.ToString().Should().Be("application/json; charset=utf-8");
-#pragma warning restore CS8602 // Dereference of a possibly null reference.
- var registeredUser = await response.Content.ReadAsExample(new { firstName = default(string), lastName = default(string), email = default(string) });
+#pragma warning restore CS8602 // Dereference of a possibly null reference.
+
+ var registeredUser = await response.Content.ReadAsExample(new { userId = default(string), firstName = default(string), lastName = default(string), email = default(string) });
registeredUser.Should().BeEquivalentTo(new
{
firstName = "Xavier",
lastName = "John",
email = "someone@somewhere.com"
});
+
+ if (registeredUser == null) return;
+ Guid.TryParse(registeredUser.userId, out var parsedUserId).Should().BeTrue();
}
}
diff --git a/Directory.Packages.props b/Directory.Packages.props
index 3d73a6e..97895a0 100644
--- a/Directory.Packages.props
+++ b/Directory.Packages.props
@@ -4,35 +4,36 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
-
-
-
+
+
\ No newline at end of file