I'm developing a REST API using Azure Functions with .NET 5 (Isolated) and I want to add an OpenAPI spec for each route. But it looks like this:
namespace AzureFunctionsREST.API.Functions
{
public class ReporterFunction : BaseFunction
{
private readonly IReporterRepository _reporterRepository;
public ReporterFunction(IReporterRepository reporterRepository)
{
this._reporterRepository = reporterRepository;
}
[Function("ReporterList")]
[OpenApiOperation(tags: new[] { "reporter" }, Summary = "Retrieve all reporters")]
[OpenApiSecurity("function_key", SecuritySchemeType.ApiKey, Name = "code", In = OpenApiSecurityLocationType.Query)]
[OpenApiResponseWithBody(statusCode: HttpStatusCode.OK, contentType: "application/json", bodyType: typeof(Reporter[]),
Description = "All reporters")]
public async Task<HttpResponseData> List([HttpTrigger(AuthorizationLevel.Function, "get", Route = "reporter")] HttpRequestData req,
FunctionContext executionContext)
{
// List reporters
}
[Function("ReporterGet")]
[OpenApiOperation(tags: new[] { "reporter" }, Summary = "Retrieve reporter")]
[OpenApiSecurity("function_key", SecuritySchemeType.ApiKey, Name = "code", In = OpenApiSecurityLocationType.Query)]
[OpenApiParameter(name: "reporterId", In = ParameterLocation.Path, Required = true, Type = typeof(string))]
[OpenApiResponseWithBody(statusCode: HttpStatusCode.OK, contentType: "application/json", bodyType: typeof(Reporter),
Description = "Reporter")]
public async Task<HttpResponseData> Get([HttpTrigger(AuthorizationLevel.Function, "get", Route = "reporter/{reporterId:required}")] HttpRequestData req,
FunctionContext executionContext)
{
// Get reporter
}
[Function("ReporterPost")]
[OpenApiOperation(tags: new[] { "reporter" }, Summary = "Create a new reporter")]
[OpenApiSecurity("function_key", SecuritySchemeType.ApiKey, Name = "code", In = OpenApiSecurityLocationType.Query)]
[OpenApiRequestBody(contentType: "application/json", bodyType: typeof(ReporterRequest))]
[OpenApiResponseWithBody(statusCode: HttpStatusCode.OK, contentType: "application/json", bodyType: typeof(Reporter),
Description = "Created reporter")]
public async Task<HttpResponseData> Post([HttpTrigger(AuthorizationLevel.Function, "post", Route = "reporter")] HttpRequestData req,
FunctionContext executionContext)
{
// Create reporter
}
[Function("ReporterPut")]
[OpenApiOperation(tags: new[] { "reporter" }, Summary = "Update a reporter")]
[OpenApiSecurity("function_key", SecuritySchemeType.ApiKey, Name = "code", In = OpenApiSecurityLocationType.Query)]
[OpenApiParameter(name: "reporterId", In = ParameterLocation.Path, Required = true, Type = typeof(string))]
[OpenApiRequestBody(contentType: "application/json", bodyType: typeof(ReporterRequest))]
[OpenApiResponseWithBody(statusCode: HttpStatusCode.OK, contentType: "application/json", bodyType: typeof(Reporter),
Description = "Updated reporter")]
public async Task<HttpResponseData> Put([HttpTrigger(AuthorizationLevel.Function, "put", Route = "reporter/{reporterId:required}")] HttpRequestData req,
FunctionContext executionContext)
{
// Update reporter
}
[Function("ReporterDelete")]
[OpenApiOperation(tags: new[] { "reporter" }, Summary = "Delete a reporter")]
[OpenApiParameter(name: "reporterId", In = ParameterLocation.Path, Required = true, Type = typeof(string))]
[OpenApiSecurity("function_key", SecuritySchemeType.ApiKey, Name = "code", In = OpenApiSecurityLocationType.Query)]
[OpenApiResponseWithBody(statusCode: HttpStatusCode.OK, contentType: "application/json", bodyType: typeof(Reporter),
Description = "Deleted reporter")]
public async Task<HttpResponseData> Delete([HttpTrigger(AuthorizationLevel.Function, "delete", Route = "reporter/{reporterId:required}")] HttpRequestData req,
FunctionContext executionContext)
{
// Delete reporter
}
}
}
All five methods are just routes for simple CRUD operations. As you can see these lines are duplicated in every function (route):
[OpenApiOperation(tags: new[] { "reporter" }, Summary = "Retrieve all reporters")]
[OpenApiSecurity("function_key", SecuritySchemeType.ApiKey, Name = "code", In = OpenApiSecurityLocationType.Query)]
[OpenApiResponseWithBody(statusCode: HttpStatusCode.OK, contentType: "application/json", bodyType: typeof(Reporter[]),
Description = "All reporters")]
I'm using a single class for interaction with each model (e.g. 10 models = 10 classes) so in each class:
- Tags will all be the same
- Security will be the same
- Response body will all be the same (except
[GET] /api/reporter
which will return as an array)
And also parameter name is also duplicated (see reporterId
):
[OpenApiParameter(name: "reporterId", In = ParameterLocation.Path, Required = true, Type = typeof(string))]
public async Task<HttpResponseData> Put([HttpTrigger(AuthorizationLevel.Function, "put", Route = "reporter/{reporterId:required}")] HttpRequestData req,
FunctionContext executionContext) { }
I've tried something like putting values in a variable and using it in the attributes (which is not possible). So, I'm wondering is there a way to refactor this so these lines won't get duplicated all over?
Additional Info:
- GitHub - https://github.com/phwt/azure-functions-rest/tree/feature/mongo
- The functions will be inside
AzureFunctionsREST.API/Functions
- This project is a PoC for REST API + OpenAPI + MongoDB + repository pattern on Azure Functions. So, there might be a lot of unrelated codes.
- The functions will be inside
- Related documentations on OpenAPI with Azure Functions
The updated version is available here
-
\$\begingroup\$ Please do not update your question after an answer has been posted. See codereview.stackexchange.com/help/someone-answers . \$\endgroup\$BCdotWEB– BCdotWEB2021年11月27日 16:10:35 +00:00Commented Nov 27, 2021 at 16:10
-
\$\begingroup\$ @BCdotWEB Can you suggest a way on how do I share my implementations? \$\endgroup\$phwt– phwt2021年11月27日 16:11:34 +00:00Commented Nov 27, 2021 at 16:11
-
\$\begingroup\$ Read the link, that is why I posted it. \$\endgroup\$BCdotWEB– BCdotWEB2021年11月27日 16:13:24 +00:00Commented Nov 27, 2021 at 16:13
-
1\$\begingroup\$ @BCdotWEB Okay, I didn't see your updated link. \$\endgroup\$phwt– phwt2021年11月27日 16:14:55 +00:00Commented Nov 27, 2021 at 16:14
1 Answer 1
The sad truth is there is no nice solution for this (at least according to my knowledge).
But there are some tiny tricks which you can apply to make your code more concise:
- Use type alias to abbreviate attribute names
- Use constant classes to capture constant values
- Avoid named arguments whenever it's unambiguous
OpenApiOperationAttribute
- Abbreviate it to
Op
using Op = Microsoft.Azure.WebJobs.Extensions.OpenApi.Core.Attributes.OpenApiOperationAttribute;
- Define a
Resource
and aSummary
classes
private static class Resource
{
public const string Name = "reporter";
}
private static class Summary
{
private const string resource = Resource.Name;
public const string List = "Retrieve all " + resource + "s";
}
- Take advantage of the
tags
parameter definition (params string[] tags
)
[Op(tags: Resource.Name, Summary = Summary.List)]
- Because it was defined as a
params
that's why we can omit the array declarationnew [] { ... }
- If we omit
tags
then we define theoperationId
parameter instead, so we have to use here the named argument feature of C#
OpenApiSecurityAttribute
- Abbreviate it to
Sec
using Sec = Microsoft.Azure.WebJobs.Extensions.OpenApi.Core.Attributes.OpenApiSecurityAttribute;
- Define a
Security
class
private static class Security
{
public const string Name = "code";
public const OpenApiSecurityLocationType In = OpenApiSecurityLocationType.Query;
public const string SchemeName = "function_key";
public const SecuritySchemeType SchemeType = SecuritySchemeType.ApiKey;
}
- Take advantage of the positions of the arguments
[Sec(Security.SchemeName, Security.SchemeType, Name = Security.Name, In = Security.In)]
OpenApiResponseWithBodyAttribute
- Abbreviate it to
Body
using Sec = using Body = Microsoft.Azure.WebJobs.Extensions.OpenApi.Core.Attributes.OpenApiResponseWithBodyAttribute;
- Define a
ResponseBody
and aDescription
classes
private static class ResponseBody
{
public const HttpStatusCode StatusCode = HttpStatusCode.OK;
public const string ContentType = "application/json";
}
private static class Description
{
private const string resource = Resource.Name;
public const string List = "All " + resource + "s";
}
- Yet again take advantage of the position of the arguments to avoid naming
[Body(ResponseBody.StatusCode, ResponseBody.ContentType, typeof(Reporter[]), Description = Description.List)]
After moving all hard coded strings and enums into static classes then the methods look like these. Here are two examples:
List
[Function(Name.List)]
[Op(Resource.Name, Summary = Summary.List)]
[Sec(Security.SchemeName, Security.SchemeType, Name = Security.Name, In = Security.In)]
[Body(ResponseBody.StatusCode, ResponseBody.ContentType, typeof(Reporter[]), Description = Description.List)]
public async Task<HttpResponseData> List(
[HttpTrigger(Trigger.Level, Method.Get, Route = Route.List)] HttpRequestData req,
FunctionContext executionContext)
{
}
Delete
[Function(Name.Delete)]
[Op(tags: Resource.Name, Summary = Summary.Delete)]
[Sec(Security.SchemeName, Security.SchemeType, Name = Security.Name, In = Security.In)]
[Param(Parameter.Name, In = Parameter.In, Required = Parameter.IsRequired, Type = typeof(string))]
[Body(ResponseBody.StatusCode, ResponseBody.ContentType, typeof(Reporter[]), Description = Description.Delete)]
public async Task<HttpResponseData> Delete(
[HttpTrigger(Trigger.Level, Method.Delete, Route = Route.Delete)] HttpRequestData req,
FunctionContext executionContext)
{
}
Finally let me share with you the definitions of the Name
, Method
, Parameter
and Route
classes:
public static class Parameter
{
public const string Name = Resource.Name + "Id";
public const ParameterLocation In = ParameterLocation.Path;
public const bool IsRequired = true;
}
private static class Name
{
private const string prefix = nameof(Reporter);
public const string List = prefix + nameof(List);
...
public const string Delete = prefix + nameof(Delete);
}
private static class Route
{
private const string prefix = Resource.Name;
public const string List = prefix;
...
public const string Delete = prefix + "/{"+ Parameter.Name + ":required}";
}
private static class Method
{
public const string Get = nameof(HttpMethod.Get);
...
public const string Delete = nameof(HttpMethod.Delete);
}
One can argue whether or not does it make sense to define that many static classes. I agree that they are still boiler-plate code. But the changes are more localized (in case of renaming or extension).
-
1\$\begingroup\$ Thanks for the very detailed solutions! Even with a lot of static classes - this still looks better. \$\endgroup\$phwt– phwt2021年11月27日 11:22:17 +00:00Commented Nov 27, 2021 at 11:22