I tried plenty of different alternatives, and none of them suited my needs.
According to GitHub issues, [SwaggerSchema(ReadOnly = true)]
doesn't seem to work; using a Schema processor doesn't work either, especially when dealing with OData controllers.
I eventually solved it by using an IDocumentFilter
.
Document Filter:
using Microsoft.AspNetCore.Mvc.ApiExplorer;
using Microsoft.AspNetCore.Mvc.ModelBinding.Metadata;
using Microsoft.OpenApi.Models;
using Swashbuckle.AspNetCore.SwaggerGen;
/// <summary>
/// Makes sure readonly parameters are not considered while creating/updating/deleting an entity
/// </summary>
public class SwaggerRemoveReadonlyParametersFromOData_DocumentFilter : IDocumentFilter
{
private const string BASE_URL = "/"; // You might need to change this
public void Apply(OpenApiDocument swaggerDoc, DocumentFilterContext context)
{
foreach (var apiDesc in context.ApiDescriptions)
{
// Ignore GET method (we DO want readonly parameters to be displayed, in this case)
if (apiDesc.HttpMethod == "GET")
continue;
var readonlyParameters = apiDesc.ParameterDescriptions.Where(p => ShouldBeFilteredOut(p, apiDesc.HttpMethod));
if (!readonlyParameters.Any())
continue;
// We need to modify the OpenApiDocument: changes to the DocumentFilterContext will only become
// visible after we manually reload the swagger.json file
var apiOperation = FindCorrespondingOperationInDocument(swaggerDoc, apiDesc);
foreach (var readonlyParameter in readonlyParameters)
{
var parameterToRemove = apiOperation.Parameters.Single(p => p.Name == readonlyParameter.Name);
apiOperation.Parameters.Remove(parameterToRemove);
}
}
}
private static bool ShouldBeFilteredOut(ApiParameterDescription parameter, string httpMethod)
{
// Only consider model binding
if (parameter.Source.DisplayName != "ModelBinding")
return false;
// Identify read only properties... (sometimes Swashbuckle/Swagger shows them anyway, especially in OData controllers)
if (parameter.ModelMetadata.IsReadOnly == true)
return true;
if (parameter.ModelMetadata is not DefaultModelMetadata modelMetadata)
return false;
// ...and properties/fields explicitly marked as readonly by decorating them with the [SwaggerReadOnly] attribute...
var readOnlyAttribute = modelMetadata.Attributes.Attributes.SingleOrDefault(a => a is SwaggerReadOnlyAttribute) as SwaggerReadOnlyAttribute;
// ...if relevant to the current HTTP method
if (readOnlyAttribute?.AppliesToHttpMethod(httpMethod) == true)
return true;
return false;
}
private OpenApiOperation FindCorrespondingOperationInDocument(OpenApiDocument swaggerDoc, ApiDescription apiDesc)
{
string pathUrl = BASE_URL + apiDesc.RelativePath;
OperationType httpMethod = GetOperationTypeFromHttpMethod(apiDesc.HttpMethod!);
OpenApiPathItem apiPath = swaggerDoc.Paths[pathUrl];
return apiPath.Operations[httpMethod];
}
private static OperationType GetOperationTypeFromHttpMethod(string httpMethod)
{
httpMethod = httpMethod.ToLower();
httpMethod = string.Concat(httpMethod[0].ToString().ToUpper(), httpMethod.AsSpan(1));
return Enum.Parse<OperationType>(httpMethod);
}
}
Attribute:
[AttributeUsage(AttributeTargets.Property | AttributeTargets.Field)]
public class SwaggerReadOnlyAttribute : Attribute
{
public string[] HttpMethods { get; set; }
public SwaggerReadOnlyAttribute() : this(["POST", "PUT", "PATCH"]) { }
public SwaggerReadOnlyAttribute(params string[] httpMethods)
{
HttpMethods = httpMethods;
}
public bool AppliesToHttpMethod(string? httpMethod)
{
if (httpMethod == null)
return true;
return HttpMethods.Contains(httpMethod.ToUpperInvariant());
}
}
Register your Document Filter with:
Services.AddSwaggerGen(options => options.DocumentFilter<SwaggerRemoveReadonlyParameters_DocumentFilter>());
Decorate your readonly properties like this:
public class MyEntity
{
[SwaggerReadOnly] // Hidden by the Document Filter
public Guid Id { get; set; }
public string Name { get; set; }
public string? Description { get; set; }
[SwaggerReadOnly] // Hidden by the Document Filter
public DateTime LastModified { get; set; }
// Automatically hidden by the Document Filter
public bool HasDescription => !string.IsNullOrEmpty(Description);
}
Alert
entity. – WeinbergAlert
entity. – Weinberg