Skip to content

Commit 9cbdf1a

Browse files
committed
* #1397 Add polymorphism support to generated OpenAPI json and UI using Swashbuckle:
- Added PolymorphicBaseClassAttribute by which base classes should be marked for OpenAPI polymorphism; - Supported OpenAPI docs generation using this attribute for independent module and common docs;
1 parent 0779722 commit 9cbdf1a

File tree

9 files changed

+175
-135
lines changed

9 files changed

+175
-135
lines changed

VirtoCommerce.Platform.Core/Common/AttributeExtensions.cs

+18-1
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
using System;
1+
using System;
22
using System.Linq;
33

44
namespace VirtoCommerce.Platform.Core.Common
@@ -21,5 +21,22 @@ public static string[] SplitString(this Attribute attribute, string original, ch
2121

2222
return result;
2323
}
24+
25+
/// <summary>
26+
/// Gets type attribute value https://stackoverflow.com/a/2656211/5907312
27+
/// </summary>
28+
/// <typeparam name="TAttribute">Attribute type</typeparam>
29+
/// <typeparam name="TValue">Value type</typeparam>
30+
/// <param name="type">Type for getting attribute</param>
31+
/// <param name="valueSelector">Attribute value selector</param>
32+
/// <returns>Attribute value</returns>
33+
public static TValue GetAttributeValue<TAttribute, TValue>(
34+
this Type type,
35+
Func<TAttribute, TValue> valueSelector)
36+
where TAttribute : Attribute
37+
{
38+
var att = type.GetCustomAttributes(typeof(TAttribute), true).FirstOrDefault() as TAttribute;
39+
return (att != null) ? valueSelector(att) : default(TValue);
40+
}
2441
}
2542
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
using System;
2+
3+
namespace VirtoCommerce.Platform.Core.Common
4+
{
5+
/// <summary>
6+
/// Class marked with this attribute is marked as a base class for OpenAPI polymorphism
7+
/// </summary>
8+
[AttributeUsage(AttributeTargets.Class)]
9+
public class PolymorphicBaseClassAttribute : Attribute
10+
{
11+
/// <summary>
12+
/// The property name used to store type information, in camelCase. By default: "type".
13+
/// </summary>
14+
public string DiscriminatorPropertyName { get; set; } = "type";
15+
}
16+
}

VirtoCommerce.Platform.Core/VirtoCommerce.Platform.Core.csproj

+1
Original file line numberDiff line numberDiff line change
@@ -78,6 +78,7 @@
7878
<Compile Include="Common\IValueObject.cs" />
7979
<Compile Include="Common\ObservableChangeTracker.cs" />
8080
<Compile Include="Common\PlatformVersion.cs" />
81+
<Compile Include="Common\PolymorphicBaseClassAttribute.cs" />
8182
<Compile Include="Common\PredicateBuilder.cs" />
8283
<Compile Include="Common\PrimaryKeyResolvingMap.cs" />
8384
<Compile Include="Common\SemanticVersion.cs" />
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,16 @@
1-
using System;
1+
using System;
22
using System.Collections.Generic;
33
using System.Linq;
44
using System.Reflection;
5-
using System.Text;
6-
using System.Threading.Tasks;
75

