Skip to content
This repository has been archived by the owner on Feb 15, 2024. It is now read-only.
/ OpenApiExtensions Public archive

Open Api (swagger) extensions to make your WebApi generate an AutoRest rich and compliant swagger documents.

License

Notifications You must be signed in to change notification settings

Azure/OpenApiExtensions

Repository files navigation

OpenApiExtensions

OpenApiExtensions Nuget Package is providing out of the box, generating rich Swagger files out of your Aspnet Api code. The Generated Swagger file is enriched by decorating your code with Attributes and comments, and therefore is able to generated a fully autorest compliant Swagger file (and request/response Example files). the generation process is relying on Swashbuckle

Getting Started

Target Project Prerequisites

  • "Aspnet core 3.1 or higher" for your WebApi

How To use

add this code on your startup ConfigureServices() method see usage

            var config = new SwaggerConfig
            {
                PolymorphicSchemaModels = new List<Type> { typeof(WeatherForecast) },
                ModelEnumsAsString = true,
                ReusableParameters = new Dictionary<string, Microsoft.OpenApi.Models.OpenApiParameter>()
                {
                    { "SomeReusableParam", ReusableParameters.GetSomeReusableParameter() },                    
                    { "ApiVersionParameter", ReusableParameters.GetApiVersionParameter() }                    
                },               
                EnableSwaggerSecurityTokenSupport = true                
            };

            services.AddAutorestCompliantSwagger(config);

and on Configure() method

add this at the beginning of the pipeline:

  app.UseAutorestCompliantSwagger(_swaggerConfig);

having these minimal Prerequisites are enough to generate a swagger file.
when running your ArmApi on "localmode" you can now browse to
http://127.0.0.1:<<YOUR PORT>>/swagger/<<YOUR API VERSION>>/swagger.json
and get the file on your browser
or just http://127.0.0.1:<<YOUR PORT>>/swagger to get the swagger ui.

Enrichment

to be able to be fully compliant with Autorest Swagger standards, your code should be decorated with Comments and attributes.

Enrich by Comments:

In order to enrich your swagger file from comments, you need to set msbuild Xml documentation file.
upon build, the complier will read all your triple slash comments and save them into a XML file, which the swagger generation process will automatically read to enrich your Classes, Operations. see example

example:

    /// <summary>
    /// Some Descriptive Summary Description (Reflected on your XML Comment -> and Swashbuckle read the        XmlDocumentation file and enrich the schemas , see https://github.com/domaindrivendev/Swashbuckle.AspNetCore#include-descriptions-from-xml-comments)
    /// </summary>
    abstract public class WeatherForecast
    {

    }

Enrich by Attributes:

As the Swagger Generation is based on Swashbuckle, you may use any of the swashbuckle annotations.
in addition to that you may refer to full list of provided annotation (attributes) by this repo

you might also want to run the code tour of this repo by opening this folder with VSCode, and start the code tour.

ArmApi

    [ApiVersion("2022-01-01-preview")]
    [Route("MyArmApiController")]
    [ApiController]
    [SwaggerApiVersionRange(fromVersion: "2022-01-01-preview")] // this tell the swagger generating to include this controller in any "2022-01-01-preview" and above (not mandatory)
    [SwaggerTag("ReadSomeResource")] // this would set up the tag for your controller
    public class MyArmApiController : ControllerBase
    {
        ....
    }

Internal Api

to avoid Internal Apis to be reflected in External Swagger docs use [HideInDocs] attribute to decorate either your Controllers or Actions

    [ApiVersion("2022-01-01-preview")]
    [Route("InternalApi")]
    [HideInDocs]
    public class MyInternalApiController : ControllerBase
    {
        ....
    }

in this sample we provide the [SwaggerOperation] attribute that enrich the swagger model, and Request+Response + Example Providers.

        [SwaggerOperation(
           Summary = "Get MyArmResources records",
           Description = "Fetches My Arm Resources",
           OperationId = "Get",
           Tags = new[] { "MyArmResources" })]
        [SwaggerResponse(200, "MyArmResources records fetched", typeof(SomeResourceList))]
        [ResponseExample(200, typeof(GetAllMyArmResourcesResponseExample))]
        [RequestExample(typeof(GetAllMyArmResourcesAsyncRequestExample))]
        [Pageable]        
        [HttpGet]
        public async Task<SomeResourceList> GetListOfResources(
            Guid subscriptionId,
            string resourceGroupName,
            string workspaceName,            
            [SwaggerHideParameter][FromHeader(Name = ArmHeaderConstants.InternalWorkspaceIdHeaderName)] Guid workspaceId,
            [LogIgnore] CancellationToken cancellationToken)
        {
          ...
        }
        /// <param name="resourceId">Some Documentation to be shown on swagger</param>
        [SwaggerOperation(
           Summary = "Get MyArmResource record",
           Description = "Fetches My Arm Resource",
           OperationId = "Get",
           Tags = new[] { "MyArmResource" })]
        [SwaggerResponse(200, "MyArmResource fetched", typeof(MyArmResource))]
        [ResponseExample(200, typeof(GetMyArmResourceResponseExample))]
        [RequestExample(typeof(GetAllSomeResourceAsyncRequestExample))]          
        [HttpGet("{resourceId}")]
        public async Task<MyArmResource> GetSingleResource(
            Guid subscriptionId,
            string resourceGroupName,
            string workspaceName,            
            string resourceId,
            [SwaggerHideParameter][FromHeader(Name = ArmHeaderConstants.InternalWorkspaceIdHeaderName)] Guid workspaceId,
            [LogIgnore] CancellationToken cancellationToken)
        {
          ...
        }        

