top of page

Setting up a Swagger-enabled API with versioning in .NET 6.0+: A beginner's guide (Part 1)

  • Writer: Olivier Verplancke
    Olivier Verplancke
  • Jan 9, 2023
  • 7 min read

Updated: Jan 4, 2024

This series of blog posts is how to set up an API (.NET) from scratch with all the boilerplate functionalities typically required in an enterprise environment. From setting up your api, to securing it, to adding a datasource

Part 1 (this post) is how to set up an API from zero using versioning and swagger.

Part 2 is how to authorize as a user to the secured API.

Part 3 is how to authorize from 1 API client to another, B2B.

Part 4 is how to implement scope based access.

Part 5 is how to set up 'advanced' configuration: multi environment, secrets ...

Part 6 is how to set up code-first migration using Entity Framework with SQL Server


ree

source:rawpixel.com

Introduction

Technology we'll be using:

  • .NET 6.0 (.NET 8 recently released - recommendation is to set up your APIs in 8)

Prerequisites

  • Visual Studio

  • Basic knowledge of programming in .NET

Before we configure our security, we obviously require an API. We'll set up a project using the 'new' minimalist api project template and add api versioning + swagger.


All of the info below can be found on my github repo.


#1 - Project setup

Create a new project - DemoAuth

Using Visual Studio - use create a new project and select ASP.NET Core Empty

ree

Give it a name - in our case DemoAuth

ree

Select your framework of choice - at moment of writing .NET 6.0 is on LTS. Leave the rest on default.

ree

This results in having a new minimalist api project as follows

ree

Optional: I update my launchSettings.json right off the bat by removing the generated IIS Express Settings in it as I don't use them. This leaves me with the following content in the file (after cleaning).

ree

Press F5 to verify if everything is working:

ree

#2 - Api endpoints setup

We're going to set up 2 endpoints. 1 GET endpoint to retrieve data, this simulates a read-only scenario. Like a movie application. All users are allowed to retrieve data anonymously.

1 DELETE endpoint to remove data from the system. Everyone can read the movie database, but only authorized users can change it.


Using the minimalist API approach, all our endpoints can be written within the same Program.cs file - we'll change this down below to keep our files clean. This is optional. Personally I like to group parts that belong together into a separate classes/files whenever possible. This keeps the overall codebase clear and understandable.

"Simplicity is the ultimate sophistication" - Leonardo Da Vinci

//simulate service call
app.MapGet("/movies", () => return Results.Ok(new[]{ "Black Panther", "John Wick" }));

