Swashbuckle:需要使用不可空的属性

时间:2017-10-05 00:45:45

标签: c# asp.net-web-api asp.net-core swagger swashbuckle

在ASP.NET Core webapp中使用Swashbuckle.AspNetCore,我们有类似的响应类型:

public class DateRange
{
    [JsonConverter(typeof(IsoDateConverter))]
    public DateTime StartDate {get; set;}

    [JsonConverter(typeof(IsoDateConverter))]
    public DateTime EndDate {get; set;}
}

使用Swashbuckle发出swagger API JSON时,会变为:

{ ...

  "DateRange": {
    "type": "object",
    "properties": {
      "startDate": {
        "format": "date-time",
        "type": "string"
      },
      "endDate": {
        "format": "date-time",
        "type": "string"
      }
    }
  }
...
}

这里的问题是DateTime是一个值类型,永远不能为null;但是,发出的Swagger API JSON并没有将这2个属性标记为required。对于所有其他值类型,此行为是相同的:int,long,byte等 - 它们都被认为是可选的。

为了完成图片,我们将Swagger API JSON提供给dtsgenerator,以生成JSON响应模式的typescript接口。例如上面的类成为:

export interface DateRange {
    startDate?: string; // date-time
    endDate?: string; // date-time
}

这显然是不正确的。在深入研究这一点之后,我得出结论,dtsgenerator正在做出正确的事情,使得非必需属性在打字稿中可以为空。也许swagger规范需要明确支持可空与必需,但目前这两个是混合的。

我知道我可以为每个值类型属性添加[Required]属性,但这跨越多个项目和数百个类,是冗余信息,并且必须进行维护。所有非可空值类型属性都不能为null,因此将它们表示为可选项似乎不正确。

Web API,实体框架和Json.net都知道值类型属性不能是null;因此在使用这些库时不需要[Required]属性。

我正在寻找一种方法来自动标记我的swagger JSON中所需的所有非可空值类型以匹配此行为。

4 个答案:

答案 0 :(得分:2)

我找到了一个解决方案:我能够实现一个Swashbuckle ISchemaFilter。实施是:

/// <summary>
/// Makes all value-type properties "Required" in the schema docs, which is appropriate since they cannot be null.
/// </summary>
/// <remarks>
/// This saves effort + maintenance from having to add <c>[Required]</c> to all value type properties; Web API, EF, and Json.net already understand
/// that value type properties cannot be null.
/// 
/// More background on the problem solved by this type: https://stackoverflow.com/questions/46576234/swashbuckle-make-non-nullable-properties-required </remarks>
public sealed class RequireValueTypePropertiesSchemaFilter : ISchemaFilter
{
    private readonly CamelCasePropertyNamesContractResolver _camelCaseContractResolver;

    /// <summary>
    /// Initializes a new <see cref="RequireValueTypePropertiesSchemaFilter"/>.
    /// </summary>
    /// <param name="camelCasePropertyNames">If <c>true</c>, property names are expected to be camel-cased in the JSON schema.</param>
    /// <remarks>
    /// I couldn't figure out a way to determine if the swagger generator is using <see cref="CamelCaseNamingStrategy"/> or not;
    /// so <paramref name="camelCasePropertyNames"/> needs to be passed in since it can't be determined.
    /// </remarks>
    public RequireValueTypePropertiesSchemaFilter(bool camelCasePropertyNames)
    {
        _camelCaseContractResolver = camelCasePropertyNames ? new CamelCasePropertyNamesContractResolver() : null;
    }

    /// <summary>
    /// Returns the JSON property name for <paramref name="property"/>.
    /// </summary>
    /// <param name="property"></param>
    /// <returns></returns>
    private string PropertyName(PropertyInfo property)
    {
        return _camelCaseContractResolver?.GetResolvedPropertyName(property.Name) ?? property.Name;
    }

    /// <summary>
    /// Adds non-nullable value type properties in a <see cref="Type"/> to the set of required properties for that type.
    /// </summary>
    /// <param name="model"></param>
    /// <param name="context"></param>
    public void Apply(Schema model, SchemaFilterContext context)
    {
        foreach (var property in context.SystemType.GetProperties())
        {
            string schemaPropertyName = PropertyName(property);
            // This check ensures that properties that are not in the schema are not added as required.
            // This includes properties marked with [IgnoreDataMember] or [JsonIgnore] (should not be present in schema or required).
            if (model.Properties?.ContainsKey(schemaPropertyName) == true)
            {
                // Value type properties are required,
                // except: Properties of type Nullable<T> are not required.
                var propertyType = property.PropertyType;
                if (propertyType.IsValueType
                    && ! (propertyType.IsConstructedGenericType && (propertyType.GetGenericTypeDefinition() == typeof(Nullable<>))))
                {
                    // Properties marked with [Required] are already required (don't require it again).
                    if (! property.CustomAttributes.Any(attr =>
                                                        {
                                                            var t = attr.AttributeType;
                                                            return t == typeof(RequiredAttribute);
                                                        }))
                    {
                        // Make the value type property required
                        if (model.Required == null)
                        {
                            model.Required = new List<string>();
                        }
                        model.Required.Add(schemaPropertyName);
                    }
                }
            }
        }
    }
}

