Adds response + problem detail RFC9457 (Problem Details for HTTP APIs)
This commit is contained in:
parent
64e696dc5a
commit
9d384cd18d
4 changed files with 149 additions and 16 deletions
|
|
@ -1,16 +1,43 @@
|
|||
namespace PlanTempus.Core.CommandQueries;
|
||||
|
||||
namespace PlanTempus.Core.CommandQueries;
|
||||
|
||||
public class CommandResponse
|
||||
/// <summary>
|
||||
/// Represents a response to a command request, typically used in asynchronous operations.
|
||||
/// This class includes details such as a unique request ID, correlation ID, command name,
|
||||
/// transaction ID, creation timestamp, and a URL to check the status of the command.
|
||||
/// </summary>
|
||||
/// <param name="correlationId">A unique identifier used to track the request across services.</param>
|
||||
/// <param name="commandName">The name of the command being executed.</param>
|
||||
/// <param name="transactionId">An optional unique identifier for the transaction associated with the command.</param>
|
||||
public class CommandResponse(Guid correlationId, string commandName, Guid? transactionId)
|
||||
{
|
||||
public Guid RequestId { get; }
|
||||
public Guid CorrelationId { get; }
|
||||
public Guid? TransactionId { get; }
|
||||
public DateTime CreatedAt { get; }
|
||||
/// <summary>
|
||||
/// A unique identifier for the request. This is automatically generated using Guid.CreateVersion7().
|
||||
/// </summary>
|
||||
public Guid RequestId { get; } = Guid.CreateVersion7();
|
||||
|
||||
public CommandResponse(Guid correlationId)
|
||||
{
|
||||
CorrelationId = correlationId;
|
||||
RequestId = Guid.CreateVersion7();
|
||||
CreatedAt = DateTime.Now;
|
||||
}
|
||||
/// <summary>
|
||||
/// A unique identifier used to track the request across services. This is provided when creating the response.
|
||||
/// </summary>
|
||||
public Guid CorrelationId { get; } = correlationId;
|
||||
|
||||
/// <summary>
|
||||
/// The name of the command being executed.
|
||||
/// </summary>
|
||||
public string CommandName { get; } = commandName;
|
||||
|
||||
/// <summary>
|
||||
/// An optional unique identifier for the transaction associated with the command.
|
||||
/// </summary>
|
||||
public Guid? TransactionId { get; } = transactionId;
|
||||
|
||||
/// <summary>
|
||||
/// The timestamp when the command response was created. This is automatically set to the current UTC time.
|
||||
/// </summary>
|
||||
public DateTime CreatedAt { get; } = DateTime.UtcNow;
|
||||
|
||||
/// <summary>
|
||||
/// A URL where the client can check the status of the command. This is typically used in asynchronous operations.
|
||||
/// </summary>
|
||||
public string StatusUrl { get; } = "statusUrl";
|
||||
}
|
||||
56
Core/CommandQueries/ProblemDetails.cs
Normal file
56
Core/CommandQueries/ProblemDetails.cs
Normal file
|
|
@ -0,0 +1,56 @@
|
|||
namespace PlanTempus.Core.CommandQueries;
|
||||
|
||||
/// <summary>
|
||||
/// Represents a standardized error response according to RFC 9457 (Problem Details for HTTP APIs).
|
||||
/// This class provides a consistent way to communicate errors in HTTP APIs, including details about the error type,
|
||||
/// status code, and additional context. It also supports extensions for custom error information.
|
||||
///
|
||||
/// RFC 9457 Documentation: https://www.rfc-editor.org/rfc/rfc9457.html
|
||||
/// </summary>
|
||||
public class ProblemDetails
|
||||
{
|
||||
/// <summary>
|
||||
/// A URI reference that identifies the problem type. This is typically a link to human-readable documentation about the error.
|
||||
/// </summary>
|
||||
public string Type { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// A short, human-readable summary of the problem. It should not change between occurrences of the same error.
|
||||
/// </summary>
|
||||
public string Title { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The HTTP status code generated by the server for this occurrence of the problem. This allows the client to understand the general category of the error.
|
||||
/// </summary>
|
||||
public int? Status { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// A human-readable explanation specific to this occurrence of the problem. It provides additional details about the error.
|
||||
/// </summary>
|
||||
public string Detail { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// A URI reference that identifies the specific occurrence of the problem. This can be used to trace the error in logs or debugging tools.
|
||||
/// </summary>
|
||||
public string Instance { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// A dictionary for additional, custom error information. This allows extending the problem details with application-specific fields.
|
||||
/// </summary>
|
||||
[Newtonsoft.Json.JsonExtensionData]
|
||||
public Dictionary<string, object> Extensions { get; } = new();
|
||||
|
||||
/// <summary>
|
||||
/// Adds a custom extension to the problem details.
|
||||
/// </summary>
|
||||
/// <param name="key">The key for the extension.</param>
|
||||
/// <param name="value">The value of the extension.</param>
|
||||
public void AddExtension(string key, object value) => Extensions.Add(key, value);
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// Removes a custom extension from the problem details.
|
||||
/// </summary>
|
||||
/// <param name="key">The key of the extension to remove.</param>
|
||||
public void RemoveExtension(string key) => Extensions.Remove(key);
|
||||
}
|
||||
|
|
@ -36,10 +36,7 @@ namespace PlanTempus.Components.Users.Create
|
|||
command.IsActive,
|
||||
});
|
||||
|
||||
|
||||
var result = data.First();
|
||||
|
||||
return new CommandResponse(command.CorrelationId);
|
||||
return new CommandResponse(command.CorrelationId, command.GetType().Name, command.TransactionId);
|
||||
}
|
||||
catch (PostgresException ex) when (ex.SqlState == "23505")
|
||||
{
|
||||
|
|
|
|||
53
Tests/CommandQueryHandlerTests/ResponseTests.cs
Normal file
53
Tests/CommandQueryHandlerTests/ResponseTests.cs
Normal file
|
|
@ -0,0 +1,53 @@
|
|||
using Newtonsoft.Json;
|
||||
using PlanTempus.Core.CommandQueries;
|
||||
using Shouldly;
|
||||
|
||||
namespace PlanTempus.X.TDD.CommandQueryHandlerTests;
|
||||
|
||||
[TestClass]
|
||||
public class ProblemDetailsTests
|
||||
{
|
||||
[TestMethod]
|
||||
public void TestFormatOfProblemDetails()
|
||||
{
|
||||
// Arrange
|
||||
var problemDetails = new ProblemDetails
|
||||
{
|
||||
Type = "https://example.com/errors/invalid-input",
|
||||
Title = "Invalid Input",
|
||||
Status = 400,
|
||||
Detail = "The request body is invalid.",
|
||||
Instance = "/api/users"
|
||||
};
|
||||
|
||||
problemDetails.AddExtension("invalidFields", new[]
|
||||
{
|
||||
new { Field = "name", Message = "The 'name' field is required." },
|
||||
new { Field = "email", Message = "The 'email' field must be a valid email address." }
|
||||
});
|
||||
|
||||
var json = JsonConvert.SerializeObject(problemDetails, Formatting.Indented);
|
||||
|
||||
var expectedJson = """
|
||||
{
|
||||
"Type": "https://example.com/errors/invalid-input",
|
||||
"Title": "Invalid Input",
|
||||
"Status": 400,
|
||||
"Detail": "The request body is invalid.",
|
||||
"Instance": "/api/users",
|
||||
"invalidFields": [
|
||||
{
|
||||
"Field": "name",
|
||||
"Message": "The 'name' field is required."
|
||||
},
|
||||
{
|
||||
"Field": "email",
|
||||
"Message": "The 'email' field must be a valid email address."
|
||||
}
|
||||
]
|
||||
}
|
||||
""";
|
||||
|
||||
json.ShouldBe(expectedJson);
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue