Im currently trying to call a field on graphql-query from code, without using the http layer. In a test case I had success using this snippet inside of a field resolver. The breakpoint hits.
var newContext = new ResolveFieldContext(context);
var query = context.ParentType;
var ticketQueryField = query.GetField("getTickets");
await (Task) ticketQueryField.Resolver.Resolve(context);
So I think its possible to fill the copied ResolveFieldContext with my real needed fields/arguments and call it like this. But its very ... complicated to fill the ResolveFieldContext by hand. So maybe there is a easier way to create the context. Like:
var newContext = new ResolveFieldContext("query test { getTickets(id: 1) { number, title } }");
That would be really awesome and in my real scenario there a more then just field which I want to access with the generated query.
Why I want to use the Graph like this? The Batch-Loader which we are using inside the GraphQL-Types are perfect for our needs.
You can execute a GraphQL query without http by using the DocumentExecutor directly, and providing your own DocumentWriter if you want the data in a specific format. There is an extension method which returns JSON, but you can write your own.
https://github.com/graphql-dotnet/graphql-dotnet/blob/master/src/GraphQL.NewtonsoftJson/DocumentWriter.cs
This is an example test base class for testing queries:
https://github.com/graphql-dotnet/graphql-dotnet/blob/master/src/GraphQL.Tests/BasicQueryTestBase.cs
This is a console example that returns JSON, not using http.
using System;
using System.Collections.Generic;
using System.Linq;
using System.Security.Claims;
using System.Threading.Tasks;
using GraphQL;
using GraphQL.Authorization;
using GraphQL.SystemTextJson;
using GraphQL.Types;
using GraphQL.Validation;
using Microsoft.Extensions.DependencyInjection;
namespace BasicSample
{
internal class Program
{
[System.Diagnostics.CodeAnalysis.SuppressMessage("Style", "IDE1006:Naming Styles", Justification = "main")]
private static async Task Main()
{
using var serviceProvider = new ServiceCollection()
.AddSingleton<IAuthorizationEvaluator, AuthorizationEvaluator>()
.AddTransient<IValidationRule, AuthorizationValidationRule>()
.AddTransient(s =>
{
var authSettings = new AuthorizationSettings();
authSettings.AddPolicy("AdminPolicy", p => p.RequireClaim("role", "Admin"));
return authSettings;
})
.BuildServiceProvider();
string definitions = #"
type User {
id: ID
name: String
}
type Query {
viewer: User
users: [User]
}
";
var schema = Schema.For(definitions, builder => builder.Types.Include<Query>());
// remove claims to see the failure
var authorizedUser = new ClaimsPrincipal(new ClaimsIdentity(new[] { new Claim("role", "Admin") }));
string json = await schema.ExecuteAsync(_ =>
{
_.Query = "{ viewer { id name } }";
_.ValidationRules = serviceProvider
.GetServices<IValidationRule>()
.Concat(DocumentValidator.CoreRules);
_.RequestServices = serviceProvider;
_.UserContext = new GraphQLUserContext { User = authorizedUser };
});
Console.WriteLine(json);
}
}
/// <summary>
/// Custom context class that implements <see cref="IProvideClaimsPrincipal"/>.
/// </summary>
public class GraphQLUserContext : Dictionary<string, object>, IProvideClaimsPrincipal
{
/// <inheritdoc />
public ClaimsPrincipal User { get; set; }
}
/// <summary>
/// CLR type to map to the 'Query' graph type.
/// </summary>
public class Query
{
/// <summary>
/// Resolver for 'Query.viewer' field.
/// </summary>
[GraphQLAuthorize("AdminPolicy")]
public User Viewer() => new User { Id = Guid.NewGuid().ToString(), Name = "Quinn" };
/// <summary>
/// Resolver for 'Query.users' field.
/// </summary>
public List<User> Users() => new List<User> { new User { Id = Guid.NewGuid().ToString(), Name = "Quinn" } };
}
/// <summary>
/// CLR type to map to the 'User' graph type.
/// </summary>
public class User
{
/// <summary>
/// Resolver for 'User.id' field. Just a simple property.
/// </summary>
public string Id { get; set; }
/// <summary>
/// Resolver for 'User.name' field. Just a simple property.
/// </summary>
public string Name { get; set; }
}
}
I have a class like this:
public class Document
{
public int DocumentType{get;set;}
[Required]
public string Name{get;set;}
[Required]
public string Name2{get;set;}
}
Now if I put a [Required] data annotation on the Name and Name2 properties, then everything is ok and if Name or Name2 are empty, validation will throw an error.
But I want Name field only to be required if DocumentType is equal to 1
and Name2 only required if DocumentType is equal to 2 .
public class Document
{
public int DocumentType{get;set;}
[Required(Expression<Func<object, bool>>)]
public string Name{get;set;}
[Required(Expression<Func<object, bool>>)]
public string Name2{get;set;}
}
but I know I can't, it causes an error. What should I do for this requirement?
RequiredIf validation attribute
I've written a RequiredIfAttribute that requires a particular property value when a different property has a certain value (what you require) or when a different property has anything but a specific value.
This is the code that may help:
/// <summary>
/// Provides conditional validation based on related property value.
/// </summary>
[AttributeUsage(AttributeTargets.Property, AllowMultiple = false)]
public sealed class RequiredIfAttribute : ValidationAttribute
{
#region Properties
/// <summary>
/// Gets or sets the other property name that will be used during validation.
/// </summary>
/// <value>
/// The other property name.
/// </value>
public string OtherProperty { get; private set; }
/// <summary>
/// Gets or sets the display name of the other property.
/// </summary>
/// <value>
/// The display name of the other property.
/// </value>
public string OtherPropertyDisplayName { get; set; }
/// <summary>
/// Gets or sets the other property value that will be relevant for validation.
/// </summary>
/// <value>
/// The other property value.
/// </value>
public object OtherPropertyValue { get; private set; }
/// <summary>
/// Gets or sets a value indicating whether other property's value should match or differ from provided other property's value (default is <c>false</c>).
/// </summary>
/// <value>
/// <c>true</c> if other property's value validation should be inverted; otherwise, <c>false</c>.
/// </value>
/// <remarks>
/// How this works
/// - true: validated property is required when other property doesn't equal provided value
/// - false: validated property is required when other property matches provided value
/// </remarks>
public bool IsInverted { get; set; }
/// <summary>
/// Gets a value that indicates whether the attribute requires validation context.
/// </summary>
/// <returns><c>true</c> if the attribute requires validation context; otherwise, <c>false</c>.</returns>
public override bool RequiresValidationContext
{
get { return true; }
}
#endregion
#region Constructor
/// <summary>
/// Initializes a new instance of the <see cref="RequiredIfAttribute"/> class.
/// </summary>
/// <param name="otherProperty">The other property.</param>
/// <param name="otherPropertyValue">The other property value.</param>
public RequiredIfAttribute(string otherProperty, object otherPropertyValue)
: base("'{0}' is required because '{1}' has a value {3}'{2}'.")
{
this.OtherProperty = otherProperty;
this.OtherPropertyValue = otherPropertyValue;
this.IsInverted = false;
}
#endregion
/// <summary>
/// Applies formatting to an error message, based on the data field where the error occurred.
/// </summary>
/// <param name="name">The name to include in the formatted message.</param>
/// <returns>
/// An instance of the formatted error message.
/// </returns>
public override string FormatErrorMessage(string name)
{
return string.Format(
CultureInfo.CurrentCulture,
base.ErrorMessageString,
name,
this.OtherPropertyDisplayName ?? this.OtherProperty,
this.OtherPropertyValue,
this.IsInverted ? "other than " : "of ");
}
/// <summary>
/// Validates the specified value with respect to the current validation attribute.
/// </summary>
/// <param name="value">The value to validate.</param>
/// <param name="validationContext">The context information about the validation operation.</param>
/// <returns>
/// An instance of the <see cref="T:System.ComponentModel.DataAnnotations.ValidationResult" /> class.
/// </returns>
protected override ValidationResult IsValid(object value, ValidationContext validationContext)
{
if (validationContext == null)
{
throw new ArgumentNullException("validationContext");
}
PropertyInfo otherProperty = validationContext.ObjectType.GetProperty(this.OtherProperty);
if (otherProperty == null)
{
return new ValidationResult(
string.Format(CultureInfo.CurrentCulture, "Could not find a property named '{0}'.", this.OtherProperty));
}
object otherValue = otherProperty.GetValue(validationContext.ObjectInstance);
// check if this value is actually required and validate it
if (!this.IsInverted && object.Equals(otherValue, this.OtherPropertyValue) ||
this.IsInverted && !object.Equals(otherValue, this.OtherPropertyValue))
{
if (value == null)
{
return new ValidationResult(this.FormatErrorMessage(validationContext.DisplayName));
}
// additional check for strings so they're not empty
string val = value as string;
if (val != null && val.Trim().Length == 0)
{
return new ValidationResult(this.FormatErrorMessage(validationContext.DisplayName));
}
}
return ValidationResult.Success;
}
}
Conditionally required property using data annotations
[RequiredIf(dependent Property name, dependent Property value)]
e.g.
[RequiredIf("Country", "Ethiopia")]
public string POBox{get;set;}
// POBox is required in Ethiopia
public string Country{get;set;}
[RequiredIf("destination", "US")]
public string State{get;set;}
// State is required in US
public string destination{get;set;}
public class RequiredIfAttribute : ValidationAttribute
{
RequiredAttribute _innerAttribute = new RequiredAttribute();
public string _dependentProperty { get; set; }
public object _targetValue { get; set; }
public RequiredIfAttribute(string dependentProperty, object targetValue)
{
this._dependentProperty = dependentProperty;
this._targetValue = targetValue;
}
protected override ValidationResult IsValid(object value, ValidationContext validationContext)
{
var field = validationContext.ObjectType.GetProperty(_dependentProperty);
if (field != null)
{
var dependentValue = field.GetValue(validationContext.ObjectInstance, null);
if ((dependentValue == null && _targetValue == null) || (dependentValue.Equals(_targetValue)))
{
if (!_innerAttribute.IsValid(value))
{
string name = validationContext.DisplayName;
string specificErrorMessage = ErrorMessage;
if (specificErrorMessage.Length < 1)
specificErrorMessage = $"{name} is required.";
return new ValidationResult(specificErrorMessage, new[] { validationContext.MemberName });
}
}
return ValidationResult.Success;
}
else
{
return new ValidationResult(FormatErrorMessage(_dependentProperty));
}
}
}
Out of the box I think this is still not possible.
But I found this promising article about Mvc.ValidationToolkit (also here, unfortunately this is only alpha, but you probably could also just extract the method(s) you need from this code and integrate it on your own), it contains the nice sounding attribute RequiredIf which seems to match exactly your cause:
you download the project from the linked zip and build it
get the built dll from your build folder and reference it in the project you are using
unfortunately this seems to require reference to MVC, too (easiest way to have that is starting an MVC-Project in VS or install-package Microsoft.AspNet.Mvc)
in the files where you want to use it, you add using Mvc.ValidationToolkit;
then you are able to write things like [RequiredIf("DocumentType", 2)] or [RequiredIf("DocumentType", 1)], so objects are valid if neither name or name2 are supplied as long as DocumentType is not equal to 1 or 2
Check out Fluent Validation
https://www.nuget.org/packages/FluentValidation/
Project Description
A small validation library for .NET that uses a fluent interface and lambda expressions for building validation rules for your business objects.
https://github.com/JeremySkinner/FluentValidation
I have always used implemented IValidatableObject from System.ComponentModel.DataAnnotations;
Example below
public IEnumerable<ValidationResult> Validate(ValidationContext validationContext)
{
if (this.SendInAppNotification)
{
if (string.IsNullOrEmpty(this.NotificationTitle) || string.IsNullOrWhiteSpace(this.NotificationTitle))
{
yield return new ValidationResult(
$"Notification Title is required",
new[] { nameof(this.NotificationTitle) });
}
}
check out the ExpressiveAnnotations .net library
Git reference
It has 'RequiredIf' and 'AssertThat' validation attributes
Check out MVC Foolproof validation. It has data annotation in model like RequiredIf (dependent Property, dependent value) if I remember correctly. You can download Foolproof from:
Visual Studio(2017) -> Tools -> Nuget Package Manager -> Manage Nuget Packages for Solution. Reference mvcfoolproof.unobtrusive.min.js in addition to the jquery files.
I solved this by extending the RequiredAttribute class, borrowing some logic from the CompareAttribute and Robert's excellent solution:
/// <summary>
/// Provides conditional <see cref="RequiredAttribute"/>
/// validation based on related property value.
/// </summary>
[AttributeUsage(AttributeTargets.Property, AllowMultiple = false)]
public sealed class RequiredIfAttribute : RequiredAttribute
{
/// <summary>
/// Gets or sets a value indicating whether other property's value should
/// match or differ from provided other property's value (default is <c>false</c>).
/// </summary>
public bool IsInverted { get; set; } = false;
/// <summary>
/// Gets or sets the other property name that will be used during validation.
/// </summary>
/// <value>
/// The other property name.
/// </value>
public string OtherProperty { get; private set; }
/// <summary>
/// Gets or sets the other property value that will be relevant for validation.
/// </summary>
/// <value>
/// The other property value.
/// </value>
public object OtherPropertyValue { get; private set; }
/// <summary>
/// Initializes a new instance of the <see cref="RequiredIfAttribute"/> class.
/// </summary>
/// <param name="otherProperty">The other property.</param>
/// <param name="otherPropertyValue">The other property value.</param>
public RequiredIfAttribute(string otherProperty, object otherPropertyValue)
: base()
{
OtherProperty = otherProperty;
OtherPropertyValue = otherPropertyValue;
}
protected override ValidationResult IsValid(
object value,
ValidationContext validationContext)
{
PropertyInfo otherPropertyInfo = validationContext
.ObjectType.GetProperty(OtherProperty);
if (otherPropertyInfo == null)
{
return new ValidationResult(
string.Format(
CultureInfo.CurrentCulture,
"Could not find a property named {0}.",
validationContext.ObjectType, OtherProperty));
}
// Determine whether to run [Required] validation
object actualOtherPropertyValue = otherPropertyInfo
.GetValue(validationContext.ObjectInstance, null);
if (!IsInverted && Equals(actualOtherPropertyValue, OtherPropertyValue) ||
IsInverted && !Equals(actualOtherPropertyValue, OtherPropertyValue))
{
return base.IsValid(value, validationContext);
}
return default;
}
}
Example usage:
public class Model {
public bool Subscribe { get; set; }
[RequiredIf(nameof(Subscribe), true)]
[DataType(DataType.EmailAddress)]
public string Email { get; set; }
}
This way, you get all the standard Required validation features.
N.B.: I am using .NET 5, but I tried to remove language features added in c# 9.0 for wider compatibility.
I wrote a simple custom validation attribute that it's very readable.
using System;
using System.ComponentModel.DataAnnotations;
namespace some.namespace
{
public class RequiredIfAttribute : ValidationAttribute
{
public string PropertyName { get; set; }
public object Value { get; set; }
public RequiredIfAttribute(string propertyName, object value = null, string errorMessage = "")
{
PropertyName = propertyName;
Value = value;
ErrorMessage = errorMessage;
}
protected override ValidationResult IsValid(object value, ValidationContext validationContext)
{
if (PropertyName == null || PropertyName.ToString() == "")
{
throw new Exception("RequiredIf: you have to indicate the name of the property to use in the validation");
}
var propertyValue = GetPropertyValue(validationContext);
if (HasPropertyValue(propertyValue) && (value == null || value.ToString() == ""))
{
return new ValidationResult(ErrorMessage);
}
else
{
return ValidationResult.Success;
}
}
private object GetPropertyValue(ValidationContext validationContext)
{
var instance = validationContext.ObjectInstance;
var type = instance.GetType();
return type.GetProperty(PropertyName).GetValue(instance);
}
private bool HasPropertyValue(object propertyValue)
{
if (Value != null)
{
return propertyValue != null && propertyValue.ToString() == Value.ToString();
}
else
{
return propertyValue != null && propertyValue.ToString() != "";
}
}
}
}
You can use it like this
public class Document
{
public int DocumentType{get;set;}
[RequiredIf("DocumentType", "1", ErrorMessage = "The field is required.")]
public string Name{get;set;}
[RequiredIf("DocumentType", "2", ErrorMessage = "The field is required.")]
public string Name2{get;set;}
}
I know this question is from a long time ago but someone asked in the comments section of Robert's answer how to use unobtrusive as part of the solution.
I wanted client side validation as well so I'm sharing my revised code to Robert's original code. It's essentially the same code except it implements IClientModelValidator and has an additional AddValidation method. The client validation still respects the IsInverted property.
Implement IClientModelValidator
public sealed class RequiredIfAttribute : ValidationAttribute, IClientModelValidator
New AddValidation method
public void AddValidation(ClientModelValidationContext context)
{
var viewContext = context.ActionContext as ViewContext;
var modelType = context.ModelMetadata.ContainerType;
var instance = viewContext?.ViewData.Model;
var model = instance?.GetType().Name == modelType.Name
? instance
: instance?.GetType()?.GetProperties().First(x => x.PropertyType.Name == modelType.Name)
.GetValue(instance, null);
object otherValue = modelType.GetProperty(this.OtherProperty)?.GetValue(model, null);
object value = modelType.GetProperty(context.ModelMetadata.Name)?.GetValue(model, null);
string displayName = context.ModelMetadata.DisplayName ?? context.ModelMetadata.Name;
string errorMessage = null;
// check if this value is actually required and validate it
if (!this.IsInverted && object.Equals(otherValue, this.OtherPropertyValue) ||
this.IsInverted && !object.Equals(otherValue, this.OtherPropertyValue))
{
if (value == null)
{
errorMessage = this.FormatErrorMessage(displayName);
}
// additional check for strings so they're not empty
string val = value as string;
if (val != null && val.Trim().Length == 0)
{
errorMessage = this.FormatErrorMessage(displayName);
}
}
if (!string.IsNullOrWhiteSpace(errorMessage))
{
context.Attributes.Add("data-val", "true");
context.Attributes.Add("data-val-required", errorMessage);
}
}
Full Code
/// <summary>
/// Provides conditional validation based on related property value.
/// </summary>
[AttributeUsage(AttributeTargets.Property, AllowMultiple = false)]
public sealed class RequiredIfAttribute : ValidationAttribute, IClientModelValidator
{
#region Properties
/// <summary>
/// Gets or sets the other property name that will be used during validation.
/// </summary>
/// <value>
/// The other property name.
/// </value>
public string OtherProperty { get; private set; }
/// <summary>
/// Gets or sets the display name of the other property.
/// </summary>
/// <value>
/// The display name of the other property.
/// </value>
public string OtherPropertyDisplayName { get; set; }
/// <summary>
/// Gets or sets the other property value that will be relevant for validation.
/// </summary>
/// <value>
/// The other property value.
/// </value>
public object OtherPropertyValue { get; private set; }
/// <summary>
/// Gets or sets a value indicating whether other property's value should match or differ from provided other property's value (default is <c>false</c>).
/// </summary>
/// <value>
/// <c>true</c> if other property's value validation should be inverted; otherwise, <c>false</c>.
/// </value>
/// <remarks>
/// How this works
/// - true: validated property is required when other property doesn't equal provided value
/// - false: validated property is required when other property matches provided value
/// </remarks>
public bool IsInverted { get; set; }
/// <summary>
/// Gets a value that indicates whether the attribute requires validation context.
/// </summary>
/// <returns><c>true</c> if the attribute requires validation context; otherwise, <c>false</c>.</returns>
public override bool RequiresValidationContext
{
get { return true; }
}
#endregion
#region Constructor
/// <summary>
/// Initializes a new instance of the <see cref="RequiredIfAttribute"/> class.
/// </summary>
/// <param name="otherProperty">The other property.</param>
/// <param name="otherPropertyValue">The other property value.</param>
public RequiredIfAttribute(string otherProperty, object otherPropertyValue)
: base("'{0}' is required because '{1}' has a value {3}'{2}'.")
{
this.OtherProperty = otherProperty;
this.OtherPropertyValue = otherPropertyValue;
this.IsInverted = false;
}
#endregion
public void AddValidation(ClientModelValidationContext context)
{
var viewContext = context.ActionContext as ViewContext;
var modelType = context.ModelMetadata.ContainerType;
var instance = viewContext?.ViewData.Model;
var model = instance?.GetType().Name == modelType.Name
? instance
: instance?.GetType()?.GetProperties().First(x => x.PropertyType.Name == modelType.Name)
.GetValue(instance, null);
object otherValue = modelType.GetProperty(this.OtherProperty)?.GetValue(model, null);
object value = modelType.GetProperty(context.ModelMetadata.Name)?.GetValue(model, null);
string displayName = context.ModelMetadata.DisplayName ?? context.ModelMetadata.Name;
string errorMessage = null;
// check if this value is actually required and validate it
if (!this.IsInverted && object.Equals(otherValue, this.OtherPropertyValue) ||
this.IsInverted && !object.Equals(otherValue, this.OtherPropertyValue))
{
if (value == null)
{
errorMessage = this.FormatErrorMessage(displayName);
}
// additional check for strings so they're not empty
string val = value as string;
if (val != null && val.Trim().Length == 0)
{
errorMessage = this.FormatErrorMessage(displayName);
}
}
if (!string.IsNullOrWhiteSpace(errorMessage))
{
context.Attributes.Add("data-val", "true");
context.Attributes.Add("data-val-required", errorMessage);
}
}
/// <summary>
/// Applies formatting to an error message, based on the data field where the error occurred.
/// </summary>
/// <param name="name">The name to include in the formatted message.</param>
/// <returns>
/// An instance of the formatted error message.
/// </returns>
public override string FormatErrorMessage(string name)
{
return string.Format(
CultureInfo.CurrentCulture,
base.ErrorMessageString,
name,
this.OtherPropertyDisplayName ?? this.OtherProperty,
this.OtherPropertyValue,
this.IsInverted ? "other than " : "of ");
}
/// <summary>
/// Validates the specified value with respect to the current validation attribute.
/// </summary>
/// <param name="value">The value to validate.</param>
/// <param name="validationContext">The context information about the validation operation.</param>
/// <returns>
/// An instance of the <see cref="T:System.ComponentModel.DataAnnotations.ValidationResult" /> class.
/// </returns>
protected override ValidationResult IsValid(object value, ValidationContext validationContext)
{
if (validationContext == null)
{
throw new ArgumentNullException("validationContext");
}
PropertyInfo otherProperty = validationContext.ObjectType.GetProperty(this.OtherProperty);
if (otherProperty == null)
{
return new ValidationResult(
string.Format(CultureInfo.CurrentCulture, "Could not find a property named '{0}'.", this.OtherProperty));
}
object otherValue = otherProperty.GetValue(validationContext.ObjectInstance);
// check if this value is actually required and validate it
if (!this.IsInverted && object.Equals(otherValue, this.OtherPropertyValue) ||
this.IsInverted && !object.Equals(otherValue, this.OtherPropertyValue))
{
if (value == null)
{
return new ValidationResult(this.FormatErrorMessage(validationContext.DisplayName));
}
// additional check for strings so they're not empty
string val = value as string;
if (val != null && val.Trim().Length == 0)
{
return new ValidationResult(this.FormatErrorMessage(validationContext.DisplayName));
}
}
return ValidationResult.Success;
}
}
This should just work, provided you have included jquery.js, jquery.validate.js and jquery.validate.unobtrusive.js script files (in that order) to your layout or razor view.
I can't give you exactly what you're asking for, but have you considered something like the following?
public abstract class Document // or interface, whichever is appropriate for you
{
//some non-validted common properties
}
public class ValidatedDocument : Document
{
[Required]
public string Name {get;set;}
}
public class AnotherValidatedDocument : Document
{
[Required]
public string Name {get;set;}
//I would suggest finding a descriptive name for this instead of Name2,
//Name2 doesn't make it clear what it's for
public string Name2 {get;set;}
}
public class NonValidatedDocument : Document
{
public string Name {get;set;}
}
//Etc...
Justification being the int DocumentType variable. You could replace this with using concrete subclass types for each "type" of document you need to deal with. Doing this gives you much better control of your property annotations.
It also appears that only some of your properties are needed in different situations, which could be a sign that your document class is trying to do too much, and supports the suggestion above.
I used https://learn.microsoft.com/en-us/aspnet/core/tutorials/getting-started-with-swashbuckle?view=aspnetcore-2.1&tabs=visual-studio#xml-comments to show my classes summaries description in SwaggerUI, it's OK but not show enum summary description !
My startup.cs
services.AddSwaggerGen(c =>
{
c.SwaggerDoc("v1", new Info
{
Version = "v1",
Title = "My App-Service",
Description = "My Description",
});
c.IncludeXmlComments(Path.Combine(AppContext.BaseDirectory, $"{Assembly.GetExecutingAssembly().GetName().Name}.xml"));
c.DescribeAllEnumsAsStrings();
});
My enum:
public enum GenderEnum
{
/// <summary>
/// Man Description
/// </summary>
Man = 1,
/// <summary>
/// Woman Description
/// </summary>
Woman = 2
}
It shows something like following:
I want to show Man Description and Woman Description in SwaggerUI
like this:
Man = 1, Man Description
Woman = 2, Woman Description
I'm using Swashbuckle.AspNetCore v4.0.1 package
As of June/2021 OpenApi now supports this and you can extend Swagger to show the details. Here is my code for C# on .NET 5.0.
First define the schema filter in a file (call it DescribeEnumMembers.cs and be sure to change YourNamespace to the name of your namespace):
using System;
using System.Text;
using System.Xml.Linq;
using System.Xml.XPath;
using Microsoft.OpenApi.Models;
using Swashbuckle.AspNetCore.SwaggerGen;
namespace YourNamespace
{
/// <summary>
/// Swagger schema filter to modify description of enum types so they
/// show the XML docs attached to each member of the enum.
/// </summary>
public class DescribeEnumMembers: ISchemaFilter
{
private readonly XDocument mXmlComments;
/// <summary>
/// Initialize schema filter.
/// </summary>
/// <param name="argXmlComments">Document containing XML docs for enum members.</param>
public DescribeEnumMembers(XDocument argXmlComments)
=> mXmlComments = argXmlComments;
/// <summary>
/// Apply this schema filter.
/// </summary>
/// <param name="argSchema">Target schema object.</param>
/// <param name="argContext">Schema filter context.</param>
public void Apply(OpenApiSchema argSchema, SchemaFilterContext argContext) {
var EnumType = argContext.Type;
if(!EnumType.IsEnum) return;
var sb = new StringBuilder(argSchema.Description);
sb.AppendLine("<p>Possible values:</p>");
sb.AppendLine("<ul>");
foreach(var EnumMemberName in Enum.GetNames(EnumType)) {
var FullEnumMemberName = $"F:{EnumType.FullName}.{EnumMemberName}";
var EnumMemberDescription = mXmlComments.XPathEvaluate(
$"normalize-space(//member[#name = '{FullEnumMemberName}']/summary/text())"
) as string;
if(string.IsNullOrEmpty(EnumMemberDescription)) continue;
sb.AppendLine($"<li><b>{EnumMemberName}</b>: {EnumMemberDescription}</li>");
}
sb.AppendLine("</ul>");
argSchema.Description = sb.ToString();
}
}
}
Then enable it in your ASP.NET ConfigureServices() method. Here is my code after snipping out the parts that don't matter for this exercise:
public void ConfigureServices(IServiceCollection argServices) {
// ...<snip other code>
argServices.AddSwaggerGen(SetSwaggerGenOptions);
// ...<snip other code>
return;
// ...<snip other code>
void SetSwaggerGenOptions(SwaggerGenOptions argOptions) {
// ...<snip other code>
AddXmlDocs();
return;
void AddXmlDocs() {
// generate paths for the XML doc files in the assembly's directory.
var XmlDocPaths = Directory.GetFiles(
path: AppDomain.CurrentDomain.BaseDirectory,
searchPattern: "YourAssemblyNameHere*.xml"
);
// load the XML docs for processing.
var XmlDocs = (
from DocPath in XmlDocPaths select XDocument.Load(DocPath)
).ToList();
// ...<snip other code>
// add pre-processed XML docs to Swagger.
foreach(var doc in XmlDocs) {
argOptions.IncludeXmlComments(() => new XPathDocument(doc.CreateReader()), true);
// apply schema filter to add description of enum members.
argOptions.SchemaFilter<DescribeEnumMembers>(doc);
}
}
}
}
Remember to change "YourAssemblyNameHere*.xml" to match your assembly name. The important line that enables the schema filter is:
argOptions.SchemaFilter<DescribeEnumMembers>(doc);
...which MUST be called AFTER the following line:
argOptions.IncludeXmlComments(() => new XPathDocument(doc.CreateReader()), true);
Using the above code, if you have an enum type defined like this for example:
/// <summary>
/// Setting to control when a no-match report is returned when searching.
/// </summary>
public enum NoMatchReportSetting
{
/// <summary>
/// Return no-match report only if the search query has no match.
/// </summary>
IfNoMatch = 0,
/// <summary>
/// Always return no-match report even if the search query has a match.
/// </summary>
Always = 1,
/// <summary>
/// Never return no-match report even if search query has no match.
/// </summary>
No = 99
}
The Swagger documentation will end up showing a description of each enum member as part of the description of the enum type itself:
This solution allows for
Show underlying value as well as name/description
Handle multiple xml documentation files, but only process docs once.
Customization of the layout without code change
Here's the class...
using System;
using System.Collections;
using System.Linq;
using System.Text;
using System.Xml.Linq;
using System.Xml.XPath;
using Microsoft.OpenApi.Models;
using Swashbuckle.AspNetCore.SwaggerGen;
namespace YourNamespace
{
/// <summary>
/// Swagger schema filter to modify description of enum types so they
/// show the XML docs attached to each member of the enum.
/// </summary>
public class DescribeEnumMembers : ISchemaFilter
{
private readonly XDocument xmlComments;
private readonly string assemblyName;
/// <summary>
/// Initialize schema filter.
/// </summary>
/// <param name="xmlComments">Document containing XML docs for enum members.</param>
public DescribeEnumMembers(XDocument xmlComments)
{
this.xmlComments = xmlComments;
this.assemblyName = DetermineAssembly(xmlComments);
}
/// <summary>
/// Pre-amble to use before the enum items
/// </summary>
public static string Prefix { get; set; } = "<p>Possible values:</p>";
/// <summary>
/// Format to use, 0 : value, 1: Name, 2: Description
/// </summary>
public static string Format { get; set; } = "<b>{0} - {1}</b>: {2}";
/// <summary>
/// Apply this schema filter.
/// </summary>
/// <param name="schema">Target schema object.</param>
/// <param name="context">Schema filter context.</param>
public void Apply(OpenApiSchema schema, SchemaFilterContext context)
{
var type = context.Type;
// Only process enums and...
if (!type.IsEnum)
{
return;
}
// ...only the comments defined in their origin assembly
if (type.Assembly.GetName().Name != assemblyName)
{
return;
}
var sb = new StringBuilder(schema.Description);
if (!string.IsNullOrEmpty(Prefix))
{
sb.AppendLine(Prefix);
}
sb.AppendLine("<ul>");
// TODO: Handle flags better e.g. Hex formatting
foreach (var name in Enum.GetValues(type))
{
// Allows for large enums
var value = Convert.ToInt64(name);
var fullName = $"F:{type.FullName}.{name}";
var description = xmlComments.XPathEvaluate(
$"normalize-space(//member[#name = '{fullName}']/summary/text())"
) as string;
sb.AppendLine(string.Format("<li>" + Format + "</li>", value, name, description));
}
sb.AppendLine("</ul>");
schema.Description = sb.ToString();
}
private string DetermineAssembly(XDocument doc)
{
var name = ((IEnumerable)doc.XPathEvaluate("/doc/assembly")).Cast<XElement>().ToList().FirstOrDefault();
return name?.Value;
}
}
}
and utilization...
services.AddSwaggerGen(c =>
{
...
// See https://github.com/domaindrivendev/Swashbuckle/issues/86
var dir = new DirectoryInfo(AppContext.BaseDirectory);
foreach (var fi in dir.EnumerateFiles("*.xml"))
{
var doc = XDocument.Load(fi.FullName);
c.IncludeXmlComments(() => new XPathDocument(doc.CreateReader()), true);
c.SchemaFilter<DescribeEnumMembers>(doc);
}
});
This then reports as
Unfortunately this does not seem to be supported in the OpenAPI specification. There is an open Github issue for this.
There is also an open Github issue for Swashbuckle.
However, in this issue there is a workaround to create a schema filter, which at least shows the enum value comments in the enum type description.
I solved this using a description attribute. Here is an example usage:
public enum GenderEnum
{
[Description("Man Description")]
Man = 1,
[Description("Woman Description")]
Woman = 2
}
Is there a way to add model information, like valid values, default values, summary, and other remarks into the swagger output?
For instance in c# how would I add the following comments and attributes into swagger?
/// <summary>
/// A clear summary
/// </summary>
/// <remarks>
/// Some remarks
/// </remarks>
public class A
{
public A()
{
_Field_A = 0;
_Field_B = string.Empty;
}
private int _Field_A { get; set; }
[Range(0, 150)]
public int Field_A
{
get
{
return _Field_A;
}
set
{
if (value != null) { _Field_A = value; }
}
}
private string _Field_B { get; set; }
/// <summary>
/// Field_B summary
/// </summary>
public string Field_B
{
get
{
return _Field_B;
}
set
{
if (value != null) { _Field_B = value; }
}
}
}
You will need to enable XML documentation file creation in your project properties:
Project Properties > Build > Check the XML Documentation File box
Then you can uncomment or add the following line to your SwaggerConfig.cs file:
c.IncludeXmlComments(GetXmlCommentsPath());
According to the Swashbuckle github, you can enable XML comments which will allow you to add the metadata accordingly.
httpConfiguration
.EnableSwagger(c =>
{
c.SingleApiVersion("v1", "A title for your API");
c.IncludeXmlComments(GetXmlCommentsPathForControllers());
c.IncludeXmlComments(GetXmlCommentsPathForModels());
});
I am a newbie to C#. I have a java REST service which returns a xml response and I am trying to deserialize the xml document using C# XmlSerializer. A sample xml document response is pasted below.
<?xml version="1.0" encoding="UTF-8"
standalone="yes" ?> <ns2:Document
xmlns:ns2="http://hxps.honeywell.com/model/impl"
xmlns:ns3="http://hxps.honeywell.com/datatypes/impl"
type="PS">
<docId>SamplePSDocument1</docId>
<fields
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:ns5="http://jaxb.dev.java.net/array"
xsi:type="ns5:anyTypeArray"> <item
xsi:type="ns3:scalarFieldImpl"
key="Name">
<value xmlns:xs="http://www.w3.org/2001/XMLSchema"
xsi:type="xs:string">HXPS A4</value>
</item> <item
xsi:type="ns3:scalarFieldImpl"
key="Creation Date">
<value xmlns:xs="http://www.w3.org/2001/XMLSchema"
xsi:type="xs:string">20 April
2007</value> </item> </fields>
<id>fb92f871-1f3d-4fa4-ba24-5ae3af0a493f</id>
<revision>1-c75f688e212fb5341ebdbd22a3867c14</revision>
- <version> <majorVersionNumber>1</majorVersionNumber>
<minorVerisonNumber>5</minorVerisonNumber>
</version> </ns2:document>
It works fine when I deserialize this xml document into Document object. My document class is pasted below
[System.SerializableAttribute()]
[System.Xml.Serialization.XmlTypeAttribute(Namespace = "http://hxps.honeywell.com/model/impl", TypeName = "PSDocument")]
[System.Xml.Serialization.SoapTypeAttribute(Namespace = "http://hxps.honeywell.com/model/impl", TypeName = "PSDocument")]
[System.Xml.Serialization.XmlRootAttribute(Namespace = "http://hxps.honeywell.com/model/impl", ElementName = "Document")]
public partial class PSDocument
{
private Com.Honeywell.Hxps.Sdk.Model.DocumentType _type;
private string _description;
private string _displayName;
private string _docId;
private Com.Honeywell.Hxps.Sdk.Model.Impl.VersionImpl _version;
private object _fields;
private string _revision;
private string _id;
/// <summary>
/// (no documentation provided)
/// </summary>
[System.Xml.Serialization.XmlAttributeAttribute(AttributeName = "type")]
[System.Xml.Serialization.SoapAttributeAttribute(AttributeName = "type")]
public Com.Honeywell.Hxps.Sdk.Model.DocumentType Type
{
get
{
return this._type;
}
set
{
this._type = value;
}
}
/// <summary>
/// Property for the XML serializer indicating whether the "Type" property should be included in the output.
/// </summary>
[System.Xml.Serialization.XmlIgnoreAttribute]
[System.Xml.Serialization.SoapIgnoreAttribute]
[System.ComponentModel.EditorBrowsable(System.ComponentModel.EditorBrowsableState.Never)]
public bool TypeSpecified
{
get
{
return this._type != Com.Honeywell.Hxps.Sdk.Model.DocumentType.NULL;
}
set
{
if (!value)
{
this._type = Com.Honeywell.Hxps.Sdk.Model.DocumentType.NULL;
}
}
}
/// <summary>
/// (no documentation provided)
/// </summary>
[System.Xml.Serialization.XmlElementAttribute(ElementName = "description", Namespace = "")]
[System.Xml.Serialization.SoapElementAttribute(ElementName = "description")]
public string Description
{
get
{
return this._description;
}
set
{
this._description = value;
}
}
/// <summary>
/// (no documentation provided)
/// </summary>
[System.Xml.Serialization.XmlElementAttribute(ElementName = "displayName", Namespace = "")]
[System.Xml.Serialization.SoapElementAttribute(ElementName = "displayName")]
public string DisplayName
{
get
{
return this._displayName;
}
set
{
this._displayName = value;
}
}
/// <summary>
/// (no documentation provided)
/// </summary>
[System.Xml.Serialization.XmlElementAttribute(ElementName = "docId", Namespace = "")]
[System.Xml.Serialization.SoapElementAttribute(ElementName = "docId")]
public string DocId
{
get
{
return this._docId;
}
set
{
this._docId = value;
}
}
/// <summary>
/// (no documentation provided)
/// </summary>
[System.Xml.Serialization.XmlElementAttribute(ElementName = "version", Namespace = "")]
[System.Xml.Serialization.SoapElementAttribute(ElementName = "version")]
public Com.Honeywell.Hxps.Sdk.Model.Impl.VersionImpl Version
{
get
{
return this._version;
}
set
{
this._version = value;
}
}
/// <summary>
/// (no documentation provided)
/// </summary>
[System.Xml.Serialization.XmlElementAttribute(ElementName = "fields", Namespace = "")]
[System.Xml.Serialization.SoapElementAttribute(ElementName = "fields")]
public object Fields
{
get
{
return this._fields;
}
set
{
this._fields = value;
}
}
/// <summary>
/// (no documentation provided)
/// </summary>
[System.Xml.Serialization.XmlElementAttribute(ElementName = "revision", Namespace = "")]
[System.Xml.Serialization.SoapElementAttribute(ElementName = "revision")]
public string Revision
{
get
{
return this._revision;
}
set
{
this._revision = value;
}
}
/// <summary>
/// (no documentation provided)
/// </summary>
[System.Xml.Serialization.XmlElementAttribute(ElementName = "id", Namespace = "")]
[System.Xml.Serialization.SoapElementAttribute(ElementName = "id")]
public string Id
{
get
{
return this._id;
}
set
{
this._id = value;
}
}
}
}
In my main program, I get an array of xmlNodes when I try
Array fields = (Array)doc.Fields;
In the server side java REST service implementation, fields is actually a arraylist which will contain instances of three implementations of an interface. (List may contain ScalarFieldImpl or ArrayFieldImpl which are custom business objects).
I want to deserialize this xml fields into ScalarFieldImpl or ArrayFieldImpl using XmlSerializer. I want to know whether it is possible? If so, how do I do that?
As far as I know this isn't possible with the ootb xml (de-)serializer, I think you'll have to write your own XmlSerializer. Or try to use LINQ to XML.
I got this working. I did few changes in my PSDocument class.
private object _fields; -> private ArrayList _fields;
Also I changed get/set method and added new metadata
[System.Xml.Serialization.XmlArray(ElementName = "fields", Namespace = "")]
[System.Xml.Serialization.XmlArrayItem(ElementName="item")]
public ArrayList Fields
{
get
{
return this._fields;
}
set
{
this._fields = value;
}
}
Previously I had the following lines in my PSDocument class
[System.Xml.Serialization.XmlElementAttribute(ElementName = "fields", Namespace = "")]
[System.Xml.Serialization.SoapElementAttribute(ElementName = "fields")]
public object Fields
{
get
{
return this._fields;
}
set
{
this._fields = value;
}
}
So while deserializing fields in PSDocument, I get an arraylist with items as elements.
Hope this helps someone.