要使用,请在Startup课程中注册:

services.AddSwaggerGen(c =>
                        {
                            c.SwaggerDoc(c_swaggerDocumentName, new Info { Title = "Upfront API", Version = "1.0" });

                            c.SchemaFilter<RequireValueTypePropertiesSchemaFilter>(/*camelCasePropertyNames:*/ true);
                        });

这导致上面的DateRange类型变为:

{ ...
  "DateRange": {
    "required": [
      "startDate",
      "endDate"
    ],
    "type": "object",
    "properties": {
      "startDate": {
        "format": "date-time",
        "type": "string"
      },
      "endDate": {
        "format": "date-time",
        "type": "string"
      }
    }
  },
  ...
}

在swagger JSON架构中,并且:

export interface DateRange {
    startDate: string; // date-time
    endDate: string; // date-time
}

在dtsgenerator输出中。我希望这有助于其他人。

答案 1 :(得分:1)

让我建议基于json模式的解决方案。 该方案已在RFC中进行了说明,因此应像通用解决方案https://json-schema.org/latest/json-schema-validation.html#rfc.section.6.1.1

一样工作
public class AssignPropertyRequiredFilter : ISchemaFilter
{
    public void Apply(Schema schema, SchemaFilterContext context)
    {
        if (schema.Properties == null || schema.Properties.Count == 0)
        {
            return;
        }

        var typeProperties = context.SystemType.GetProperties(BindingFlags.Public | BindingFlags.Instance);
        foreach (var property in schema.Properties)
        {
            if (IsSourceTypePropertyNullable(typeProperties, property.Key))
            {
                continue;
            }

            // "null", "boolean", "object", "array", "number", or "string"), or "integer" which matches any number with a zero fractional part.
            // see also: https://json-schema.org/latest/json-schema-validation.html#rfc.section.6.1.1
            switch (property.Value.Type)
            {
                case "boolean":
                case "integer":
                case "number":
                    AddPropertyToRequired(schema, property.Key);
                    break;
                case "string":
                    switch (property.Value.Format)
                    {
                        case "date-time":
                        case "uuid":
                            AddPropertyToRequired(schema, property.Key);
                            break;
                    }
                    break;
            }
        }
    }

    private bool IsNullable(Type type)
    {
        return Nullable.GetUnderlyingType(type) != null;
    }

    private bool IsSourceTypePropertyNullable(PropertyInfo[] typeProperties, string propertyName)
    { 
        return typeProperties.Any(info => info.Name.Equals(propertyName, StringComparison.OrdinalIgnoreCase)
                                        && IsNullable(info.PropertyType));
    }

    private void AddPropertyToRequired(Schema schema, string propertyName)
    {
        if (schema.Required == null)
        {
            schema.Required = new List<string>();
        }

        if (!schema.Required.Contains(propertyName))
        {
            schema.Required.Add(propertyName);
        }
    }
}

答案 2 :(得分:1)

使用以下模式过滤器和Swashbuckle 5.4.1,我可以获得与接受的答案相同的效果:

public class RequireValueTypePropertiesSchemaFilter : ISchemaFilter
{
    private readonly HashSet<OpenApiSchema> _valueTypes = new HashSet<OpenApiSchema>();

    public void Apply(OpenApiSchema model, SchemaFilterContext context)
    {
        if (context.Type.IsValueType)
        {
            _valueTypes.Add(model);
        }

        if (model.Properties != null)
        {
            foreach (var prop in model.Properties)
            {
                if (_valueTypes.Contains(prop.Value))
                {
                    model.Required.Add(prop.Key);
                }
            }
        }
    }
}

这依赖于以下事实:必须先将ISchemaFilter应用于每个属性的简单模式,然后才能将其应用于包含这些属性的复杂模式-因此,我们要做的就是跟踪与之相关的简单模式到ValueType,然后如果以后遇到具有这些ValueType架构之一作为属性的架构,则可以将该属性名称标记为必需。

答案 3 :(得分:0)

或者你可以尝试这个

public class AssignPropertyRequiredFilter : ISchemaFilter {

    public void Apply(Schema schema, SchemaRegistry schemaRegistry, Type type) {

        var requiredProperties = type.GetProperties()
            .Where(x => x.PropertyType.IsValueType)
            .Select(t => char.ToLowerInvariant(t.Name[0]) + t.Name.Substring(1));

        if (schema.required == null) {
            schema.required = new List<string>();
        }
        schema.required = schema.required.Union(requiredProperties).ToList();
    }
}

并使用

services.AddSwaggerGen(c =>
{   
    ...
    c.SchemaFilter<AssignPropertyRequiredFilter>();
});