One of the great features of netcore is that it comes with the Swagger library installed by default, and not only that, but it also integrates perfectly with the language, because it is capable of generating our swaggerfile automatically, without us having to do anything.
Table of Contents
However, it has one issue, and that is it does not allow, or at least not in a simple way, the inclusion of headers in the request itself.
1 - Problem Introduction
While it’s true that in this example I’ll break down the problem to its basics, the premise is simple: being able to add headers to our calls via swagger;
This will allow us to manually test in an easier, simpler way using Swagger itself, since by default, it’s impossible to add such information.
For example, in the previous post of this series, we saw how to modify our API response depending on the accept-language attribute using the JsonConverter. What we’re going to do in this post is show how to modify swagger so that it asks for that attribute.
Additionally, we’ll be able to make it optional or required.
2 - Create an Attribute for Swagger
The way we’re going to create the attribute is through filters, and this filter can be enabled either on a single endpoint or the entire controller.
Also, the attribute itself won’t be generic, it’s not something we can do in a general and magical way, so we’ll need to create an attribute for each required header, although we can still abstract quite a bit of code.
All this code is available on GitHub in the WebPersonal repository (you have the link at the beginning of the post).
The first thing we’re going to do is create a folder named Filters
, where we’ll create our filters.
The one we’re going to create now is to request a header, so we’ll create it as such:
namespace WebPersonal.BackEnd.API.Filters
{
public class AcceptedLanguageHeader : Attribute
{
}
}
For it to appear in swagger, the first requirement is that it implements the IOperationFilter
interface, which is inside the Swashbuckle.AspNetCore.SwaggerGen
library.
And it will require us to implement this interface:
public class AcceptedLanguageHeader : Attribute, IOperationFilter
{
public void Apply(OpenApiOperation operation, OperationFilterContext context)
{
throw new NotImplementedException();
}
}
For now, what we’re interested in is the operation parameter, since that’s what will visually modify the interface, and inside operation we access Parameters and add a new one directly:
public class AcceptedLanguageHeader : Attribute, IOperationFilter
{
public void Apply(OpenApiOperation operation, OperationFilterContext context)
{
operation.Parameters.Add(new OpenApiParameter()
{
Name = "accept-language",
In = ParameterLocation.Header,
Required = false,
Schema = new OpenApiSchema() { Type = "string" }
});
}
}
Note: as you can see, there is a property called In
, this property allows us to indicate where the parameter goes, and we can choose header
, query
, path
, or cookies
.
With this, we can configure swagger to show the attribute.
If we go to our startup
file (or program.cs
, depending on your version) you can go to the services and add the following code, which will generate the attribute in the UI:
services.AddSwaggerGen(c =>
{
c.OperationFilter<AcceptedLanguageHeader>();
});
But this doesn’t help us, since we’re making that header appear in all endpoints, when in fact we only need it in a single one.
3 - Add a Swagger Attribute to a Controller
This is where the reason comes in for creating our AddAcceptedLanguageHeader
class inheriting from Attribute
. It’s because we’re going to use it as such.
For those not very familiar with attributes, we can filter them to be used on different types of elements. In our case, we’re going to indicate Class, since a controller is a class, and methods, so it can be used on individual endpoints.
[AttributeUsage(AttributeTargets.Class | AttributeTargets.Method)]
public class AcceptedLanguageHeader : Attribute, IOperationFilter
{
...
}
Additionally, from here, the context inside apply comes into play, because that’s where we’ll have access to the information of the endpoint being called.
What we’ll do is create a static class that simply receives OperationFilterContext
and returns a boolean
, which will be true if the endpoint we’re calling has the specified attribute, and whether it’s mandatory, which for now is false.
public class CustomAttribute
{
public readonly bool ContainsAttribute;
public readonly bool Mandatory;
public CustomAttribute(bool containsAttribute, bool mandatory)
{
ContainsAttribute = containsAttribute;
Mandatory = mandatory;
}
}
public static class OperationFilterContextExtensions
{
public static CustomAttribute RequireAttribute<T>(this OperationFilterContext context)
{
IEnumerable<IFilterMetadata> globalAttributes = context
.ApiDescription
.ActionDescriptor
.FilterDescriptors
.Select(p => p.Filter);
object[] controllerAttributes = context
.MethodInfo?
.DeclaringType?
.GetCustomAttributes(true) ?? Array.Empty<object>();
object[] methodAttributes = context
.MethodInfo?
.GetCustomAttributes(true)?? Array.Empty<object>();
List<T> containsHeaderAttributes = globalAttributes
.Union(controllerAttributes)
.Union(methodAttributes)
.OfType<T>()
.ToList();
return containsHeaderAttributes.Count == 0
? new CustomAttribute(false, false)
: new CustomAttribute(true, false);
}
}
If you’re using NET6 or higher, you can create a record instead of a class to represent CustomAttribute
.
NOTE: As you know, when you specify something on the controller, it applies to all endpoints within that controller.
Now we have to modify our filter, so it reads this information and acts accordingly:
[AttributeUsage(AttributeTargets.Class | AttributeTargets.Method)]
public class AcceptedLanguageHeader : Attribute, IOperationFilter
{
public void Apply(OpenApiOperation operation, OperationFilterContext context)
{
CustomAttribute acceptedLanguageHeader = context.RequireAttribute<AcceptedLanguageHeader>();
if (!acceptedLanguageHeader.ContainsAttribute)
return;
operation.Parameters.Add(new OpenApiParameter()
{
Name = "accept-language",
In = ParameterLocation.Header,
Required = acceptedLanguageHeader.Mandatory,
Schema = new OpenApiSchema() { Type = "string" }
});
}
}
If the endpoint we’re calling does not contain the attribute, we simply return, which means it will NOT include the Header
in the interface. If it needs it, we’ll add the parameter. Also, we set the Required property based on the method’s response.
Now if we run the application, no endpoint will have the field to enter the header.
If we want it to appear, we must add the Attribute to the controller:
[ApiController]
[AcceptedLanguageHeader] //HERE
[Route("api/[controller]")]
public class ExampleErrorController : ControllerBase
{
[HttpGet]
public IActionResult Get()
{
return Result.Failure(Guid.Parse("ce6887fb-f8fa-49b7-bcb4-d8538b6c9932"))
.ToActionResult();
}
}
3.1 - Specify if an Attribute is Required in Swagger
Now we’re going to indicate if the header has to be required or not.
First, we’re going to create an interface, which will only have one property called IsMandatory
:
public interface ICustomAttribute
{
public bool IsMandatory { get; }
}
And we’ll implement this interface in each of our custom attributes.
Likewise, we’ll pass in the constructor of the filter whether it’s true or false:
[AttributeUsage(AttributeTargets.Class | AttributeTargets.Method)]
public class AcceptedLanguageHeader : Attribute, ICustomAttribute, IOperationFilter
{
public static string HeaderName = "accept-language";
public bool IsMandatory { get; }
public AcceptedLanguageHeader(bool isMandatory = false)
{
IsMandatory = isMandatory;
}
public void Apply(OpenApiOperation operation, OperationFilterContext context)
{
CustomAttribute acceptedLanguageHeader = context.RequireAttribute<AcceptedLanguageHeader>();
if (!acceptedLanguageHeader.ContainsAttribute)
return;
operation.Parameters.Add(new OpenApiParameter()
{
Name = HeaderName,
In = ParameterLocation.Header,
Required = acceptedLanguageHeader.Mandatory,
Schema = new OpenApiSchema() { Type = "string" }
});
}
}
Now we need to modify our class so the generic type we’ve indicated is required to be an ICustomAttribute
, and we also modify the last return to check that property:
public static CustomAttribute RequireAttribute<T>(this OperationFilterContext context)
where T : ICustomAttribute
{
....
return containsHeaderAttributes.Count == 0
? new CustomAttribute(false, false)
: new CustomAttribute(true, containsHeaderAttributes.First().IsMandatory);
}
Finally, we modify the endpoint so the header is marked as required:
[ApiController]
[Route("api/[controller]")]
public class ExampleErrorController : ControllerBase
{
[AcceptedLanguageHeader(true)]
[HttpGet]
public IActionResult Get()
{
return Result.Failure(Guid.Parse("ce6887fb-f8fa-49b7-bcb4-d8538b6c9932"))
.ToActionResult();
}
}
Now you can see in the interface that it is marked as required:
NOTE: if you have the attribute specified both at controller and method level, the one that takes precedence is the endpoint’s.
4 - Middleware to Prevent Hacks
One of the problems of this solution is that it only works in swagger, so if we want to enforce this at API level, we need to implement a middleware that does similar functionality: check if it exists or not.
namespace WebPersonal.BackEnd.API.Middlewares
{
public class CustomHeaderValidatorMiddleware
{
private readonly RequestDelegate _next;
private readonly string _headerName;
public CustomHeaderValidatorMiddleware(RequestDelegate next, string headerName)
{
_next = next;
_headerName = headerName;
}
public async Task Invoke(HttpContext context)
{
if (IsHeaderValidated(context))
{
await _next.Invoke(context);
}
else
{
throw new Exception($"the header {_headerName} is mandatory and it is missing");
}
}
private bool IsHeaderValidated(HttpContext context)
{
Endpoint? endpoint = context.GetEndpoint();
if (endpoint == null)
return true;
bool isRequired = IsHeaderRequired(endpoint);
if (!isRequired)
return true;
bool isIncluded = IsHeaderIncluded(context);
if (isRequired && isIncluded)
return true;
return false;
}
private bool IsHeaderIncluded(HttpContext context)
=> context.Request.Headers.Keys.Select(a=>a.ToLower()).Contains(_headerName.ToLower());
private static bool IsHeaderRequired(Endpoint endpoint)
{
var attribute = endpoint.Metadata.GetMetadata<ICustomAttribute>();
return attribute is { IsMandatory: true };
}
}
}
I won't go into much detail, but basically it’s a series of If
statements that check if the header passed in the constructor is necessary, if it is present, and if not, it throws an exception.
Now we just need to specify the middleware in the request pipeline, that is, in program.cs
:
app.UseMiddleware<CustomHeaderValidatorMiddleware>(AcceptedLanguageHeader.HeaderName);
If we test without sending the header, it will return the exception:
In another post we might see how to make everything look nicer (unifying the API response), but to prevent hacks, this solution is more than enough.
Conclusion
In this post we have seen how to add a header within the swagger interface
We have learned to specify that header in certain endpoints only
Also how to specify a header in swagger as optional or required.
If there is any problem you can add a comment bellow or contact me in the website's contact form