I need to send a DTO with an IPAddress at the endpoint.
public class TestDto
{
public TestDto(IPAddress property)
{
Property = property;
}
public IPAddress Property { get; }
}
I have a custom JSON converter. And it works properly.
public sealed class IPAddressConverter : JsonConverter<IPAddress>
{
public override IPAddress Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
{
var ip = reader.GetString();
var ipAddress = IPAddress.Parse(ip!);
return ipAddress;
}
public override void Write(Utf8JsonWriter writer, IPAddress value, JsonSerializerOptions options)
{
var ip = value.ToString();
writer.WriteStringValue(ip);
}
}
Unfortunately default model binder validator tries to get ScopeId and Address, which causes an exception.
System.Net.Sockets.SocketException (10045): The attempted operation is not supported for the type of object referenced.
at System.Net.IPAddress.get_ScopeId()
at Microsoft.Extensions.Internal.PropertyHelper.CallNullSafePropertyGetter[TDeclaringType,TValue](Func`2 getter, Object target)
at Microsoft.AspNetCore.Mvc.ModelBinding.Validation.DefaultComplexObjectValidationStrategy.Enumerator.<>c__DisplayClass13_1.<MoveNext>b__1()
at Microsoft.AspNetCore.Mvc.ModelBinding.Validation.ValidationEntry.get_Model()
at Microsoft.AspNetCore.Mvc.ModelBinding.Validation.ValidationVisitor.VisitChildren(IValidationStrategy strategy)
at Microsoft.AspNetCore.Mvc.ModelBinding.Validation.ValidationVisitor.VisitComplexType(IValidationStrategy defaultStrategy)
at Microsoft.AspNetCore.Mvc.ModelBinding.Validation.ValidationVisitor.VisitImplementation(ModelMetadata& metadata, String& key, Object model)
at Microsoft.AspNetCore.Mvc.ModelBinding.Validation.ValidationVisitor.Visit(ModelMetadata metadata, String key, Object model)
at Microsoft.AspNetCore.Mvc.ModelBinding.Validation.ValidationVisitor.VisitChildren(IValidationStrategy strategy)
at Microsoft.AspNetCore.Mvc.ModelBinding.Validation.ValidationVisitor.VisitComplexType(IValidationStrategy defaultStrategy)
at Microsoft.AspNetCore.Mvc.ModelBinding.Validation.ValidationVisitor.VisitImplementation(ModelMetadata& metadata, String& key, Object model)
at Microsoft.AspNetCore.Mvc.ModelBinding.Validation.ValidationVisitor.Visit(ModelMetadata metadata, String key, Object model)
at Microsoft.AspNetCore.Mvc.ModelBinding.Validation.ValidationVisitor.Validate(ModelMetadata metadata, String key, Object model, Boolean alwaysValidateAtTopLevel, Object container)
at Microsoft.AspNetCore.Mvc.ModelBinding.ObjectModelValidator.Validate(ActionContext actionContext, ValidationStateDictionary validationState, String prefix, Object model, ModelMetadata metadata, Object container)
at Microsoft.AspNetCore.Mvc.ModelBinding.ParameterBinder.EnforceBindRequiredAndValidate(ObjectModelValidator baseObjectValidator, ActionContext actionContext, ParameterDescriptor parameter, ModelMetadata metadata, ModelBindingContext modelBindingContext, ModelBindingResult modelBindingResult, Object container)
at Microsoft.AspNetCore.Mvc.ModelBinding.ParameterBinder.BindModelAsync(ActionContext actionContext, IModelBinder modelBinder, IValueProvider valueProvider, ParameterDescriptor parameter, ModelMetadata metadata, Object value, Object container)
at Microsoft.AspNetCore.Mvc.Controllers.ControllerBinderDelegateProvider.<>c__DisplayClass0_0.<<CreateBinderDelegate>g__Bind|0>d.MoveNext()
--- End of stack trace from previous location ---
at Microsoft.AspNetCore.Mvc.Infrastructure.ControllerActionInvoker.<InvokeInnerFilterAsync>g__Awaited|13_0(ControllerActionInvoker invoker, Task lastTask, State next, Scope scope, Object state, Boolean isCompleted)
at Microsoft.AspNetCore.Mvc.Infrastructure.ResourceInvoker.<InvokeFilterPipelineAsync>g__Awaited|20_0(ResourceInvoker invoker, Task lastTask, State next, Scope scope, Object state, Boolean isCompleted)
at Microsoft.AspNetCore.Mvc.Infrastructure.ResourceInvoker.<InvokeAsync>g__Logged|17_1(ResourceInvoker invoker)
at Microsoft.AspNetCore.Mvc.Infrastructure.ResourceInvoker.<InvokeAsync>g__Logged|17_1(ResourceInvoker invoker)
at Microsoft.AspNetCore.Routing.EndpointMiddleware.<Invoke>g__AwaitRequestTask|6_0(Endpoint endpoint, Task requestTask, ILogger logger)
at Microsoft.AspNetCore.Authorization.AuthorizationMiddleware.Invoke(HttpContext context)
at Microsoft.AspNetCore.Diagnostics.DeveloperExceptionPageMiddleware.Invoke(HttpContext context)
How to fix it?
This is because the ValidationVisitor treats the IPAddress type as a complex type and attempts to validate all of its properties. However, when it comes to IPv4, the ScopeId property's get method is not accessible, which causes the error.
So we just need to make sure that the ValidationVisitor treats the IPAddress type as a simple type during validation to avoid this error. Whether or not a type is considered complex depends on the IsComplexType property of the Metadata.
if (Metadata.IsComplexType)
{
return VisitComplexType(DefaultComplexObjectValidationStrategy.Instance);
}
The value of the IsComplexType property depends on whether or not there is a converter from string to the specified type.
IsComplexType = !TypeDescriptor.GetConverter(ModelType).CanConvertFrom(typeof(string));
So we just need to add an appropriate converter for IPAddress.
Here is my solution:
public class IPAddressDescriptionProvider : TypeDescriptionProvider
{
private static TypeDescriptionProvider defaultTypeProvider = TypeDescriptor.GetProvider(typeof(IPAddress));
public IPAddressDescriptionProvider() : base(defaultTypeProvider)
{
}
public override ICustomTypeDescriptor GetTypeDescriptor(Type objectType, object instance)
{
return new IPAddressDescriptor();
}
}
public class IPAddressDescriptor : ICustomTypeDescriptor
{
public TypeConverter GetConverter()
{
return new IPAddressConverter();
}
//Other methods are not used in this problem and can be implemented with throw new NotImplementedException();
}
public class IPAddressConverter: TypeConverter
{
public override bool CanConvertFrom(ITypeDescriptorContext context, Type sourceType)
{
if (sourceType == typeof(string))
{
return true;
}
return base.CanConvertFrom(context, sourceType);
}
}
Add the Provider in Startup:
TypeDescriptor.AddProvider(new IPAddressTypeDescriptionProvider(), typeof(IPAddress));