使用 Web API 默认模型活页夹自定义属性名称?
Customize attribute names with Web API default model binder?
我有一个请求模型 class,我正在尝试使用默认的 Web API 2 模型绑定 (.NET 4.6.1)。一些查询字符串参数与模型属性匹配,但有些不匹配。
public async Task<IHttpActionResult> Get([FromUri]MyRequest request) {...}
示例查询字符串:
/api/endpoint?country=GB
样本模型属性:
public class MyRequest
{
[JsonProperty("country")] // Did not work
[DataMember(Name = "country")] // Also did not work
public string CountryCode { get; set; }
// ... other properties
}
有没有办法在我的模型上使用属性(就像您可能使用 [JsonProperty("country")]
)来避免实现自定义模型绑定?或者最好的方法是为 QueryString 创建一个特定的模型来绑定,然后使用 AutoMapper 来定制差异?
根据进一步研究,Web API 中的默认模型绑定行为不支持 JsonProperty
或 DataMember
属性,最可能的解决方案似乎是 (1) 自定义模型绑定器或 (2) 维护 2 组模型和它们之间的映射。
- JSON to Model property binding using JsonProperty
- Changing the parameter name Web Api model binding
我选择了自定义模型活页夹(下面的实现)所以我可以重复使用它而不必复制我的所有模型(并维护每个模型之间的映射)。
用法
下面的实现允许我让任何模型有选择地使用 JsonProperty
进行模型绑定,但如果未提供,将默认仅使用 属性 名称。它支持来自标准 .NET 类型(字符串、整数、双精度等)的映射。尚未完全准备好生产,但到目前为止它满足我的用例。
[ModelBinder(typeof(AttributeModelBinder))]
public class PersonModel
{
[JsonProperty("pid")]
public int PersonId { get; set; }
public string Name { get; set; }
}
这允许在请求中映射以下查询字符串:
/api/endpoint?pid=1&name=test
实施
首先,该解决方案定义了一个映射 属性 来跟踪模型的源 属性 以及在设置来自值提供程序的值时要使用的目标名称。
public class MappedProperty
{
public MappedProperty(PropertyInfo source)
{
this.Info = source;
this.Source = source.Name;
this.Target = source.GetCustomAttribute<JsonPropertyAttribute>()?.PropertyName ?? source.Name;
}
public PropertyInfo Info { get; }
public string Source { get; }
public string Target { get; }
}
然后,定义自定义模型绑定器来处理映射。它缓存反射的模型属性以避免在后续调用中重复反射。它可能还没有完全准备好生产,但初步测试很有希望。
public class AttributeModelBinder : IModelBinder
{
public static object _lock = new object();
private static Dictionary<Type, IEnumerable<MappedProperty>> _mappings = new Dictionary<Type, IEnumerable<MappedProperty>>();
public IEnumerable<MappedProperty> GetMapping(Type type)
{
if (_mappings.TryGetValue(type, out var result)) return result; // Found
lock (_lock)
{
if (_mappings.TryGetValue(type, out result)) return result; // Check again after lock
return (_mappings[type] = type.GetProperties().Select(p => new MappedProperty(p)));
}
}
public object Convert(Type target, string value)
{
try
{
var converter = TypeDescriptor.GetConverter(target);
if (converter != null)
return converter.ConvertFromString(value);
else
return target.IsValueType ? Activator.CreateInstance(target) : null;
}
catch (NotSupportedException)
{
return target.IsValueType ? Activator.CreateInstance(target) : null;
}
}
public void SetValue(object model, MappedProperty p, IValueProvider valueProvider)
{
var value = valueProvider.GetValue(p.Target)?.AttemptedValue;
if (value == null) return;
p.Info.SetValue(model, this.Convert(p.Info.PropertyType, value));
}
public bool BindModel(HttpActionContext actionContext, ModelBindingContext bindingContext)
{
try
{
var model = Activator.CreateInstance(bindingContext.ModelType);
var mappings = this.GetMapping(bindingContext.ModelType);
foreach (var p in mappings)
this.SetValue(model, p, bindingContext.ValueProvider);
bindingContext.Model = model;
return true;
}
catch (Exception ex)
{
return false;
}
}
}
迟到的答案,但我最近也遇到了这个问题。您可以简单地使用 BindProperty
属性:
public class MyRequest
{
[BindProperty(Name = "country")]
public string CountryCode { get; set; }
}
在 .NET Core 2.1 和 2.2 上测试
我有一个请求模型 class,我正在尝试使用默认的 Web API 2 模型绑定 (.NET 4.6.1)。一些查询字符串参数与模型属性匹配,但有些不匹配。
public async Task<IHttpActionResult> Get([FromUri]MyRequest request) {...}
示例查询字符串:
/api/endpoint?country=GB
样本模型属性:
public class MyRequest
{
[JsonProperty("country")] // Did not work
[DataMember(Name = "country")] // Also did not work
public string CountryCode { get; set; }
// ... other properties
}
有没有办法在我的模型上使用属性(就像您可能使用 [JsonProperty("country")]
)来避免实现自定义模型绑定?或者最好的方法是为 QueryString 创建一个特定的模型来绑定,然后使用 AutoMapper 来定制差异?
根据进一步研究,Web API 中的默认模型绑定行为不支持 JsonProperty
或 DataMember
属性,最可能的解决方案似乎是 (1) 自定义模型绑定器或 (2) 维护 2 组模型和它们之间的映射。
- JSON to Model property binding using JsonProperty
- Changing the parameter name Web Api model binding
我选择了自定义模型活页夹(下面的实现)所以我可以重复使用它而不必复制我的所有模型(并维护每个模型之间的映射)。
用法
下面的实现允许我让任何模型有选择地使用 JsonProperty
进行模型绑定,但如果未提供,将默认仅使用 属性 名称。它支持来自标准 .NET 类型(字符串、整数、双精度等)的映射。尚未完全准备好生产,但到目前为止它满足我的用例。
[ModelBinder(typeof(AttributeModelBinder))]
public class PersonModel
{
[JsonProperty("pid")]
public int PersonId { get; set; }
public string Name { get; set; }
}
这允许在请求中映射以下查询字符串:
/api/endpoint?pid=1&name=test
实施
首先,该解决方案定义了一个映射 属性 来跟踪模型的源 属性 以及在设置来自值提供程序的值时要使用的目标名称。
public class MappedProperty
{
public MappedProperty(PropertyInfo source)
{
this.Info = source;
this.Source = source.Name;
this.Target = source.GetCustomAttribute<JsonPropertyAttribute>()?.PropertyName ?? source.Name;
}
public PropertyInfo Info { get; }
public string Source { get; }
public string Target { get; }
}
然后,定义自定义模型绑定器来处理映射。它缓存反射的模型属性以避免在后续调用中重复反射。它可能还没有完全准备好生产,但初步测试很有希望。
public class AttributeModelBinder : IModelBinder
{
public static object _lock = new object();
private static Dictionary<Type, IEnumerable<MappedProperty>> _mappings = new Dictionary<Type, IEnumerable<MappedProperty>>();
public IEnumerable<MappedProperty> GetMapping(Type type)
{
if (_mappings.TryGetValue(type, out var result)) return result; // Found
lock (_lock)
{
if (_mappings.TryGetValue(type, out result)) return result; // Check again after lock
return (_mappings[type] = type.GetProperties().Select(p => new MappedProperty(p)));
}
}
public object Convert(Type target, string value)
{
try
{
var converter = TypeDescriptor.GetConverter(target);
if (converter != null)
return converter.ConvertFromString(value);
else
return target.IsValueType ? Activator.CreateInstance(target) : null;
}
catch (NotSupportedException)
{
return target.IsValueType ? Activator.CreateInstance(target) : null;
}
}
public void SetValue(object model, MappedProperty p, IValueProvider valueProvider)
{
var value = valueProvider.GetValue(p.Target)?.AttemptedValue;
if (value == null) return;
p.Info.SetValue(model, this.Convert(p.Info.PropertyType, value));
}
public bool BindModel(HttpActionContext actionContext, ModelBindingContext bindingContext)
{
try
{
var model = Activator.CreateInstance(bindingContext.ModelType);
var mappings = this.GetMapping(bindingContext.ModelType);
foreach (var p in mappings)
this.SetValue(model, p, bindingContext.ValueProvider);
bindingContext.Model = model;
return true;
}
catch (Exception ex)
{
return false;
}
}
}
迟到的答案,但我最近也遇到了这个问题。您可以简单地使用 BindProperty
属性:
public class MyRequest
{
[BindProperty(Name = "country")]
public string CountryCode { get; set; }
}
在 .NET Core 2.1 和 2.2 上测试