-
Notifications
You must be signed in to change notification settings - Fork 25
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
Comments
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 |
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();
}
} |
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
would be working. But for this, a replacement for the new Asp.net core ApiExplorer is needed.
The text was updated successfully, but these errors were encountered: