I notice weird behavior in the Serialization.
Though, I have the settings
var SerializerSettings = new JsonSerializerSettings() {
NullValueHandling = NullValueHandling.Ignore, DefaultValueHandling= DefaultValueHandling.Ignore}
SerializerSettings.Converters.Add(new JsonArrayToNullConverter());
var val = new company()
{
name = "Bobo Company Renamed"
}
var str = JsonConvert.SerializeObject(val, SerializerSettings );
The result would be:
{"document_type":2,"locations":null, ...
Without the custom converter, it would be
{"document_type":2,"locations":[], ...
you get the point?
But, since it becomes null, it -should- listen to NullValueHandling = NullValueHandling.Ignore
but obviously, Newton, looks at the object value to be serialized, not at the issued writer.WriteNull();
:(
Any workaround? I've spent some hours on this. Thanks!
using Newtonsoft.Json;
using System;
using System.Collections;
using System.Collections.Generic;
namespace Company.Model.TypeConverters
{
/// <summary>
/// undo's the forced array creation because OData needs it, but for PATCH, we want no [] empty collections, it is considered to be a
/// </summary>
public class JsonArrayToNullConverter : JsonConverter
{
public override bool CanConvert(Type objectType)
{
var canConvert = objectType.IsArray || (objectType.IsGenericType && objectType.GetGenericTypeDefinition().IsAssignableFrom(typeof(IEnumerable<>)));
return canConvert;
}
public override bool CanRead
{
get
{
return false;
}
}
public override bool CanWrite
{
get
{
return true;
}
}
public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer)
{
throw new NotImplementedException();
}
public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer)
{
var enumerator = (IEnumerable)value;
if (enumerator != null && enumerator.GetEnumerator().MoveNext() == false)
{
writer.WriteNull();
//value = null;
return;
}
serializer.Serialize(writer, value);
}
}
}
The solution seemed to be using a contractresolver and set that on the JsonSerializerSettings.
public class NullToEmptyListResolver : DefaultContractResolver
{
protected override IValueProvider CreateMemberValueProvider(MemberInfo member)
{
IValueProvider provider = base.CreateMemberValueProvider(member);
if (member.MemberType == MemberTypes.Property)
{
Type propType = ((PropertyInfo)member).PropertyType;
if (propType.IsArray || ( propType.IsGenericType &&
propType.GetGenericTypeDefinition().IsAssignableFrom(typeof(IEnumerable<>))))
{
return new EmptyListValueProvider(provider, propType);
}
}
return provider;
}
public class EmptyListValueProvider : IValueProvider
{
private readonly IValueProvider innerProvider;
public EmptyListValueProvider(IValueProvider innerProvider, Type listType)
{
this.innerProvider = innerProvider;
}
public void SetValue(object target, object value)
{
throw new NotImplementedException();
}
public object GetValue(object target)
{
var val = innerProvider.GetValue(target) ;
if (val == null) return null;
var enumerator = (IEnumerable)val;
return enumerator.GetEnumerator().MoveNext() == false ? null : val;
}
}
Related
I have an endpoint in .NET 6 Microsoft.NET.Sdk.Web project that deserialize query strings into a .NET object by using the standard [FromQuery]
[Route("[controller]")]
public class SamplesController
: ControllerBase
{
[HttpGet]
public IActionResult Get([FromQuery]QueryModel queryModel)
{
if (!queryModel.Status.HasValue)
{
return BadRequest("Problem in deserialization");
}
return Ok(queryModel.Status.Value.GetEnumDisplayName());
}
}
The model contains an enum
public class QueryModel
{
/// <summary>
/// The foo parameter
/// </summary>
/// <example>bar</example>
public string? Foo { get; init; } = null;
/// <summary>
/// The status
/// </summary>
/// <example>on-hold</example>
public Status? Status { get; set; } = null;
}
And the enum has EnumMember attributes which value I want to use to deserialize from.
public enum Status
{
[EnumMember(Value = "open")]
Open,
[EnumMember(Value = "on-hold")]
OnHold
}
By default, .NET 6 does not take into consideration the EnumMember when deserializing.
The goal is to be able to send requests such as
http://localhost:5000/Samples?Foo=bar&Status=on-hold
and have the controller's action deserialize the QueryModel with the proper Status.OnHold value by using its EnumMember
I have tried without luck an extensions library that contains a converter, but the converter is not getting triggered when using [FromQuery]. See https://github.com/Macross-Software/core/issues/30
I have added a project to reproduce problem and as a sandbox to provide a solution**
https://gitlab.com/sunnyatticsoftware/issues/string-to-enum-mvc/-/tree/feature/1-original-problem
NOTE: I would need a solution where the Enum and the does not require any external dependency (just .NET sdk).
A custom Enum converter might be your choice. By leveraging the existing EnumConverter class what we need is to have a customized ConvertFrom method:
public class CustomEnumConverter : EnumConverter
{
public CustomEnumConverter([DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicParameterlessConstructor | DynamicallyAccessedMemberTypes.PublicFields)] Type type) : base(type)
{
}
public override object? ConvertFrom(ITypeDescriptorContext? context, CultureInfo? culture, object value)
{
if (value is string strValue)
{
try
{
foreach (var name in Enum.GetNames(EnumType))
{
var field = EnumType.GetField(name);
if (field != null)
{
var enumMember = (EnumMemberAttribute)(field.GetCustomAttributes(typeof(EnumMemberAttribute), true).Single());
if (strValue.Equals(enumMember.Value, StringComparison.OrdinalIgnoreCase))
{
return Enum.Parse(EnumType, name, true);
}
}
}
}
catch (Exception e)
{
throw new FormatException((string)value, e);
}
}
return base.ConvertFrom(context, culture, value);
}
}
And then decorate the converter to your Model class:
[TypeConverter(typeof(CustomEnumConverter))]
public enum Status
{
[EnumMember(Value = "open")]
Open,
[EnumMember(Value = "on-hold")]
OnHold
}
then we can get the "on-hold" parsed. You might also want to override the ConverTo() for printing the EnumMember value to swagger. It is a bit hacky, but if you want a pure .NET solution this should be one of the minimal viable solutions.
Following the documentation guide Custom Model Binding in ASP.NET Core, you can create your own versions of Microsoft's classes EnumTypeModelBinderProvider, EnumTypeModelBinder (and base class SimpleTypeModelBinder) that replace incoming enum value names that have been renamed via EnumMemberAttribute with the original enum names before binding:
// Begin code for enum model binding
public class EnumMemberEnumTypeModelBinderProvider : IModelBinderProvider
{
public EnumMemberEnumTypeModelBinderProvider(MvcOptions options) { }
public IModelBinder? GetBinder(ModelBinderProviderContext context)
{
if (context == null)
throw new ArgumentNullException(nameof(context));
if (context.Metadata.IsEnum)
{
var enumType = context.Metadata.UnderlyingOrModelType;
Debug.Assert(enumType.IsEnum);
var loggerFactory = context.Services.GetRequiredService<ILoggerFactory>();
if (EnumExtensions.TryGetEnumMemberOverridesToOriginals(enumType, out var overridesToOriginals))
return new EnumMemberEnumTypeModelBinder(suppressBindingUndefinedValueToEnumType: true, enumType, loggerFactory, overridesToOriginals);
}
return null;
}
}
public class EnumMemberEnumTypeModelBinder : ExtensibleSimpleTypeModelBinder
{
// Adapted from https://github.com/dotnet/aspnetcore/blob/c85baf8db0c72ae8e68643029d514b2e737c9fae/src/Mvc/Mvc.Core/src/ModelBinding/Binders/EnumTypeModelBinder.cs#L58
readonly Type enumType;
readonly bool isFlagged;
readonly Dictionary<ReadOnlyMemory<char>, string> overridesToOriginals;
readonly TypeConverter typeConverter;
public EnumMemberEnumTypeModelBinder(bool suppressBindingUndefinedValueToEnumType, Type modelType, ILoggerFactory loggerFactory, Dictionary<ReadOnlyMemory<char>, string> overridesToOriginals) : base(modelType, loggerFactory)
{
this.enumType = Nullable.GetUnderlyingType(modelType) ?? modelType;
if (!this.enumType.IsEnum)
throw new ArgumentException();
this.isFlagged = Attribute.IsDefined(enumType, typeof(FlagsAttribute));
this.overridesToOriginals = overridesToOriginals ?? throw new ArgumentNullException(nameof(overridesToOriginals));
this.typeConverter = TypeDescriptor.GetConverter(this.enumType);
}
protected override string? GetValueFromBindingContext(ValueProviderResult valueProviderResult) =>
EnumExtensions.ReplaceRenamedEnumValuesToOriginals(base.GetValueFromBindingContext(valueProviderResult), isFlagged, overridesToOriginals);
protected override void CheckModel(ModelBindingContext bindingContext, ValueProviderResult valueProviderResult, object? model)
{
if (model == null)
{
base.CheckModel(bindingContext, valueProviderResult, model);
}
else if (IsDefinedInEnum(model, bindingContext))
{
bindingContext.Result = ModelBindingResult.Success(model);
}
else
{
bindingContext.ModelState.TryAddModelError(
bindingContext.ModelName,
bindingContext.ModelMetadata.ModelBindingMessageProvider.ValueIsInvalidAccessor(
valueProviderResult.ToString()));
}
}
private bool IsDefinedInEnum(object model, ModelBindingContext bindingContext)
{
// Adapted from https://github.com/dotnet/aspnetcore/blob/c85baf8db0c72ae8e68643029d514b2e737c9fae/src/Mvc/Mvc.Core/src/ModelBinding/Binders/EnumTypeModelBinder.cs#L58
var modelType = bindingContext.ModelMetadata.UnderlyingOrModelType;
// Check if the converted value is indeed defined on the enum as EnumTypeConverter
// converts value to the backing type (ex: integer) and does not check if the value is defined on the enum.
if (bindingContext.ModelMetadata.IsFlagsEnum)
{
var underlying = Convert.ChangeType(
model,
Enum.GetUnderlyingType(modelType),
CultureInfo.InvariantCulture).ToString();
var converted = model.ToString();
return !string.Equals(underlying, converted, StringComparison.OrdinalIgnoreCase);
}
return Enum.IsDefined(modelType, model);
}
}
public class ExtensibleSimpleTypeModelBinder : IModelBinder
{
// Adapted from https://github.com/dotnet/aspnetcore/blob/c85baf8db0c72ae8e68643029d514b2e737c9fae/src/Mvc/Mvc.Core/src/ModelBinding/Binders/SimpleTypeModelBinder.cs
private readonly TypeConverter _typeConverter;
private readonly ILogger _logger;
public ExtensibleSimpleTypeModelBinder(Type type, ILoggerFactory loggerFactory) : this(type, loggerFactory, null) { }
public ExtensibleSimpleTypeModelBinder(Type type, ILoggerFactory loggerFactory, TypeConverter? typeConverter)
{
if (type == null)
throw new ArgumentNullException(nameof(type));
if (loggerFactory == null)
throw new ArgumentNullException(nameof(loggerFactory));
_typeConverter = typeConverter ?? TypeDescriptor.GetConverter(type);
_logger = loggerFactory.CreateLogger<ExtensibleSimpleTypeModelBinder>();
}
protected virtual string? GetValueFromBindingContext(ValueProviderResult valueProviderResult) => valueProviderResult.FirstValue;
/// <inheritdoc />
public Task BindModelAsync(ModelBindingContext bindingContext)
{
if (bindingContext == null)
throw new ArgumentNullException(nameof(bindingContext));
//_logger.AttemptingToBindModel(bindingContext);
var valueProviderResult = bindingContext.ValueProvider.GetValue(bindingContext.ModelName);
if (valueProviderResult == ValueProviderResult.None)
{
//_logger.FoundNoValueInRequest(bindingContext);
// no entry
//_logger.DoneAttemptingToBindModel(bindingContext);
return Task.CompletedTask;
}
bindingContext.ModelState.SetModelValue(bindingContext.ModelName, valueProviderResult);
try
{
var value = GetValueFromBindingContext(valueProviderResult);
object? model;
if (bindingContext.ModelType == typeof(string))
{
// Already have a string. No further conversion required but handle ConvertEmptyStringToNull.
if (bindingContext.ModelMetadata.ConvertEmptyStringToNull && string.IsNullOrWhiteSpace(value))
model = null;
else
model = value;
}
else if (string.IsNullOrWhiteSpace(value))
{
// Other than the StringConverter, converters Trim() the value then throw if the result is empty.
model = null;
}
else
{
model = _typeConverter.ConvertFrom(context: null,culture: valueProviderResult.Culture, value: value);
}
CheckModel(bindingContext, valueProviderResult, model);
//_logger.DoneAttemptingToBindModel(bindingContext);
return Task.CompletedTask;
}
catch (Exception exception)
{
var isFormatException = exception is FormatException;
if (!isFormatException && exception.InnerException != null)
{
// TypeConverter throws System.Exception wrapping the FormatException,
// so we capture the inner exception.
exception = System.Runtime.ExceptionServices.ExceptionDispatchInfo.Capture(exception.InnerException).SourceException;
}
bindingContext.ModelState.TryAddModelError(bindingContext.ModelName,exception, bindingContext.ModelMetadata);
// Were able to find a converter for the type but conversion failed.
return Task.CompletedTask;
}
}
/// <inheritdoc/>
protected virtual void CheckModel(
ModelBindingContext bindingContext,
ValueProviderResult valueProviderResult,
object? model)
{
// When converting newModel a null value may indicate a failed conversion for an otherwise required
// model (can't set a ValueType to null). This detects if a null model value is acceptable given the
// current bindingContext. If not, an error is logged.
if (model == null && !bindingContext.ModelMetadata.IsReferenceOrNullableType)
{
bindingContext.ModelState.TryAddModelError(
bindingContext.ModelName,
bindingContext.ModelMetadata.ModelBindingMessageProvider.ValueMustNotBeNullAccessor(
valueProviderResult.ToString()));
}
else
{
bindingContext.Result = ModelBindingResult.Success(model);
}
}
}
// End code for enum model binding
/********************************************************/
// Begin general enum parsing code
public class CharMemoryComparer : IEqualityComparer<ReadOnlyMemory<char>>
{
public static CharMemoryComparer OrdinalIgnoreCase { get; } = new CharMemoryComparer(StringComparison.OrdinalIgnoreCase);
public static CharMemoryComparer Ordinal { get; } = new CharMemoryComparer(StringComparison.Ordinal);
readonly StringComparison comparison;
CharMemoryComparer(StringComparison comparison) => this.comparison = comparison;
public bool Equals(ReadOnlyMemory<char> x, ReadOnlyMemory<char> y) => MemoryExtensions.Equals(x.Span, y.Span, comparison);
public int GetHashCode(ReadOnlyMemory<char> obj) => String.GetHashCode(obj.Span, comparison);
}
public static partial class EnumExtensions
{
public const char FlagSeparatorChar = ',';
public const string FlagSeparatorString = ", ";
public static bool TryGetEnumMemberOverridesToOriginals(Type enumType, [System.Diagnostics.CodeAnalysis.NotNullWhen(returnValue: true)] out Dictionary<ReadOnlyMemory<char>, string>? overridesToOriginals)
{
if (enumType == null)
throw new ArgumentNullException(nameof(enumType));
if (!enumType.IsEnum)
throw new ArgumentException(nameof(enumType));
overridesToOriginals = null;
foreach (var name in Enum.GetNames(enumType))
{
if (TryGetEnumAttribute<EnumMemberAttribute>(enumType, name, out var attr) && !string.IsNullOrWhiteSpace(attr.Value))
{
overridesToOriginals = overridesToOriginals ?? new(CharMemoryComparer.OrdinalIgnoreCase);
overridesToOriginals.Add(attr.Value.AsMemory(), name);
}
}
return overridesToOriginals != null;
}
public static bool TryGetEnumAttribute<TAttribute>(Type type, string name, [System.Diagnostics.CodeAnalysis.NotNullWhen(returnValue: true)] out TAttribute? attribute) where TAttribute : System.Attribute
{
var member = type.GetMember(name).SingleOrDefault();
attribute = member?.GetCustomAttribute<TAttribute>(false);
return attribute != null;
}
public static string? ReplaceRenamedEnumValuesToOriginals(string? value, bool isFlagged, Dictionary<ReadOnlyMemory<char>, string> overridesToOriginals)
{
if (string.IsNullOrWhiteSpace(value))
return value;
var trimmed = value.AsMemory().Trim();
if (overridesToOriginals.TryGetValue(trimmed, out var #override))
value = #override;
else if (isFlagged && trimmed.Length > 0)
{
var sb = new StringBuilder();
bool replaced = false;
foreach (var n in trimmed.Split(EnumExtensions.FlagSeparatorChar, StringSplitOptions.TrimEntries))
{
ReadOnlySpan<char> toAppend;
if (overridesToOriginals.TryGetValue(n, out var #thisOverride))
{
toAppend = thisOverride.AsSpan();
replaced = true;
}
else
toAppend = n.Span;
sb.Append(sb.Length == 0 ? null : EnumExtensions.FlagSeparatorString).Append(toAppend);
}
if (replaced)
value = sb.ToString();
}
return value;
}
}
public static class StringExtensions
{
public static IEnumerable<ReadOnlyMemory<char>> Split(this ReadOnlyMemory<char> chars, char separator, StringSplitOptions options = StringSplitOptions.None)
{
int index;
while ((index = chars.Span.IndexOf(separator)) >= 0)
{
var slice = chars.Slice(0, index);
if ((options & StringSplitOptions.TrimEntries) == StringSplitOptions.TrimEntries)
slice = slice.Trim();
if ((options & StringSplitOptions.RemoveEmptyEntries) == 0 || slice.Length > 0)
yield return slice;
chars = chars.Slice(index + 1);
}
if ((options & StringSplitOptions.TrimEntries) == StringSplitOptions.TrimEntries)
chars = chars.Trim();
if ((options & StringSplitOptions.RemoveEmptyEntries) == 0 || chars.Length > 0)
yield return chars;
}
}
Then add the binder in ConfigureServices() like so:
public void ConfigureServices(IServiceCollection services)
{
services.AddControllers(options =>
{
options.ModelBinderProviders.Insert(0, new EnumMemberEnumTypeModelBinderProvider(options));
});
}
Notes:
EnumTypeModelBinder and base class SimpleTypeModelBinder provide no useful extension points to customize the parsing of the incoming value string, thus it was necessary to copy some of their logic.
Precisely emulating the logic of SimpleTypeModelBinder is somewhat difficult because it supports both numeric and textual enum values -- including mixtures of both for flags enums. The binder above retains that capability, but at a cost of also allowing original enum names to be bound successfully. Thus the values on-hold and onhold will be bound to Status.OnHold.
Conversely, if you do not want to support binding of numeric values for enums, you could adapt the code of JsonEnumMemberStringEnumConverter from this answer to System.Text.Json: How do I specify a custom name for an enum value?. Demo fiddle here. This approach also avoids binding to the original, unrenamed enum names.
Matching of override names with original enum names is case-insensitive, so override names that differ only in case are not supported.
I want set property names at runtime. I already achieve this for serialization.
For example. I have a simple model like as below:
[JsonConverter(typeof(DataModelConverter))]
public class DataModel
{
public string Name { get; set; }
public int Age { get; set; }
}
And I have a simple DataModelConverter, that inherited from JsonConverter:
public class DataModelConverter : JsonConverter
{
public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer)
{
Type type = value.GetType();
JObject jo = new JObject();
foreach (PropertyInfo prop in type.GetProperties())
{
jo.Add(prop.Name == "Name" ? "FullName" : prop.Name, new JValue(prop.GetValue(value)));
}
jo.WriteTo(writer);
}
public override bool CanRead
{
get { return false; }
}
public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer)
{
throw new NotImplementedException();
}
public override bool CanConvert(Type objectType)
{
return objectType == typeof(DataModel);
}
}
And I have a simple controller like as below:
[Route("api/[controller]")]
[ApiController]
public class NewtonController : ControllerBase
{
public IEnumerable<DataModel> GetNewtonDatas([FromBody] DataModel input)
{
return new List<DataModel>()
{
new DataModel
{
Name="Ramil",
Age=25
},
new DataModel
{
Name="Yusif",
Age=26
}
};
}
}
If I call this API, result will like as below (Showing FullName Instead of Name):
[
{
"FullName": "Ramil",
"Age": 25
},
{
"FullName": "Yusif",
"Age": 26
}
]
But I have a problem. This is not working for deserialization.
For example: If I call this API with this body, then Name will null.
{
"FullName":"Ramil"
}
My attribute is not working for deserialization. I want set property name via attribute for deserialization at runtime .
I don't want use some middleware, I want to achieve this only by using the any attribute at runtime. I must read JSON property names from my appsettings.json file.
Thanks for help!
You have overridden CanRead to return false:
public override bool CanRead
{
get { return false; }
}
This causes Json.NET not to call your your converter's DataModelConverter.ReadJson() method during deserialization, and instead use default deserialization. Since "FullName" does not have the same (case-invariant) name as the Name property, it never gets set, and remains null.
To fix this, remove the override for CanRead (the default implementation returns true) and implement ReadJson(), e.g. as follows:
public class DataModelConverter : NameRemappingConverterBase
{
static string AlternateName => "FullName";
static string OriginalName => "Name";
public override bool CanConvert(Type objectType) => objectType == typeof(DataModel);
// Replace the below logic with name mappings from appsettings.json
protected override string ToJsonPropertyName(JsonProperty property) =>
string.Equals(property.UnderlyingName, OriginalName, StringComparison.OrdinalIgnoreCase) ? AlternateName : base.ToJsonPropertyName(property);
protected override string FromJsonPropertyName(string name) =>
string.Equals(name, AlternateName, StringComparison.OrdinalIgnoreCase) ? OriginalName : base.FromJsonPropertyName(name);
}
public abstract class NameRemappingConverterBase : JsonConverter
{
protected virtual string ToJsonPropertyName(JsonProperty property) => property.PropertyName;
protected virtual string FromJsonPropertyName(string name) => name;
public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer)
{
if (reader.MoveToContentAndAssert().TokenType == JsonToken.Null)
return null;
if (reader.TokenType != JsonToken.StartObject)
throw new JsonSerializationException(string.Format("Unexpected token {0}", reader.TokenType));
var contract = (JsonObjectContract)serializer.ContractResolver.ResolveContract(objectType);
var value = existingValue ?? contract.DefaultCreator();
while (reader.ReadToContentAndAssert().TokenType != JsonToken.EndObject)
{
if (reader.TokenType != JsonToken.PropertyName)
throw new JsonSerializationException(string.Format("Unexpected token {0}", reader.TokenType));
var name = FromJsonPropertyName((string)reader.Value);
reader.ReadToContentAndAssert();
var property = contract.Properties.GetProperty(name, StringComparison.OrdinalIgnoreCase);
if (!ShouldDeserialize(property))
{
reader.Skip();
}
else
{
var propertyValue = serializer.Deserialize(reader, property.PropertyType);
property.ValueProvider.SetValue(value, propertyValue);
}
}
return value;
}
public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer)
{
var contract = (JsonObjectContract)serializer.ContractResolver.ResolveContract(value.GetType());
writer.WriteStartObject();
foreach (var property in contract.Properties.Where(p => ShouldSerialize(p, value)))
{
var propertyValue = property.ValueProvider.GetValue(value);
if (propertyValue == null && serializer.NullValueHandling == NullValueHandling.Ignore)
continue;
var name = ToJsonPropertyName(property);
writer.WritePropertyName(name);
serializer.Serialize(writer, propertyValue);
}
writer.WriteEndObject();
}
protected virtual bool ShouldDeserialize(JsonProperty property) =>
property != null && property.Writable;
protected virtual bool ShouldSerialize(JsonProperty property, object value) =>
property.Readable && !property.Ignored && (property.ShouldSerialize == null || property.ShouldSerialize(value));
}
public static partial class JsonExtensions
{
public static JsonReader ReadToContentAndAssert(this JsonReader reader) =>
reader.ReadAndAssert().MoveToContentAndAssert();
public static JsonReader MoveToContentAndAssert(this JsonReader reader)
{
if (reader == null)
throw new ArgumentNullException();
if (reader.TokenType == JsonToken.None) // Skip past beginning of stream.
reader.ReadAndAssert();
while (reader.TokenType == JsonToken.Comment) // Skip past comments.
reader.ReadAndAssert();
return reader;
}
public static JsonReader ReadAndAssert(this JsonReader reader)
{
if (reader == null)
throw new ArgumentNullException();
if (!reader.Read())
throw new JsonReaderException("Unexpected end of JSON stream.");
return reader;
}
}
Demo fiddle here.
here is my code:
Classes:
[Serializable]
public abstract class ChallengeDetailsDto : ChallengeDto
{
[JsonConverter(typeof(PagesConverter))]
public PageDto[] Pages { get; set; } = new PageDto[] { };
}
[Serializable]
public class CaseDto : ChallengeDto
{
public CaseDto()
{
Discriminator = CaseDtoDiscriminator;
}
}
Controller:
[HttpPost]
public async Task<IActionResult> CreateAsync([FromForm]CaseDetailsDto dto)
{
}
Custom Converter
internal class ChallengeConverter : JsonConverter
{
public override bool CanConvert(Type objectType)
{
if (objectType == typeof(ChallengeDto))
{
return true;
}
return false;
}
public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer)
{
ChallengeDto challengeDto = null;
var jsonChallange = JObject.Load(reader);
var discriminator = jsonChallange.Properties().First(p => p.Name == "discriminator").Value.ToString();
if (discriminator == ChallengeDto.CaseDtoDiscriminator)
{
challengeDto = (CaseDto)JsonConvert.DeserializeObject(jsonChallange.ToString(), typeof(CaseDto));
}
/*...*/
return challengeDto;
}
public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer)
{
var challenge = value as ChallengeDto;
if (challenge.Discriminator == ChallengeDto.CaseDtoDiscriminator)
{
serializer.Serialize(writer, (CaseDto)challenge);
}
/*...*/
}
}
Error:
Could not create an instance of type
'Controllers.Challenges.Dtos.PageDto'. Model bound complex types must
not be abstract or value types and must have a parameterless
constructor.
If I use [FromBody], every thing works fine. From what I can understand, using [FromForm] the binding does not use Json.
What is the best way to bind Form Values to the property in this case?
Let's say I have the following custom JsonConverter for serialization and/or deserialization:
public class VersionConverter : JsonConverter<Version>
{
public override void WriteJson(JsonWriter writer, Version value, JsonSerializer serializer)
{
writer.WriteValue(value.ToString());
}
public override Version ReadJson(JsonReader reader, Type objectType, Version existingValue, bool hasExistingValue, JsonSerializer serializer)
{
string s = (string)reader.Value;
return new Version(s);
}
}
public class NuGetPackage
{
public string PackageId { get; set; }
public Version Version { get; set; }
public string Description { get; set; }
}
Let's say I have the following code snippet in my application:
NuGetPackage p1 = new NuGetPackage
{
PackageId = "Newtonsoft.Json",
Version = new Version(10, 0, 4),
Description = null
};
string json = JsonConvert.SerializeObject(p1, Formatting.Indented, new VersionConverter());
I want the Json.NET converter to Ignore the Description member variable of the NuGetPackage class.
Note: I do Not Want to use the following "marker boolean" member variable:
public bool ShouldSerializeINSERT_YOUR_PROPERTY_NAME_HERE()
{
if(someCondition){
return true;
}else{
return false;
}
}
I would rather specify the ignoring of a specific member variable somewhere
a) when my code invokes the JsonConvert.SerializeObject?
b) or within the VersionConverter code class itself?
Could someone please show me how to ignore the specific member variable in such a way?
Since NuGetPackage is fairly simple, you could just write an additional JsonConverter for NuGetPackage that serializes only the members you need, e.g.:
public class SimplifiedNuGetPackageConverter : JsonConverter
{
public override bool CanConvert(Type objectType) { return objectType == typeof(NuGetPackage); }
public override bool CanRead { get { return false; } }
public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer)
{
throw new NotImplementedException();
}
public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer)
{
var package = (NuGetPackage)value;
serializer.Serialize(writer, new { package.PackageId, package.Version });
}
}
Then serialize as follows:
var settings = new JsonSerializerSettings
{
Converters = { new VersionConverter() },
};
if (!someCondition)
settings.Converters.Add(new SimplifiedNuGetPackageConverter());
var json = JsonConvert.SerializeObject(p1, Formatting.Indented, settings);
If you are serializing multiple instances of NuGetPackage at once and need to write Description for some but not all, you could add the logic for someCondition inside WriteJson() itself:
public class ConditionalNuGetPackageConverter : JsonConverter
{
public override bool CanConvert(Type objectType) { return objectType == typeof(NuGetPackage); }
public override bool CanRead { get { return false; } }
public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer)
{
throw new NotImplementedException();
}
public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer)
{
var package = (NuGetPackage)value;
// Replace with your logic:
var someCondition = !string.IsNullOrWhiteSpace(package.Description);
if (someCondition)
serializer.Serialize(writer, new { package.PackageId, package.Version, package.Description });
else
serializer.Serialize(writer, new { package.PackageId, package.Version });
}
}
And then serialize as follows:
var settings = new JsonSerializerSettings
{
Converters = { new ConditionalNuGetPackageConverter(), new VersionConverter() },
};
var json = JsonConvert.SerializeObject(p1, Formatting.Indented, settings);
Working .Net fiddle here.
I currently have this controller
/// <summary>
/// API methods for working with test
/// </summary>
[RoutePrefix("api/terminal")]
public class TerminalController : ApiController
{
[HttpGet]
[Route("{terminalId}/validation")]
public IHttpActionResult ValidateTerminal([MinUnsignValue(10)] long terminalId)
{
return Ok();
}
}
And custom validator for input parameter
public class MinUnsignValueAttribute : ValidationAttribute
{
private readonly ulong _minValue;
public MinUnsignValueAttribute(ulong minValue)
{
_minValue = minValue;
}
protected override ValidationResult IsValid(object value, ValidationContext validationContext)
{
//if value < _minValue return new ValidationResult("false")
return ValidationResult.Success;
}
}
When I send 6 the validator ignored and action invoked. It`s caused because web api pipeline consist of IActionFilter[] IAuthenticationFilter[] IAuthorizationFilter[] authorizationFilters IExceptionFilter[].
Is there a way to integrate my custom attribute to the pipeline?
ok, I didn't found an answer. wrote it myself
public class ParametersFilter : ActionFilterAttribute
{
class Wrapper
{
public Wrapper(MethodInfo methodInfo, object validationAttributeInstance)
{
MethodInfo = methodInfo;
ValidationAttributeInstance = validationAttributeInstance;
}
public MethodInfo MethodInfo { get; }
public object ValidationAttributeInstance { get; }
}
static readonly ConcurrentDictionary<string, Wrapper> Cache = new ConcurrentDictionary<string, Wrapper>();
static readonly HashSet<string> Registry = new HashSet<string>();
public override void OnActionExecuting(HttpActionContext actionContext)
{
if (!Registry.Contains(actionContext.ActionDescriptor.ActionName))
{
Registry.Add(actionContext.ActionDescriptor.ActionName);
//first invokation, have to check
Type controllerType = actionContext.ControllerContext.Controller.GetType();
foreach (MethodInfo methodInfo in controllerType.GetMethods())
{
if (actionContext.ActionDescriptor.ActionName != methodInfo.Name) continue;
ParameterInfo[] parameters = methodInfo.GetParameters();
if (parameters.Length != actionContext.ActionArguments.Count) continue;
foreach (ParameterInfo parameterInfo in parameters)
{
//The primitive types are Boolean, Byte, SByte, Int16, UInt16, Int32, UInt32, Int64, UInt64, IntPtr, UIntPtr, Char, Double, and Single.
if (!parameterInfo.ParameterType.IsPrimitive) continue;
if (!actionContext.ActionArguments.ContainsKey(parameterInfo.Name)) continue;
var customAttributesData = parameterInfo.GetCustomAttributesData();
foreach (CustomAttributeData customAttributeData in customAttributesData)
{
if (!customAttributeData.AttributeType.IsSubclassOf(typeof(ValidationAttribute))) continue;
var validationAttributeInstance = customAttributeData.Constructor.Invoke(customAttributeData.ConstructorArguments.Select(x => x.Value).ToArray()) as ValidationAttribute;
if (validationAttributeInstance == null) continue;
MethodInfo method = validationAttributeInstance
.GetType()
.GetMethod("IsValid", BindingFlags.NonPublic | BindingFlags.Instance);
Cache.TryAdd(
$"{actionContext.ActionDescriptor.ActionName}{parameterInfo.Name}",
new Wrapper(method, validationAttributeInstance));
}
}
}
}
foreach (var actionArgument in actionContext.ActionArguments)
{
Wrapper wrapper;
if (!Cache.TryGetValue(
$"{actionContext.ActionDescriptor.ActionName}{actionArgument.Key}", out wrapper))
{
continue;
}
object instance = actionArgument.Value;
var context = new ValidationContext(instance)
{
DisplayName = actionArgument.Key,
MemberName = actionArgument.Key
};
var results = wrapper.MethodInfo.Invoke(wrapper.ValidationAttributeInstance, new[] { instance, context }) as ValidationResult;
if (results != ValidationResult.Success)
{
var response = new Utils.WebApi.Common.Response.WebResponse
{
IsSuccess = false,
ErrorMessage = results?.ErrorMessage
};
actionContext.Response = new HttpResponseMessage
{
StatusCode = HttpStatusCode.BadRequest,
Content = new ObjectContent(
response.GetType(),
response,
new JsonMediaTypeFormatter())
};
break;
}
}
base.OnActionExecuting(actionContext);
}
}