86
namespace VirtoCommerce.Platform.Data.Common
97
{
108
public static class AssemblyExtensions
119
{
1210
public static IEnumerable<Type> GetLoadableTypes(this Assembly assembly)
1311
{
14-
if (assembly == null) throw new ArgumentNullException("assembly");
12+
if (assembly == null)
13+
throw new ArgumentNullException("assembly");
1514
try
1615
{
1716
return assembly.GetTypes();
@@ -21,5 +20,34 @@ public static IEnumerable<Type> GetLoadableTypes(this Assembly assembly)
2120
return e.Types.Where(t => t != null);
2221
}
2322
}
23+
24+
/// <summary>
25+
/// Gets all assembly types with custom attribute specified.
26+
/// </summary>
27+
/// <param name="assembly">Assembly to get types</param>
28+
/// <param name="customAttributeType">Custom attribute type to check</param>
29+
/// <param name="inherited"></param>
30+
/// <returns></returns>
31+
public static IEnumerable<Type> GetTypesWithAttribute(this Assembly assembly, Type customAttributeType, bool inherited)
32+
{
33+
if (assembly == null)
34+
{
35+
throw new ArgumentNullException(nameof(assembly));
36+
}
37+
38+
if (customAttributeType == null)
39+
{
40+
throw new ArgumentNullException(nameof(customAttributeType));
41+
}
42+
43+
try
44+
{
45+
return assembly.GetTypes().Where(x => x.GetCustomAttributes(customAttributeType, inherited).Length > 0);
46+
}
47+
catch (ReflectionTypeLoadException e)
48+
{
49+
return e.Types.Where(x => x.GetCustomAttributes(customAttributeType, inherited).Length > 0);
50+
}
51+
}
2452
}
2553
}

VirtoCommerce.Platform.Web/Controllers/Api/PolyController.cs

-65
This file was deleted.
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
using System;
2+
using System.Collections.Generic;
3+
using System.Linq;
4+
using Swashbuckle.Swagger;
5+
using VirtoCommerce.Platform.Core.Common;
6+
7+
namespace VirtoCommerce.Platform.Web.Swagger
8+
{
9+
public class PolymorphismDocumentFilter : IDocumentFilter
10+
{
11+
private readonly Type[] _types;
12+
13+
public PolymorphismDocumentFilter(Type[] types)
14+
{
15+
_types = types;
16+
}
17+
18+
[CLSCompliant(false)]
19+
public void Apply(SwaggerDocument swaggerDoc, SchemaRegistry schemaRegistry, System.Web.Http.Description.IApiExplorer apiExplorer)
20+
{
21+
foreach (var type in _types)
22+
{
23+
RegisterSubClasses(schemaRegistry, type);
24+
}
25+
}
26+
27+
private static void RegisterSubClasses(SchemaRegistry schemaRegistry, Type abstractType)
28+
{
29+
var discriminatorName = abstractType.GetAttributeValue<PolymorphicBaseClassAttribute, string>(x => x.DiscriminatorPropertyName) ?? "type";
30+
31+
// Need to make first property character lower to avoid properties duplication because of case, as all properties in OpenApi spec are in camelCase
32+
discriminatorName = char.ToLowerInvariant(discriminatorName[0]) + discriminatorName.Substring(1);
33+
34+
var typeName = schemaRegistry.Definitions.ContainsKey(abstractType.FullName) ? abstractType.FullName : abstractType.FriendlyId();
35+
var parentSchema = schemaRegistry.Definitions[typeName];
36+
37+
//set up a discriminator property (it must be required)
38+
parentSchema.discriminator = discriminatorName;
39+
parentSchema.required = new List<string> { discriminatorName };
40+
41+
if (!parentSchema.properties.ContainsKey(discriminatorName))
42+
{
43+
parentSchema.properties.Add(discriminatorName, new Schema { type = "string" });
44+
}
45+
46+
//register all subclasses
47+
var derivedTypes = abstractType.Assembly
48+
.GetTypes()
49+
.Where(x => abstractType != x && abstractType.IsAssignableFrom(x));
50+
51+
foreach (var item in derivedTypes)
52+
{
53+
schemaRegistry.GetOrRegister(item);
54+
}
55+
}
56+
}
57+
}

VirtoCommerce.Platform.Web/Swagger/PolymorphismSchemaFilter.cs

