diff --git a/devdocs/todo.txt b/devdocs/todo.txt index 70110ab2..9afa5dce 100644 --- a/devdocs/todo.txt +++ b/devdocs/todo.txt @@ -11,6 +11,15 @@ UPDATE all the things before commencing work - https://github.com/domaindrivendev/Swashbuckle.AspNetCore#swashbuckleaspnetcoreannotations - +Metrics and swagger broken after update. +Need to consider re-thinking versioning system of api to remove the complexity of swagger bullshit when using versioned api. + +Test out versioning in the test project I made following the Swashbuckle guide for multiple docs: +https://github.com/domaindrivendev/Swashbuckle.AspNetCore#list-multiple-swagger-documents + +Because maybe this is the way to go? + + Need a sprint to get to a fully testable client with entry form, list and as much as possible all features from COMMON-* specs list Do the stuff in the Client todo first then back to the server as required. diff --git a/server/AyaNova/Controllers/ApiRootController.cs b/server/AyaNova/Controllers/ApiRootController.cs index dc74c239..68a33a8a 100644 --- a/server/AyaNova/Controllers/ApiRootController.cs +++ b/server/AyaNova/Controllers/ApiRootController.cs @@ -14,7 +14,8 @@ namespace AyaNova.Api.Controllers [ApiVersion("8.0")] [Route("api/v{version:apiVersion}/")] [AllowAnonymous] - public class ApiMetaController : Controller + [ApiController] + public class ApiMetaController : ControllerBase { private readonly ApiServerState serverState; private readonly ILogger _log; diff --git a/server/AyaNova/Startup.cs b/server/AyaNova/Startup.cs index 2dad823f..503ea180 100644 --- a/server/AyaNova/Startup.cs +++ b/server/AyaNova/Startup.cs @@ -14,14 +14,17 @@ using Microsoft.AspNetCore.Http; using Microsoft.OpenApi.Models; +using Microsoft.Extensions.Options; +using Swashbuckle.AspNetCore.Swagger; +using Swashbuckle.AspNetCore.SwaggerGen; + using AyaNova.Models; using AyaNova.Util; using AyaNova.Generator; using AyaNova.Biz; -using Swashbuckle.AspNetCore.Swagger; -using Swashbuckle.AspNetCore.SwaggerGen; + using Swashbuckle.AspNetCore.SwaggerUI; using System.IO; @@ -88,10 +91,7 @@ namespace AyaNova - // add the versioned api explorer, which also adds IApiVersionDescriptionProvider service - // note: the specified format code will format the version as "'v'major[.minor][-status]" - _log.LogDebug("BOOT: init ApiExplorer service"); - services.AddVersionedApiExplorer(o => o.GroupNameFormat = "'v'VVV"); + _log.LogDebug("BOOT: ensuring user and backup folders exist and are separate locations..."); FileUtil.EnsureUserAndUtilityFoldersExistAndAreNotIdentical(_hostingEnvironment.ContentRootPath); @@ -147,15 +147,562 @@ namespace AyaNova #endregion - _log.LogDebug("BOOT: init ApiVersioning service"); - // services.AddApiVersioning(o => o.ReportApiVersions = true); - services - .AddApiVersioning(options => - { - options.AssumeDefaultVersionWhenUnspecified = true; - options.DefaultApiVersion = Microsoft.AspNetCore.Mvc.ApiVersion.Parse("8.0"); - options.ReportApiVersions = true; - }); + + + + + + + #region Swagger + + // services + // .AddApiVersioning(options => + // { + // options.AssumeDefaultVersionWhenUnspecified = true; + // options.DefaultApiVersion = Microsoft.AspNetCore.Mvc.ApiVersion.Parse("8.0"); + // options.ReportApiVersions = true; + // }); + + // services.AddVersionedApiExplorer(o => o.GroupNameFormat = "'v'VVV"); + + services.AddApiVersioning(); + services.AddVersionedApiExplorer(options => options.GroupNameFormat = "'v'VVV"); + + // services.AddSwaggerGen( + // c => + // { + // c.SwaggerDoc("v8", new OpenApiInfo { Title = "XXX API - V8", Version = "v8" }); + // c.DocInclusionPredicate((docName, apiDesc) => + // { + // if (!apiDesc.TryGetMethodInfo(out MethodInfo methodInfo)) return false; + + // var versions = methodInfo.DeclaringType + // .GetCustomAttributes(true) + // .OfType() + // .SelectMany(attr => attr.Versions); + + // return versions.Any(v => $"v{v.ToString()}" == docName); + // }); + // }); + services.AddTransient, ConfigureSwaggerOptions>(); + services.AddSwaggerGen(); + + + #endregion + + + #region JWT AUTHENTICATION + //get the key if specified + var secretKey = ServerBootConfig.AYANOVA_JWT_SECRET; + + //If no key specified make a unique one + //This means the jwt creds won't survive a server reboot + //so in that case users need to specify an AyaNova_JWT_SECRET environment variable + if (string.IsNullOrWhiteSpace(secretKey)) + { + secretKey = Util.Hasher.GenerateSalt(); + } + //WAS "UNLICENSED5G*QQJ8#bQ7$Xr_@sXfHq4" + + + //If secretKey is less than 32 characters, pad it + if (secretKey.Length < 32) + { + secretKey = secretKey.PadRight(32, '-'); + } + + ServerBootConfig.AYANOVA_JWT_SECRET = secretKey; + var signingKey = new SymmetricSecurityKey(System.Text.Encoding.ASCII.GetBytes(ServerBootConfig.AYANOVA_JWT_SECRET)); + + _log.LogDebug("BOOT: init Authorization service"); + services.AddAuthentication(options => + { + options.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme; + options.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme; + }).AddJwtBearer(options => + { + // options.AutomaticAuthenticate = true; + // options.AutomaticChallenge = true; + options.TokenValidationParameters = new TokenValidationParameters + { + // Token signature will be verified using a private key. + ValidateIssuerSigningKey = true, + RequireSignedTokens = true, + IssuerSigningKey = signingKey, + ValidateIssuer = true, + ValidIssuer = "ayanova.com", + ValidateAudience = false, + //ValidAudience = "http://localhost:7575/" + + // Token will only be valid if not expired yet, with 5 minutes clock skew. + ValidateLifetime = true, + RequireExpirationTime = true, + ClockSkew = new TimeSpan(0, 5, 0), + }; + }); + + + + #endregion + + _log.LogDebug("BOOT: init Generator service"); + services.AddSingleton(); + + + } + + + + //////////////////////////////////////////////////////////// + // This method gets called by the runtime. Use this method to configure the HTTP request pipeline. + // + public void Configure(IApplicationBuilder app, Microsoft.AspNetCore.Hosting.IWebHostEnvironment env, + AyContext dbContext, IApiVersionDescriptionProvider provider, AyaNova.Api.ControllerHelpers.ApiServerState apiServerState, IServiceProvider serviceProvider) + { + _log.LogDebug("BOOT: configuring request pipeline..."); + + if (env.IsDevelopment()) + { + app.UseDeveloperExceptionPage(); + } + //Store a reference to the dependency injection service for static classes + ServiceProviderProvider.Provider = app.ApplicationServices; + + //Enable ability to handle reverse proxy + app.UseForwardedHeaders(new ForwardedHeadersOptions + { + ForwardedHeaders = Microsoft.AspNetCore.HttpOverrides.ForwardedHeaders.XForwardedFor | Microsoft.AspNetCore.HttpOverrides.ForwardedHeaders.XForwardedProto + }); + + #region STATIC FILES + _log.LogDebug("BOOT: pipeline - static files"); + app.UseDefaultFiles(); + app.UseStaticFiles(); + //Might need the following if the page doesn't update in the client properly + //however the vue build process will automatically uniquify each build file names so maybe not required + // app.UseStaticFiles(new StaticFileOptions + // { + // OnPrepareResponse = context => + // { + // if (context.File.Name == "index.html") + // { + // context.Context.Response.Headers.Add("Cache-Control", "no-cache, no-store"); + // context.Context.Response.Headers.Add("Expires", "-1"); + // } + // } + // }); + #endregion + + _log.LogDebug("BOOT: pipeline - ROUTING"); + app.UseRouting();//this wasn't here for 2.2 but added for 3.0, needs to come before the stuff after + + _log.LogDebug("BOOT: pipeline - CORS"); + app.UseCors("CorsPolicy"); + + + #region AUTH / ROLES + _log.LogDebug("BOOT: pipeline - authentication"); + //Use authentication middleware + app.UseAuthentication(); + + _log.LogDebug("BOOT: pipeline - authorization"); + app.UseAuthorization(); + + + //Custom middleware to get user roles and put them into the request so + //they can be authorized in routes. + app.Use(async (context, next) => + { + if (!context.User.Identity.IsAuthenticated) + { + context.Request.HttpContext.Items["AY_ROLES"] = 0; + } + else + { + //Get user ID from claims + long userId = Convert.ToInt64(context.User.FindFirst(c => c.Type == "id").Value); + + //Get the database context + var ct = context.RequestServices.GetService(); + + //get the user record + var u = ct.User.AsNoTracking().Where(a => a.Id == userId).Select(m => new { roles = m.Roles, name = m.Name, id = m.Id, localeId = m.LocaleId }).First(); + context.Request.HttpContext.Items["AY_ROLES"] = u.roles; + context.Request.HttpContext.Items["AY_USERNAME"] = u.name; + context.Request.HttpContext.Items["AY_USER_ID"] = u.id; + context.Request.HttpContext.Items["AY_LOCALE_ID"] = u.localeId; + + + + } + await next.Invoke(); + }); + + #endregion + + _log.LogDebug("BOOT: pipeline - ENDPOINTS"); + app.UseEndpoints(endpoints => + { + endpoints.MapControllers(); + }); + + #region SWAGGER + + _log.LogDebug("BOOT: pipeline - api explorer"); + // Enable middleware to serve generated Swagger as a JSON endpoint. + // app.UseSwagger(); + + // app.UseSwaggerUI(c => + // { + // // build a swagger endpoint for each discovered API version + // foreach (var description in provider.ApiVersionDescriptions) + // { + // c.SwaggerEndpoint($"/swagger/{description.GroupName}/swagger.json", description.GroupName.ToUpperInvariant()); + // } + + // //clean up the swagger explorer UI page and remove the branding + // //via our own css + // //NOTE: this broke when updated to v2.x of swagger and it can be fixed according to docs: + // //https://github.com/domaindrivendev/Swashbuckle.AspNetCore#inject-custom-css + // // c.InjectStylesheet("/api/sw.css"); + + // c.DefaultModelsExpandDepth(-1); + // c.DocumentTitle = "AyaNova API explorer"; + // c.RoutePrefix = "api-docs"; + // }); + + app.UseSwagger(); + + app.UseSwaggerUI( + options => + { + foreach (var description in provider.ApiVersionDescriptions) + { + options.SwaggerEndpoint( + $"/swagger/{description.GroupName}/swagger.json", + description.GroupName.ToUpperInvariant()); + } + options.DocumentTitle = "AyaNova API explorer"; + options.RoutePrefix = "api-docs"; + }); + + + #endregion swagger + + + + + + + //According to https://docs.microsoft.com/en-us/aspnet/core/migration/22-to-30?view=aspnetcore-2.2&tabs=visual-studio#migrate-startupconfigure + //replace use mvc with use endpoints + // //USE MVC + // _log.LogDebug("BOOT: pipeline - MVC"); + // app.UseMvc(); + // _log.LogDebug("BOOT: pipeline - ENDPOINTS"); + // app.UseEndpoints(endpoints => { + // endpoints.MapControllers(); + // }); + + // ****************************************************************** + // ******************** TESTING WIPE DB ***************************** + // + //Set this to true to wipe the db and reinstall a trial license and re-seed the data + var TESTING_REFRESH_DB = false;//####################################################################################### + +#if (DEBUG) + //TESTING + if (TESTING_REFRESH_DB) + ServerBootConfig.AYANOVA_PERMANENTLY_ERASE_DATABASE = TESTING_REFRESH_DB; + //TESTING +#endif + + + if (ServerBootConfig.AYANOVA_PERMANENTLY_ERASE_DATABASE) + { + _log.LogWarning("BOOT: AYANOVA_PERMANENTLY_ERASE_DATABASE is true, dropping and recreating database"); + Util.DbUtil.DropAndRecreateDb(_log); + AySchema.CheckAndUpdate(dbContext, _log); + } + + //Check schema + _log.LogDebug("BOOT: db schema check"); + AySchema.CheckAndUpdate(dbContext, _log); + + //Check database integrity + _log.LogDebug("BOOT: db integrity check"); + DbUtil.CheckFingerPrint(AySchema.EXPECTED_COLUMN_COUNT, AySchema.EXPECTED_INDEX_COUNT, _log); + + //Initialize license + AyaNova.Core.License.Initialize(apiServerState, dbContext, _log); + + //Ensure locales are present, not missing any keys and that there is a server default locale that exists + LocaleBiz lb = new LocaleBiz(dbContext, 1, ServerBootConfig.AYANOVA_DEFAULT_LANGUAGE_ID, AuthorizationRoles.OpsAdminFull); + lb.ValidateLocales(); + + + +#if (DEBUG) + //TESTING + if (TESTING_REFRESH_DB) + { + AyaNova.Core.License.Fetch(apiServerState, dbContext, _log); + Util.Seeder.SeedDatabase(Util.Seeder.SeedLevel.SmallOneManShopTrialDataSet, -7);//############################################################################################# + } + //TESTING +#endif + + + //AUTOID VALUES INITIALIZATION + ServerBootConfig.SetMostRecentAutoIdValuesFromDatabase(dbContext); + + //SPA FALLBACK ROUTE + app.Use(async (context, next) => + { + //to support html5 pushstate routing in spa + //this ensures that a refresh at the client will not 404 but rather force back to the index.html app page and then handled internally by the client + await next(); + if (!context.Response.HasStarted && context.Request.Path.Value != "/docs" && context.Response.StatusCode == 404 && !Path.HasExtension(context.Request.Path.Value)) + { + context.Request.Path = "/index.html"; + context.Response.StatusCode = 200; + context.Response.ContentType = "text/html"; + await context.Response.SendFileAsync(Path.Combine(env.WebRootPath, "index.html")); + } + }); + + + //Log the active user count so it's in the log record + _log.LogInformation($"BOOT: Active techs - {UserBiz.ActiveCount}"); + + //Log the license info so it's on the record + _log.LogInformation($"BOOT: License -\r\n=-=-=-=-=-=-=-=-=-=-\r\n{AyaNova.Core.License.LicenseInfo}=-=-=-=-=-=-=-=-=-=-"); + + + + //Open up the server for visitors + apiServerState.SetOpen(); + + //final startup log + _log.LogInformation("BOOT: COMPLETED - SERVER IS NOW OPEN"); + + } + + + #region Swagger and API Versioning utilities + + static string XmlCommentsFilePath + { + get + { + //Obsolete, used new method: https://developers.de/blogs/holger_vetter/archive/2017/06/30/swagger-includexmlcomments-platformservices-obsolete-replacement.aspx + //var basePath = PlatformServices.Default.Application.ApplicationBasePath; + var basePath = AppContext.BaseDirectory; + var fileName = typeof(Startup).GetTypeInfo().Assembly.GetName().Name + ".xml"; + return Path.Combine(basePath, fileName); + } + } + + // static Microsoft.OpenApi.Models.OpenApiInfo CreateInfoForApiVersion(ApiVersionDescription description) + // { + // var info = new Microsoft.OpenApi.Models.OpenApiInfo() + // { + // Title = $"AyaNova API {description.ApiVersion}", + // Version = description.ApiVersion.ToString() + // }; + + // if (description.IsDeprecated) + // { + // info.Description += " This API version has been deprecated."; + // } + + // return info; + // } + + + + //update for v3 from here https://github.com/microsoft/aspnet-api-versioning/wiki/API-Documentation#aspnet-core + //and example here: https://github.com/microsoft/aspnet-api-versioning/blob/master/samples/aspnetcore/SwaggerSample/ConfigureSwaggerOptions.cs + // public class ConfigureSwaggerOptions : IConfigureOptions + // { + // readonly IApiVersionDescriptionProvider provider; + + // public ConfigureSwaggerOptions(IApiVersionDescriptionProvider provider) => + // this.provider = provider; + + // public void Configure(SwaggerGenOptions options) + // { + // foreach (var description in provider.ApiVersionDescriptions) + // { + // options.SwaggerDoc( + // description.GroupName, + // new OpenApiInfo() + // { + // Title = $"AyaNova API {description.ApiVersion}", + // Version = description.ApiVersion.ToString(), + // }); + // } + // } + // } + + #endregion + } +} + + +/* + + +using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Hosting; +using Microsoft.AspNetCore.Mvc.ApiExplorer; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; + +using Microsoft.Extensions.Logging; +using Microsoft.EntityFrameworkCore; +using Microsoft.AspNetCore.Authentication.JwtBearer; +using Microsoft.IdentityModel.Tokens; +using Microsoft.Extensions.Hosting; +using Microsoft.AspNetCore.Http; + +using Microsoft.OpenApi.Models; + +using Microsoft.Extensions.Options; +using Swashbuckle.AspNetCore.Swagger; +using Swashbuckle.AspNetCore.SwaggerGen; + + +using AyaNova.Models; +using AyaNova.Util; +using AyaNova.Generator; +using AyaNova.Biz; + + +using Swashbuckle.AspNetCore.SwaggerUI; + +using System.IO; +using System.Reflection; +using System.Linq; +using System; +using System.Collections.Generic; +using Newtonsoft.Json.Serialization; + + +namespace AyaNova +{ + + public class Startup + { + + + ///////////////////////////////////////////////////////////// + // + public Startup(ILogger logger, ILoggerFactory logFactory, Microsoft.AspNetCore.Hosting.IWebHostEnvironment hostingEnvironment) + { + _log = logger; + _hostingEnvironment = hostingEnvironment; + AyaNova.Util.ApplicationLogging.LoggerFactory = logFactory; + //this must be set here + ServerBootConfig.AYANOVA_CONTENT_ROOT_PATH = hostingEnvironment.ContentRootPath; + + } + + private readonly ILogger _log; + private string _connectionString = ""; + private readonly Microsoft.AspNetCore.Hosting.IWebHostEnvironment _hostingEnvironment; + + //////////////////////////////////////////////////////////// + // This method gets called by the runtime. Use this method to add services to the container. + // + public void ConfigureServices(IServiceCollection services) + { + _log.LogDebug("BOOT: initializing services..."); + + + + //Server state service for shutting people out of api + _log.LogDebug("BOOT: init ApiServerState service"); + services.AddSingleton(new AyaNova.Api.ControllerHelpers.ApiServerState()); + + //Init controllers + _log.LogDebug("BOOT: init controllers"); + var MvcBuilder = services.AddControllers(config => + { + config.Filters.Add(new AyaNova.Api.ControllerHelpers.ApiCustomExceptionFilter(AyaNova.Util.ApplicationLogging.LoggerFactory)); + }); + + _log.LogDebug("BOOT: init JSON"); + MvcBuilder.AddNewtonsoftJson(options => + { + options.SerializerSettings.DateTimeZoneHandling = Newtonsoft.Json.DateTimeZoneHandling.Utc; + }); + + //2019-10-17 METRICS will not work just yet with .netcore 3.1 see here https://github.com/AppMetrics/AppMetrics/issues/480 + //awaiting a new release from them + //_log.LogDebug("BOOT: init Metrics service"); + //MvcBuilder.AddMetrics(); + + + + + + _log.LogDebug("BOOT: ensuring user and backup folders exist and are separate locations..."); + FileUtil.EnsureUserAndUtilityFoldersExistAndAreNotIdentical(_hostingEnvironment.ContentRootPath); + + #region DATABASE + _connectionString = ServerBootConfig.AYANOVA_DB_CONNECTION; + + //Check DB server exists and can be connected to + _log.LogDebug("BOOT: Testing database server connection..."); + + //parse the connection string properly + DbUtil.ParseConnectionString(_log, _connectionString); + + //Probe for database server + //Will retry every 10 seconds for up to 5 minutes before bailing + if (!DbUtil.DatabaseServerExists(_log, "BOOT: waiting for db server ")) + { + var err = $"BOOT: E1000 - AyaNova can't connect to the database server after trying for 5 minutes (connection string is:\"{DbUtil.DisplayableConnectionString}\")"; + _log.LogCritical(err); + throw new System.ApplicationException(err); + } + + + + _log.LogInformation("BOOT: Connected to database server - {0}", DbUtil.DisplayableConnectionString); + + + //ensure database is ready and present + DbUtil.EnsureDatabaseExists(_log); + + bool LOG_SENSITIVE_DATA = false; + +#if (DEBUG) + // LOG_SENSITIVE_DATA = true; + +#endif + + _log.LogDebug("BOOT: init EF service"); + + services.AddEntityFrameworkNpgsql().AddDbContext( + options => options.UseNpgsql(_connectionString + //,opt => opt.EnableRetryOnFailure()//REMOVED THIS BECAUSE IT WAS INTEFERING WITH TRANSACTIONS BUT THEN DIDN'T USE THE TRANSACTION BUT IT SEEMS FASTER WITHOUT IT AS WELL SO...?? + )//http://www.npgsql.org/efcore/misc.html?q=execution%20strategy#execution-strategy + .ConfigureWarnings(warnings => //https://livebook.manning.com/#!/book/entity-framework-core-in-action/chapter-12/v-10/85 + warnings.Throw( //Throw an exception on client eval, not necessarily an error but a smell + // Microsoft.EntityFrameworkCore.Diagnostics.RelationalEventId.QueryClientEvaluationWarning + )) + .EnableSensitiveDataLogging(LOG_SENSITIVE_DATA) + ); + + + + + #endregion + + // Add service and create Policy with options @@ -175,6 +722,23 @@ namespace AyaNova #region Swagger + _log.LogDebug("BOOT: init ApiVersioning service"); + // services.AddApiVersioning(o => o.ReportApiVersions = true); + services + .AddApiVersioning(options => + { + options.AssumeDefaultVersionWhenUnspecified = true; + options.DefaultApiVersion = Microsoft.AspNetCore.Mvc.ApiVersion.Parse("8.0"); + options.ReportApiVersions = true; + }); + + // add the versioned api explorer, which also adds IApiVersionDescriptionProvider service + // note: the specified format code will format the version as "'v'major[.minor][-status]" + _log.LogDebug("BOOT: init ApiExplorer service"); + services.AddVersionedApiExplorer(o => o.GroupNameFormat = "'v'VVV"); + + services.AddTransient, ConfigureSwaggerOptions>(); + //https://docs.microsoft.com/en-us/aspnet/core/tutorials/web-api-help-pages-using-swagger?tabs=visual-studio-code //https://swagger.io/ //https://github.com/domaindrivendev/Swashbuckle.AspNetCore @@ -183,20 +747,61 @@ namespace AyaNova services.AddSwaggerGen( c => { - // resolve the IApiVersionDescriptionProvider service - // note: that we have to build a temporary service provider here because one has not been created yet - var provider = services.BuildServiceProvider().GetRequiredService(); - // 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) - { - c.SwaggerDoc(description.GroupName, CreateInfoForApiVersion(description)); - } + +// c.DocInclusionPredicate((docName, apiDesc) => +// { +// if (!apiDesc.TryGetMethodInfo(out MethodInfo methodInfo)) return false; + +// var versions = methodInfo.DeclaringType +// .GetCustomAttributes(true) +// .OfType() +// .SelectMany(attr => attr.Versions); + +// return versions.Any(v => $"v{v.ToString()}" == docName); +// }); + + +// c.DocInclusionPredicate((docName, apiDesc) => +// { +// if (!apiDesc.TryGetMethodInfo(out MethodInfo methodInfo)) return false; + +// // Get the MapToApiVersion attributes of the action +// var mapApiVersions = methodInfo +// .GetCustomAttributes(true) +// .OfType() +// .SelectMany(attr => attr.Versions); + +// //if it contains MapToApiVersion attributes, then we should check those as the ApiVersion ones are ignored +// if (mapApiVersions.Any() && mapApiVersions.Any(v => $"v{v.ToString()}" == docName)) +// return true; + +// // Get the ApiVersion attributes of the controller +// var versions = methodInfo.DeclaringType +// .GetCustomAttributes(true) +// .OfType() +// .SelectMany(attr => attr.Versions); + +// return versions.Any(v => $"v{v.ToString()}" == docName); +// }); + + + // resolve the IApiVersionDescriptionProvider service + + //v3 commented this out + // // note: that we have to build a temporary service provider here because one has not been created yet + // var provider = services.BuildServiceProvider().GetRequiredService(); + + // // 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) + // { + // c.SwaggerDoc(description.GroupName, CreateInfoForApiVersion(description)); + // } // add a custom operation filter which sets default values //Removed because will no longer compile the SwaggerDefaultValues but may be needed, not sure it's all a bit muddled - // c.OperationFilter(); + c.OperationFilter(); // integrate xml comments c.IncludeXmlComments(XmlCommentsFilePath); @@ -218,11 +823,6 @@ namespace AyaNova - //Obsolete way - // c.AddSecurityRequirement(new System.Collections.Generic.Dictionary> - // { - // { "Bearer", new string[] { } } - // }); //https://stackoverflow.com/questions/56234504/migrating-to-swashbuckle-aspnetcore-version-5 //First we define the security scheme @@ -431,6 +1031,9 @@ namespace AyaNova c.DocumentTitle = "AyaNova API explorer"; c.RoutePrefix = "api-docs"; }); + + + #endregion swagger @@ -547,25 +1150,53 @@ namespace AyaNova } } - static Microsoft.OpenApi.Models.OpenApiInfo CreateInfoForApiVersion(ApiVersionDescription description) - { - var info = new Microsoft.OpenApi.Models.OpenApiInfo() - { - Title = $"AyaNova API {description.ApiVersion}", - Version = description.ApiVersion.ToString() - }; + // static Microsoft.OpenApi.Models.OpenApiInfo CreateInfoForApiVersion(ApiVersionDescription description) + // { + // var info = new Microsoft.OpenApi.Models.OpenApiInfo() + // { + // Title = $"AyaNova API {description.ApiVersion}", + // Version = description.ApiVersion.ToString() + // }; - if (description.IsDeprecated) - { - info.Description += " This API version has been deprecated."; - } + // if (description.IsDeprecated) + // { + // info.Description += " This API version has been deprecated."; + // } + + // return info; + // } + + + + //update for v3 from here https://github.com/microsoft/aspnet-api-versioning/wiki/API-Documentation#aspnet-core + //and example here: https://github.com/microsoft/aspnet-api-versioning/blob/master/samples/aspnetcore/SwaggerSample/ConfigureSwaggerOptions.cs + // public class ConfigureSwaggerOptions : IConfigureOptions + // { + // readonly IApiVersionDescriptionProvider provider; + + // public ConfigureSwaggerOptions(IApiVersionDescriptionProvider provider) => + // this.provider = provider; + + // public void Configure(SwaggerGenOptions options) + // { + // foreach (var description in provider.ApiVersionDescriptions) + // { + // options.SwaggerDoc( + // description.GroupName, + // new OpenApiInfo() + // { + // Title = $"AyaNova API {description.ApiVersion}", + // Version = description.ApiVersion.ToString(), + // }); + // } + // } + // } - return info; - } #endregion - - } } + + + */ diff --git a/server/AyaNova/SwaggerDefaultValues.cs b/server/AyaNova/SwaggerDefaultValues.cs index eff78291..fc01dc14 100644 --- a/server/AyaNova/SwaggerDefaultValues.cs +++ b/server/AyaNova/SwaggerDefaultValues.cs @@ -1,8 +1,11 @@ // namespace AyaNova // { +// using Microsoft.OpenApi.Models; // using Swashbuckle.AspNetCore.SwaggerGen; // using System.Linq; +// using Microsoft.AspNetCore.Authorization; +// //https://github.com/domaindrivendev/Swashbuckle.AspNetCore/releases/tag/v5.0.0-rc3 // /// // /// Represents the Swagger/Swashbuckle operation filter used to document the implicit API version parameter. @@ -10,38 +13,124 @@ // /// This is only required due to bugs in the . // /// Once they are fixed and published, this class can be removed. // public class SwaggerDefaultValues : IOperationFilter -// { +// { // /// // /// Applies the filter to the specified operation using the given context. // /// // /// The operation to apply the filter to. // /// The current operation filter context. -// public void Apply( Operation operation, OperationFilterContext context ) +// public void Apply(OpenApiOperation operation, OperationFilterContext context) // { // // REF: https://github.com/domaindrivendev/Swashbuckle.AspNetCore/issues/412 // // REF: https://github.com/domaindrivendev/Swashbuckle.AspNetCore/pull/413 -// foreach ( var parameter in operation.Parameters.OfType() ) +// foreach (var parameter in operation.Parameters) // { -// var description = context.ApiDescription.ParameterDescriptions.First( p => p.Name == parameter.Name ); +// var description = context.ApiDescription.ParameterDescriptions.First(p => p.Name == parameter.Name); // var routeInfo = description.RouteInfo; -// if ( parameter.Description == null ) +// if (parameter.Description == null) // { // parameter.Description = description.ModelMetadata?.Description; // } -// if ( routeInfo == null ) +// if (routeInfo == null) // { // continue; // } -// if ( parameter.Default == null ) -// { -// parameter.Default = routeInfo.DefaultValue; -// } +// // if (parameter.Default == null) +// // { +// // parameter.Default = routeInfo.DefaultValue; +// // } + +// if (parameter.Schema.Default == null) { parameter.Schema.Default = new OpenApiString(description.DefaultValue.ToString()); } // parameter.Required |= !routeInfo.IsOptional; // } // } // } -// } \ No newline at end of file + +namespace AyaNova +{ + using Microsoft.AspNetCore.Mvc.ApiExplorer; + using Microsoft.OpenApi.Any; + using Microsoft.OpenApi.Models; + using Swashbuckle.AspNetCore.SwaggerGen; + using System.Linq; + + /// + /// Represents the Swagger/Swashbuckle operation filter used to document the implicit API version parameter. + /// + /// This is only required due to bugs in the . + /// Once they are fixed and published, this class can be removed. + public class SwaggerDefaultValues : IOperationFilter + { + /// + /// Applies the filter to the specified operation using the given context. + /// + /// The operation to apply the filter to. + /// The current operation filter context. + public void Apply(OpenApiOperation operation, OperationFilterContext context) + { + var apiDescription = context.ApiDescription; + + operation.Deprecated |= apiDescription.IsDeprecated(); + + 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) + { + parameter.Schema.Default = new OpenApiString(description.DefaultValue.ToString()); + } + + parameter.Required |= description.IsRequired; + } + } + } +} + + + +// public void ConfigureServices(IServiceCollection services) +// { + + +// #region Swagger + +// services +// .AddApiVersioning(options => +// { +// options.AssumeDefaultVersionWhenUnspecified = true; +// options.DefaultApiVersion = Microsoft.AspNetCore.Mvc.ApiVersion.Parse("8.0"); +// options.ReportApiVersions = true; +// }); + +// services.AddVersionedApiExplorer(o => o.GroupNameFormat = "'v'VVV"); + +// services.AddTransient, ConfigureSwaggerOptions>(); + + +// services.AddSwaggerGen( +// c => +// { +// c.OperationFilter(); +// }); + + +// #endregion + +