So in my last post I've shown very simple way to filter fields, that should not be shown in documentation. In real application we're using something more complicated, as we have different documentations for different consumers.

Now the problems with previous native approach was, that:
- it didn't allowed variations between different API versions - it didn't removed unused schemas, so in documentation it might be not visible, but in REST client generation using tools like nswag it might be annoying.

So to attack first problem, I've implemented to do it in two passes. The first pass would mark which properties need to be hidden and second pass would actually hide them. This was needed, because in SchemaFilter I didn't had ApiDescription, so I could not access ActionDescriptor to see in which API versions type is used and in OperationFilter I didn't had Type information, so I would need to bind schemas to types and I wished to avoid that.

First I've created attribute for marking fields to be hidden:

public class HideInDocumentationAttribute : Attribute
{
    private readonly string[] apiVersions;

    public HideInDocumentationAttribute(params string[] apiVersions)
    {
        this.apiVersions = apiVersions;
    }

    public string[] ApiVersions
    {
        get { return apiVersions; }
    }
}

And then I can created Schema Filter:

 public class EicsSchemaFilter : ISchemaFilter
{
    public const string HiddenInVersions = "hiddenInVersions";

    public void Apply(Schema schema, SchemaRegistry schemaRegistry, Type type)
    {
        var hiddenProperties = type.GetProperties(BindingFlags.Instance | BindingFlags.Public)
            .Where(x => x.GetCustomAttribute<HideInDocumentationAttribute>() != null)
            .ToDictionary(x => x.Name.ToLowerFirstLetter(), x => x.GetCustomAttribute<HideInDocumentationAttribute>().ApiVersions);                

        var machingProperties = schema.properties
            .Where(x => hiddenProperties.ContainsKey(x.Key))
            .ToList();

        foreach (var machingProperty in machingProperties)
        {
            machingProperty.Value.vendorExtensions.Add(HiddenInVersions, hiddenProperties[machingProperty.Key]);
        }            
    }
}

What it does is it goes through each processed type properties and marks them as hidden in vendor extensions.

And the second pass is performed in OperationFilter, main idea looks like this:

 private static void HideFieldDocumentation(SchemaRegistry schemaRegistry, ApiDescription apiDescription)
    {
        var operationVersions = apiDescription.ActionDescriptor.GetCustomAttributes<ApiVersionsAttribute>()
            .SelectMany(x => x.Versions)
            .ToList();

        foreach (var schema in schemaRegistry.Definitions)
        {
            var propertiesToHide = schema.Value.properties
                .Where(x => x.Value.vendorExtensions.ContainsKey(EicsSchemaFilter.HiddenInVersions))
                .ToDictionary(x => x.Key, x => x.Value);

            var propertiesToHideInVersion = propertiesToHide
                .Where(x => IsFieldHiddenInVersion(x, operationVersions))
                .Select(x => x.Key)
                .ToList();

            foreach (var propertyToHide in propertiesToHide)
            {
                propertyToHide.Value.vendorExtensions.Remove(EicsSchemaFilter.HiddenInVersions);
            }

            schema.Value.properties = schema.Value.properties
                .Where(x => !propertiesToHideInVersion.Contains(x.Key))
                .ToDictionary(x => x.Key, x => x.Value);
        }
    }

And the last part is for removing unused definitions, performed in DocumentFilter:

public void Apply(SwaggerDocument swaggerDoc, SchemaRegistry schemaRegistry, IApiExplorer apiExplorer)
    {           
        var usedTypes = new List<string>();

        foreach (var pathItem in swaggerDoc.paths.Values)
        {
            if (pathItem.post == null)
            {
                continue;
            }

            usedTypes.AddRange(pathItem.post.responses.Values
                .Where(x => x.schema != null)
                .Select(x => GetType(x.schema)));

            if (pathItem.post.parameters != null)
            {
                usedTypes.AddRange(pathItem.post.parameters.Select(x => GetType(x.schema)));
            }
        }

        usedTypes = usedTypes
            .Where(x => x != null)
            .Distinct()
            .ToList();            

        foreach (var usedType in usedTypes.ToList())
        {
            AddUsedTypesRecursively(usedType, schemaRegistry, usedTypes);                
        }

        var responseTypes = new HashSet<string>(usedTypes.Where(x => x != null).Select(GetKeyFromRef));

        var unusedDefinitions = schemaRegistry.Definitions
            .Where(x => !responseTypes.Contains(x.Key))
            .ToList();

        foreach (var schema in unusedDefinitions)
        {
            schemaRegistry.Definitions.Remove(schema.Key);
        }
    }

    private string GetType(Schema schema)
    {
        if (schema.@ref == null && schema.type == "array")
        {
            return schema.items.@ref;
        }

        return schema.@ref;
    }

    private void AddUsedTypesRecursively(string usedType, SchemaRegistry schemaRegistry, List<string> types)
    {            
        var key = GetKeyFromRef(usedType);

        if (!schemaRegistry.Definitions.ContainsKey(key))
        {
            return;
        }

        var type = schemaRegistry.Definitions[key];

        if (types.Contains(usedType))
        {
            return;
        }

        types.Add(usedType);

        foreach (var property in type.properties)
        {
            var propertyType = GetType(property.Value);
            if (propertyType != null && !types.Contains(GetKeyFromRef(propertyType)))
            {
                AddUsedTypesRecursively(propertyType, schemaRegistry, types);
            }
        }
    }
c#  webapi