diff --git a/Core/CommandQueries/CommandResponse.cs b/Core/CommandQueries/CommandResponse.cs index 1b0bac7..52dd0f5 100644 --- a/Core/CommandQueries/CommandResponse.cs +++ b/Core/CommandQueries/CommandResponse.cs @@ -1,16 +1,43 @@ -namespace PlanTempus.Core.CommandQueries; + +namespace PlanTempus.Core.CommandQueries; -public class CommandResponse +/// +/// 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. +/// +/// A unique identifier used to track the request across services. +/// The name of the command being executed. +/// An optional unique identifier for the transaction associated with the command. +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; } + /// + /// A unique identifier for the request. This is automatically generated using Guid.CreateVersion7(). + /// + public Guid RequestId { get; } = Guid.CreateVersion7(); - public CommandResponse(Guid correlationId) - { - CorrelationId = correlationId; - RequestId = Guid.CreateVersion7(); - CreatedAt = DateTime.Now; - } + /// + /// A unique identifier used to track the request across services. This is provided when creating the response. + /// + public Guid CorrelationId { get; } = correlationId; + + /// + /// The name of the command being executed. + /// + public string CommandName { get; } = commandName; + + /// + /// An optional unique identifier for the transaction associated with the command. + /// + public Guid? TransactionId { get; } = transactionId; + + /// + /// The timestamp when the command response was created. This is automatically set to the current UTC time. + /// + public DateTime CreatedAt { get; } = DateTime.UtcNow; + + /// + /// A URL where the client can check the status of the command. This is typically used in asynchronous operations. + /// + public string StatusUrl { get; } = "statusUrl"; } \ No newline at end of file diff --git a/Core/CommandQueries/ProblemDetails.cs b/Core/CommandQueries/ProblemDetails.cs new file mode 100644 index 0000000..7e2f6d9 --- /dev/null +++ b/Core/CommandQueries/ProblemDetails.cs @@ -0,0 +1,56 @@ +namespace PlanTempus.Core.CommandQueries; + +/// +/// 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 +/// +public class ProblemDetails +{ + /// + /// A URI reference that identifies the problem type. This is typically a link to human-readable documentation about the error. + /// + public string Type { get; set; } + + /// + /// A short, human-readable summary of the problem. It should not change between occurrences of the same error. + /// + public string Title { get; set; } + + /// + /// 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. + /// + public int? Status { get; set; } + + /// + /// A human-readable explanation specific to this occurrence of the problem. It provides additional details about the error. + /// + public string Detail { get; set; } + + /// + /// A URI reference that identifies the specific occurrence of the problem. This can be used to trace the error in logs or debugging tools. + /// + public string Instance { get; set; } + + /// + /// A dictionary for additional, custom error information. This allows extending the problem details with application-specific fields. + /// + [Newtonsoft.Json.JsonExtensionData] + public Dictionary Extensions { get; } = new(); + + /// + /// Adds a custom extension to the problem details. + /// + /// The key for the extension. + /// The value of the extension. + public void AddExtension(string key, object value) => Extensions.Add(key, value); + + + /// + /// Removes a custom extension from the problem details. + /// + /// The key of the extension to remove. + public void RemoveExtension(string key) => Extensions.Remove(key); +} \ No newline at end of file diff --git a/PlanTempus.Components/Users/Create/CreateUserHandler.cs b/PlanTempus.Components/Users/Create/CreateUserHandler.cs index 651f899..d5acd1c 100644 --- a/PlanTempus.Components/Users/Create/CreateUserHandler.cs +++ b/PlanTempus.Components/Users/Create/CreateUserHandler.cs @@ -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") { diff --git a/Tests/CommandQueryHandlerTests/ResponseTests.cs b/Tests/CommandQueryHandlerTests/ResponseTests.cs new file mode 100644 index 0000000..d808f12 --- /dev/null +++ b/Tests/CommandQueryHandlerTests/ResponseTests.cs @@ -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); + } +} \ No newline at end of file