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);
            }
        }
    }
Comments c#  webapi 

If you're creating public API, there's no question, that Swagger is one of the best ways to create documentation for it. If you're using WebApi, swashbuckle is one of the most popular libraries to use for creating API description in Swagger 2.0 format.

Sometimes, you're trying to reuse same API for public/internal use and some of the fields are not important to public consumers, so it would be better not to show them (don't forget though, that security through obscurity is not a way to secure stuff, so even if you hide them in documentation, make sure about safe usage on backend).

Currently to do so, you have two ways:

1) Use Obsolete attribute, which is not the ideal way, because it means something different (and in our sonar configuration usage is a major issue)
2) Write own attribute and use it in SchemaFilter:

public class HideInDocumentationAttribute : Attribute
{         
}

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

        schema.properties = schema.properties
            .Where(x => !hiddenProperties.Contains(x.Key))
            .ToDictionary(x => x.Key, x => x.Value);          
    }
}   
Comments c#  webapi