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

Swashbuckle with ASP.NET Core #26

Open
PulsarFX opened this issue Dec 6, 2018 · 2 comments
Open

Swashbuckle with ASP.NET Core #26

PulsarFX opened this issue Dec 6, 2018 · 2 comments
Labels

Comments

@PulsarFX
Copy link

PulsarFX commented Dec 6, 2018

Hi,
did you already figure out, how to handle the new way of swashbuckle to look for controllers, when run with asp.net core?
Maybe this is no problem if the filter

var filter = new ControllerlessActionOperationFilter(xmlCommentsPath);
c.OperationFilter(() => filter);

would be working. But for this, a replacement for the new Asp.net core ApiExplorer is needed.

@dotnetjunkie
Copy link
Owner

dotnetjunkie commented Dec 6, 2018

I never found the time to look at this more thoroughly. As you might know, I'm quite busy finishing my little project.

I do accept PRs :-D

@dotnetjunkie dotnetjunkie changed the title Swashbuckle with Asp.net Core Swashbuckle with ASP.NET Core Jan 29, 2020
@PulsarFX
Copy link
Author

This is some code which will add Swagger doc to an ASP.NET Core 2.1 endpoint using Swashbuckle.AspNetCore 4.0.1

configuration class to integrate swagger from Startup:

