Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

.NET 5 Support #22

Closed
justinyoo opened this issue Jan 29, 2021 · 7 comments · Fixed by #142
Closed

.NET 5 Support #22

justinyoo opened this issue Jan 29, 2021 · 7 comments · Fixed by #142
Assignees
Labels
enhancement New feature or request resolved Request has been resolved v0.8.0

Comments

@justinyoo
Copy link
Contributor

@justinyoo justinyoo self-assigned this Jan 29, 2021
@justinyoo justinyoo added investigating Need time to investigating v0.3.0 labels Jan 29, 2021
@justinyoo
Copy link
Contributor Author

As of the worker runtime version 1.0.0-preview3, .NET 5 support is not possible.

@joostvanhassel
Copy link
Contributor

@justinyoo the issue you mentioned in this issue seems to be resolved in this PR

@justinyoo
Copy link
Contributor Author

@joostvanhassel Thanks for the heads up!

@PehrGit
Copy link

PehrGit commented Jun 4, 2021

Just FYI I would also like NET5 support. I've got a basic version working already using a custom HttpRequest class:

    public class GenerateSwaggerFunction
    {
        private static readonly IOpenApiHttpTriggerContext Context = new OpenApiHttpTriggerContext();
        private readonly IServiceProvider serviceProvider;

        public GenerateSwaggerFunction(IServiceProvider serviceProvider)
        {
            this.serviceProvider = serviceProvider;
        }

        [Function(nameof(GenerateSwaggerCustom))]
        [OpenApiIgnore]
        public static async Task<HttpResponseData> GenerateSwaggerCustom(
            [HttpTrigger(AuthorizationLevel.Anonymous, "get", Route = "swagger2.json")]
            HttpRequestData req,
            FunctionContext context,
            Microsoft.Azure.WebJobs.ExecutionContext ctx)
        {
            var log = context.GetLogger<GenerateSwaggerFunction>();
            var extension = "json";
            log.LogInformation($"swagger.{extension} was requested.");

            var result = default(string);
            var response = default(HttpResponseData);
            try
            {
                var functionAppDirectory = Path.GetDirectoryName(context.FunctionDefinition.PathToAssembly);
                result = await (await Context.SetApplicationAssemblyAsync(functionAppDirectory, false))
                    .Document
                    .InitialiseDocument()
                    .AddMetadata(Context.OpenApiConfigurationOptions.Info)
                    .AddServer(new HttpRequest(req), Context.HttpSettings.RoutePrefix, Context.OpenApiConfigurationOptions)
                    .AddNamingStrategy(Context.NamingStrategy)
                    .AddVisitors(Context.GetVisitorCollection())
                    .Build(Context.ApplicationAssembly, Context.OpenApiConfigurationOptions.OpenApiVersion)
                    .RenderAsync(Context.GetOpenApiSpecVersion(Context.OpenApiConfigurationOptions.OpenApiVersion),
                        Context.GetOpenApiFormat(extension))
                    .ConfigureAwait(false);

                response = req.CreateResponse(statusCode: HttpStatusCode.OK);
                response.Headers.Add("content-type", Context.GetOpenApiFormat(extension).GetContentType());
                await response.WriteStringAsync(result);
            }
            catch (Exception ex)
            {
                log.LogError(ex.Message);

                result = ex.Message;
                if (Context.IsDevelopment)
                {
                    result += "\r\n\r\n";
                    result += ex.StackTrace;
                }
                
                response = req.CreateResponse(statusCode: HttpStatusCode.InternalServerError);
                response.Headers.Add("content-type", "text/plain");
                await response.WriteStringAsync(result);
            }

            return response;
        }


        /// <summary>
        ///     Invokes the HTTP trigger endpoint to render Swagger UI in HTML.
        /// </summary>
        /// <param name="req"><see cref="HttpRequest" /> instance.</param>
        /// <param name="ctx"><see cref="Microsoft.Azure.WebJobs.ExecutionContext" /> instance.</param>
        /// <param name="log"><see cref="ILogger" /> instance.</param>
        /// <returns>Swagger UI in HTML.</returns>
        [Function(nameof(RenderSwaggerUiCustom))]
        [OpenApiIgnore]
        public async Task<HttpResponseData> RenderSwaggerUiCustom(
            [HttpTrigger(AuthorizationLevel.Anonymous, "get", Route = "swagger2/ui")]
            HttpRequestData req,
            FunctionContext context
            )
        {
            var log = context.GetLogger<GenerateSwaggerFunction>();
            log.LogInformation("SwaggerUI page was requested.");

            string result;
            var response = default(HttpResponseData);
            try
            {
                var functionAppDirectory = Path.GetDirectoryName(context.FunctionDefinition.PathToAssembly);
                result = await (await Context.SetApplicationAssemblyAsync(functionAppDirectory, false))
                    .SwaggerUI
                    .AddMetadata(Context.OpenApiConfigurationOptions.Info)
                    .AddServer(new HttpRequest(req), Context.HttpSettings.RoutePrefix, Context.OpenApiConfigurationOptions)
                    .BuildAsync(Context.PackageAssembly, Context.OpenApiCustomUIOptions)
                    .RenderAsync("swagger2.json", Context.GetDocumentAuthLevel(), Context.GetSwaggerAuthKey())
                    .ConfigureAwait(false);

                response = req.CreateResponse(statusCode: HttpStatusCode.OK);
                response.Headers.Add("content-type", "text/html");
                await response.WriteStringAsync(result);
            }
            catch (Exception ex)
            {
                log.LogError(ex.Message);

                result = ex.Message;
                if (Context.IsDevelopment)
                {
                    result += "\r\n\r\n";
                    result += ex.StackTrace;
                }
                response = req.CreateResponse(statusCode: HttpStatusCode.InternalServerError);
                response.Headers.Add("content-type", "text/plain");
                await response.WriteStringAsync(result);
            }

            return response;
        }
    }

    /// <summary>
    /// Just here temporarily to make the swagger gen work
    /// </summary>
    internal class HttpRequest : Microsoft.AspNetCore.Http.HttpRequest
    {
        private readonly HttpRequestData httpRequestData;

        public HttpRequest(HttpRequestData httpRequestData)
        {
            this.httpRequestData = httpRequestData;
        }

        public override HttpContext HttpContext { get; }
        public override string Method
        {
            get => httpRequestData.Method;
            set => throw new NotImplementedException();
        }

        public override string Scheme
        {
            get => httpRequestData.Url.Scheme;
            set { throw new NotImplementedException(); }
        }

        public override bool IsHttps { get; set; }
        public override HostString Host
        {
            get => new HostString(httpRequestData.Url.Host, httpRequestData.Url.Port);
            set => throw new NotImplementedException();
        }

        public override PathString PathBase { get; set; }
        public override PathString Path { get; set; }
        public override QueryString QueryString { get; set; }
        public override IQueryCollection Query { get; set; }
        public override string Protocol { get; set; }
        public override IHeaderDictionary Headers { get; }
        public override IRequestCookieCollection Cookies { get; set; }
        public override long? ContentLength { get; set; }
        public override string ContentType { get; set; }
        public override Stream Body { get; set; }
        public override bool HasFormContentType { get; }
        public override IFormCollection Form { get; set; }

        public override async Task<IFormCollection> ReadFormAsync(CancellationToken cancellationToken = new())
        {
            throw new NotImplementedException();
        }
    }