by specifying above your action these attributes : [ResponseExample] and [RequestExample], examples will be auto generated for you. see sample

See [samples](./samples/] projects for more info
for Virtual inheritance in your swagger file refer this sample
(virtual inheritance is when you have polymorphism on your APIs, and the classes in your swagger doesnt exist on your code)

Renaming your WebModels

sometimes you want to call your models differently on the swagger doc than your code.
you have a few options to deal with this.

  1. Renaming your class in code to match swagger, as WebModels class renaming is not a breaking change. you can do so if you can.

  2. If you are not able to control your WebModels, in such cases when returning a generic Object, MyObjectGenericsWrapper<T>, you still can :

    1. Not using MyObjectGenericsWrapper<T> and use your own class.
    2. inherit from MyObjectGenericsWrapper<T> and return this inheritance from your controllers actions.
  3. Use provided Swagger Attributes to control the naming:
    here are some examples:

    • Basic constant renaming:

       // in this example MyBoringClassResource would be named MySwaggerAwesomeName
        [SwaggerSchemaNameStrategy("MySwaggerAwesomeName")]
        public class MyBoringClassResource
        {
        }
    • Naming a generic wrapper:

        [SwaggerSchemaNameStrategy(NamingStrategy.ApplyToParentWrapper, "MySwaggerAwesomeNameModel")]
        public class MyResourceProperties
        {
        }
    
    
        public class MyObjectGenericsWrapper<T> // this class when used as MyObjectGenericsWrapper<MyResourceProperties> will be named in swagger as MySwaggerAwesomeNameModel
        {
        }
    // in this example a Generic Wrapper that holds ConcreteModel would be named according to the logic of SwaggerAwesomeClassNameProvider
    [SwaggerSchemaNameStrategy(NamingStrategy.ApplyToParentWrapper, typeof(SwaggerAwesomeClassNameProvider) )]
    public class ConcreteModel
    {               
    }
    
    public class SwaggerAwesomeClassNameProvider : ICustomSchemaNameProvider
    {
        public string GetCustomName(Type type)
        {
            if (typeof(MyObjectGenericsWrapper<ConcreteModel>) == type)
            {
                return "ConcreteClass";
            }
    
            if (typeof(MyObjectsCollectionGenericsWrapper<ConcreteModel>) == type)
            {
                return "ConcreteClassesList";
            }
    
            throw new InvalidOperationException("Add more here ..");
        }
    }      

Open Api Extension also provide out of the box Custom name providers such as ArmResourceWrapperNameProvider

Models hierarchy

You might need to create a model hierarch in swagger that doesn't reflect your code. to do so, you can use the CustomSwaggerSchemaInheritance to create an object Hierarchy that doesn't reflect your code.
see this example:

    /// <summary>
    /// The Some Resource Properties model.
    /// </summary>    
    [ClientFlatten]    
    [CustomSwaggerSchemaInheritance(externalSchemaName: "MySwaggerOnlyBaseCLass", notInheritedPropertiesName: new[] { nameof(Properties) }, CommonObjectType.ResourceProviderCommonDefinition )]
    public class SomeResourceModel
    {
        /// <summary>
        /// Gets or sets nested level of properties which contains the resource content
        /// </summary>
        [JsonProperty("properties", Required = Required.Always)]
        public MyResourceProperties Properties { get; set; }
    }

will result this Definition in swagger:

 "definitions": {
    "SomeResourceModel": {
      "description": "The Some Resource Properties model.",
      "required": [
        "properties"
      ],
      "type": "object",
      "allOf": [
        {
          "$ref": "../../../common/2.0/types.json#/definitions/MySwaggerOnlyBaseClass"
        }
      ],
      "properties": {
        "properties": {
          "description": "Gets or sets nested level of properties which contains the resource content",
          "type": "object",
          "allOf": [
            {
              "$ref": "#/definitions/MyResourceProperties"
            }
          ],
          "x-ms-client-flatten": true,          
        }
      }
    }
 }

Polymorphic in this context means, that your request or response is/contains a base (or abstract) class.
this can lead to some challenges. in order to overcome it, OpenApiExtension has some out of the box solutions.
for starters you need to register your polymorphic class in the SwaggerConfig:

  • in case of "Virtual Inheritance" - means the inherited classes are not in your code, please refer (to sample)ArmResourceProviderDemo.
  • in other case you need to provide the Discriminator see reference here

Api version is supported out of box by reading the [ApiVersion] attributes on your Controllers.
to support also the "Api Version Fallback" you may Provide the [SwaggerApiVersionRangeAttribute] as well, to specify ranges version of that your api supports.

intro

Since your code now contains everything that is needed to generate a well defined Swagger file, you can leverage an automation script that will:

  1. generate your swagger file
  2. "prettify" it so it would be valid for linters
  3. Generate Autorest csharp SDK so you can observe it.
  4. Validate that the swagger file is compliant

Implement

In order to support automate the process of generating and validating your swagger outcome, you need to set up 2 things:

  1. Setup the script configuration section.
  2. a WebHost function that hosts the app, and the cli tool can call it to run the swagger pipeline. see this example for more info.

About

Open Api (swagger) extensions to make your WebApi generate an AutoRest rich and compliant swagger documents.

Resources

License

Code of conduct

Security policy

Stars

Watchers

Forks

Releases

No releases published

Packages

No packages published

Languages