//simulate delete
app.MapDelete("/movies/{id}", (int id) => return Results.Ok($"Movie with id '{id}' has been removed.");

For demo purposes, we'll add a service class which puts our "business logic" into a separate class. This also illustrates a basic DI setup.


Tip: If you're debugging and wish your api to always go to a specific url by default, update your launchSettings.json by adding the property "launchUrl" and give it a path. This will make your application to always go to that url whenever you start debugging.

Typically this is used when you set up Swagger, so your api starts up by default in the Swagger url.

ree

We'll add a model class Movie.cs

public class Movie 
{
    public int Id { get; set; }
    public string Title { get; set; }
    
    public Movie(int id, string title) 
    {
        Id = id;
        Title = title;
    }
    
    public static Movie Create(int id, string title) => new(id, title);
}

Add a new interface & service class which implements the interface - IMovieService & MovieService

MovieService.cs

public interface IMovieService
{
    IEnumerable<Movie> GetMovies();
}

public class MovieService : IMovieService
{
    public IEnumerable<Movie> GetMovies()
    {
        yield return Movie.Create(1, "Black Panther");
        yield return Movie.Create(2, "John Wick");
    }
}

We'll wire the service using the .NET dependency injection extension methods in the Program.cs

AddScoped() - This method creates a Scoped service. A new instance of a Scoped service is created once per request within the scope. For example, in a web application it creates 1 instance per http request but uses the same instance in the other calls within that same web request.

builder.Services.AddScoped<IMovieService, MovieService>();
...
app.MapGet("/movies", (IMovieService service) => 
{
    return Results.Ok(service.GetMovies());
}).AllowAnonymous();

app.MapDelete("/movies/{id}", (int id) =>
{
    return Results.Ok($"Movie with id '{id}' has been removed.");
}).RequireAuthorization();

Press <F5> to run your api - we now should get a list of 2 movies back.

ree

When testing the HTTP DELETE /movies/1 we'll get an exception back

System.InvalidOperationException: Endpoint HTTP: DELETE /movies/{id} contains authorization metadata, but a middleware was not found that supports authorization.


This is normal as indeed didn't configure any middleware yet to manage authorization.


#3 - Versioning and Swagger setup

We'll configure API versioning. It is good practice to think of a way to version your APIs. Even though you might not need it in the first few months. The setup for this requires little to no effort and if you do require it, you'll already have it configured and your client applications are already passing along a version parameter. Less refactoring required down the line.


Secondly we'll configure Swashbuckle / Swagger. Swashbuckle is a list of NuGet packages we can add into our project to easily test our Api code with. It generates a user interface for developers to quickly test if everything works as expected. Get started with Swashbuckle (Microsoft doc).


Before we start, we have to install 3 (+2 optional *) NuGet packages

  • Swashbuckle.AspNetCore.Swagger: Middleware to expose SwaggerDocuments as a JSON Endpoint

  • Swashbuckle.AspNetCore.SwaggerGen: A generator which creates a SwaggerDocument object based off the documentation on routes, controllers, models.

  • Swashbuckle.AspNetCore.SwaggerUI: Middleware to expose an embedded version of the swagger-ui from an ASP.NET Core application

  • Swashbuckle.AspNetCore.Newtonsoft (*): Swagger Generator opt-in component to support Newtonsoft.Json serializer behaviors. This is a personal choice for custom formatting of data.

  • Asp.Versioning.Http (*): Middleware to support versioning of your API endpoints. I marked this as optional as technically you don't need versioning. However, if you're working in an enterprise environment or you are creating APIs as a product, you can not escape versioning.

Let's begin!


3.1 - Api versioning

First move the configuration of our API endpoints into a separate Extensions class. In my demo I'll call it EndpointExtensions, file: Program.EndpointExtensions.cs

Add 2 static methods: MapMoviesEndpoints(), GetVersionSet()

private static ApiVersionSet GetVersionSet(this WebApplication app) { }
public static void MapMoviesEndpoints(this WebApplication app){ }

Define which versions of your API exist. For showcasing multiple versions, we'll define 2

private static ApiVersionSet? _versionSet;
private static ApiVersionSet GetVersionSet(this WebApplication app) 
{
    if(_versionSet != null) return _versionSet;
    _versionSet = app.NewApiVersionSet()
        .HasApiVersion(new Asp.Versioning.ApiVersion(1.0))
        .HasApiVersion(new Asp.Versioning.ApiVersion(2.0))
        .ReportApiVersions()
        .Build();
}

We have defined our available versions, we now need to configure our API endpoints.

Go back to the Program.EndpointExtensions.cs file and into the MapMoviesEndpoints() method

public static void MapMoviesEndpoints(this WebApplication app)
{
    app.MapGet("/movies", (IMovieService service) => { return Results.Ok(service.GetMovies()); })
        .AllowAnonymous()
        .WithApiVersionSet(GetVersionSet(app))
        .MapToApiVersion(new ApiVersion(1.0));
      
    app.MapDelete("/movies/{id}", (int id) => { return Results.Ok($"Movie with id '{id}' has been removed."); })
        .RequireAuthorization()
        .WithApiVersionSet(GetVersionSet(app))
        .MapToApiVersion(new ApiVersion(1.0));
}

Next we need to tell, using middleware, we are versioning our api. Without this, the above won't be recognized.

Go back to the Program.cs class and add the following:

builder.Services.AddVersioning(options => 
{
    options.DefaultApiVersion = new ApiVersion(1.0);
    options.AssumeDefaultVersionWhenUnspecified = true;
    options.ReportApiVersions = true;
    options.ApiVersionReader = ApiVersionReader.Combine(
        new HeaderApiVersionReader("api-version"),
        new QueryStringApiVersionReader("api-version")
    );
});
...
app.MapMoviesEndpoints();

Now we can either pass the "api-version" parameter in the header or via a query string.

ReportApiVersions is used to return the supported api-versions in the response headers.

ree

Press <F5> to test out the solution

ree

3.2 - Swagger

Create a new Extensions class, like how it was done for the versioning. In my demo I call it Program.SwaggerExtensions.cs. Add 2 static methods in here: AddSwaggerGen() & UseSwagger().


AddSwaggerGen() is used to generate the swagger documentation.
UseSwagger() is used to register the Swagger & Swagger UI middleware with optional setup actions for DI-injected options.
public static void AddSwaggerGen(this IServiceCollection services)
{
	services.AddSwaggerGen(c =>
	{
		c.CustomSchemaIds(type => type.ToString());

		c.SwaggerDoc("v1", new OpenApiInfo
		{
			Title = "Swagger for DemoAuth",
			Version = "v1",
			Contact = new OpenApiContact { Email = "olivier@itigai.com" }
		});

		c.OperationFilter<ApiVersionOperationFilter>();

		var xmlDocumentation = Path.Combine(AppContext.BaseDirectory, "DemoAuth.xml");
		c.IncludeXmlComments(xmlDocumentation);
	});
}
public static void UseSwagger(this WebApplication app)
{
    app.useSwagger();
    app.UseSwaggerUI(c => 
    {
        c.SwaggerEndpoint("/swagger/v1/swagger.json", "Demo v1");
        c.RoutePrefix = string.Empty;
    });
}

c.RoutePrefix is used to determine at which route the swagger UI is to be available at. In our case it's empty meaning when browsing to your API's base url, it will redirect you to the swagger UI. Don't forget to update the launchSettings.json to make the launchUrl empty. This will load your Swagger UI everytime you start debugging your API.


We'll have 4 parts within the AddSwaggerGen function which require context:

c.CustomSchemaIds(type => type.ToString());

This is to solve the Swagger "Can't use schemaId for type ... the same schemaId is already used for type" issue.

Example: we have 2 api calls with the parameter "request", swagger will generate a schemaId called "$request" for both calls. However, both request parameters are different classes. This results in an issue when generating the openapi schema.

To solve this issue, we generate the schemaId with the full namespace of the type.

c.SwaggerDoc(...);

Is used to add metadata on the generated Swagger screen

ree

c.OperationFilter<ApiVersionOperationFilter>();

Add a custom parameter in our Swagger UI, api-version. We'll cover the class down below. This is custom written. If you followed all the steps until now, this will fail compilation as we didn't create the class yet. Because we're versioning our API, Swashbuckle doesn't automatically recognize the api-version attributes we defined using our versioning package.

ree

var xmlDocumentation = Path.Combine(AppContext.BaseDirectory, "DemoAuth.xml");
c.IncludeXmlComments(xmlDocumentation);

This is to include our /// xml tag documentation as part of the swagger output.


Add support for api-version - for demo purposes we'll implement it so the api-version is passed in the header. Remember, we added support for versioning via both query-string & header.


Create a new class ApiVersionOperationFilter which implements the IOperationFilter interface provided by SwashBuckle.

public class ApiVersionOperationFilter : IOperationFilter
{
	public void Apply(OpenApiOperation operation, OperationFilterContext context)
	{
		var actionMetaData = context.ApiDescription.ActionDescriptor.EndpointMetadata;
		operation.Parameters ??= new List<OpenApiParameter>();

		var apiVersionMetadata = actionMetaData.Any(item => item is ApiVersionMetadata);
		if (apiVersionMetadata)
		{
			operation.Parameters.Add(new OpenApiParameter
			{
				Name = "api-version",
				In = ParameterLocation.Header,
				Description = "Api version header value",
				Schema = new OpenApiSchema
				{
					Type = "String",
					Default = new OpenApiString("1.0")
				}
			});
		}
	}
}

Press F5 to verify if everything is (still) working.

After having done all of the steps until now, you should have a solution looking as follows:

ree
ree

Thank you for reading. Stay tuned for part 2 - Authentication. This will continue to build on the solution we created here.

 
 
 

Comments


  • LinkedIn - Black Circle
  • Facebook - Black Circle

© 2021-2025 by Olivier Verplancke

Follow me on social networks

bottom of page