Resume Blogs Contact
  • Swagger documentation with OData
    01/18/2021
  • Blazor PWA with AAD Authentication
    06/01/2020
  • Azure Durable Functions
    02/22/2019
  • Going Serverless with Azure Functions
    11/05/2017
  • Automated UI Testing in MVC
    02/12/2015
  • SharePoint web.config Modification
    05/23/2012
  • SharePoint 2010 Client Object Model Part 1
    10/13/2011
  • SharePoint 2010 Client Object Model Part 2
    10/13/2011
  • In-place Editing with ASP.NET Repeater
    08/29/2011
  • Custom SharePoint List Forms
    07/11/2011
  • SharePoint 2010 Taxonomy Import
    05/09/2011
  • SharePoint Custom Provisioning Provider
    04/21/2011
  • USING SPMetal
    03/14/2011
  • SharePoint 2010 Site Actions Menu
    11/14/2010
  • Import List VS 2010 SharePoint Extension
    09/24/2010
  • Windows Azure Storage
    05/24/2010
  • Deploy and use SPCopy Part 2
    04/27/2010
  • Using SPExport and SPImport Part 1
    04/13/2010
  • Entity Framework Performance
    10/31/2009
  • WSS List and Database Synchronization
    11/30/2008
  • ASP.NET MVC Part2
    06/05/2008
  • ASP.NET MVC Part 1
    03/23/2008
  • LINQ to XML
    03/14/2008
  • Hierarchical Exception Handling with the Enterprise Library
    10/06/2007
  • Introduction to the Validation Application Block
    02/02/2007
  • CSS Variables
    11/17/2006
  • Custom XPath Functions
    08/28/2006
  • Using SqlDependency for data change events
    11/18/2005
  • Application Suite Template
    02/10/2004
  • ExpandingTable with Design-Time support
    12/13/2003
  • DataGridDemo
    06/15/2002
  • C# Album Viewer
    04/12/2002
  • Screen Scraping with C# for ASP.NET
    03/01/2002
  • XML TIming
    11/03/2001
  • Using XML, XSL, DHTML and Javascript in your Windows applications
    06/24/2001

Swagger documentation with OData

1/18/2021

OData and Swagger are two well-known technologies that separately can provide enhanced capabilities and documentation to your project and are easily implemented with a few lines of code. Getting them to work together, however, takes a little more setup and configuration. I will not be covering the depths of Swagger or OData here; there are a plethora existing articles covering these. Rather, this article will look at how to get them to work together.

What is Swagger

Swagger, also known as Open API, is a standard for documenting REST APIs. It doesn't implement documentation but rather defines how the documentation should appear. Implementation is via tools, such as Swashbuckle, which is integrated into ASP.NET projects. When creating a new project it is easy to implement by clicking the checkbox.

This code will be added to the Startup class in the ConfigureServices and Configure methods respectively.

    services.AddSwaggerGen(c =>
    {
        c.SwaggerDoc("v1", new OpenApiInfo { Title = "SwaggerOdata", Version = "v1" });
    });
   
    if (env.IsDevelopment())
    {
        app.UseDeveloperExceptionPage();
        app.UseSwagger();
        app.UseSwaggerUI(c => c.SwaggerEndpoint("/swagger/v1/swagger.json", "SwaggerOdata v1"));
    }   
 

What is OData?

OData, or Open Data Protocol, is a set of best practices for building and consuming REST APIs. Again, I won't be going into depth on OData, but briefly, lets say we have a model such as this

    public class TodoItem
    {
        public int ID { get; set; }
        public string Name{ get; set; }
        public bool IsComplete { get; set; } 
        public DateTime Created { get; set; }
        public DateTime? Completed { get; set; }
    }    

and a method on the Controller as such

    [HttpGet]
    public IEnumerable Get()   

which you would expect to return a collection of TodoItems and all the properties for each. What if you were only interested in Name and Created? The first inclination may be to create another model with only those two properties and return it instead of TodoItem. This would be fine until someone wanted only Name or wanted Completed added. Creating multiple models would quickly become unmaintainable.

OData allows you to shape the data by using a querystring parameters, such as,

  • http://localhost/api/todo?$select=Name
  • http://localhost/api/todo?$select=Name,Created

In the case of the first query only the Name parameter would be returned in the resultset. Likewise with second, only Name and Created would be returned. This allows for much better support for callers based on their needs and easier to implement, (Actually, there is nothing to implement), then multiple models and endpoints.

Playing well together

For this demo I'll start with a default ASP.NET Core Web API project targeted to .NET 5.0.

To get Swagger, or technically Swashbuckle, to recognize OData and play nice together and generate the appropriate documentation we'll need to add additional Nuget packages and create a couple of helper methods.

To start with, add these Nuget packages to the project.

As the name implies on the last packages, these add support for versioning. Though technically not required for this scenario, versioning your API is a good practice.