public static class ConfigureSwagger
    {
        public static bool IsConfigured { get; set; }
        public static IEnumerable<QueryInfo> KnownQueries { get; private set; }
        public static IEnumerable<Type> KnownCommands { get; private set; }

        public static void ConfigureServices(IServiceCollection services, IEnumerable<QueryInfo> knownQueryTypes, IEnumerable<Type> knownCommandTypes, string appName)
        {
            KnownQueries = knownQueryTypes;
            KnownCommands = knownCommandTypes;

            // https://csharp.hotexamples.com/de/examples/-/IServiceCollection/ConfigureSwaggerGen/php-iservicecollection-configureswaggergen-method-examples.html
            // https://mattfrear.com/2018/07/21/add-an-authorization-header-to-your-swagger-ui-with-swashbuckle-revisited/

            var version = Assembly.GetExecutingAssembly().GetName().Version.ToString(3);
            services.AddSwaggerGen(x =>
            {
                x.DescribeAllEnumsAsStrings();
                x.SwaggerDoc($"apiDocV{version}", new Info
                {
                    Title = appName + " API",
                    Version = version,
                });
                x.AddSecurityDefinition("oauth2", new ApiKeyScheme
                {
                    Description = "Standard Authorization header using the Bearer scheme. Example: \"bearer {token}\"",
                    In = "header",
                    Name = "Authorization",
                    Type = "apiKey"
                });
                x.DocumentFilter<WebApiDocumentFilter>();
            });
        }

        /// <summary>
        /// register the api endpoints to the doc generator 
        /// </summary>
        /// <param name="app"></param>
        public static void Configure(IApplicationBuilder app, string appName)
        {
            var version = Assembly.GetExecutingAssembly().GetName().Version.ToString(3);

            // https://docs.microsoft.com/de-de/aspnet/core/tutorials/getting-started-with-swashbuckle?view=aspnetcore-2.2&tabs=visual-studio
            app.UseStaticFiles();
            app.UseSwagger();
            app.UseSwaggerUI(c =>
            {
                c.SwaggerEndpoint($"/swagger/apiDocV{version}/swagger.json", appName + " API");
            });

            IsConfigured = true;
        }

Needed for parameter documentation in WebApiDocumentFilter

/// <summary>
    /// describes a parameter for the api-doc
    /// </summary>
    internal class SwaggerParameter : IParameter
    {

        public override string ToString()
        {
            return "SwaggerParameter: " + Name;
        }

        public string Name { get; set; }
        public string In { get; set; }
        public string Description { get; set; }
        public bool Required { get; set; }
        public Dictionary<string, object> Extensions { get; }

        public List<SwaggerParameter> Properties { get; set; }
        public string Default { get; internal set; }
        public string Type { get; internal set; }
        public Schema Schema { get; internal set; }
    }

actual class to build the swagger doc

/// <summary>
    /// this is a generic filter implementation to add all commands and queries to the swagger document file.
    /// it is based on swagger/openApi v2.0 compatible with swashbuckle 4.0.1.
    /// https://swagger.io/docs/specification/2-0
    /// </summary>
    internal class WebApiDocumentFilter : IDocumentFilter
    {
        private readonly IEnumerable<QueryInfo> _knownQueryTypes;
        private readonly IEnumerable<Type> _knownCommandTypes;
        private readonly List<Type> _complexTypes = new List<Type>();
        private bool _typeListLocked = false;

        /// <summary>
        /// ctor. called by asp.net core
        /// </summary>
        public WebApiDocumentFilter()
        {
            _knownQueryTypes = ConfigureSwagger.KnownQueries;
            _knownCommandTypes = ConfigureSwagger.KnownCommands;
        }

        /// <summary>
        /// Entry point for Swagger
        /// </summary>
        /// <param name="swaggerDoc"></param>
        /// <param name="context"></param>
        public void Apply(SwaggerDocument swaggerDoc, DocumentFilterContext context)
        {
            DiscoverComplexTypes();

            swaggerDoc.Tags = new List<Tag>();
            if (_knownQueryTypes.Any())
            {
                AddQueriesToSwagger(swaggerDoc, _knownQueryTypes.OrderBy(i => i.QueryType.Name));
                swaggerDoc.Tags.Add(new Tag()
                {
                    Name = "Queries",
                    Description = "... which don't alter the systems state. That is: read access."
                });
            }
            if (_knownCommandTypes.Any())
            {
                AddCommandsToSwagger(swaggerDoc, _knownCommandTypes.OrderBy(i => i.Name));
                swaggerDoc.Tags.Add(new Tag()
                {
                    Name = "Commands",
                    Description = "... which will alter the systems state. That is: write access."
                });
            }

            AddComplexSchemaDefinitions(swaggerDoc);
        }

        /// <summary>
        /// head to start collecting a list of complex types 
        /// which will be later added to the swagger definition list
        /// </summary>
        private void DiscoverComplexTypes()
        {
            _complexTypes.Clear();
            _typeListLocked = false;

            foreach (var t in _knownQueryTypes.Select(i => i.QueryType).OrderBy(i => i.Name))
            {
                CollectComplexType(t);
            }
            foreach (var t in _knownQueryTypes.Select(i => i.ResultType))
            {
                CollectComplexType(t);
            }
            foreach (var t in _knownCommandTypes)
            {
                CollectComplexType(t);
            }

            _typeListLocked = true;
        }

        private void CollectComplexType(Type t)
        {
            if (_complexTypes.Any(c => c.Name == t.Name))
            {
                return;
            }

            var isNested = IsNestedArrayType(t);
            if (!IsComplexType(t) && !isNested)
            {//not a complex type and not nesed 
                return;
            }

            if (isNested)
            {//check generic types which map to arrays
                if (t.IsGenericType)
                {
                    foreach (var gen in t.GenericTypeArguments)
                    {
                        CollectComplexType(gen);
                    }
                }
                else if (t.IsArray)
                {
                    CollectComplexType(t.GetElementType());
                }
            }
            else
            {//check all props of complex types
                _complexTypes.Add(t);
                if (t.IsGenericType)
                {
                    foreach (var gen in t.GenericTypeArguments)
                    {
                        CollectComplexType(gen);
                    }
                }
                foreach (var propInfo in t.GetProperties(BindingFlags.Public | BindingFlags.Instance | BindingFlags.SetProperty))
                {
                    CollectComplexType(propInfo.PropertyType);
                }
            }
        }

        /// <summary>
        /// types which are mapped to array 
        /// </summary>
        /// <param name="t"></param>
        /// <returns></returns>
        private bool IsNestedArrayType(Type t)
        {
            if (t.IsGenericType && t.GetGenericTypeDefinition() == typeof(Dictionary<,>))
            {
                return true;
            }
            if (t.IsArray)
            {
                return true;
            }
            if (t.GetInterfaces().Any(i => i.IsGenericType && i.GetGenericTypeDefinition() == typeof(IEnumerable<>)))
            {
                return true;
            }
            return false;
        }

        private void AddCommandsToSwagger(SwaggerDocument swaggerDoc, IEnumerable<Type> commandTypes)
        {
            foreach (var c in commandTypes)
            {
                var qName = c.Name.Replace("Command", string.Empty);
                PathItem pi = new PathItem();
                pi.Post = new Operation();
                pi.Post.Tags = new List<string> { "Commands" };
                pi.Post.Consumes = new List<string>() { "application/json" };
                pi.Post.Produces = new List<string>() { "application/json" };
                pi.Post.Deprecated = false;

                var sp = new SwaggerParameter();
                sp.Name = c.Name;
                sp.Description = c.GetCustomAttribute<DescriptionAttribute>()?.Description ?? null;
                sp.In = "body";
                sp.Required = false;
                sp.Type = c.Name;
                sp.Schema = MapToSwaggerSchema(c);

                pi.Post.Parameters = new List<IParameter>() { sp };
                pi.Post.Responses = new Dictionary<string, Response>();
                pi.Post.Responses.Add("200", new Response
                {
                    Description = "OK",
                    //Schema = commands return void
                });

                AddAccessRestrictInfo(c, pi);
                swaggerDoc.Paths.Add("/api/commands/" + qName, pi);
            }
        }

        private void AddQueriesToSwagger(SwaggerDocument swaggerDoc, IEnumerable<QueryInfo> queries)
        {
            foreach (var q in queries)
            {
                var qName = q.QueryType.Name.Replace("Query", string.Empty);
                PathItem pi = new PathItem();
                pi.Post = new Operation();
                pi.Post.Tags = new List<string> { "Queries" };
                pi.Post.Consumes = new List<string>() { "application/json" };
                pi.Post.Produces = new List<string>() { "application/json" };
                pi.Post.Deprecated = false;

                var sp = new SwaggerParameter();
                sp.Name = q.QueryType.Name;
                sp.Description = q.QueryType.GetCustomAttribute<DescriptionAttribute>()?.Description ?? string.Empty;
                sp.In = "body";
                sp.Required = false;
                sp.Type = q.QueryType.Name;
                sp.Schema = MapToSwaggerSchema(q.QueryType);

                pi.Post.Parameters = new List<IParameter>() { sp };
                pi.Post.Responses = new Dictionary<string, Response>();
                pi.Post.Responses.Add("200", new Response
                {
                    Description = "OK",
                    Schema = MapToSwaggerSchema(q.ResultType)
                });

                AddAccessRestrictInfo(q.QueryType, pi);

                swaggerDoc.Paths.Add("/api/queries/" + qName, pi);
            }
        }

        /// <summary>
        /// adds security information (if there is any) for the path item
        /// </summary>
        /// <param name="t"></param>
        /// <param name="pi"></param>
        private static void AddAccessRestrictInfo(Type t, PathItem pi)
        {
            var isAccessRestricted = t.GetCustomAttributes(false).OfType<AccessRestrictedAttribute>().Any();
            if (isAccessRestricted)
            {
                pi.Post.Responses.Add("401", new Response { Description = "Unauthorized" });
                //pi.Post.Responses.Add("403", new Response { Description = "Forbidden" });

                pi.Post.Security = new List<IDictionary<string, IEnumerable<string>>>
                {
                    new   Dictionary<string, IEnumerable<string>>
                    {
                        [ "oauth2" ] = new[] { "authrequired" } //same as in ConfigureSwagger
                    }
                };
            }
        }

        /// <summary>
        /// this is the definition list for complex types
        /// </summary>
        /// <param name="swaggerDoc"></param>
        private void AddComplexSchemaDefinitions(SwaggerDocument swaggerDoc)
        {
            foreach (var type in _complexTypes.OrderBy(i => i.Name))
            {
                var typename = EscapeGenericTypeString(type);
                if (!swaggerDoc.Definitions.ContainsKey(typename))
                {
                    var schemaDefinition = new KeyValuePair<string, Schema>(typename, new Schema()
                    {
                        Type = "object",
                        Properties = CollectSchemaProperties(type)
                    });
                    swaggerDoc.Definitions.Add(schemaDefinition);
                }
            }
        }

        private IDictionary<string, Schema> CollectSchemaProperties(Type t)
        {
            var properties = new Dictionary<string, Schema>();
            var typeProps = t.GetProperties(BindingFlags.Public | BindingFlags.Instance | BindingFlags.SetProperty);
            foreach (PropertyInfo propInfo in typeProps)
            {
                var propSchema = MapToSwaggerSchema(propInfo.PropertyType);
                if (propInfo.PropertyType == t)
                {//recursion
                    //nullable property types are not supported in swagger/openApi v2. comes with v3
                    //https://swagger.io/docs/specification/data-models/data-types/#null
                }
                if (propSchema.Ref != null && propInfo.PropertyType != t)
                {
                    propSchema.Properties = CollectSchemaProperties(propInfo.PropertyType);
                }
                properties.Add(propInfo.Name, propSchema);
            }

            return properties;
        }

        /// <summary>
        /// types which can not be directly mapped to an swagger type
        /// </summary>
        /// <param name="t"></param>
        /// <returns></returns>
        private bool IsComplexType(Type t)
        {
            if (t == typeof(string) || t == typeof(char))
            {
                return false;
            }
            if (t == typeof(Int16) ||
                t == typeof(Int32) ||
                t == typeof(Int64) ||
                t == typeof(UInt16) ||
                t == typeof(UInt32) ||
                t == typeof(UInt64))
            {
                return false;
            }
            if (t == typeof(float) ||
                t == typeof(double) ||
                t == typeof(decimal))
            {
                return false;
            }
            if (t == typeof(bool))
            {
                return false;
            }
            if (t.IsEnum)
            {
                return false;
            }
            if (t == typeof(DateTime))
            {
                return false;
            }
            if (t == typeof(byte))
            {
                return false;
            }
            if (t.IsGenericType && t.GetGenericTypeDefinition() == typeof(Dictionary<,>))
            {
                return false;
            }
            if (t.IsArray)
            {
                return false;
            }
            if (t.GetInterfaces().Any(i => i.IsGenericType && i.GetGenericTypeDefinition() == typeof(IEnumerable<>)))
            {
                return false;
            }

            return true;
        }

        private Schema MapToSwaggerSchema(Type t)
        {
            if (t == typeof(string) || t == typeof(char))
            {
                return new Schema
                {
                    Type = "string"
                };
            }
            if (t == typeof(Int16) ||
                t == typeof(Int32) ||
                t == typeof(Int64) ||
                t == typeof(UInt16) ||
                t == typeof(UInt32) ||
                t == typeof(UInt64))
            {
                return new Schema
                {
                    Type = "integer",
                    Format = (t == typeof(Int32) ? "int32" : t == typeof(Int64) ? "int64" : "")
                };
            }
            if (t == typeof(float) ||
                t == typeof(double) ||
                t == typeof(decimal))
            {
                return new Schema
                {
                    Type = "number",
                    Format = (t == typeof(float) ? "float" : t == typeof(double) ? "double" : "")
                };
            }
            if (t == typeof(bool))
            {
                return new Schema
                {
                    Type = "boolean"
                };
            }
            if (t.IsEnum)
            {
                return new Schema
                {
                    Type = "string",
                    Enum = Enum.GetNames(t)
                };
            }
            if (t == typeof(DateTime))
            {
                return new Schema
                {
                    Type = "string",
                    Format = "date-time"
                };
            }
            if (t == typeof(byte))
            {
                return new Schema
                {
                    Type = "string",
                    Format = "byte"
                };
            }
            if (t.IsGenericType && t.GetGenericTypeDefinition() == typeof(Dictionary<,>))
            {//dictionary types
                return new Schema
                {
                    Type = "object",
                    AdditionalProperties = MapToSwaggerSchema(t.GetGenericArguments()[1])
                };
            }
            if (t.IsArray)
            {//array types
                return new Schema
                {
                    Type = "array",
                    Items = MapToSwaggerSchema(t.GetElementType())
                };
            }
            if (t.GetInterfaces().Any(i => i.IsGenericType && i.GetGenericTypeDefinition() == typeof(IEnumerable<>)))
            {//generic lists
                return new Schema
                {
                    Type = "array",
                    Items = MapToSwaggerSchema(t.GenericTypeArguments[0])
                };
            }
            if (t.GetInterfaces().Contains(typeof(IEnumerable)))
            {//untyped lists?
                throw new NotImplementedException();
            }

            //still here: we have a complex type
            //complex types are added to the swaggerDoc.Definitions part. just set an marker
            var s = new Schema
            {
                Ref = "#/definitions/" + EscapeGenericTypeString(t),
            };

            if (_typeListLocked && !_complexTypes.Contains(t))
            {//debug check
                throw new InvalidOperationException("Type discovery is finished but type is still unknown: " + t.Name);
            }

            return s;
        }


        private static string EscapeGenericTypeString(Type t)
        {
            if (t.IsGenericType)
            {
                var ret = t.Name.Substring(0, t.Name.IndexOf('`'));
                foreach (var gen in t.GenericTypeArguments)
                {
                    ret += "_" + EscapeGenericTypeString(gen) + "_";
                }
                return ret;
            }
            else
            { return t.Name; }
        }
    }

excerpt from application start point:

public class Startup
{
        public void ConfigureServices(IServiceCollection services)
        { 
           ....
           services
                .AddMvc()
                .SetCompatibilityVersion(CompatibilityVersion.Version_2_1);

            services.AddSimpleInjector(_container, options =>
            {
                options.AddAspNetCore();//enables AsyncScopedLifestyle.BeginScope for each request to the server
            });
            ConfigureSwagger.ConfigureServices(services, Bootstrapper.KnownQueryTypes, Bootstrapper.KnownCommandTypes, ServerBootstrapper.AppName);
        }

        public void Configure(IApplicationBuilder app, IHostingEnvironment env)
        {
             ...
             ConfigureSwagger.Configure(app, ServerBootstrapper.AppName);
             _container.Verify();
        }
}

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Projects
None yet
Development

No branches or pull requests

2 participants