From 5e635d1360859d2f0f451e3cdc8ec985131e5ed5 Mon Sep 17 00:00:00 2001 From: DXCore Technology Date: Tue, 22 Apr 2025 00:05:11 +0700 Subject: [PATCH] improve swagger --- .../Controllers/PageContentApiController.cs | 95 ++++++++++++ .../Controllers/PostContentApiController.cs | 104 ++++++++++++- src/applications/mixcore/mixcore.csproj | 2 + .../mixcore/wwwroot/mix-app/css/swagger.css | 103 ++++++++++++- .../Filters/SwaggerDefaultResponsesFilter.cs | 145 ++++++++++++++++++ .../Models/ApiResponse/ApiErrorResponse.cs | 120 +++++++++++++++ .../Models/ApiResponse/ApiSuccessResponse.cs | 127 +++++++++++++++ src/platform/mix.library/Startup/Swagger.cs | 57 ++++++- src/platform/mix.library/mix.library.csproj | 1 + 9 files changed, 742 insertions(+), 12 deletions(-) create mode 100644 src/platform/mix.library/Filters/SwaggerDefaultResponsesFilter.cs create mode 100644 src/platform/mix.library/Models/ApiResponse/ApiErrorResponse.cs create mode 100644 src/platform/mix.library/Models/ApiResponse/ApiSuccessResponse.cs diff --git a/src/applications/mixcore/Controllers/PageContentApiController.cs b/src/applications/mixcore/Controllers/PageContentApiController.cs index ca70b4f1bd..78e2e1f45a 100644 --- a/src/applications/mixcore/Controllers/PageContentApiController.cs +++ b/src/applications/mixcore/Controllers/PageContentApiController.cs @@ -9,14 +9,26 @@ using Mix.Services.Databases.Lib.Interfaces; using Mix.Shared.Models; using Mix.SignalR.Interfaces; +using System.Net; +using Swashbuckle.AspNetCore.Annotations; namespace Mixcore.Controllers { + /// + /// API controller for managing page content operations + /// [Route("api/v2/rest/mixcore/page-content")] + [ApiController] + [Produces("application/json")] + [SwaggerTag("Page Content Management")] public sealed class PageContentApiController : MixQueryApiControllerBase { private readonly IMixMetadataService _metadataService; private readonly IMixDbDataService _mixDbDataService; + + /// + /// Constructor for PageContentApiController + /// public PageContentApiController( IHttpContextAccessor httpContextAccessor, IConfiguration configuration, @@ -35,11 +47,94 @@ public PageContentApiController( _mixDbDataService = mixDbDataService; } + /// + /// Retrieves a specific page content by its identifier + /// + /// The page content identifier + /// Cancellation token + /// The page content details + [SwaggerResponse((int)HttpStatusCode.OK, "Successfully retrieved page content", typeof(PageContentViewModel))] + [SwaggerResponse((int)HttpStatusCode.NotFound, "Page content not found")] protected override async Task GetById(int id, CancellationToken cancellationToken = default) { var result = await base.GetById(id, cancellationToken); await result.LoadDataAsync(_mixDbDataService, _metadataService, new(), CacheService); return result; } + + /// + /// Gets a paginated list of page content items + /// + /// Search and pagination parameters + /// Paginated list of page content items + [HttpGet] + [SwaggerOperation( + Summary = "Get paginated page content items", + Description = "Retrieves a paginated list of page content items based on search criteria", + OperationId = "GetPageContent", + Tags = new[] { "Page Content" } + )] + [SwaggerResponse((int)HttpStatusCode.OK, "Successfully retrieved page content items", typeof(PagingResponseModel))] + [SwaggerResponse((int)HttpStatusCode.BadRequest, "Invalid request parameters")] + public override async Task>> Get([FromQuery] SearchRequestDto request) + { + return await base.Get(request); + } + + /// + /// Filters page content items based on criteria + /// + /// Filter and pagination parameters + /// Filtered and paginated list of page content items + [HttpPost("filter")] + [SwaggerOperation( + Summary = "Filter page content items", + Description = "Filters page content items based on specified criteria", + OperationId = "FilterPageContent", + Tags = new[] { "Page Content" } + )] + [SwaggerResponse((int)HttpStatusCode.OK, "Successfully filtered page content items", typeof(PagingResponseModel))] + [SwaggerResponse((int)HttpStatusCode.BadRequest, "Invalid filter parameters")] + public override async Task>> Filter([FromBody] SearchRequestDto request) + { + return await base.Filter(request); + } + + /// + /// Gets a specific page content by ID + /// + /// The page content identifier + /// The page content details + [HttpGet("{id}")] + [SwaggerOperation( + Summary = "Get page content by ID", + Description = "Retrieves a specific page content by its unique identifier", + OperationId = "GetPageContentById", + Tags = new[] { "Page Content" } + )] + [SwaggerResponse((int)HttpStatusCode.OK, "Successfully retrieved page content", typeof(PageContentViewModel))] + [SwaggerResponse((int)HttpStatusCode.NotFound, "Page content not found")] + public override async Task> GetSingle(int id) + { + return await base.GetSingle(id); + } + + /// + /// Gets a default page content template + /// + /// A default page content object + [HttpGet("default")] + [SwaggerOperation( + Summary = "Get default page content template", + Description = "Retrieves a default page content object with initialized values", + OperationId = "GetDefaultPageContent", + Tags = new[] { "Page Content" } + )] + [SwaggerResponse((int)HttpStatusCode.OK, "Successfully retrieved default page content", typeof(PageContentViewModel))] + [SwaggerResponse((int)HttpStatusCode.BadRequest, "Error creating default page content")] + public override async Task> GetDefaultAsync() + { + return await base.GetDefaultAsync(); + } } } diff --git a/src/applications/mixcore/Controllers/PostContentApiController.cs b/src/applications/mixcore/Controllers/PostContentApiController.cs index c5209cd170..13996118d2 100644 --- a/src/applications/mixcore/Controllers/PostContentApiController.cs +++ b/src/applications/mixcore/Controllers/PostContentApiController.cs @@ -11,11 +11,19 @@ using Mix.Services.Databases.Lib.Interfaces; using Mix.Shared.Models; using Mix.SignalR.Interfaces; +using System.Net; +using Swashbuckle.AspNetCore.Annotations; namespace Mixcore.Controllers { + /// + /// API controller for managing post content operations + /// [EnableCors(MixCorsPolicies.PublicApis)] [Route("api/v2/rest/mixcore/post-content")] + [ApiController] + [Produces("application/json")] + [SwaggerTag("Post Content Management")] public sealed class PostContentApiController : MixQueryApiControllerBase { private readonly IMixDbDataService _mixDbDataService; @@ -23,6 +31,10 @@ public sealed class PostContentApiController : MixQueryApiControllerBase + /// Constructor for PostContentApiController + /// public PostContentApiController( IHttpContextAccessor httpContextAccessor, IConfiguration configuration, @@ -45,7 +57,14 @@ public PostContentApiController( _mixDbDataService = mixDbDataService; } - + /// + /// Searches for post content based on the provided criteria + /// + /// Search request parameters + /// Cancellation token + /// Paged list of post content items + [SwaggerResponse((int)HttpStatusCode.OK, "Successfully retrieved post content items", typeof(PagingResponseModel))] + [SwaggerResponse((int)HttpStatusCode.BadRequest, "Invalid request parameters")] protected override async Task> SearchHandler(SearchRequestDto req, CancellationToken cancellationToken = default) { var searchPostQuery = new SearchPostQueryModel(Request, req, CurrentTenant.Id); @@ -59,11 +78,94 @@ protected override async Task> SearchH return RestApiService.ParseSearchResult(req, result); } + /// + /// Retrieves a specific post content by its identifier + /// + /// The post content identifier + /// Cancellation token + /// The post content details + [SwaggerResponse((int)HttpStatusCode.OK, "Successfully retrieved post content", typeof(PostContentViewModel))] + [SwaggerResponse((int)HttpStatusCode.NotFound, "Post content not found")] protected override async Task GetById(int id, CancellationToken cancellationToken = default) { var result = await base.GetById(id); await result.LoadAdditionalDataAsync(_mixDbDataService, _metadataService, CacheService, cancellationToken); return result; } + + /// + /// Gets a paginated list of post content items + /// + /// Search and pagination parameters + /// Paginated list of post content items + [HttpGet] + [SwaggerOperation( + Summary = "Get paginated post content items", + Description = "Retrieves a paginated list of post content items based on search criteria", + OperationId = "GetPostContent", + Tags = new[] { "Post Content" } + )] + [SwaggerResponse((int)HttpStatusCode.OK, "Successfully retrieved post content items", typeof(PagingResponseModel))] + [SwaggerResponse((int)HttpStatusCode.BadRequest, "Invalid request parameters")] + public override async Task>> Get([FromQuery] SearchRequestDto request) + { + return await base.Get(request); + } + + /// + /// Filters post content items based on criteria + /// + /// Filter and pagination parameters + /// Filtered and paginated list of post content items + [HttpPost("filter")] + [SwaggerOperation( + Summary = "Filter post content items", + Description = "Filters post content items based on specified criteria", + OperationId = "FilterPostContent", + Tags = new[] { "Post Content" } + )] + [SwaggerResponse((int)HttpStatusCode.OK, "Successfully filtered post content items", typeof(PagingResponseModel))] + [SwaggerResponse((int)HttpStatusCode.BadRequest, "Invalid filter parameters")] + public override async Task>> Filter([FromBody] SearchRequestDto request) + { + return await base.Filter(request); + } + + /// + /// Gets a specific post content by ID + /// + /// The post content identifier + /// The post content details + [HttpGet("{id}")] + [SwaggerOperation( + Summary = "Get post content by ID", + Description = "Retrieves a specific post content by its unique identifier", + OperationId = "GetPostContentById", + Tags = new[] { "Post Content" } + )] + [SwaggerResponse((int)HttpStatusCode.OK, "Successfully retrieved post content", typeof(PostContentViewModel))] + [SwaggerResponse((int)HttpStatusCode.NotFound, "Post content not found")] + public override async Task> GetSingle(int id) + { + return await base.GetSingle(id); + } + + /// + /// Gets a default post content template + /// + /// A default post content object + [HttpGet("default")] + [SwaggerOperation( + Summary = "Get default post content template", + Description = "Retrieves a default post content object with initialized values", + OperationId = "GetDefaultPostContent", + Tags = new[] { "Post Content" } + )] + [SwaggerResponse((int)HttpStatusCode.OK, "Successfully retrieved default post content", typeof(PostContentViewModel))] + [SwaggerResponse((int)HttpStatusCode.BadRequest, "Error creating default post content")] + public override async Task> GetDefaultAsync() + { + return await base.GetDefaultAsync(); + } } } diff --git a/src/applications/mixcore/mixcore.csproj b/src/applications/mixcore/mixcore.csproj index 4dbe2f1551..b4fcb40fe4 100644 --- a/src/applications/mixcore/mixcore.csproj +++ b/src/applications/mixcore/mixcore.csproj @@ -14,6 +14,8 @@ False False none + true + $(NoWarn);1591 diff --git a/src/applications/mixcore/wwwroot/mix-app/css/swagger.css b/src/applications/mixcore/wwwroot/mix-app/css/swagger.css index b7dcaf7fe6..4e59317d9c 100644 --- a/src/applications/mixcore/wwwroot/mix-app/css/swagger.css +++ b/src/applications/mixcore/wwwroot/mix-app/css/swagger.css @@ -13,17 +13,106 @@ .topbar{ display: none; } + +/* Improve the API endpoint sections */ .swagger-ui .opblock-tag { font-size: 18px; - /* color: #3b4151; - font-family: sans-serif; - margin: 0 0 5px; - background: #333;*/ + margin: 12px 0; + padding: 10px; + border-radius: 4px; + transition: all 0.3s ease; +} + +.swagger-ui .opblock-tag-section { + margin-bottom: 16px; +} + +.swagger-ui .opblock { + margin: 0 0 15px; + border-radius: 6px; + box-shadow: 0 2px 6px rgba(0, 0, 0, 0.1); +} + +/* Improve the GET, POST, PUT, DELETE method coloring */ +.swagger-ui .opblock-get { + border-color: #61affe; + background: rgba(97, 175, 254, 0.1); +} + +.swagger-ui .opblock-post { + border-color: #49cc90; + background: rgba(73, 204, 144, 0.1); +} + +.swagger-ui .opblock-put { + border-color: #fca130; + background: rgba(252, 161, 48, 0.1); +} + +.swagger-ui .opblock-delete { + border-color: #f93e3e; + background: rgba(249, 62, 62, 0.1); +} + +/* Improve typography */ +.swagger-ui, .swagger-ui .opblock-tag, .swagger-ui .opblock .opblock-summary-operation-id, +.swagger-ui .opblock .opblock-summary-path, .swagger-ui .opblock-description-wrapper p, +.swagger-ui .response-col_status, .swagger-ui table thead tr td, +.swagger-ui table thead tr th, .swagger-ui .parameter__name, +.swagger-ui .tab { + font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Oxygen-Sans, Ubuntu, Cantarell, "Helvetica Neue", sans-serif; +} + +/* Improve the model schema display */ +.swagger-ui .model-box { + padding: 12px; + border-radius: 4px; + background: rgba(0, 0, 0, 0.02); +} + +.swagger-ui .model-title { + font-size: 16px; + font-weight: 600; +} + +/* Improve the response section */ +.swagger-ui .responses-table { + border-radius: 4px; + overflow: hidden; +} + +.swagger-ui .response-col_status { + font-weight: 600; +} + +/* Add hover effects */ +.swagger-ui .opblock:hover { + box-shadow: 0 4px 10px rgba(0, 0, 0, 0.15); } -.opblock-tag-section.is-open h3{ + +/* Improve tag section when open */ +.opblock-tag-section.is-open h3 { color: #fff; background: #333; + border-radius: 4px; + padding: 10px; +} + +/* Improve the authorize button */ +.swagger-ui .btn.authorize { + border-color: #49cc90; + color: #49cc90; + transition: all 0.3s ease; } -.swagger-ui .opblock-tag:hover { - color: #333; + +.swagger-ui .btn.authorize:hover { + background-color: #49cc90; + color: #fff; +} + +/* Make operation summaries more readable */ +.swagger-ui .opblock .opblock-summary-description { + font-size: 14px; + color: #3b4151; + padding: 0 8px; } \ No newline at end of file diff --git a/src/platform/mix.library/Filters/SwaggerDefaultResponsesFilter.cs b/src/platform/mix.library/Filters/SwaggerDefaultResponsesFilter.cs new file mode 100644 index 0000000000..747d4f912d --- /dev/null +++ b/src/platform/mix.library/Filters/SwaggerDefaultResponsesFilter.cs @@ -0,0 +1,145 @@ +using Microsoft.AspNetCore.Authorization; +using Microsoft.OpenApi.Any; +using Microsoft.OpenApi.Models; +using Mix.Lib.Models.ApiResponse; +using Swashbuckle.AspNetCore.SwaggerGen; +using System.Net; +using System.Reflection; + +namespace Mix.Lib.Filters +{ + /// + /// Swagger operation filter that adds standard responses to all API endpoints + /// + public class SwaggerDefaultResponsesFilter : IOperationFilter + { + public void Apply(OpenApiOperation operation, OperationFilterContext context) + { + // Ensure we have a response dictionary + if (operation.Responses == null) + { + operation.Responses = new OpenApiResponses(); + } + + // Add common error responses for all API endpoints + AddErrorResponse(operation, HttpStatusCode.InternalServerError, "Internal server error occurred"); + AddErrorResponse(operation, HttpStatusCode.BadRequest, "Invalid request"); + + // Add auth-related responses if the endpoint requires authorization + if (context.MethodInfo.DeclaringType.GetCustomAttributes(true).Any() || + context.MethodInfo.GetCustomAttributes(true).Any()) + { + AddErrorResponse(operation, HttpStatusCode.Unauthorized, "Unauthorized access"); + AddErrorResponse(operation, HttpStatusCode.Forbidden, "Access forbidden"); + } + + // Add example responses when applicable + AddResponseExamples(operation, context); + } + + private void AddErrorResponse(OpenApiOperation operation, HttpStatusCode statusCode, string description) + { + string statusCodeString = ((int)statusCode).ToString(); + + if (!operation.Responses.ContainsKey(statusCodeString)) + { + var response = new OpenApiResponse + { + Description = description, + Content = new Dictionary + { + ["application/json"] = new OpenApiMediaType + { + Schema = new OpenApiSchema + { + Reference = new OpenApiReference + { + Type = ReferenceType.Schema, + Id = nameof(ApiErrorResponse) + } + } + } + } + }; + + operation.Responses.Add(statusCodeString, response); + } + } + + private void AddResponseExamples(OpenApiOperation operation, OperationFilterContext context) + { + // Add example responses for specific status codes + foreach (var (key, response) in operation.Responses) + { + if (int.TryParse(key, out var statusCode)) + { + switch (statusCode) + { + case 400: // Bad Request + AddErrorExample(response, HttpStatusCode.BadRequest, "Validation failed", + "One or more validation errors occurred", + new Dictionary + { + ["name"] = new[] { "Name is required" }, + ["email"] = new[] { "Invalid email format" } + }); + break; + + case 401: // Unauthorized + AddErrorExample(response, HttpStatusCode.Unauthorized, "Unauthorized", + "Authentication credentials are missing or invalid"); + break; + + case 403: // Forbidden + AddErrorExample(response, HttpStatusCode.Forbidden, "Forbidden", + "You don't have permission to access this resource"); + break; + + case 404: // Not Found + AddErrorExample(response, HttpStatusCode.NotFound, "Resource not found", + "The requested resource could not be found"); + break; + + case 500: // Internal Server Error + AddErrorExample(response, HttpStatusCode.InternalServerError, "Internal server error", + "An unexpected error occurred"); + break; + } + } + } + } + + private void AddErrorExample(OpenApiResponse response, HttpStatusCode statusCode, string message, string details, + Dictionary errors = null) + { + if (response.Content.TryGetValue("application/json", out var mediaType)) + { + var example = new OpenApiObject + { + ["statusCode"] = new OpenApiInteger((int)statusCode), + ["message"] = new OpenApiString(message), + ["details"] = new OpenApiString(details), + ["timestamp"] = new OpenApiString(DateTime.UtcNow.ToString("o")) + }; + + // Add errors if provided + if (errors != null) + { + var errorsObj = new OpenApiObject(); + foreach (var (key, values) in errors) + { + var valuesArray = new OpenApiArray(); + foreach (var value in values) + { + valuesArray.Add(new OpenApiString(value)); + } + errorsObj.Add(key, valuesArray); + } + example.Add("errors", errorsObj); + } + + mediaType.Example = example; + } + } + } +} \ No newline at end of file diff --git a/src/platform/mix.library/Models/ApiResponse/ApiErrorResponse.cs b/src/platform/mix.library/Models/ApiResponse/ApiErrorResponse.cs new file mode 100644 index 0000000000..417f79d986 --- /dev/null +++ b/src/platform/mix.library/Models/ApiResponse/ApiErrorResponse.cs @@ -0,0 +1,120 @@ +using System.Net; +using System.Text.Json.Serialization; + +namespace Mix.Lib.Models.ApiResponse +{ + /// + /// Standardized error response for API endpoints + /// + public class ApiErrorResponse + { + /// + /// Error status code + /// + [JsonPropertyName("statusCode")] + public int StatusCode { get; set; } + + /// + /// Error message + /// + [JsonPropertyName("message")] + public string Message { get; set; } + + /// + /// Detailed error information + /// + [JsonPropertyName("details")] + public string Details { get; set; } + + /// + /// Timestamp when the error occurred + /// + [JsonPropertyName("timestamp")] + public DateTime Timestamp { get; set; } = DateTime.UtcNow; + + /// + /// Additional error data + /// + [JsonPropertyName("errors")] + public Dictionary Errors { get; set; } + + /// + /// Creates a new API error response + /// + /// HTTP status code + /// Error message + /// Detailed error information + /// Additional validation errors + /// The API error response + public static ApiErrorResponse Create( + HttpStatusCode statusCode, + string message, + string details = null, + Dictionary errors = null) + { + return new ApiErrorResponse + { + StatusCode = (int)statusCode, + Message = message, + Details = details, + Errors = errors + }; + } + + /// + /// Creates a not found error response + /// + /// Name of the resource that wasn't found + /// ID of the resource that wasn't found + /// The API error response + public static ApiErrorResponse NotFound(string resourceName, object resourceId) + { + return Create( + HttpStatusCode.NotFound, + $"{resourceName} not found", + $"The requested {resourceName} with ID {resourceId} could not be found." + ); + } + + /// + /// Creates a bad request error response + /// + /// Error message + /// Validation errors + /// The API error response + public static ApiErrorResponse BadRequest(string message, Dictionary errors = null) + { + return Create( + HttpStatusCode.BadRequest, + message, + errors: errors + ); + } + + /// + /// Creates an unauthorized error response + /// + /// Error message + /// The API error response + public static ApiErrorResponse Unauthorized(string message = "Unauthorized access") + { + return Create( + HttpStatusCode.Unauthorized, + message + ); + } + + /// + /// Creates a forbidden error response + /// + /// Error message + /// The API error response + public static ApiErrorResponse Forbidden(string message = "Access forbidden") + { + return Create( + HttpStatusCode.Forbidden, + message + ); + } + } +} \ No newline at end of file diff --git a/src/platform/mix.library/Models/ApiResponse/ApiSuccessResponse.cs b/src/platform/mix.library/Models/ApiResponse/ApiSuccessResponse.cs new file mode 100644 index 0000000000..09786d9027 --- /dev/null +++ b/src/platform/mix.library/Models/ApiResponse/ApiSuccessResponse.cs @@ -0,0 +1,127 @@ +using System.Net; +using System.Text.Json.Serialization; + +namespace Mix.Lib.Models.ApiResponse +{ + /// + /// Standardized success response for API endpoints + /// + /// Type of the data being returned + public class ApiSuccessResponse + { + /// + /// Success status code + /// + [JsonPropertyName("statusCode")] + public int StatusCode { get; set; } = (int)HttpStatusCode.OK; + + /// + /// Success message + /// + [JsonPropertyName("message")] + public string Message { get; set; } = "Operation completed successfully"; + + /// + /// Timestamp when the response was created + /// + [JsonPropertyName("timestamp")] + public DateTime Timestamp { get; set; } = DateTime.UtcNow; + + /// + /// The response data + /// + [JsonPropertyName("data")] + public T Data { get; set; } + + /// + /// Creates a new API success response + /// + /// Response data + /// HTTP status code + /// Success message + /// The API success response + public static ApiSuccessResponse Create(T data, HttpStatusCode statusCode = HttpStatusCode.OK, string message = null) + { + return new ApiSuccessResponse + { + StatusCode = (int)statusCode, + Message = message ?? "Operation completed successfully", + Data = data + }; + } + + /// + /// Creates an OK response + /// + /// Response data + /// Success message + /// The API success response + public static ApiSuccessResponse Ok(T data, string message = "Operation completed successfully") + { + return Create(data, HttpStatusCode.OK, message); + } + + /// + /// Creates a Created response + /// + /// Created entity data + /// Success message + /// The API success response + public static ApiSuccessResponse Created(T data, string message = "Resource created successfully") + { + return Create(data, HttpStatusCode.Created, message); + } + + /// + /// Creates a No Content response + /// + /// Success message + /// The API success response + public static ApiSuccessResponse NoContent(string message = "Operation completed successfully with no content") + { + return Create(default, HttpStatusCode.NoContent, message); + } + } + + /// + /// Non-generic success response for endpoints that don't return data + /// + public class ApiSuccessResponse : ApiSuccessResponse + { + /// + /// Creates a success response with no data + /// + /// HTTP status code + /// Success message + /// The API success response + public static new ApiSuccessResponse Create(HttpStatusCode statusCode = HttpStatusCode.OK, string message = null) + { + return new ApiSuccessResponse + { + StatusCode = (int)statusCode, + Message = message ?? "Operation completed successfully", + Data = null + }; + } + + /// + /// Creates an OK response with no data + /// + /// Success message + /// The API success response + public static new ApiSuccessResponse Ok(string message = "Operation completed successfully") + { + return (ApiSuccessResponse)Create(HttpStatusCode.OK, message); + } + + /// + /// Creates a No Content response + /// + /// Success message + /// The API success response + public static new ApiSuccessResponse NoContent(string message = "Operation completed successfully with no content") + { + return (ApiSuccessResponse)Create(HttpStatusCode.NoContent, message); + } + } +} \ No newline at end of file diff --git a/src/platform/mix.library/Startup/Swagger.cs b/src/platform/mix.library/Startup/Swagger.cs index 4f8f0af093..beb89183d7 100644 --- a/src/platform/mix.library/Startup/Swagger.cs +++ b/src/platform/mix.library/Startup/Swagger.cs @@ -2,6 +2,10 @@ using Microsoft.AspNetCore.Builder; using Microsoft.OpenApi.Models; using Mix.Lib.Filters; +using Mix.Lib.Extensions; +using Swashbuckle.AspNetCore.Annotations; +using System; +using System.IO; using System.Reflection; namespace Microsoft.Extensions.DependencyInjection @@ -11,15 +15,51 @@ public static partial class ServiceCollectionExtensions // Swagger must be after AddMvc() public static IServiceCollection AddMixSwaggerServices(this IServiceCollection services, Assembly assembly) { - string title = assembly.ManifestModule.Name.Replace(".dll", string.Empty).ToHyphenCase(' '); + string title = assembly.ManifestModule.Name.Replace(".dll", string.Empty); + // Convert title to hyphen-case format if needed + title = title.Contains(" ") ? title : title; string version = "v2"; string swaggerBasePath = string.Empty; services.AddSwaggerGen(c => { - c.SwaggerDoc(version, new OpenApiInfo { Title = title, Version = version }); + c.SwaggerDoc(version, new OpenApiInfo { + Title = title, + Version = version, + Description = "MixCore API endpoints documentation", + Contact = new OpenApiContact + { + Name = "Mixcore", + Url = new Uri("https://mixcore.org") + }, + License = new OpenApiLicense + { + Name = "Mixcore License", + Url = new Uri("https://github.com/mixcore/mix.core/blob/master/LICENSE") + } + }); + c.OperationFilter(); c.CustomSchemaIds(x => x.FullName); + + // Include XML comments for all assemblies in the API + var xmlFile = $"{assembly.GetName().Name}.xml"; + var xmlPath = Path.Combine(AppContext.BaseDirectory, xmlFile); + if (File.Exists(xmlPath)) + { + c.IncludeXmlComments(xmlPath); + } + + // Also add XML comments from Mix.Lib + var libXmlFile = "Mix.Lib.xml"; + var libXmlPath = Path.Combine(AppContext.BaseDirectory, libXmlFile); + if (File.Exists(libXmlPath)) + { + c.IncludeXmlComments(libXmlPath); + } + + // Add operation filters for standardizing responses + c.OperationFilter(); // add JWT Authentication var securityScheme = new OpenApiSecurityScheme @@ -43,6 +83,9 @@ public static IServiceCollection AddMixSwaggerServices(this IServiceCollection s {securityScheme, new string[] { }} } ); + + // Configure parameter descriptions + c.DescribeAllParametersInCamelCase(); }); return services; } @@ -59,11 +102,15 @@ public static IApplicationBuilder UseMixSwaggerApps(this IApplicationBuilder app //if (isDevelop) //{ - var xmlFilename = $"{Assembly.GetExecutingAssembly().GetName().Name}.xml"; - app.UseSwagger(opt => { opt.RouteTemplate = routeTemplate; + opt.PreSerializeFilters.Add((swaggerDoc, httpReq) => + { + swaggerDoc.Servers = new List { + new OpenApiServer { Url = $"{httpReq.Scheme}://{httpReq.Host.Value}" } + }; + }); }); app.UseSwaggerUI(c => { @@ -75,6 +122,8 @@ public static IApplicationBuilder UseMixSwaggerApps(this IApplicationBuilder app c.DocumentTitle = "Mixcore - API Specification"; c.EnableFilter(); c.EnableDeepLinking(); + c.DisplayRequestDuration(); + c.DefaultModelsExpandDepth(0); // Hide schemas section by default }); //} return app; diff --git a/src/platform/mix.library/mix.library.csproj b/src/platform/mix.library/mix.library.csproj index 4e6da157a9..1b4a769d5e 100644 --- a/src/platform/mix.library/mix.library.csproj +++ b/src/platform/mix.library/mix.library.csproj @@ -91,6 +91,7 @@ +