About Swagger optimization

background

( Although. net6 has been released for a long time, but the company's projects are still based on it for a variety of reasons. net Framework. With the iteration of versions, the api interface of the back end keeps increasing, and each time you debug, the front end develops bitterly: "Little fat, your swagger pages are getting more and more stuck, so optimize and optimize!".
( Looking at swagger page loading first takes time:

( The above are:

  • v1 loaded twice
  • After recompiling the program, the swagger page opens and v1 (api json) takes more than two minutes to load.
  • Re-refreshing the page after the first full page load and re-viewing swagger took a lot of time. This time, obviously, the page load speed has increased a lot, but it is still uninteresting. Rendering takes too long after json returns.

Exploration & Resolution

( The swagger load card slow problem, inspired the idea of optimizing swagger, just started to search the network by traditional skills and still did not find a solution. Fortunately swashbuckle is open source and can analyze it yourself. Download the source first GitHub - domaindrivendev/Swashbuckle.WebApi: Seamlessly adds a swagger to WebApi projects!

1. First look at v1 loading slowly, but it has to load twice.

( It is not difficult to see from the above figure that the second v1 load follows lang.js, which is actually used for sinicization. Open this file in the project

( Originally to add a controller comment, revisit the back end to fetch the interface document once. After viewing the source js, a simpler way to translate the page into Chinese is to use the window directly after the data has been rendered on the page. SwaggerApi. SwaggerObject. Controller Desc.

 setControllerSummary: function () {
        var summaryDict = window.swaggerApi.swaggerObject.ControllerDesc;
        var id, controllerName, strSummary;
        $("#resources_container .resource").each(function (i, item) {
            id = $(item).attr("id");
            if (id) {
                controllerName = id.substring(9);
                try {
                    strSummary = summaryDict[controllerName];
                    if (strSummary) {
                        $(item).children(".heading").children(".options").first().prepend('<li class="controller-summary" style="color:green;" title="' + strSummary + '">' + strSummary + '</li>');
                    }
                } catch (e) {
                    console.log(e);
                }
            }
        });
    },

( After modifying the file, and then look at the loading of the page, it will not be repeated to access v1.

2. Processing v1 next is slow to load

( First look at the swagger configuration for the project:

            GlobalConfiguration.Configuration
                .EnableSwagger(c =>
                {
                    c.IncludeXmlComments(GetXmlCommentsPath(thisAssembly.GetName().Name));
                    c.IncludeXmlComments(GetXmlCommentsPath("xxxx.Api.Dto"));
                    c.SingleApiVersion("v1", "xxxx.Api");
                    c.CustomProvider((defaultProvider) => new CachingSwaggerProvider(defaultProvider));
                })

( Not many configurations, one of which is CachingSwaggerProvider, which implements the GetSwagger method to customize the return data. In this method, you can know that the api document is actually cached, and the data loaded by v1 is this SwaggerDocument. This also means that the reason v1 loads slowly is here.

public SwaggerDocument GetSwagger(string rootUrl, string apiVersion)
        {
            var cacheKey = string.Format("{0}_{1}", rootUrl, apiVersion);
            SwaggerDocument srcDoc = null;
            //Read only once
            if (!_cache.TryGetValue(cacheKey, out srcDoc))
            {
                srcDoc = (_swaggerProvider as Swashbuckle.Swagger.SwaggerGenerator).GetSwagger(rootUrl, apiVersion);
                srcDoc.vendorExtensions = new Dictionary<string, object> { { "ControllerDesc", GetControllerDesc() } };
                _cache.TryAdd(cacheKey, srcDoc);
            }
            return srcDoc;
        }

( The GetSwagger method provided by swashbuckle takes up a significant amount of time while debugging the program. Swashbuckle will be the source code. Core quotes it, there will be a small problem reopening swagger, resource file reported 404 error, this is because the embedded resource file was not found

  <ItemGroup>
    <EmbeddedResource Include="..\swagger-ui\dist\**\*.*">
      <LogicalName>%(RecursiveDir)%(FileName)%(Extension)</LogicalName>
      <InProject>false</InProject>
    </EmbeddedResource>
  </ItemGroup>

( From the path view, there is nothing under swagger-ui. Place files found elsewhere or organized from decompiled files in this directory, and use swagger-ui as a dependency. After the project is recompiled, it is normal for the swagger page to load resource files. (If you still cannot find the resource file, add the dependency again to compile the project)

( Now you can start debugging, and after a few twists and turns, you've finally located the culprit in the GetSwagger method in SwaggerGenerator to get paths, which is actually taking too long to use the CreatePathItem

   var paths = GetApiDescriptionsFor(apiVersion)
                .Where(apiDesc => !(_options.IgnoreObsoleteActions && apiDesc.IsObsolete()))
                .OrderBy(_options.GroupingKeySelector, _options.GroupingKeyComparer)
                .GroupBy(apiDesc => apiDesc.RelativePathSansQueryString())
                .ToDictionary(group => "/" + group.Key, group => CreatePathItem(group, schemaRegistry));

( The first attempt at multithreaded processing did reduce the time it took to obtain json data, but there were two problems:

  • Threads are insecure and page failures occur from time to time
  • Even if the json data can be returned quickly, the problem of slow page rendering remains unresolved. Just as GetSwagger was cached in our previous project, there was still a card slow problem when refreshing swagger.

3. Will need to return json data

( Optimizing swagger loading takes into account the slow loading of pages caused by front-end rendering and back-end combing of json data. Is there any good way? The swashbuckle core version supports grouping, but the Framework version used by the project does not support it. Since it does not support it, directly modify the source code, group by controllers, and do whatever you say:

( Locate the EnableSwagger method of the HttpConfigurationExtensions class, which is used to configure routing

public static SwaggerEnabledConfiguration EnableSwagger(
            this HttpConfiguration httpConfig,
            string routeTemplate,
            Action<SwaggerDocsConfig> configure = null)
        {
            var config = new SwaggerDocsConfig();
            if (configure != null) configure(config);
    
    		httpConfig.Routes.MapHttpRoute(
                name: "swagger_docs" + routeTemplate,
                routeTemplate: routeTemplate,
                defaults: null,
                constraints: new { apiVersion = @".+" },
                handler: new SwaggerDocsHandler(config)
            );
			
    		//Configure Controller Routing
    		string controllRouteTemplate=DefaultRouteTemplate+"/{controller}";
            httpConfig.Routes.MapHttpRoute(
                name: "swagger_docs" + controllRouteTemplate,
                routeTemplate: controllRouteTemplate,
                defaults: null,
                constraints: new { apiVersion = @".+" },
                handler: new SwaggerDocsHandler(config)
            );

            return new SwaggerEnabledConfiguration(
                httpConfig,
                config.GetRootUrl,
                config.GetApiVersions().Select(version => routeTemplate.Replace("{apiVersion}", version)));
        }

( Next, find the SwaggerDocsHandler class, modify the SendAsync method, get the controller, and pass the controller to GetSwagger

protected override Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
        {
            var swaggerProvider = _config.GetSwaggerProvider(request);
            var rootUrl = _config.GetRootUrl(request);
            var apiVersion = request.GetRouteData().Values["apiVersion"].ToString();
            var controller = request.GetRouteData().Values["controller"]?.ToString();
			if (string.IsNullOrEmpty(controller))
            {
                controller = "Account";
            }
    
            try
            {
                var swaggerDoc = swaggerProvider.GetSwagger(rootUrl, apiVersion, controller);
                var content = ContentFor(request, swaggerDoc);
                return TaskFor(new HttpResponseMessage { Content = content });
            }
            catch (UnknownApiVersion ex)
            {
                return TaskFor(request.CreateErrorResponse(HttpStatusCode.NotFound, ex));
            }
        }

( Correspondingly modify the ISwagger interface, as well as the interface's implementation class SwaggerGenerator, adding filter by Controller

    public interface ISwaggerProvider
    {
        SwaggerDocument GetSwagger(string rootUrl, string apiVersion,string controller);
    }

( GetSwagger modification of SwaggerGenerator:

 var temps = GetApiDescriptionsFor(apiVersion)
                .Where(apiDesc => !(_options.IgnoreObsoleteActions && apiDesc.IsObsolete()));
            if (string.IsNullOrEmpty(controller) == false)
            {
                temps = temps.Where(apiDesc => apiDesc.ActionDescriptor.ControllerDescriptor.ControllerName.ToLower() == controller.ToLower());
            }

            var paths = temps
                .OrderBy(_options.GroupingKeySelector, _options.GroupingKeyComparer)
                .GroupBy(apiDesc => apiDesc.RelativePathSansQueryString())
                .ToDictionary(group => "/" + group.Key, group => CreatePathItem(group, schemaRegistry));

( The ISwagger implementation in your own project will also be modified, then you can start recompiling your own project and reopen the swagger page, which is also very fast to open for the first time after backend compilation. By default, the interface under the Account controller is opened, and if you switch to an interface under another controller, you only need to add the corresponding / Controller after the url

4. Modify Swagger Page

( Above we have solved the problem of slow page loading, but is it too difficult to switch controllers, can you improve the front-end developer experience and provide a drop-down list of options better? Keep working!

( Find SwaggerUi\CustomAssets\Index in the source directory. HTML file, add an id to select_baseUrl's select drop-down selection box and input_baseurl Input Box Hidden

Modify window under swagger-ui-js. SwaggerUi's render method (remember to change the reference to swagger-ui-min-js in index.html to swagger-ui-js) adds JS code to populate the drop-down data and add drop-down boxes to trigger events

( Find SwaggerUi.Views.HeaderView, add drop-down events

( After recompiling, refresh the page to try the effect

epilogue

( Regarding swagger optimization, in view of my limited level, there are still many deficiencies and errors, I would like to ask you guys to correct, thank you!

Posted by lettie_dude on Sun, 17 Apr 2022 01:32:09 +0930