3
\$\begingroup\$

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:


The updated version is available here

\$\endgroup\$
4
  • \$\begingroup\$ Please do not update your question after an answer has been posted. See codereview.stackexchange.com/help/someone-answers . \$\endgroup\$
    – BCdotWEB
    Nov 27 '21 at 16:10
  • \$\begingroup\$ @BCdotWEB Can you suggest a way on how do I share my implementations? \$\endgroup\$
    – phwt
    Nov 27 '21 at 16:11
  • \$\begingroup\$ Read the link, that is why I posted it. \$\endgroup\$
    – BCdotWEB
    Nov 27 '21 at 16:13
  • 1
    \$\begingroup\$ @BCdotWEB Okay, I didn't see your updated link. \$\endgroup\$
    – phwt
    Nov 27 '21 at 16:14
3
\$\begingroup\$

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

  1. Abbreviate it to Op
using Op = Microsoft.Azure.WebJobs.Extensions.OpenApi.Core.Attributes.OpenApiOperationAttribute;
  1. Define a Resource and a Summary 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";
}
  1. 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 declaration new [] { ... }
  • If we omit tags then we define the operationId parameter instead, so we have to use here the named argument feature of C#

OpenApiSecurityAttribute

  1. Abbreviate it to Sec
using Sec = Microsoft.Azure.WebJobs.Extensions.OpenApi.Core.Attributes.OpenApiSecurityAttribute;
  1. 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;
}
  1. Take advantage of the positions of the arguments
[Sec(Security.SchemeName, Security.SchemeType, Name = Security.Name, In = Security.In)]

OpenApiResponseWithBodyAttribute

  1. Abbreviate it to Body
using Sec = using Body = Microsoft.Azure.WebJobs.Extensions.OpenApi.Core.Attributes.OpenApiResponseWithBodyAttribute;
  1. Define a ResponseBody and a Description 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";
}
  1. 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).

\$\endgroup\$
1
  • 1
    \$\begingroup\$ Thanks for the very detailed solutions! Even with a lot of static classes - this still looks better. \$\endgroup\$
    – phwt
    Nov 27 '21 at 11:22

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.