Update Startup.cs and replace ConfigureServices with this code

    public void ConfigureServices(IServiceCollection services)
    {
        services.AddControllers();
        services.AddApiVersioning(options =>
        {
            options.ReportApiVersions = true;
            options.AssumeDefaultVersionWhenUnspecified = true;
            options.DefaultApiVersion = new ApiVersion(1, 0);
            // Allow version to be specified in header
            options.ApiVersionReader = ApiVersionReader.Combine(
                new QueryStringApiVersionReader(),
                new HeaderApiVersionReader("api-version", "x-ms-version"));
        });
        services.AddOData().EnableApiVersioning();
        services.AddODataApiExplorer(
            options =>
            {
                // add the versioned api explorer, which also adds IApiVersionDescriptionProvider service
                // note: the specified format code will format the version as "'v'major[.minor][-status]"
                options.GroupNameFormat = "'v'VVV";

                // note: this option is only necessary when versioning by url segment. the SubstitutionFormat
                // can also be used to control the format of the API version in route templates
                options.SubstituteApiVersionInUrl = true;
            });
        services.AddTransient<IConfigureOptions, ConfigureSwaggerOptions>();
        services.AddSwaggerGen(
            options =>
            {
                // add a custom operation filter which sets default values
                options.OperationFilter<SwaggerDefaultValues>();

                // integrate xml comments
                options.IncludeXmlComments(XmlCommentsFilePath());
            });

        string XmlCommentsFilePath()
        {
                var basePath = PlatformServices.Default.Application.ApplicationBasePath;
                var fileName = typeof(Startup).GetTypeInfo().Assembly.GetName().Name + ".xml";
                return Path.Combine(basePath, fileName);
        }
    }   

and replace Configure with this code

    public void Configure(IApplicationBuilder app, 
    IWebHostEnvironment env, 
    VersionedODataModelBuilder modelBuilder, 
    IApiVersionDescriptionProvider provider)
    {
        if (env.IsDevelopment())
        {
            app.UseDeveloperExceptionPage();
        }

        app.UseHttpsRedirection();
        app.UseRouting();
        app.UseAuthorization();

        app.UseEndpoints(endpoints =>
        {
            // Indicate what OData methods to support
            endpoints.Select().Filter().Count().OrderBy();
            endpoints.MapVersionedODataRoute("odata", "api", modelBuilder);
        });

        app.UseSwagger();
        app.UseSwaggerUI(
            options =>
            {
                // build a swagger endpoint for each discovered API version
                foreach (var description in provider.ApiVersionDescriptions)
                {
                    options.SwaggerEndpoint($"/swagger/{description.GroupName}/swagger.json", description.GroupName.ToUpperInvariant());
                }
            });
    }

Again, not going to delve into the specifics of these lines since this article is focused on just getting OData and Swagger working together, not about the nuances of API versioning and Swagger generation.

Now we'll add the classes SwaggerDefaultValues and ConfigureSwaggerOptions

    /// 
    /// 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;
            }

            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;
            }
        }
    }    

    /// 
    /// Configures the Swagger generation options.
    /// 
    /// This allows API versioning to define a Swagger document per API version after the
    ///  service has been resolved from the service container.
    public class ConfigureSwaggerOptions : IConfigureOptions
    {
        private readonly IApiVersionDescriptionProvider _provider;
        private static IConfiguration _configuration;

        /// 
        /// Initializes a new instance of the  class.
        /// 
        /// The provider used to generate Swagger documents.
        public ConfigureSwaggerOptions(IApiVersionDescriptionProvider provider, IConfiguration configuration)
        {
            _provider = provider ?? throw new ArgumentNullException(nameof(provider));
            _configuration = configuration ?? throw new ArgumentNullException(nameof(configuration));
        }

        /// 
        public void Configure(SwaggerGenOptions options)
        {
            // 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)
            {
                options.SwaggerDoc(description.GroupName, CreateInfoForApiVersion(description));
            }
        }

        static OpenApiInfo CreateInfoForApiVersion(ApiVersionDescription description)
        {
            var info = new OpenApiInfo();

            // Define values in appConfig for extensibility
            _configuration.GetSection(nameof(OpenApiInfo)).Bind(info);

            info.License = new OpenApiLicense() { Name = "MIT", Url = new Uri("https://opensource.org/licenses/MIT") };

            if (description.IsDeprecated)
            {
                info.Description += " This API version has been deprecated.";
            }

            return info;
        }
    }   

We'll also need to generate documentation as the Swagger is built from it.

Now, we'll switch to the WeatherForecastController and add the necessary attributes to it and any methods necessary.

    [ApiVersion("1.0")]
    [ODataRoutePrefix("WeatherForecast")]
    public class WeatherForecastController : ODataController

Two critical pieces here are that ODataRoutePrefix must match the name of the controller, WeatherForecast, in this case. Also, it must derive from ODataController.

Any method that supports OData will need these attributes applied.

    [ODataRoute]
    [EnableQuery]   

The first is, of course, setting the route for OData and the latter is what enables a caller to use OData queries. This attribute has a number of options that allow you to control at the method level what queries may be supported, overrriding the global settings added earlier.

With all of this in place you should now be able to see the Swagger page with the availble methods and OData support.

Conclusion

This article was not written with the intention to go into the depths of OData or Swagger. However, I have hopefully given an example of how to enable them in an ASP.NET Core API project and correct any gotchas that may come up.

References

An error has occurred. This application may no longer respond until reloaded. Reload 🗙