+30-55
Original file line numberDiff line numberDiff line change
@@ -6,79 +6,54 @@
66
namespace VirtoCommerce.Platform.Web.Swagger
77
{
88

9-
public class PolymorphismSchemaFilter<T> : ISchemaFilter
9+
public class PolymorphismSchemaFilter : ISchemaFilter
1010
{
11-
private readonly Lazy<HashSet<Type>> derivedTypes = new Lazy<HashSet<Type>>(Init);
11+
private readonly Type[] _types;
12+
private readonly Lazy<HashSet<Type>> _derivedTypes;
1213

13-
private static HashSet<Type> Init()
14+
public PolymorphismSchemaFilter(Type[] types)
1415
{
15-
var abstractType = typeof(T);
16-
var dTypes = abstractType.Assembly
17-
.GetTypes()
18-
.Where(x => abstractType != x && abstractType.IsAssignableFrom(x));
16+
_types = types;
17+
_derivedTypes = new Lazy<HashSet<Type>>(Init);
18+
}
1919

20+
private HashSet<Type> Init()
21+
{
2022
var result = new HashSet<Type>();
2123

22-
foreach (var item in dTypes)
24+
var derivedTypes = _types.SelectMany(x =>
25+
x.Assembly
26+
.GetTypes()
27+
.Where(y => x != y && y.IsAssignableFrom(x)));
28+
29+
foreach (var item in derivedTypes)
30+
{
2331
result.Add(item);
32+
}
2433

2534
return result;
2635
}
2736

2837
[CLSCompliant(false)]
2938
public void Apply(Schema schema, SchemaRegistry schemaRegistry, Type type)
3039
{
31-
if (!derivedTypes.Value.Contains(type))
32-
return;
33-
34-
var clonedSchema = new Schema
40+
if (_derivedTypes.Value.Contains(type))
3541
{
36-
properties = schema.properties,
37-
type = schema.type,
38-
required = schema.required
39-
};
42+
var clonedSchema = new Schema
43+
{
44+
properties = schema.properties,
45+
type = schema.type,
46+
required = schema.required
47+
};
4048

41-
//schemaRegistry.Definitions[typeof(T).Name]; does not work correctly in SwashBuckle
42-
var parentSchema = new Schema { @ref = "#/definitions/" + typeof(T).Name };
49+
//schemaRegistry.Definitions[typeof(T).Name]; does not work correctly in SwashBuckle
50+
var parentSchema = new Schema { @ref = "#/definitions/" + type.BaseType.Name };
4351

44-
schema.allOf = new List<Schema> { parentSchema, clonedSchema };
52+
schema.allOf = new List<Schema> { parentSchema, clonedSchema };
4553

46-
//reset properties for they are included in allOf, should be null but code does not handle it
47-
schema.properties = new Dictionary<string, Schema>();
48-
}
49-
}
50-
51-
52-
public class PolymorphismDocumentFilter<T> : IDocumentFilter
53-
{
54-
[CLSCompliant(false)]
55-
public void Apply(SwaggerDocument swaggerDoc, SchemaRegistry schemaRegistry, System.Web.Http.Description.IApiExplorer apiExplorer)
56-
{
57-
RegisterSubClasses(schemaRegistry, typeof(T));
58-
}
59-
60-
61-
private static void RegisterSubClasses(SchemaRegistry schemaRegistry, Type abstractType)
62-
{
63-
const string discriminatorName = "type";
64-
65-
var typeName = schemaRegistry.Definitions.ContainsKey(abstractType.FullName) ? abstractType.FullName : abstractType.FriendlyId();
66-
var parentSchema = schemaRegistry.Definitions[typeName];
67-
68-
//set up a discriminator property (it must be required)
69-
parentSchema.discriminator = discriminatorName;
70-
parentSchema.required = new List<string> { discriminatorName };
71-
72-
if (!parentSchema.properties.ContainsKey(discriminatorName))
73-
parentSchema.properties.Add(discriminatorName, new Schema { type = "string" });
74-
75-
//register all subclasses
76-
var derivedTypes = abstractType.Assembly
77-
.GetTypes()
78-
.Where(x => abstractType != x && abstractType.IsAssignableFrom(x));
79-
80-
foreach (var item in derivedTypes)
81-
schemaRegistry.GetOrRegister(item);
54+
//reset properties for they are included in allOf, should be null but code does not handle it
55+
schema.properties = new Dictionary<string, Schema>();
56+
}
8257
}
8358
}
8459
}

0 commit comments

Comments
 (0)