For now I'm putting both a FunctionNameAttribute and a FunctionAttribute and both the old and new HttpTriggerAttributes on the functions. So it's a bit of duplication but at least for now it allows us to move ahead with the .NET5 upgrade. Maybe this helps someone else 😄

@PehrGit
Copy link

PehrGit commented Jul 9, 2021

@justinyoo great to see that the PR with out-of-proc support has been merged, any ETA on an updated NuGet package containing the new goodies?

@matthewkrieger
Copy link

@justinyoo Why is nuget offering up the .net 5 versions if they aren't compatible with the project type?

@kzryzstof
Copy link

Following PehrGit's workaround, I have created an DocumentHelperAdapter class that wraps the original DocumentHelper. The new wrapper creates a bridge between the legacy FunctionNameAttribute / HttpTriggerAttribute and the new FunctionAttribute / `HttpTriggerAttribute.

Here is the wrapper. Aside from the methods GetHttpTriggerMethods, GetFunctionNameAttribute and GetHttpTriggerAttribute which now deals with the new attributes, all the calls are redirected to the existing DocumentHelper:

internal class DocumentHelperAdapter : IDocumentHelper
  {
      #region Constants

      private readonly IDocumentHelper _legacyDocumentHelper;

      #endregion

      #region Constructors

      public DocumentHelperAdapter(RouteConstraintFilter filter, IOpenApiSchemaAcceptor acceptor)
      {
          _legacyDocumentHelper = new DocumentHelper(filter, acceptor);
      }

      #endregion

      #region Public Methods

      public FunctionNameAttribute GetFunctionNameAttribute(MethodInfo element)
      {
          return RetrieveLegacyFunctionNameAttribute(element);
      }

      public string GetHttpEndpoint(FunctionNameAttribute function, HttpTriggerAttribute trigger)
      {
          return _legacyDocumentHelper.GetHttpEndpoint(function, trigger);
      }

      public HttpTriggerAttribute GetHttpTriggerAttribute(MethodInfo element)
      {
          return RetrieveLegacyHttpTriggerAttribute(element);
      }

      public List<MethodInfo> GetHttpTriggerMethods(Assembly assembly)
      {
          return assembly
              .GetTypes()
              .SelectMany(p => p.GetMethods())
              .Where(p => p.ExistsCustomAttribute<FunctionAttribute>())
              .Where(p => p.ExistsCustomAttribute<OpenApiOperationAttribute>())
              .Where(p => !p.ExistsCustomAttribute<OpenApiIgnoreAttribute>())
              .Where(p => p.GetParameters().FirstOrDefault(q => q.ExistsCustomAttribute<Microsoft.Azure.Functions.Worker.HttpTriggerAttribute>()) != null)
              .ToList();
      }

      public OperationType GetHttpVerb(HttpTriggerAttribute trigger)
      {
          return _legacyDocumentHelper.GetHttpVerb(trigger);
      }

      public OpenApiOperation GetOpenApiOperation(
          MethodInfo element,
          FunctionNameAttribute function,
          OperationType verb)
      {
          return _legacyDocumentHelper.GetOpenApiOperation(element, function, verb);
      }

      public List<OpenApiParameter> GetOpenApiParameters(
          MethodInfo element,
          HttpTriggerAttribute trigger,
          NamingStrategy namingStrategy,
          VisitorCollection collection)
      {
          return _legacyDocumentHelper.GetOpenApiParameters(element, trigger, namingStrategy, collection);
      }

      public OpenApiPathItem GetOpenApiPath(string path, OpenApiPaths paths)
      {
          return _legacyDocumentHelper.GetOpenApiPath(path, paths);
      }

      public OpenApiRequestBody GetOpenApiRequestBody(
          MethodInfo element,
          NamingStrategy namingStrategy,
          VisitorCollection collection,
          OpenApiVersionType version = OpenApiVersionType.V2)
      {
          return _legacyDocumentHelper.GetOpenApiRequestBody(element, namingStrategy, collection, version);
      }

      [Obsolete("This method is obsolete from 2.0.0. Use GetOpenApiResponses instead", true)]
      public OpenApiResponses GetOpenApiResponseBody(
          MethodInfo element,
          NamingStrategy namingStrategy = null)
      {
          return GetOpenApiResponses(element, namingStrategy, null);
      }

      public OpenApiResponses GetOpenApiResponses(
          MethodInfo element,
          NamingStrategy namingStrategy,
          VisitorCollection collection,
          OpenApiVersionType version = OpenApiVersionType.V2)
      {
          return _legacyDocumentHelper.GetOpenApiResponses(element, namingStrategy, collection, version);
      }

      public Dictionary<string, OpenApiSchema> GetOpenApiSchemas(
          List<MethodInfo> elements,
          NamingStrategy namingStrategy,
          VisitorCollection collection)
      {
          return _legacyDocumentHelper.GetOpenApiSchemas(elements, namingStrategy, collection);
      }

      public List<OpenApiSecurityRequirement> GetOpenApiSecurityRequirement(
          MethodInfo element,
          NamingStrategy namingStrategy = null)
      {
          return _legacyDocumentHelper.GetOpenApiSecurityRequirement(element, namingStrategy);
      }

      [Obsolete("This method is obsolete from 3.2.0. Use GetOpenApiSecuritySchemes(List<MethodInfo> elements, NamingStrategy namingStrategy = null) instead", true)]
      public Dictionary<string, OpenApiSecurityScheme> GetOpenApiSecuritySchemes()
      {
          return _legacyDocumentHelper.GetOpenApiSecuritySchemes();
      }

      public Dictionary<string, OpenApiSecurityScheme> GetOpenApiSecuritySchemes(
          List<MethodInfo> elements,
          NamingStrategy namingStrategy = null)
      {
          return _legacyDocumentHelper.GetOpenApiSecuritySchemes(elements, namingStrategy);
      }

      #endregion

      #region Private Methods

      private static FunctionNameAttribute RetrieveLegacyFunctionNameAttribute(MethodInfo element)
      {
          var newFunctionAttribute = element.GetCustomAttribute<FunctionAttribute>(false);
          return new FunctionNameAttribute(newFunctionAttribute.Name);
      }

      private static HttpTriggerAttribute RetrieveLegacyHttpTriggerAttribute(MethodInfo element)
      {
          var newHttpTriggerAttribute = element.GetParameters().First().GetCustomAttribute<Microsoft.Azure.Functions.Worker.HttpTriggerAttribute>(false);
              
          return new HttpTriggerAttribute(newHttpTriggerAttribute.Methods)
          {
              Route = newHttpTriggerAttribute.Route
          };
      }

      #endregion
  }

