From 61d97d40b0fb11f9637ed8b2783ce980070ff528 Mon Sep 17 00:00:00 2001 From: Bangara Raju Kottedi Date: Mon, 29 Apr 2024 16:03:01 +0530 Subject: [PATCH] Implemented API versioning and added to the swagger documentation --- PortBlog.API/Controllers/CvController.cs | 68 ++++++++- PortBlog.API/Models/ResumeDto.cs | 54 +++++-- PortBlog.API/PortBlog.API.csproj | 3 + PortBlog.API/PortBlog.API.xml | 174 +++++++++++++++++++++++ PortBlog.API/Program.cs | 90 +++++++++--- 5 files changed, 354 insertions(+), 35 deletions(-) create mode 100644 PortBlog.API/PortBlog.API.xml diff --git a/PortBlog.API/Controllers/CvController.cs b/PortBlog.API/Controllers/CvController.cs index ace7c6f..9096633 100644 --- a/PortBlog.API/Controllers/CvController.cs +++ b/PortBlog.API/Controllers/CvController.cs @@ -1,4 +1,5 @@ -using AutoMapper; +using Asp.Versioning; +using AutoMapper; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc; using PortBlog.API.Entities; @@ -7,8 +8,9 @@ using PortBlog.API.Repositories.Contracts; namespace PortBlog.API.Controllers { - [Route("api/cv")] + [Route("api/v{versions:apiVersion}/cv")] [ApiController] + [ApiVersion(1)] public class CvController : ControllerBase { private readonly ILogger _logger; @@ -24,7 +26,19 @@ namespace PortBlog.API.Controllers _mapper = mapper; } + /// + /// Get CV details of the candidate by candidateid. + /// + /// The id of the candidate whose cv to get + /// CV details of the candidate + /// Returns the requested cv of the candidate [HttpGet("{candidateId}")] + [Obsolete] + [ApiVersion(0.1, Deprecated = true)] + [ProducesResponseType(StatusCodes.Status404NotFound)] + [ProducesResponseType(StatusCodes.Status400BadRequest)] + [ProducesResponseType(StatusCodes.Status500InternalServerError)] + [ProducesResponseType(StatusCodes.Status200OK)] public async Task> Get(int candidateId) { try @@ -47,7 +61,17 @@ namespace PortBlog.API.Controllers } } + /// + /// Get hobbies of the candidate by candidateid + /// + /// The id of the candidate whose hobbies to get + /// Hobbies of the candidate + /// Returns the requested hobbies of the candidate [HttpGet("GetHobbies/{candidateId}")] + [ProducesResponseType(StatusCodes.Status404NotFound)] + [ProducesResponseType(StatusCodes.Status400BadRequest)] + [ProducesResponseType(StatusCodes.Status500InternalServerError)] + [ProducesResponseType(StatusCodes.Status200OK)] public async Task> GetHobbies(int candidateId) { try @@ -70,7 +94,17 @@ namespace PortBlog.API.Controllers } } + /// + /// Get Candidate details with social links by candidateid + /// + /// The id of the candidate whose detials to get with social links + /// Candidate details with sociallinks + /// Returns the requested candidate details with social links [HttpGet("GetCandidateWithSocialLinks/{candidateId}")] + [ProducesResponseType(StatusCodes.Status404NotFound)] + [ProducesResponseType(StatusCodes.Status400BadRequest)] + [ProducesResponseType(StatusCodes.Status500InternalServerError)] + [ProducesResponseType(StatusCodes.Status200OK)] public async Task> GetContact(int candidateId) { try @@ -93,7 +127,17 @@ namespace PortBlog.API.Controllers } } + /// + /// Get Candidate resume by candidateid + /// + /// The id of the candidate whose resume to get + /// Candidate resume + /// Returns the requested candidate resume [HttpGet("GetResume/{candidateId}")] + [ProducesResponseType(StatusCodes.Status404NotFound)] + [ProducesResponseType(StatusCodes.Status400BadRequest)] + [ProducesResponseType(StatusCodes.Status500InternalServerError)] + [ProducesResponseType(StatusCodes.Status200OK)] public async Task> GetResume(int candidateId) { try @@ -116,7 +160,17 @@ namespace PortBlog.API.Controllers } } + /// + /// Get Candidate projects by candidateid + /// + /// The id of the candidate whose projects to get + /// Candidate projects + /// Returns the requested candidate projects [HttpGet("GetProjects/{candidateId}")] + [ProducesResponseType(StatusCodes.Status404NotFound)] + [ProducesResponseType(StatusCodes.Status400BadRequest)] + [ProducesResponseType(StatusCodes.Status500InternalServerError)] + [ProducesResponseType(StatusCodes.Status200OK)] public async Task> GetProjects(int candidateId) { try @@ -139,7 +193,17 @@ namespace PortBlog.API.Controllers } } + /// + /// Get Candidate blog with posts by candidateid + /// + /// The id of the candidate whose blog with posts to get + /// Candidate blog with posts + /// Returns the requested candidate blog with posts [HttpGet("GetBlog/{candidateId}")] + [ProducesResponseType(StatusCodes.Status404NotFound)] + [ProducesResponseType(StatusCodes.Status400BadRequest)] + [ProducesResponseType(StatusCodes.Status500InternalServerError)] + [ProducesResponseType(StatusCodes.Status200OK)] public async Task> GetBlog(int candidateId) { try diff --git a/PortBlog.API/Models/ResumeDto.cs b/PortBlog.API/Models/ResumeDto.cs index d124ed7..173f40e 100644 --- a/PortBlog.API/Models/ResumeDto.cs +++ b/PortBlog.API/Models/ResumeDto.cs @@ -1,17 +1,33 @@ namespace PortBlog.API.Models { + /// + /// CV details of the candidate + /// public class ResumeDto { + /// + /// The id of the cv + /// public int ResumeId { get; set; } - + /// + /// The title of the candidate + /// public string Title { get; set; } = string.Empty; - + /// + /// A brief description about the candidate + /// public string About { get; set; } = string.Empty; - + /// + /// Candidate's information + /// public CandidateDto? Candidate { get; set; } - + /// + /// Candidate's Social Media links + /// public SocialLinksDto? SocialLinks { get; set; } - + /// + /// Candidate's blog posts + /// public ICollection Posts { get @@ -19,19 +35,33 @@ return SocialLinks?.Posts ?? new List(); } } - + /// + /// The education details of the candidate + /// public ICollection Academics { get; set; } = new List(); - + /// + /// The skills of the candidate + /// public ICollection Skills { get; set; } = new List(); - + /// + /// The work experiences of the candidate + /// public ICollection Experiences { get; set; } = new List(); - + /// + /// The certifications done by the candidate + /// public ICollection Certifications { get; set; } = new List(); - + /// + /// The hobbies of the candidate + /// public ICollection Hobbies { get; set; } = new List(); - + /// + /// The projects of the candidate + /// public ICollection Projects { get; set; } = new List(); - + /// + /// The project categories of all the projects + /// public ICollection ProjectsCategories { get diff --git a/PortBlog.API/PortBlog.API.csproj b/PortBlog.API/PortBlog.API.csproj index 7633d15..51498ba 100644 --- a/PortBlog.API/PortBlog.API.csproj +++ b/PortBlog.API/PortBlog.API.csproj @@ -4,9 +4,12 @@ net8.0 enable enable + True + PortBlog.API.xml + diff --git a/PortBlog.API/PortBlog.API.xml b/PortBlog.API/PortBlog.API.xml new file mode 100644 index 0000000..c8adac2 --- /dev/null +++ b/PortBlog.API/PortBlog.API.xml @@ -0,0 +1,174 @@ + + + + PortBlog.API + + + + + Get CV details of the candidate by candidateid. + + The id of the candidate whose cv to get + CV details of the candidate + Returns the requested cv of the candidate + + + + Get hobbies of the candidate by candidateid + + The id of the candidate whose hobbies to get + Hobbies of the candidate + Returns the requested hobbies of the candidate + + + + Get Candidate details with social links by candidateid + + The id of the candidate whose detials to get with social links + Candidate details with sociallinks + Returns the requested candidate details with social links + + + + Get Candidate resume by candidateid + + The id of the candidate whose resume to get + Candidate resume + Returns the requested candidate resume + + + + Get Candidate projects by candidateid + + The id of the candidate whose projects to get + Candidate projects + Returns the requested candidate projects + + + + Get Candidate blog with posts by candidateid + + The id of the candidate whose blog with posts to get + Candidate blog with posts + Returns the requested candidate blog with posts + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + CV details of the candidate + + + + + The id of the cv + + + + + The title of the candidate + + + + + A brief description about the candidate + + + + + Candidate's information + + + + + Candidate's Social Media links + + + + + Candidate's blog posts + + + + + The education details of the candidate + + + + + The skills of the candidate + + + + + The work experiences of the candidate + + + + + The certifications done by the candidate + + + + + The hobbies of the candidate + + + + + The projects of the candidate + + + + + The project categories of all the projects + + + + diff --git a/PortBlog.API/Program.cs b/PortBlog.API/Program.cs index 52d1a0a..c3ad400 100644 --- a/PortBlog.API/Program.cs +++ b/PortBlog.API/Program.cs @@ -1,3 +1,5 @@ +using Asp.Versioning; +using Asp.Versioning.ApiExplorer; using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.FileProviders; using Microsoft.OpenApi.Models; @@ -5,6 +7,7 @@ using PortBlog.API.DbContexts; using PortBlog.API.Extensions; using PortBlog.API.Middleware; using Serilog; +using System.Reflection; Log.Logger = new LoggerConfiguration() .MinimumLevel.Debug() @@ -27,8 +30,64 @@ builder.Services.AddProblemDetails(); // Learn more about configuring Swagger/OpenAPI at https://aka.ms/aspnetcore/swashbuckle builder.Services.AddEndpointsApiExplorer(); + +var connectionString = builder.Configuration.GetConnectionString("PortBlogDBConnectionString"); + +if (builder.Configuration.GetValue("ConnectionStrings:Encryption")) +{ + connectionString = builder.Configuration.DecryptConnectionString(connectionString); +} + +if (string.IsNullOrEmpty(connectionString)) +{ + throw new Exception("Connection string cannot be empty"); +} + +builder.Services +.AddDbContext(dbContextOptions +=> dbContextOptions.UseMySql(connectionString, ServerVersion.AutoDetect(connectionString))); + +builder.Services.AddRepositories(); + +builder.Services.AddAutoMapper(AppDomain.CurrentDomain.GetAssemblies()); + + +// Registering API Versioning Specification services +builder.Services.AddApiVersioning(setupAction => +{ + setupAction.ReportApiVersions = true; + setupAction.AssumeDefaultVersionWhenUnspecified = true; + setupAction.DefaultApiVersion = new ApiVersion(1, 0); +}).AddMvc() +.AddApiExplorer(setupAction => +{ + setupAction.SubstituteApiVersionInUrl = true; +}); + +var apiVersionDescriptionProvider = builder.Services.BuildServiceProvider() + .GetRequiredService(); + builder.Services.AddSwaggerGen(c => { + foreach(var description in apiVersionDescriptionProvider.ApiVersionDescriptions) + { + c.SwaggerDoc( + $"{description.GroupName}", + new() + { + Title = "Portfolio Blog API", + Version = description.ApiVersion.ToString(), + Description = "Through this API you can access candidate cv details and along with other details." + }); + } + + // XML Comments file for API Documentation + var xmlCommentsFile = $"{Assembly.GetExecutingAssembly().GetName().Name}.xml"; + var xmlCommentsFullPath = $"{Path.Combine(AppContext.BaseDirectory, xmlCommentsFile)}"; + + c.IncludeXmlComments(xmlCommentsFullPath); + + // Add Security Definition to the Swagger c.AddSecurityDefinition("ApiKey", new OpenApiSecurityScheme { Description = "ApiKey must appear in header", @@ -56,26 +115,6 @@ builder.Services.AddSwaggerGen(c => c.AddSecurityRequirement(requirement); }); -var connectionString = builder.Configuration.GetConnectionString("PortBlogDBConnectionString"); - -if (builder.Configuration.GetValue("ConnectionStrings:Encryption")) -{ - connectionString = builder.Configuration.DecryptConnectionString(connectionString); -} - -if (string.IsNullOrEmpty(connectionString)) -{ - throw new Exception("Connection string cannot be empty"); -} - -builder.Services -.AddDbContext(dbContextOptions -=> dbContextOptions.UseMySql(connectionString, ServerVersion.AutoDetect(connectionString))); - -builder.Services.AddRepositories(); - -builder.Services.AddAutoMapper(AppDomain.CurrentDomain.GetAssemblies()); - var app = builder.Build(); if (!app.Environment.IsDevelopment()) @@ -87,7 +126,16 @@ if (!app.Environment.IsDevelopment()) if (app.Environment.IsDevelopment()) { app.UseSwagger(); - app.UseSwaggerUI(); + app.UseSwaggerUI(setupAction => + { + var descriptions = app.DescribeApiVersions(); + foreach(var description in descriptions) + { + setupAction.SwaggerEndpoint( + $"/swagger/{description.GroupName}/swagger.json", + description.GroupName.ToUpperInvariant()); + } + }); } app.UseHttpsRedirection();