To use, we need to subclass OpenApiHttpTriggerContext so that we can override the Document property and provide our new DocumentHelperAdapter:

internal class CustomOpenApiHttpTriggerContext : OpenApiHttpTriggerContext
    {
        #region Constants

        #endregion

        #region Properties

        public override IDocument Document { get; }

        #endregion

        #region Constructors

        public CustomOpenApiHttpTriggerContext()
        {
            // Creates the DocumentHelperAdapter here, instead of DocumentHelper.
            Document = new Document(new DocumentHelperAdapter(new RouteConstraintFilter(), new OpenApiSchemaAcceptor()));
        }

        #endregion
    }

Finally, in the OpenApiFunction provided by PehrGit earlier, we use the new CustomOpenApiHttpTriggerContext:

internal sealed class OpenApiFunction
    {
        #region Constants

        private static readonly IOpenApiHttpTriggerContext Context = new CustomOpenApiHttpTriggerContext();

        #endregion

        #region Public Methods

        [Function("get-swagger-json")]
        [OpenApiIgnore]
        public static async Task<HttpResponseData> GetSwaggerJson
        (
            [HttpTrigger(AuthorizationLevel.Anonymous, "get", Route = "swagger2.json")]
            HttpRequestData httpRequestData,
            FunctionContext functionContext
        )
        {
            ILogger<OpenApiFunction>? log = functionContext.GetLogger<OpenApiFunction>();
            var extension = "json";
            log.LogInformation($"swagger.{extension} was requested.");

            string result;
            HttpResponseData response;

            try
            {
                string functionPath = functionContext.FunctionDefinition.PathToAssembly;
                
                string? functionAppDirectory = Path.GetDirectoryName(functionPath);

                IOpenApiHttpTriggerContext context = await Context.SetApplicationAssemblyAsync(functionAppDirectory, false);
                    
                result = await context
                    .Document
                    .InitialiseDocument()
                    .AddMetadata(Context.OpenApiConfigurationOptions.Info)
                    .AddServer(new HttpRequest(httpRequestData), Context.HttpSettings.RoutePrefix, Context.OpenApiConfigurationOptions)
                    .AddNamingStrategy(Context.NamingStrategy)
                    .AddVisitors(Context.GetVisitorCollection())
                    .Build(Context.ApplicationAssembly, Context.OpenApiConfigurationOptions.OpenApiVersion)
                    .RenderAsync(Context.GetOpenApiSpecVersion(Context.OpenApiConfigurationOptions.OpenApiVersion),
                                 Context.GetOpenApiFormat(extension))
                    .ConfigureAwait(false);

                response = httpRequestData.CreateResponse(HttpStatusCode.OK);
                response.Headers.Add("content-type", Context.GetOpenApiFormat(extension).GetContentType());
                await response.WriteStringAsync(result);
            }
            catch (Exception ex)
            {
                log.LogError(ex.Message);

                result = ex.Message;

                if (Context.IsDevelopment)
                {
                    result += "\r\n\r\n";
                    result += ex.StackTrace;
                }

                response = httpRequestData.CreateResponse(HttpStatusCode.InternalServerError);
                response.Headers.Add("content-type", "text/plain");
                await response.WriteStringAsync(result);
            }

            return response;
        }

        /// <summary>
        /// Invokes the HTTP trigger endpoint to render Swagger UI in HTML.
        /// </summary>
        /// <param name="httpRequestData"><see cref="HttpRequest" /> instance.</param>
        /// <param name="functionContext"></param>
        /// <returns>Swagger UI in HTML.</returns>
        [Function("get-swagger-ui")]
        [OpenApiIgnore]
        public async Task<HttpResponseData> GetSwaggerUi
        (
            [HttpTrigger(AuthorizationLevel.Anonymous, "get", Route = "swagger2/ui")]
            HttpRequestData httpRequestData,
            FunctionContext functionContext
        )
        {
            ILogger<OpenApiFunction> log = functionContext.GetLogger<OpenApiFunction>();
            log.LogInformation("SwaggerUI page was requested.");

            string result;
            HttpResponseData response;

            try
            {
                string functionPath = functionContext.FunctionDefinition.PathToAssembly;
                
                string? functionAppDirectory = Path.GetDirectoryName(functionPath);
                
                result = await (await Context.SetApplicationAssemblyAsync(functionAppDirectory, false))
                    .SwaggerUI
                    .AddMetadata(Context.OpenApiConfigurationOptions.Info)
                    .AddServer(new HttpRequest(httpRequestData), Context.HttpSettings.RoutePrefix, Context.OpenApiConfigurationOptions)
                    .BuildAsync(Context.PackageAssembly, Context.OpenApiCustomUIOptions)
                    .RenderAsync("swagger2.json", Context.GetDocumentAuthLevel(), Context.GetSwaggerAuthKey())
                    .ConfigureAwait(false);

                response = httpRequestData.CreateResponse(HttpStatusCode.OK);
                response.Headers.Add("content-type", "text/html");
                await response.WriteStringAsync(result);
            }
            catch (Exception ex)
            {
                log.LogError(ex.Message);

                result = ex.Message;

                if (Context.IsDevelopment)
                {
                    result += "\r\n\r\n";
                    result += ex.StackTrace;
                }

                response = httpRequestData.CreateResponse(HttpStatusCode.InternalServerError);
                response.Headers.Add("content-type", "text/plain");
                await response.WriteStringAsync(result);
            }

            return response;
        }

        #endregion
    }

All this allows me to keep my function clean from any legacy pre-net5 stuff :)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
enhancement New feature or request resolved Request has been resolved v0.8.0
Projects
None yet
Development

Successfully merging a pull request may close this issue.

5 participants